⚠️ WIP migrating tmp to patch

This commit is contained in:
pakintada@gmail.com 2024-02-22 16:04:34 +07:00
parent 89ce1f361c
commit fed315367a
13 changed files with 317 additions and 270 deletions

View file

@ -34,7 +34,19 @@
alt="Tao Bin Logo"
/>
</a>
<!-- Redis Status -->
<div class="p-2 rounded-lg border border-double border-black" [ngStyle]="redisStatus == 'Online'?{'background-color':'greenyellow'}:{'background-color':'tomato'}">
<p class="text-center font-bold">{{redisStatus}}</p>
</div>
</div>
<!-- File Change Status -->
<button *ngIf="showDetectChanges">
<h1 class="text-center font-extrabold text-2xl text-red-500 animate-pulse">Detect Changes! Click</h1>
</button>
<div class="flex items-center">
<div class="flex items-center ml-3">
<div class="flex flex-row">
@ -155,3 +167,5 @@
</div>
</div>
</div>
<!--Modal-->

View file

@ -1,10 +1,15 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { DatePipe, NgFor, NgIf, NgOptimizedImage } from '@angular/common';
import { CommonModule, DatePipe, NgFor, NgIf, NgOptimizedImage } from '@angular/common';
import { GoogleButtonComponent } from 'src/app/shared/googleButton/googleButton.component';
import { UserService } from '../services/user.service';
import { User } from '../models/user.model';
import { Subject, Subscription, map, share, takeUntil, timer } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import { RecipeService } from '../services/recipe.service';
import { AsyncStorage } from 'src/app/shared/helpers/asyncStorage';
import { MergeComponent } from "../../features/merge/merge.component";
interface MenuItem {
name: string;
@ -23,7 +28,9 @@ interface MenuItem {
GoogleButtonComponent,
DatePipe,
NgOptimizedImage,
],
CommonModule,
MergeComponent
]
})
export class LayoutComponent implements OnInit, OnDestroy {
current_department = this._router.snapshot.paramMap.get('department')!;
@ -51,12 +58,17 @@ export class LayoutComponent implements OnInit, OnDestroy {
user: User | null = null;
exit$ = new Subject<void>();
redisStatus:string = "Offline";
showDetectChanges: boolean = false;
constructor(
private _userService: UserService,
private _router: ActivatedRoute
private _router: ActivatedRoute,
private _httpClient: HttpClient,
private _recipeService: RecipeService
) {}
ngOnInit(): void {
async ngOnInit(): Promise<void> {
this._userService.currentUser
.pipe(takeUntil(this.exit$))
.subscribe((user) => (this.user = user));
@ -69,6 +81,32 @@ export class LayoutComponent implements OnInit, OnDestroy {
.subscribe((time) => {
this.date = time;
});
this._httpClient.get(environment.api + "/health/redis").subscribe((status) => {
this.redisStatus = (status as any)["status"];
});
// check if saves existed
this._recipeService.getSavedTmp(
await this._recipeService.getCurrentCountry(),
this._recipeService.getCurrentFile()
).subscribe({
next: async (data: any) => {
if(data != undefined && typeof data === 'object'){
// check if attr exists
if(data.files != null){
this.showDetectChanges = true;
await AsyncStorage.setItem("detectChanges", "true");
} else {
this.showDetectChanges = false;
await AsyncStorage.setItem("detectChanges", "false");
}
} else {
this.showDetectChanges = false;
await AsyncStorage.setItem("detectChanges", "false");
}
}
});
}
ngOnDestroy() {

View file

@ -47,7 +47,10 @@ export class RecipeService {
return this.tmp_files;
}
constructor(private _httpClient: HttpClient, private _route: ActivatedRoute) {}
constructor(
private _httpClient: HttpClient,
private _route: ActivatedRoute
) {}
getRecipesDashboard(
params: any = {
@ -95,8 +98,9 @@ export class RecipeService {
);
}
async getRecipeDetail(productCode: string): Promise<Observable<RecipeDetail>> {
async getRecipeDetail(
productCode: string
): Promise<Observable<RecipeDetail>> {
let asyncCountry = await this.getCurrentCountry(this.department!);
console.log('get detail by asyncCountry', asyncCountry);
@ -115,8 +119,7 @@ export class RecipeService {
async getRecipeDetailMat(
productCode: string
): Promise<Observable<{ result: RecipeDetailMat[]; }>> {
): Promise<Observable<{ result: RecipeDetailMat[] }>> {
let asyncCountry = await this.getCurrentCountry(this.department!);
return this._httpClient.get<{ result: RecipeDetailMat[] }>(
@ -133,10 +136,8 @@ export class RecipeService {
}
getCurrentFile(): string {
// TODO: get default from server
const currentRecipeFile = localStorage.getItem('currentRecipeFile');
if (currentRecipeFile) {
return currentRecipeFile;
@ -150,9 +151,7 @@ export class RecipeService {
}
async getCurrentCountry(department?: string): Promise<string> {
if(department){
if (department) {
// translate back to full name
let fullname = getCountryMapSwitcher(department);
@ -167,7 +166,9 @@ export class RecipeService {
// const currentRecipeCountry = localStorage.getItem('currentRecipeCountry');
const currentRecipeCountry = await AsyncStorage.getItem<string>('currentRecipeCountry');
const currentRecipeCountry = await AsyncStorage.getItem<string>(
'currentRecipeCountry'
);
if (currentRecipeCountry) {
return currentRecipeCountry;
}
@ -275,13 +276,37 @@ export class RecipeService {
);
}
async getRawRecipeOfProductCode(country: string, filename: string, productCode: string): Promise<Observable<{}>> {
async getRawRecipeOfProductCode(
country: string,
filename: string,
productCode: string
): Promise<Observable<{}>> {
return this._httpClient.get<{}>(
environment.api + '/recipes/' + country + '/' + filename + '/' + productCode + '/raw_full',
environment.api +
'/recipes/' +
country +
'/' +
filename +
'/' +
productCode +
'/raw_full',
{
withCredentials: true,
responseType: 'json',
}
);
}
async getPatchListOfCurrentFile(
country: string,
filename: string
): Promise<Observable<any>> {
console.log("try get patches", country, filename);
return this._httpClient.get<any>(
environment.api + '/recipes/patch/get/' + country + '/' + filename ,
{ withCredentials: true, responseType: 'json' }
);
}
}

View file

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { MergeComponent } from '../merge/merge.component';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment.development';
import { CommonModule } from '@angular/common';
@ -10,7 +10,7 @@ import { FetchLogService } from 'src/app/shared/services/fetch-log.service';
standalone: true,
templateUrl: './changelog.component.html',
styleUrls: ['./changelog.component.css'],
imports: [CommonModule, MergeComponent],
imports: [CommonModule],
})
export class ChangelogComponent {
public displayableLogs: string[] = [];

View file

@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { MergeServiceService } from './merge-service.service';
describe('MergeServiceService', () => {
let service: MergeServiceService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(MergeServiceService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -1,14 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MergeServiceService {
master_version: number = 0;
dev_version: number = 0;
output_path:string = "";
changelog_path:string = "";
constructor() { }
}

View file

@ -1,55 +1 @@
<div class="flex-3 bg-stone-400 h-screen justify-centers items-center">
<h3 class="text-2xl text-center py-4 font-semibold">Merge 2 json</h3>
<div class="bg-stone-600 p-1 m-2 rounded-md">
<div class="bg-stone-500 m-2 rounded-md">
<p class="px-1 py-5 font-semibold">
❓ What does this function do?
</p>
<div class="bg-stone-300 rounded">
<p class="px-2 ">
Apply changes from the `dev` version
<br>into the `master` version
</p>
</div>
</div>
<div class="bg-stone-200 p-1 m-2 rounded-md">
<p class="px-2 py-4 text-md text-center font-bold">
❗Beware❗
<br>`master` = base version
<br>`dev` = your version
</p>
</div>
<form class="space-y-6 p-3 bg-stone-500 rounded" [formGroup]="mergeForm" (ngSubmit)="fetchMerge()">
<div class="flex">
<label class="flex-1 text-red-700 font-bold bg-yellow-100 rounded text-center" for="master_version">Master</label>
<input class="flex-1 mx-1 bg-slate-300 hover:bg-blue-400 text-center border border-collapse rounded-md" id="master_version" formControlName="master_version" type="text" required>
</div>
<div class="flex">
<label class="flex-1 bg-yellow-100 font-bold rounded text-center" for="dev_version">Dev</label>
<input class="flex-1 mx-1 bg-slate-300 hover:bg-blue-400 text-center border border-collapse rounded-md" id="dev_version" formControlName="dev_version" type="text" required>
</div>
<!-- Output path -->
<div class="flex">
<label class="flex-1 bg-yellow-100 font-bold rounded text-center" for="output_path">Output Path (.json)</label>
<input class="flex-1 mx-1 bg-slate-300 hover:bg-blue-400 text-center border border-collapse rounded-md" id="output_path" formControlName="output_path" type="text" required>
</div>
<!-- Changelog path -->
<div class="flex">
<label class="flex-1 bg-yellow-100 font-bold rounded text-center" for="changelog_path">Changelog Path (.json)</label>
<input class="flex-1 mx-1 bg-slate-300 hover:bg-blue-400 text-center border border-collapse rounded-md" id="changelog_path" formControlName="changelog_path" type="text" required>
</div>
<button class="button font-semibold bg-red-300 p-4 border border-collapse rounded-md" type="submit">Begin Merge</button>
</form>
</div>
</div>
<p>merge works!</p>

View file

@ -1,21 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MergeComponent } from './merge.component';
describe('MergeComponent', () => {
let component: MergeComponent;
let fixture: ComponentFixture<MergeComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [MergeComponent]
});
fixture = TestBed.createComponent(MergeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -1,90 +1,31 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { MergeServiceService } from './merge-service.service';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from 'src/environments/environment.development';
import { Observable } from 'rxjs';
import { ChangelogComponent } from '../changelog/changelog.component';
import { FetchLogService } from 'src/app/shared/services/fetch-log.service';
import { RecipeService } from 'src/app/core/services/recipe.service';
@Component({
selector: 'app-merge',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, ChangelogComponent],
imports: [CommonModule],
templateUrl: './merge.component.html',
styleUrls: ['./merge.component.css']
})
export class MergeComponent<T> {
export class MergeComponent implements OnInit {
exceptionValues = [0, null, undefined]
mergeForm = this.formBuilder.group({
master_version: 0,
dev_version: 0,
output_path: "",
changelog_path: ""
});
default_output_path = "cofffeemachineConfig/merge/"
default_changelog_path = "cofffeemachineConfig/changelog/"
mergeLogs: Map<string, string> | void | undefined
patchMap: any = {}
constructor(
private targets: MergeServiceService,
private formBuilder: FormBuilder,
private httpClient: HttpClient,
private chlog: ChangelogComponent
){
// Default fetching logs
private _recipeService: RecipeService
) { }
// fetch html
// this.fetchLogsToDisplay("", true, false);
// // fetch log file
// this.fetchLogsToDisplay("", false, false);
// // fetch json
// this.mergeLogs = this.fetchLogsToDisplay("", false, true);
async ngOnInit(): Promise<void> {
(await this._recipeService.getPatchListOfCurrentFile(
await this._recipeService.getCurrentCountry(),
this._recipeService.getCurrentFile()
)).subscribe({
next: (data: any) => {
this.patchMap = data;
console.log("patches",this.patchMap);
}
});
}
private isException(value: any){
return this.exceptionValues.includes(value)
}
fetchMerge(){
if(this.isException(this.mergeForm.value.master_version) || this.isException(this.mergeForm.value.dev_version)){
return
}
this.targets.master_version = this.mergeForm.value.master_version!;
this.targets.dev_version = this.mergeForm.value.dev_version!;
this.targets.output_path = this.default_output_path + this.mergeForm.value.output_path!;
this.targets.changelog_path = this.default_changelog_path + this.mergeForm.value.changelog_path!;
// TODO: Fetch merge. Modify this to websocket
this.httpClient.post<T>(environment.api+"/merge", {
master: this.targets.master_version,
dev: this.targets.dev_version,
output: this.targets.output_path,
changelog: this.targets.changelog_path
}, {
withCredentials: true
}).subscribe({
next: (value: T) => {
console.log(value)
if(typeof value === "object" && value !== null){
if("message" in value){
// fetch html
// this.fetchLogsToDisplay("", true, false);
// fetch log file
// this.fetchLogsToDisplay("", false, false);
// fetch json
this.mergeLogs = new FetchLogService(this.httpClient).fetchLogsToDisplay("", false, true,this.targets.changelog_path);
this.chlog.fetchLoglist();
this.chlog.translateLogDirToString();
}
}
},
})
}
}

View file

@ -89,7 +89,7 @@ export class RecipesComponent implements OnInit, OnDestroy, AfterViewInit {
savedTmpfiles: any[] = [];
saveTab: boolean = false;
showSaveNoti: boolean = true;
showSaveNoti: boolean = false;
department: string = this.route.parent!.snapshot.params['department'];
copyList: any[] = [];
@ -197,7 +197,7 @@ export class RecipesComponent implements OnInit, OnDestroy, AfterViewInit {
})
);
// FIXME: Lag assigned
// : Lag assigned
this.recipesDashboard$.subscribe(async (data) => {
this.currentVersion = data.configNumber;
@ -241,37 +241,37 @@ export class RecipesComponent implements OnInit, OnDestroy, AfterViewInit {
// end of FIXME
this._recipeService
.getSavedTmp(
await this._recipeService.getCurrentCountry(this.department),
this._recipeService.getCurrentFile()
)
.subscribe({
next: (files: any) => {
console.log('Obtain saves: ', typeof files, files);
this.showSaveNoti = false;
if (files != undefined && typeof files === 'object') {
if (files.files != null) {
console.log(
'Obtain saves object: ',
files.files[0],
typeof files
);
this.savedTmpfiles = files.files;
} else {
this.showSaveNoti = false;
this.savedTmpfiles = [];
console.log(this.showSaveNoti, this.savedTmpfiles);
}
// let svf = (document.getElementById('select_savefile_modal') as HTMLInputElement)!.checked;
// console.log("isSavedModalOpened",svf)
} else {
this.showSaveNoti = false;
this.savedTmpfiles = [];
console.log(this.showSaveNoti, this.savedTmpfiles);
}
},
});
// this._recipeService
// .getSavedTmp(
// await this._recipeService.getCurrentCountry(this.department),
// this._recipeService.getCurrentFile()
// )
// .subscribe({
// next: (files: any) => {
// console.log('Obtain saves: ', typeof files, files);
// this.showSaveNoti = false;
// if (files != undefined && typeof files === 'object') {
// if (files.files != null) {
// console.log(
// 'Obtain saves object: ',
// files.files[0],
// typeof files
// );
// this.savedTmpfiles = files.files;
// } else {
// this.showSaveNoti = false;
// this.savedTmpfiles = [];
// console.log(this.showSaveNoti, this.savedTmpfiles);
// }
// // let svf = (document.getElementById('select_savefile_modal') as HTMLInputElement)!.checked;
// // console.log("isSavedModalOpened",svf)
// } else {
// this.showSaveNoti = false;
// this.savedTmpfiles = [];
// console.log(this.showSaveNoti, this.savedTmpfiles);
// }
// },
// });
(await this._materialService.getMaterialCodes())
.pipe(

View file

@ -135,3 +135,39 @@ func (r *RedisCli) SetKeyTimeout(key string, value interface{}, timeout int) err
fmt.Println("error on EXPIRE ", err)
return err
}
func (r *RedisCli) KeyList() ([]string, error) {
// if cannot pass healthcheck, return err
if err := r.HealthCheck(); err != nil {
fmt.Println("HS> KEYS error ", err)
return nil, err
}
keys := r.Client.Keys(context.Background(), "*")
return keys.Result()
}
// list operations
func (r *RedisCli) GetList(key string) ([]string, error) {
// if cannot pass healthcheck, return err
if err := r.HealthCheck(); err != nil {
fmt.Println("HS> List.GET error ", err)
return nil, err
}
return r.Client.LRange(context.Background(), key, 0, -1).Result()
}
func (r *RedisCli) Add(key string, value interface{}) error {
// if cannot pass healthcheck, return err
if err := r.HealthCheck(); err != nil {
fmt.Println("HS> List.ADD error ", err)
return err
}
err := r.Client.RPush(context.Background(), key, value)
return err.Err()
}

View file

@ -101,6 +101,8 @@ func (rr *RecipeRouter) Route(r chi.Router) {
r.Get("/saved/{country}/{filename_version_only}", rr.getSavedRecipes)
r.Get("/patch/get/{country}/{filename}", rr.getSavedAsPatches)
r.Get("/departments", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
@ -447,6 +449,7 @@ func (rr *RecipeRouter) updateRecipe(w http.ResponseWriter, r *http.Request) {
tempRecipe := models.Recipe01{}
tempRecipe = tempRecipe.FromMap(changeMap)
rr.data.SetValuesToRecipe(targetRecipe.Recipe01, tempRecipe)
rr.taoLogger.Log.Debug("ApplyChange", zap.Any("status", "passed"))
// check if changed
@ -462,7 +465,7 @@ func (rr *RecipeRouter) updateRecipe(w http.ResponseWriter, r *http.Request) {
// gen hash
commit_hash, err := data.HashCommit(8)
rr.cache_db.SetToKey(commit_hash, targetRecipe)
// rr.cache_db.SetToKey(commit_hash, targetRecipe)
commit := data.CommitLog{
@ -474,7 +477,35 @@ func (rr *RecipeRouter) updateRecipe(w http.ResponseWriter, r *http.Request) {
Relation: filename,
}
err = data.Insert(&commit)
// ------------------------ SKIP THIS ------------------------
// TODO: save only changes.
// get new tempfile name if redis is connected;
productCodeNoSpl := strings.ReplaceAll(changes.ProductCode, "-", "")
patchName := "Recipe_" + productCodeNoSpl + "-" + commit_hash + "_" + filename + ".patch"
// if cache service online
if rr.cache_db.HealthCheck() == nil {
// do change mode
commit.Change_file = patchName
commit.Relation = commit.Relation + "/patch"
// add to patch list of that filename
// filename:patchlist
err := rr.cache_db.Add(countryID+"."+filename+":patchList", commit.Id)
// add failed
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.UpdateRecipe", zap.Error(errors.WithMessage(err, "Error when tried to add to patch list")))
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
} else {
// this following codes do need users to pass update to each other
// otherwise, the changes will diverge
file, _ := os.Create(temp_file_name)
if err != nil {
@ -483,11 +514,14 @@ func (rr *RecipeRouter) updateRecipe(w http.ResponseWriter, r *http.Request) {
return
}
// write to local if cannot connect
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
// full
err = encoder.Encode(targetRecipe)
// -------------------------------------------------------------
// partial
// err = encoder.Encode(changes)
@ -498,7 +532,13 @@ func (rr *RecipeRouter) updateRecipe(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
err = rr.cache_db.SetToKey(commit_hash+"_"+filename, changes)
}
// add to commit
err = data.Insert(&commit)
err = rr.cache_db.SetToKey(patchName, changes)
// TODO: ^----- change this to patch
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.UpdateRecipeCache", zap.Error(errors.WithMessage(err, "Error when write file")))
@ -567,6 +607,64 @@ func (rr *RecipeRouter) getSavedRecipes(w http.ResponseWriter, r *http.Request)
json.NewEncoder(w).Encode(map[string]interface{}{"files": commits})
}
func (rr *RecipeRouter) getSavedAsPatches(w http.ResponseWriter, r *http.Request) {
filename := chi.URLParam(r, "filename")
country := chi.URLParam(r, "country")
countryID, err := rr.data.GetCountryIDByName(country)
if err != nil {
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
return
}
patchList, err := rr.cache_db.GetList(countryID + "." + filename + ":patchList")
if err != nil {
// silent return, no patch
return
}
rr.taoLogger.Log.Debug("RecipeRouter.getSavedAsPatches", zap.Any("targetPatchOf", countryID+"."+filename+":patchList"), zap.Any("patchList", patchList))
// find patch content from patch list
keys, err := rr.cache_db.KeyList()
if err != nil {
// keys found nothing
http.Error(w, "Internal Error", http.StatusInternalServerError)
return
}
// loop through keys, if contain patch id
patchMap := map[string]models.Recipe01{}
for _, key := range keys {
if strings.Contains(key, filename) && strings.Contains(key, "patch") {
// check if legit saved file from patchList
for _, patchID := range patchList {
if strings.Contains(key, patchID) {
// get patch content
var recipePatch models.Recipe01
err := rr.cache_db.GetKeyTo(key, &recipePatch)
if err != nil {
// silent return, no patch
return
}
// append to patch list
// patchContents = append(patchContents, recipePatch)
patchMap[patchID] = recipePatch
}
}
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(patchMap)
rr.taoLogger.Log.Debug("RecipeRouter.getSavedAsPatches", zap.Any("patchMap", patchMap))
}
func (rr *RecipeRouter) getToppings(w http.ResponseWriter, r *http.Request) {
countryID := chi.URLParam(r, "country")
@ -626,7 +724,7 @@ func (rr *RecipeRouter) getRawRecipeOfProductCode(w http.ResponseWriter, r *http
productCode := chi.URLParam(r, "product_code")
// debug
rr.taoLogger.Log.Debug("RecipeRouter.getRawRecipeOfProductCode", zap.Any("countryID", countryID), zap.Any("filename", filename), zap.Any("productCode", productCode))
// rr.taoLogger.Log.Debug("RecipeRouter.getRawRecipeOfProductCode", zap.Any("countryID", countryID), zap.Any("filename", filename), zap.Any("productCode", productCode))
w.Header().Add("Content-Type", "application/json")
@ -638,7 +736,7 @@ func (rr *RecipeRouter) getRawRecipeOfProductCode(w http.ResponseWriter, r *http
}
// return recipe
rr.taoLogger.Log.Debug("RecipeRouter.getRawRecipeOfProductCode", zap.Any("recipe", recipe))
// rr.taoLogger.Log.Debug("RecipeRouter.getRawRecipeOfProductCode", zap.Any("recipe", recipe))
json.NewEncoder(w).Encode(recipe)
}