Config-editor-ui: search in url + save previous searches (#578)

This commit is contained in:
Celie Valentiny
2022-04-01 17:24:55 +01:00
committed by GitHub
parent b760a2904b
commit b7446c913f
29 changed files with 427 additions and 110 deletions

File diff suppressed because one or more lines are too long

View File

@@ -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",

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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',
}]

View File

@@ -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 {

View File

@@ -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"

View File

@@ -13,7 +13,7 @@
}
#myGrid {
margin-top: 15px;
margin-top: 5px;
height: 100%;
width: 100%;
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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");
}
}

View File

@@ -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,
});
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -23,6 +23,7 @@ export interface AppConfig {
homeHelpLinks?: HelpLink[];
managementLinks?: HelpLink[];
historyMaxSize?: number;
searchMaxSize?: number;
blockingTimeout?: number;
useImporters?: boolean;
}

View File

@@ -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'];

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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] {

View File

@@ -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);
});
});

View File

@@ -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;
}
}

View File

@@ -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);
})

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -71,3 +71,4 @@ describe('UrlHistoryService', () => {
expect(service.getHistoryPreviousUrls()).toHaveSize(5);
}));
});

View File

@@ -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) ||

View File

@@ -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;

View File

@@ -20,7 +20,7 @@ export const mockStore: ConfigStoreState = {
pastedConfig: undefined,
configManagerRowData: [],
countChangesInRelease: 0,
serviceFilters: {},
serviceFilters: [],
isAnyFilterPresent: false,
serviceFilterConfig: {},
user: "siembol",