mirror of
https://github.com/optim-enterprises-bv/siembol.git
synced 2025-11-02 11:28:15 +00:00
Config-editor-ui: search in url + save previous searches (#578)
This commit is contained in:
2
config-editor/config-editor-ui/package-lock.json
generated
2
config-editor/config-editor-ui/package-lock.json
generated
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rule-editor.ui",
|
||||
"version": "2.4.3-dev",
|
||||
"version": "2.4.4-dev",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
@@ -23,17 +23,17 @@
|
||||
"@ag-grid-community/angular": "^27.1.1",
|
||||
"@ag-grid-community/client-side-row-model": "^27.1.0",
|
||||
"@ag-grid-community/core": "^27.1.0",
|
||||
"@angular-devkit/build-angular": "^13.3.0",
|
||||
"@angular/animations": "^13.3.0",
|
||||
"@angular/cdk": "^13.3.0",
|
||||
"@angular/common": "^13.3.0",
|
||||
"@angular/compiler": "^13.3.0",
|
||||
"@angular/core": "^13.3.0",
|
||||
"@angular/forms": "^13.3.0",
|
||||
"@angular/material": "^13.3.0",
|
||||
"@angular/platform-browser": "^13.3.0",
|
||||
"@angular/platform-browser-dynamic": "^13.3.0",
|
||||
"@angular/router": "^13.3.0",
|
||||
"@angular-devkit/build-angular": "^13.3.1",
|
||||
"@angular/animations": "^13.3.1",
|
||||
"@angular/cdk": "^13.3.2",
|
||||
"@angular/common": "^13.3.1",
|
||||
"@angular/compiler": "^13.3.1",
|
||||
"@angular/core": "^13.3.1",
|
||||
"@angular/forms": "^13.3.1",
|
||||
"@angular/material": "^13.3.2",
|
||||
"@angular/platform-browser": "^13.3.1",
|
||||
"@angular/platform-browser-dynamic": "^13.3.1",
|
||||
"@angular/router": "^13.3.1",
|
||||
"@ngx-formly/core": "^6.0.0-next.9",
|
||||
"@ngx-formly/material": "^6.0.0-next.9",
|
||||
"@types/json-schema": "^7.0.10",
|
||||
@@ -62,16 +62,16 @@
|
||||
"@angular-eslint/eslint-plugin": "^13.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "^13.1.0",
|
||||
"@angular-eslint/template-parser": "^13.1.0",
|
||||
"@angular/cli": "^13.3.0",
|
||||
"@angular/compiler-cli": "^13.3.0",
|
||||
"@angular/language-service": "^13.3.0",
|
||||
"@types/jasmine": "^4.0.0",
|
||||
"@angular/cli": "^13.3.1",
|
||||
"@angular/compiler-cli": "^13.3.1",
|
||||
"@angular/language-service": "^13.3.1",
|
||||
"@types/jasmine": "^4.0.1",
|
||||
"@types/node": "^17.0.22",
|
||||
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
||||
"@typescript-eslint/parser": "^5.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.17.0",
|
||||
"@typescript-eslint/parser": "^5.17.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsdoc": "^38.0.6",
|
||||
"eslint-plugin-jsdoc": "^38.1.4",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
||||
@@ -82,10 +82,10 @@
|
||||
"karma-chrome-launcher": "^3.1.1",
|
||||
"karma-cli": "^2.0.0",
|
||||
"karma-coverage-istanbul-reporter": "^3.0.3",
|
||||
"karma-jasmine": "^4.0.1",
|
||||
"karma-jasmine": "^4.0.2",
|
||||
"karma-jasmine-html-reporter": "^1.6.0",
|
||||
"karma-nunit2-reporter": "^0.3.0",
|
||||
"prettier": "^2.6.0",
|
||||
"prettier": "^2.6.1",
|
||||
"prettier-eslint": "^13.0.0",
|
||||
"protractor": "^7.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
|
||||
@@ -83,6 +83,7 @@ import { ModuleRegistry } from '@ag-grid-community/core';
|
||||
import { ConfigNameCellRendererComponent } from './components/config-manager/cell-renderers/config-name-cell-renderer.component';
|
||||
import { StoreHeaderGroupComponent } from './components/config-manager/header-groups/store-header-group.component';
|
||||
import { CheckboxFiltersComponent } from './components/checkbox-filters/checkbox-filters.component';
|
||||
import { SearchHistoryComponent } from './components/search-history/search-history.component';
|
||||
|
||||
ModuleRegistry.registerModules([
|
||||
ClientSideRowModelModule,
|
||||
@@ -110,6 +111,7 @@ const DEV_PROVIDERS = [...PROD_PROVIDERS];
|
||||
bootstrap: [AppComponent],
|
||||
declarations: [
|
||||
CheckboxFiltersComponent,
|
||||
SearchHistoryComponent,
|
||||
StoreHeaderGroupComponent,
|
||||
ReleaseHeaderGroupComponent,
|
||||
LabelCellRendererComponent,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { UrlInfo } from '@app/model/config-model';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export function copyTextToClipboard(text: string): boolean {
|
||||
@@ -21,22 +20,10 @@ export function copyTextToClipboard(text: string): boolean {
|
||||
return success;
|
||||
}
|
||||
|
||||
export function parseUrl(path: string): UrlInfo {
|
||||
const url = new URL(path, location.origin);
|
||||
const paths = url.pathname.substring(1).split('/');
|
||||
|
||||
const service = paths[0];
|
||||
const mode = paths[1] === 'admin' ? 'admin' : '';
|
||||
const configName = url.searchParams.get('configName');
|
||||
const testCaseName = url.searchParams.get('testCaseName');
|
||||
|
||||
return { service, mode, configName, testCaseName };
|
||||
}
|
||||
|
||||
export function replacer(key, value) {
|
||||
return value === null ? undefined : value;
|
||||
}
|
||||
|
||||
export function areJsonEqual(config1: any, config2: any) {
|
||||
return isEqual(config1, config2);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { UserRole } from '@app/model/config-model';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { HomeViewComponent } from '../home-view/home-view.component';
|
||||
import { ManagementViewComponent } from '../management-view/management-view.component';
|
||||
import { SearchGuard } from '@app/guards/search.guard';
|
||||
|
||||
@Component({
|
||||
template: '',
|
||||
@@ -24,10 +25,16 @@ export class AppInitComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly configRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ConfigManagerComponent,
|
||||
path: '',
|
||||
canActivate: [AuthGuard, EditorServiceGuard],
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [{
|
||||
path: '',
|
||||
canActivate: [SearchGuard],
|
||||
component: ConfigManagerComponent,
|
||||
runGuardsAndResolvers: 'paramsOrQueryParamsChange',
|
||||
}],
|
||||
},
|
||||
{
|
||||
component: EditorViewComponent,
|
||||
@@ -37,7 +44,7 @@ export class AppInitComponent implements OnInit, OnDestroy {
|
||||
path: '',
|
||||
component: EditorViewComponent,
|
||||
canActivate: [ConfigEditGuard],
|
||||
runGuardsAndResolvers: 'paramsOrQueryParamsChange'
|
||||
runGuardsAndResolvers: 'paramsOrQueryParamsChange',
|
||||
}],
|
||||
runGuardsAndResolvers: 'paramsOrQueryParamsChange',
|
||||
}]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { CheckboxEvent, FILTER_DELIMITER, ServiceFilters } from "@app/model/config-model";
|
||||
import { CheckboxEvent, FILTER_DELIMITER } from "@app/model/config-model";
|
||||
import { FilterConfig } from "@app/model/ui-metadata-map";
|
||||
|
||||
@Component({
|
||||
@@ -10,7 +10,7 @@ import { FilterConfig } from "@app/model/ui-metadata-map";
|
||||
<span mat-subheader>{{ checkboxFilter.key.replace('_', ' ') | titlecase }}</span>
|
||||
<mat-checkbox
|
||||
*ngFor="let singleCheckbox of checkboxFilter.value | keyvalue"
|
||||
[(ngModel)]="selectedCheckboxes[this.getName(checkboxFilter.key, singleCheckbox.key)]"
|
||||
[ngModel]="selectedCheckboxes.includes(this.getName(checkboxFilter.key, singleCheckbox.key))"
|
||||
(change)="clickCheckbox($event.checked, checkboxFilter.key, singleCheckbox.key)"
|
||||
>
|
||||
{{ singleCheckbox.key.replace('_', ' ') | titlecase }}
|
||||
@@ -47,11 +47,11 @@ import { FilterConfig } from "@app/model/ui-metadata-map";
|
||||
})
|
||||
export class CheckboxFiltersComponent {
|
||||
@Input() checkboxFilters: FilterConfig;
|
||||
@Input() selectedCheckboxes: ServiceFilters;
|
||||
@Input() selectedCheckboxes: string[];
|
||||
@Output() readonly selectedCheckbox: EventEmitter<CheckboxEvent> = new EventEmitter<CheckboxEvent>();
|
||||
|
||||
clickCheckbox(event: boolean, group_name: string, name: string) {
|
||||
this.selectedCheckbox.emit({ checked: event, name: this.getName(group_name, name)});
|
||||
clickCheckbox(event: boolean, groupName: string, checkboxName: string) {
|
||||
this.selectedCheckbox.emit({ checked: event, name: this.getName(groupName, checkboxName)});
|
||||
}
|
||||
|
||||
getName(group_name: string, name: string): string {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<re-search
|
||||
[searchTerm]="searchTerm$ | async"
|
||||
(searchTermChange)="onSearch($event)"
|
||||
(saveSearch)="onSaveSearch()"
|
||||
>
|
||||
</re-search>
|
||||
<div class="add-button">
|
||||
@@ -76,6 +77,7 @@
|
||||
</span>
|
||||
</ng-template>
|
||||
</div>
|
||||
<re-search-history [searchHistory]="searchHistory"></re-search-history>
|
||||
<ag-grid-angular
|
||||
#agGrid
|
||||
id="myGrid"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
|
||||
#myGrid {
|
||||
margin-top: 15px;
|
||||
margin-top: 5px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ import { ReleaseDialogComponent } from '../release-dialog/release-dialog.compone
|
||||
import { JsonViewerComponent } from '../json-viewer/json-viewer.component';
|
||||
import { FileHistory } from '../../model';
|
||||
import { ConfigStoreService } from '../../services/store/config-store.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { BlockUI, NgBlockUI } from 'ng-block-ui';
|
||||
import { AppConfigService } from '@app/services/app-config.service';
|
||||
import { CheckboxEvent, ConfigManagerRow, Importers, ServiceFilters, Type } from '@app/model/config-model';
|
||||
import { CheckboxEvent, ConfigManagerRow, FILTER_PARAM_KEY, Importers, SEARCH_PARAM_KEY, ServiceSearchHistory, Type } from '@app/model/config-model';
|
||||
import { ImporterDialogComponent } from '../importer-dialog/importer-dialog.component';
|
||||
import { CloneDialogComponent } from '../clone-dialog/clone-dialog.component';
|
||||
import { configManagerColumns } from './columns';
|
||||
@@ -41,7 +41,7 @@ export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
isAnyFilterPresent$: Observable<boolean>;
|
||||
isAnyFilterPresent: boolean;
|
||||
serviceFilterConfig: FilterConfig;
|
||||
serviceFilters$: Observable<ServiceFilters>;
|
||||
serviceFilters$: Observable<string[]>;
|
||||
releaseHistory;
|
||||
disableEditingFeatures: boolean;
|
||||
importers$: Observable<Importers>;
|
||||
@@ -76,7 +76,9 @@ export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
api: GridApi;
|
||||
countChangesInRelease$ : Observable<number>;
|
||||
searchHistory: ServiceSearchHistory[];
|
||||
private rowMoveStartIndex: number;
|
||||
private currentParams: ParamMap;
|
||||
|
||||
private ngUnsubscribe = new Subject<void>();
|
||||
private configStore: ConfigStoreService;
|
||||
@@ -87,7 +89,8 @@ export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
private snackbar: PopupService,
|
||||
private editorService: EditorService,
|
||||
private router: Router,
|
||||
private configService: AppConfigService
|
||||
private configService: AppConfigService,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
this.context = { componentParent: this };
|
||||
|
||||
@@ -109,6 +112,7 @@ export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
this.isAnyFilterPresent$ = this.configStore.isAnyFilterPresent$;
|
||||
this.countChangesInRelease$ = this.configStore.countChangesInRelease$;
|
||||
this.serviceFilters$ = this.configStore.serviceFilters$;
|
||||
this.searchHistory = this.editorService.searchHistoryService.getSearchHistory();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -131,6 +135,9 @@ export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
this.configStore.serviceFilterConfig$.pipe(first()).subscribe(s => {
|
||||
this.serviceFilterConfig = cloneDeep(s);
|
||||
});
|
||||
this.route.queryParamMap.subscribe(params => {
|
||||
this.currentParams = params;
|
||||
})
|
||||
}
|
||||
|
||||
onGridReady(params: any) {
|
||||
@@ -149,7 +156,10 @@ export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onSearch(searchTerm: string) {
|
||||
this.configStore.updateSearchTerm(searchTerm);
|
||||
this.router.navigate([this.editorService.serviceName], {
|
||||
queryParams: { [SEARCH_PARAM_KEY]: searchTerm !== ''? searchTerm: undefined },
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
|
||||
upgrade(name: string) {
|
||||
@@ -230,7 +240,10 @@ export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onClickCheckbox(event: CheckboxEvent) {
|
||||
this.configStore.updateServiceFilters(event);
|
||||
this.router.navigate([this.editorService.serviceName], {
|
||||
queryParams: { [FILTER_PARAM_KEY]: this.editorService.getLatestFilters(event, this.currentParams) },
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
|
||||
onSyncWithGit() {
|
||||
@@ -273,4 +286,8 @@ export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
getRowId: GetRowIdFunc = function (params) {
|
||||
return params.data.config_name;
|
||||
};
|
||||
|
||||
onSaveSearch() {
|
||||
this.searchHistory = this.editorService.onSaveSearch(this.currentParams);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<ng-container
|
||||
class="expansion-panel-container"
|
||||
*ngFor="let url of history.slice().reverse();">
|
||||
<mat-expansion-panel [expanded]="false" (click)="routeTo(url)" [hideToggle]="true">
|
||||
<mat-expansion-panel-header [matTooltip]="root + url" #panelH *ngIf="parseUrl(url); let tag" (click)="panelH._toggle()">
|
||||
<div *ngFor="let key of ['service', 'mode', 'configName', 'testCaseName']">
|
||||
<div class="tag-chip" *ngIf="tag[key] != undefined && tag[key] != null && tag[key] != ''">
|
||||
<mat-expansion-panel [expanded]="false" (click)="routeTo(url.rawUrl)" [hideToggle]="true">
|
||||
<mat-expansion-panel-header [matTooltip]="root + url.rawUrl" #panelH (click)="panelH._toggle()">
|
||||
<div *ngFor="let label of url.labels | keyvalue">
|
||||
<div class="tag-chip" *ngIf="label.value">
|
||||
<div class="tag-text">
|
||||
{{key | titlecase}}: {{tag[key]}}
|
||||
{{label.key | titlecase}}: {{label.value}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,8 @@ import { UrlHistoryService } from '@app/services/url-history.service';
|
||||
import { AppConfigService } from '@app/services/app-config.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { HelpLink } from '@app/model/app-config';
|
||||
import { ServiceInfo } from '@app/model/config-model';
|
||||
import { HistoryUrl, ServiceInfo } from '@app/model/config-model';
|
||||
import { AppService } from '@app/services/app.service';
|
||||
import { parseUrl } from '@app/commons/helper-functions'
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@@ -14,7 +13,7 @@ import { parseUrl } from '@app/commons/helper-functions'
|
||||
templateUrl: './home-view.component.html',
|
||||
})
|
||||
export class HomeViewComponent implements OnInit {
|
||||
history: string[];
|
||||
history: HistoryUrl[];
|
||||
root: string;
|
||||
homeHelpLinks: HelpLink[];
|
||||
userServices: ServiceInfo[];
|
||||
@@ -24,15 +23,11 @@ export class HomeViewComponent implements OnInit {
|
||||
private appService: AppService,
|
||||
private router: Router) { }
|
||||
|
||||
get parseUrl() {
|
||||
return parseUrl;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.userServices = this.appService.userServices;
|
||||
this.root = this.appConfigService.serviceRoot.slice(0, -1);
|
||||
this.homeHelpLinks = this.appConfigService.homeHelpLinks;
|
||||
this.history = this.historyService.getHistoryPreviousUrls();
|
||||
this.history = this.historyService.getPreviousUrls();
|
||||
}
|
||||
|
||||
routeTo(url: string) {
|
||||
@@ -42,6 +37,4 @@ export class HomeViewComponent implements OnInit {
|
||||
openLink(link: string) {
|
||||
window.open(link, "_blank");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { ServiceSearchHistory } from "@app/model/config-model";
|
||||
|
||||
@Component({
|
||||
selector: "re-search-history",
|
||||
template: `
|
||||
<mat-expansion-panel *ngIf="searchHistory">
|
||||
<mat-expansion-panel-header class="title-header">Saved Searches</mat-expansion-panel-header>
|
||||
<mat-accordion multi="true">
|
||||
<ng-container
|
||||
class="expansion-panel-container"
|
||||
*ngFor="let search of searchHistory.slice().reverse()">
|
||||
<mat-expansion-panel [expanded]="false" (click)="routeTo(search)" [hideToggle]="true">
|
||||
<mat-expansion-panel-header #panelH (click)="panelH._toggle()">
|
||||
<div *ngFor="let param of search | keyvalue">
|
||||
<div *ngIf="param.value" class="tag-chip">
|
||||
<div class="tag-text">
|
||||
{{param.key | titlecase}}: {{param.value}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel-header>
|
||||
</mat-expansion-panel>
|
||||
</ng-container>
|
||||
<p *ngIf="!searchHistory">
|
||||
No history to show
|
||||
</p>
|
||||
</mat-accordion>
|
||||
</mat-expansion-panel>
|
||||
`,
|
||||
styles: [`
|
||||
.tag-chip {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
padding: 2px 5px;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
border-radius: 9999px;
|
||||
color: #eee;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
font-family: monospace;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
padding-top: 2px;
|
||||
font-size: 10pt;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.title-header {
|
||||
font-weight: 600;
|
||||
}
|
||||
.mat-expansion-panel {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class SearchHistoryComponent {
|
||||
@Input() searchHistory: ServiceSearchHistory[];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
routeTo(params: any) {
|
||||
this.router.navigate(
|
||||
[],
|
||||
{
|
||||
relativeTo: this.route,
|
||||
queryParams: params,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,8 @@
|
||||
<button mat-button *ngIf="searchTerm" matSuffix mat-icon-button aria-label="Clear" (click)="onClearSearch()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<button mat-button matSuffix mat-icon-button title="Save Search" (click)="onSaveSearch()">
|
||||
<mat-icon>save</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@@ -13,8 +13,8 @@ export class SearchComponent implements OnInit {
|
||||
@ViewChild('searchBox', { static: true }) searchBox;
|
||||
@Input() searchTerm: string;
|
||||
@Output() readonly searchTermChange: EventEmitter<string> = new EventEmitter<string>();
|
||||
debouncer: Subject<string> = new Subject<string>();
|
||||
|
||||
@Output() readonly saveSearch: EventEmitter<void> = new EventEmitter<void>();
|
||||
debouncer: Subject<string> = new Subject<string>();
|
||||
myConfigs = true;
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -34,4 +34,8 @@ export class SearchComponent implements OnInit {
|
||||
this.onSearch('');
|
||||
this.searchTerm = '';
|
||||
}
|
||||
|
||||
onSaveSearch() {
|
||||
this.saveSearch.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
|
||||
|
||||
import { EditorService } from '../services/editor.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SearchGuard implements CanActivate {
|
||||
constructor(private editorService: EditorService) {}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean {
|
||||
this.editorService.configStore.updateSearchTermAndFilters(route.queryParamMap);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export interface AppConfig {
|
||||
homeHelpLinks?: HelpLink[];
|
||||
managementLinks?: HelpLink[];
|
||||
historyMaxSize?: number;
|
||||
searchMaxSize?: number;
|
||||
blockingTimeout?: number;
|
||||
useImporters?: boolean;
|
||||
}
|
||||
|
||||
@@ -263,11 +263,21 @@ export enum ConfigStatus {
|
||||
|
||||
export interface CheckboxEvent {
|
||||
checked: boolean,
|
||||
name: string,
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ServiceFilters {
|
||||
[type: string]: boolean;
|
||||
export const FILTER_DELIMITER = '|';
|
||||
|
||||
export interface ServiceSearchHistory {
|
||||
[type: string]: string[] | string;
|
||||
}
|
||||
|
||||
export const FILTER_DELIMITER = '|';
|
||||
export interface HistoryUrl {
|
||||
rawUrl: string,
|
||||
labels: UrlInfo
|
||||
}
|
||||
|
||||
export const FILTER_PARAM_KEY = "filter"
|
||||
export const SEARCH_PARAM_KEY = "search"
|
||||
|
||||
export const HISTORY_PARAMS = ['configName', 'testCaseName'];
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Config, Release, FileHistory } from '.';
|
||||
import { TestCaseMap, TestCaseWrapper } from './test-case';
|
||||
import { AdminConfig, ConfigManagerRow, ServiceFilters as ServiceFilters } from './config-model';
|
||||
import { AdminConfig, ConfigManagerRow } from './config-model';
|
||||
import { FilterConfig } from './ui-metadata-map';
|
||||
|
||||
export interface ConfigStoreState {
|
||||
@@ -18,7 +18,7 @@ export interface ConfigStoreState {
|
||||
pastedConfig: any;
|
||||
countChangesInRelease: number;
|
||||
configManagerRowData: ConfigManagerRow[];
|
||||
serviceFilters: ServiceFilters;
|
||||
serviceFilters: string[];
|
||||
isAnyFilterPresent: boolean;
|
||||
user: string;
|
||||
serviceFilterConfig: FilterConfig;
|
||||
|
||||
@@ -107,6 +107,10 @@ export class AppConfigService {
|
||||
return this._config.historyMaxSize ? this._config.historyMaxSize : 5;
|
||||
}
|
||||
|
||||
get searchMaxSize(): number {
|
||||
return this._config.searchMaxSize ? this._config.searchMaxSize : 5;
|
||||
}
|
||||
|
||||
get blockingTimeout(): number {
|
||||
return this._config.blockingTimeout ? this._config.blockingTimeout : 30000;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ import { AppService } from './app.service';
|
||||
import { mergeMap, map } from 'rxjs/operators';
|
||||
import { ConfigSchemaService } from './schema/config-schema-service';
|
||||
import { AdminSchemaService } from './schema/admin-schema.service';
|
||||
import { CheckboxEvent, FILTER_PARAM_KEY, ServiceSearchHistory } from '@app/model/config-model';
|
||||
import { SearchHistoryService } from './search-history.service';
|
||||
import { ParamMap } from '@angular/router';
|
||||
|
||||
export class ServiceContext {
|
||||
metaDataMap: UiMetadata;
|
||||
@@ -20,6 +23,7 @@ export class ServiceContext {
|
||||
serviceName: string;
|
||||
testSpecificationSchema?: JSONSchema7;
|
||||
adminMode: boolean;
|
||||
searchHistoryService?: SearchHistoryService;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
@@ -56,7 +60,15 @@ export class EditorService {
|
||||
return this.serviceContext.testSpecificationSchema;
|
||||
}
|
||||
|
||||
constructor(private http: HttpClient, private config: AppConfigService, private appService: AppService) {}
|
||||
get searchHistoryService() {
|
||||
return this.serviceContext.searchHistoryService;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private config: AppConfigService,
|
||||
private appService: AppService
|
||||
) {}
|
||||
|
||||
setServiceContext(serviceContext: ServiceContext): boolean {
|
||||
this.serviceContext = serviceContext;
|
||||
@@ -96,6 +108,7 @@ export class EditorService {
|
||||
metaDataMap,
|
||||
serviceName,
|
||||
testSpecificationSchema: testSpecSchema,
|
||||
searchHistoryService: new SearchHistoryService(this.config, serviceName),
|
||||
};
|
||||
}
|
||||
throwError(() => 'Can not load service');
|
||||
@@ -125,6 +138,23 @@ export class EditorService {
|
||||
}));
|
||||
}
|
||||
|
||||
getLatestFilters(event: CheckboxEvent, currentParams: ParamMap): any {
|
||||
const filters = [];
|
||||
currentParams.getAll(FILTER_PARAM_KEY).forEach(filter => {
|
||||
if (filter !== event.name || event.checked === true) {
|
||||
filters.push(filter);
|
||||
}
|
||||
});
|
||||
if (!filters[event.name] && event.checked === true) {
|
||||
filters.push(event.name);
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
onSaveSearch(currentParams: ParamMap): ServiceSearchHistory[] {
|
||||
return this.serviceContext.searchHistoryService.addToSearchHistory(currentParams);
|
||||
}
|
||||
|
||||
private initialiseContext(
|
||||
serviceName: string
|
||||
): [UiMetadata, string, ConfigLoaderService, ConfigStoreService] {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { SearchHistoryService } from './search-history.service';
|
||||
import { AppConfigService } from '@app/services/app-config.service';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { convertToParamMap, ParamMap } from '@angular/router';
|
||||
|
||||
export class MockAuth {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
isCallbackSearch(s: string) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
describe('SearchHistoryService', () => {
|
||||
let service: SearchHistoryService;
|
||||
beforeEach(() => {
|
||||
const store = {};
|
||||
const mockLocalStorage = {
|
||||
getItem: (key: string): string => (key in store ? store[key] : undefined),
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = `${value}`;
|
||||
},
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
SearchHistoryService,
|
||||
{
|
||||
provide: AppConfigService,
|
||||
useValue: jasmine.createSpyObj(
|
||||
'AppConfigService',
|
||||
{
|
||||
environment: 'test',
|
||||
searchMaxSize: 5,
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
spyOn(localStorage, 'getItem').and.callFake(mockLocalStorage.getItem);
|
||||
spyOn(localStorage, 'setItem').and.callFake(mockLocalStorage.setItem);
|
||||
const appService = TestBed.inject(AppConfigService);
|
||||
service = new SearchHistoryService(appService, "myalerts");
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have one search', () => {
|
||||
service.addToSearchHistory(convertToParamMap({ filter: ["group1|param1", "group1|param2"], search: "test", hi: "hi"}));
|
||||
expect(service.getSearchHistory()).toContain({ group1: [ 'param1', 'param2' ], search: "test" });
|
||||
expect(service.getSearchHistory()).toHaveSize(1);
|
||||
});
|
||||
|
||||
it('should ignore duplicate params', () => {
|
||||
service.addToSearchHistory(convertToParamMap({ filter: ["group1|param1"]}));
|
||||
service.addToSearchHistory(convertToParamMap({ filter: ["group1|param1"]}));
|
||||
service.addToSearchHistory(convertToParamMap({ filter: "group1|param1"}));
|
||||
expect(service.getSearchHistory()).toContain({ group1: ['param1'] });
|
||||
expect(service.getSearchHistory()).toHaveSize(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ParamMap, Params } from '@angular/router';
|
||||
import { FILTER_DELIMITER, FILTER_PARAM_KEY, SEARCH_PARAM_KEY, ServiceSearchHistory } from '@app/model/config-model';
|
||||
import { AppConfigService } from '@app/services/app-config.service';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SearchHistoryService {
|
||||
private readonly maxSize: number;
|
||||
private readonly SEARCH_HISTORY_KEY: string;
|
||||
|
||||
constructor(private appService: AppConfigService, serviceName: string) {
|
||||
this.SEARCH_HISTORY_KEY = 'siembol_search_history-' + serviceName + '-' + this.appService.environment;
|
||||
this.maxSize = this.appService.searchMaxSize;
|
||||
}
|
||||
|
||||
addToSearchHistory(search: ParamMap): ServiceSearchHistory[] {
|
||||
let history = this.getSearchHistory();
|
||||
const parsedParams = this.parseParams(search);
|
||||
if (Object.keys(parsedParams).length > 0) {
|
||||
history.push(parsedParams);
|
||||
history = this.crop(this.removeOldestDuplicates(history));
|
||||
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(history));
|
||||
}
|
||||
return history;
|
||||
}
|
||||
|
||||
getSearchHistory(): ServiceSearchHistory[] {
|
||||
const history = localStorage.getItem(this.SEARCH_HISTORY_KEY);
|
||||
return history ? JSON.parse(history) : [];
|
||||
}
|
||||
|
||||
private parseParams(params: ParamMap): Params {
|
||||
const result = {};
|
||||
for (const param of params.getAll(FILTER_PARAM_KEY)) {
|
||||
const [groupName, filterName] = param.split(FILTER_DELIMITER, 2);
|
||||
if (!result[groupName]) {
|
||||
result[groupName] = [];
|
||||
}
|
||||
result[groupName].push(filterName);
|
||||
}
|
||||
const search = params.get(SEARCH_PARAM_KEY);
|
||||
if (search && search !== '') {
|
||||
result[SEARCH_PARAM_KEY] = search;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private removeOldestDuplicates(history: ServiceSearchHistory[]): ServiceSearchHistory[] {
|
||||
return history.filter((value, index) =>
|
||||
index === history.map(obj => this.areParamsEqual(obj, value)).lastIndexOf(true)
|
||||
);
|
||||
}
|
||||
|
||||
private areParamsEqual(obj1, obj2): boolean {
|
||||
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
|
||||
return false;
|
||||
}
|
||||
return Object.keys(obj1).every(key => {
|
||||
let value1 = obj1[key];
|
||||
let value2 = obj2[key];
|
||||
if (!Array.isArray(value1)) {
|
||||
value1 = [value1];
|
||||
}
|
||||
if (!Array.isArray(value2)) {
|
||||
value2 = [value2];
|
||||
}
|
||||
return isEqual(value1, value2);
|
||||
})
|
||||
}
|
||||
|
||||
private crop(history: ServiceSearchHistory[]): ServiceSearchHistory[] {
|
||||
while (history.length > this.maxSize) {
|
||||
history.shift();
|
||||
}
|
||||
return history;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { cloneDeep } from 'lodash';
|
||||
import { Config, Release } from '@app/model';
|
||||
import { mockUiMetadataAlert } from 'testing/uiMetadataMap';
|
||||
import { ConfigStatus } from '@app/model/config-model';
|
||||
import { convertToParamMap } from '@angular/router';
|
||||
|
||||
|
||||
const mockConfigsUnsorted = [
|
||||
@@ -288,7 +289,7 @@ describe('ConfigStoreStateBuilder', () => {
|
||||
|
||||
const state = builder
|
||||
.serviceFilterConfig(mockUiMetadataAlert)
|
||||
.updateServiceFilters({checked: true, name: "general|upgradable"})
|
||||
.updateServiceFilters(["general|upgradable"])
|
||||
.computeConfigManagerRowData()
|
||||
.build();
|
||||
|
||||
@@ -322,7 +323,7 @@ describe('ConfigStoreStateBuilder', () => {
|
||||
|
||||
const state = builder
|
||||
.serviceFilterConfig(mockUiMetadataAlert)
|
||||
.updateServiceFilters({checked: true, name: "general|unreleased"})
|
||||
.updateServiceFilters(["general|unreleased"])
|
||||
.computeConfigManagerRowData()
|
||||
.build();
|
||||
|
||||
@@ -356,7 +357,7 @@ describe('ConfigStoreStateBuilder', () => {
|
||||
|
||||
const state = builder
|
||||
.serviceFilterConfig(mockUiMetadataAlert)
|
||||
.updateServiceFilters({checked: true, name: "general|my_edits"})
|
||||
.updateServiceFilters(["general|my_edits"])
|
||||
.computeConfigManagerRowData()
|
||||
.build();
|
||||
|
||||
@@ -392,10 +393,11 @@ describe('ConfigStoreStateBuilder', () => {
|
||||
|
||||
const state = builder
|
||||
.serviceFilterConfig(mockUiMetadataAlertWithCheckboxes)
|
||||
.updateServiceFilters({checked: true, name: "test_group|box1"})
|
||||
.updateServiceFilters(["test_group|box1"])
|
||||
.computeConfigManagerRowData()
|
||||
.build();
|
||||
|
||||
expect(state.serviceFilters).toEqual(["test_group|box1"])
|
||||
expect(state.isAnyFilterPresent).toEqual(true);
|
||||
expect(state.configManagerRowData).toEqual(expectedRowData);
|
||||
})
|
||||
@@ -428,11 +430,11 @@ describe('ConfigStoreStateBuilder', () => {
|
||||
|
||||
const state = builder
|
||||
.serviceFilterConfig(mockUiMetadataAlertWithCheckboxes)
|
||||
.updateServiceFilters({checked: true, name: "test_group|box1"})
|
||||
.updateServiceFilters({checked: true, name: "test_group|box2"})
|
||||
.updateServiceFilters(["test_group|box1","test_group|box2"])
|
||||
.computeConfigManagerRowData()
|
||||
.build();
|
||||
|
||||
expect(state.serviceFilters).toEqual(["test_group|box1", "test_group|box2"])
|
||||
expect(state.isAnyFilterPresent).toEqual(true);
|
||||
expect(state.configManagerRowData).toEqual(expectedRowData);
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Config, Release, FileHistory } from '../../model';
|
||||
import { TestCaseMap } from '@app/model/test-case';
|
||||
import { TestCaseWrapper, TestCaseResult } from '../../model/test-case';
|
||||
import { AdminConfig, CheckboxEvent, ConfigManagerRow, ConfigStatus, FILTER_DELIMITER, ServiceFilters } from '@app/model/config-model';
|
||||
import { AdminConfig, ConfigManagerRow, ConfigStatus, FILTER_DELIMITER } from '@app/model/config-model';
|
||||
import { FilterConfig, UiMetadata } from '@app/model/ui-metadata-map';
|
||||
|
||||
export class ConfigStoreStateBuilder {
|
||||
@@ -113,8 +113,11 @@ export class ConfigStoreStateBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
updateServiceFilters(event: CheckboxEvent): ConfigStoreStateBuilder {
|
||||
this.state.serviceFilters[event.name] = event.checked;
|
||||
updateServiceFilters(filters: string[]): ConfigStoreStateBuilder {
|
||||
this.state.serviceFilters = [];
|
||||
filters.forEach(filter => {
|
||||
this.state.serviceFilters.push(filter)
|
||||
})
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -205,25 +208,16 @@ export class ConfigStoreStateBuilder {
|
||||
}
|
||||
|
||||
computeConfigManagerRowData() {
|
||||
this.state.isAnyFilterPresent = this.isServiceFilterPresent(this.state.serviceFilters);
|
||||
this.state.isAnyFilterPresent = this.state.serviceFilters.length > 0;
|
||||
this.state.configManagerRowData = this.state.sortedConfigs.map(
|
||||
(config: Config) => this.getRowFromConfig(config, this.state.release)
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
private isServiceFilterPresent(filters: ServiceFilters): boolean {
|
||||
for (const checked of Object.values(filters)) {
|
||||
if (checked) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private evaluateFilters(node: ConfigManagerRow): boolean {
|
||||
for (const [name, checked] of Object.entries(this.state.serviceFilters)) {
|
||||
if (checked && !this.evaluateSingleFilter(name, node)) {
|
||||
for (const name of this.state.serviceFilters) {
|
||||
if (!this.evaluateSingleFilter(name, node)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import { UiMetadata } from '../../model/ui-metadata-map';
|
||||
import { ConfigLoaderService } from '../config-loader.service';
|
||||
import { ConfigStoreStateBuilder } from './config-store-state.builder';
|
||||
import { TestStoreService } from './test-store.service';
|
||||
import { AdminConfig, CheckboxEvent, ConfigAndTestsToClone, ConfigToImport, ExistingConfigError, Importers, Type } from '@app/model/config-model';
|
||||
import { AdminConfig, ConfigAndTestsToClone, ConfigToImport, ExistingConfigError, FILTER_PARAM_KEY, Importers, SEARCH_PARAM_KEY, Type } from '@app/model/config-model';
|
||||
import { ClipboardStoreService } from '../clipboard-store.service';
|
||||
import { ConfigHistoryService } from '../config-history.service';
|
||||
import { AppConfigService } from '../app-config.service';
|
||||
import { AppService } from '../app.service';
|
||||
import { ParamMap } from '@angular/router';
|
||||
|
||||
const initialConfigStoreState: ConfigStoreState = {
|
||||
adminConfig: undefined,
|
||||
@@ -28,7 +29,7 @@ const initialConfigStoreState: ConfigStoreState = {
|
||||
pastedConfig: undefined,
|
||||
countChangesInRelease: 0,
|
||||
configManagerRowData: [],
|
||||
serviceFilters: {},
|
||||
serviceFilters: [],
|
||||
isAnyFilterPresent: false,
|
||||
serviceFilterConfig: undefined,
|
||||
user: undefined,
|
||||
@@ -150,15 +151,6 @@ export class ConfigStoreService {
|
||||
this.store.next(newState);
|
||||
}
|
||||
|
||||
updateServiceFilters(event: CheckboxEvent) {
|
||||
const newState = new ConfigStoreStateBuilder(this.store.getValue())
|
||||
.updateServiceFilters(event)
|
||||
.computeConfigManagerRowData()
|
||||
.build();
|
||||
|
||||
this.store.next(newState);
|
||||
}
|
||||
|
||||
addConfigToRelease(name: string) {
|
||||
const newState = new ConfigStoreStateBuilder(this.store.getValue())
|
||||
.addConfigToRelease(name)
|
||||
@@ -593,6 +585,16 @@ export class ConfigStoreService {
|
||||
this.store.next(newState);
|
||||
}
|
||||
|
||||
updateSearchTermAndFilters(params: ParamMap) {
|
||||
const newState = new ConfigStoreStateBuilder(this.store.getValue())
|
||||
.searchTerm(params.get(SEARCH_PARAM_KEY))
|
||||
.updateServiceFilters(params.getAll(FILTER_PARAM_KEY))
|
||||
.computeConfigManagerRowData()
|
||||
.build();
|
||||
|
||||
this.store.next(newState);
|
||||
}
|
||||
|
||||
private updateReleaseSubmitInFlight(releaseSubmitInFlight: boolean) {
|
||||
const newState = new ConfigStoreStateBuilder(this.store.getValue())
|
||||
.releaseSubmitInFlight(releaseSubmitInFlight)
|
||||
|
||||
@@ -71,3 +71,4 @@ describe('UrlHistoryService', () => {
|
||||
expect(service.getHistoryPreviousUrls()).toHaveSize(5);
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router, NavigationEnd } from '@angular/router';
|
||||
import { AppConfigService } from '@app/services/app-config.service';
|
||||
import { HistoryUrl, HISTORY_PARAMS } from '@app/model/config-model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -24,6 +25,28 @@ export class UrlHistoryService {
|
||||
return history ? JSON.parse(history) : [];
|
||||
}
|
||||
|
||||
getPreviousUrls(): HistoryUrl[] {
|
||||
const listUrls = this.getHistoryPreviousUrls();
|
||||
return listUrls.map(url => this.getHistoryUrl(url));
|
||||
}
|
||||
|
||||
private getHistoryUrl(path: string): HistoryUrl {
|
||||
const url = new URL(path, location.origin);
|
||||
const paths = url.pathname.substring(1).split('/');
|
||||
|
||||
const service = paths[0];
|
||||
const mode = paths[1] === 'admin' ? 'admin' : '';
|
||||
|
||||
const newUrl = new URL(url.pathname, location.origin);
|
||||
for (const param of HISTORY_PARAMS) {
|
||||
if (url.searchParams.get(param)) {
|
||||
newUrl.searchParams.append(param, url.searchParams.get(param));
|
||||
}
|
||||
}
|
||||
const params = Object.fromEntries(newUrl.searchParams.entries());
|
||||
return {rawUrl: newUrl.pathname + newUrl.search, labels: { service, mode, ...params }};
|
||||
}
|
||||
|
||||
private add(item: string, history: string[]): string[] {
|
||||
if (
|
||||
this.appService.isHomePath(item) ||
|
||||
|
||||
@@ -46,9 +46,9 @@ body {
|
||||
|
||||
.ag-theme-alpine-dark {
|
||||
@include ag-theme-alpine-dark ((
|
||||
background-color: #444444,
|
||||
odd-row-background-color: ag-derived(background-color, $mix: #303030 60% ),
|
||||
header-background-color: ag-derived(background-color, $mix: #303030 60% ),
|
||||
background-color: ag-derived(odd-row-background-color, $mix: #303030 60% ),
|
||||
odd-row-background-color: #444444,
|
||||
header-background-color: #444444,
|
||||
font-family: (Roboto, "Helvetica Neue", sans-serif),
|
||||
));
|
||||
font-size: 15px;
|
||||
|
||||
@@ -20,7 +20,7 @@ export const mockStore: ConfigStoreState = {
|
||||
pastedConfig: undefined,
|
||||
configManagerRowData: [],
|
||||
countChangesInRelease: 0,
|
||||
serviceFilters: {},
|
||||
serviceFilters: [],
|
||||
isAnyFilterPresent: false,
|
||||
serviceFilterConfig: {},
|
||||
user: "siembol",
|
||||
|
||||
Reference in New Issue
Block a user