From 282e0b40394c89f01c298d574efa76640148ebbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:07:47 +0000 Subject: [PATCH 01/17] chore(deps-dev): bump the types group with 2 updates Bumps the types group with 2 updates: [@types/jasmine](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jasmine) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/jasmine` from 5.1.12 to 5.1.13 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jasmine) Updates `@types/node` from 24.10.0 to 24.10.1 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/jasmine" dependency-version: 5.1.13 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: types - dependency-name: "@types/node" dependency-version: 24.10.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: types ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1c775a5fb9..98f10ab7ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6524,9 +6524,9 @@ } }, "node_modules/@types/jasmine": { - "version": "5.1.12", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.12.tgz", - "integrity": "sha512-1BzPxNsFDLDfj9InVR3IeY0ZVf4o9XV+4mDqoCfyPkbsA7dYyKAPAb2co6wLFlHcvxPlt1wShm7zQdV7uTfLGA==", + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.13.tgz", + "integrity": "sha512-MYCcDkruFc92LeYZux5BC0dmqo2jk+M5UIZ4/oFnAPCXN9mCcQhLyj7F3/Za7rocVyt5YRr1MmqJqFlvQ9LVcg==", "dev": true, "license": "MIT" }, @@ -6568,9 +6568,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", - "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "dependencies": { From 463ab5372ead5d0995726f045c27acd8b2090e39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:40:02 +0000 Subject: [PATCH 02/17] chore(deps-dev): bump cypress from 15.6.0 to 15.7.0 Bumps [cypress](https://github.com/cypress-io/cypress) from 15.6.0 to 15.7.0. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md) - [Commits](https://github.com/cypress-io/cypress/compare/v15.6.0...v15.7.0) --- updated-dependencies: - dependency-name: cypress dependency-version: 15.7.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98f10ab7ef..784be6baed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9716,9 +9716,9 @@ "license": "MIT" }, "node_modules/cypress": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.6.0.tgz", - "integrity": "sha512-Vqo66GG1vpxZ7H1oDX9umfmzA3nF7Wy80QAc3VjwPREO5zTY4d1xfQFNPpOWleQl9vpdmR2z1liliOcYlRX6rQ==", + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.7.0.tgz", + "integrity": "sha512-1C81zKxnQckYm2XGi37rPV4rN0bzUoWhydhKdOyshJn5gJKszEx5as9VLSZI0jp0ye49QxmnbU4TtMpcD+OmGQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9759,7 +9759,6 @@ "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.7.1", "supports-color": "^8.1.1", "systeminformation": "5.27.7", "tmp": "~0.2.4", From 545d1ab4cca19a1ec7662836cbd06d0ced171901 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:53:15 +0000 Subject: [PATCH 03/17] chore(deps-dev): bump js-yaml from 3.14.1 to 3.14.2 Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 3.14.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 784be6baed..60dfff5be1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3566,9 +3566,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -9550,9 +9550,9 @@ "license": "Python-2.0" }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -13600,9 +13600,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { From 51cdc85207e2dd6c8fbd3b7a0d08dab9563bf1fb Mon Sep 17 00:00:00 2001 From: junjiequan Date: Fri, 5 Dec 2025 14:04:43 +0100 Subject: [PATCH 04/17] first commit --- angular.json | 2 +- .../app-header/app-header.component.html | 5 + .../app-header/app-header.component.scss | 7 + src/app/_layout/layout.module.ts | 2 + .../admin-config-edit.component.html | 33 + .../admin-config-edit.component.scss | 0 .../admin-config-edit.component.ts | 97 +++ .../admin-dashboard.component.html | 33 + .../admin-dashboard.component.scss | 0 .../admin-dashboard.component.ts | 82 ++ .../admin-userlist-view.component.html | 1 + .../admin-userlist-view.component.scss | 0 .../admin-userlist-view.component.ts | 11 + src/app/admin/admin.module.ts | 36 + .../schema/frontend.config.jsonforms.json | 718 ++++++++++++++++++ src/app/app-config.service.ts | 7 +- src/app/app-routing/admin.guard.ts | 13 +- src/app/app-routing/app-routing.module.ts | 10 +- .../admin-routing/admin.feature.module.ts | 8 + .../admin-routing/admin.routing.module.ts | 30 + .../expand-group-renderer.html | 21 + .../expand-group-renderer.scss | 0 .../expand-group-renderer.ts | 40 + .../jsonforms-custom-renderers.module.ts | 11 +- .../actions/admin.action.spec.ts | 0 .../state-management/actions/admin.action.ts | 25 + .../effects/admin.effects.spec.ts | 0 .../state-management/effects/admin.effects.ts | 60 ++ .../reducers/admin.reducer.spec.ts | 0 .../reducers/admin.reducer.ts | 32 + .../selectors/admin.selectors.spec.ts | 0 .../selectors/admin.selectors.ts | 9 + src/app/state-management/state/admin.store.ts | 7 + src/styles.scss | 25 +- 34 files changed, 1310 insertions(+), 15 deletions(-) create mode 100644 src/app/admin/admin-config-edit/admin-config-edit.component.html create mode 100644 src/app/admin/admin-config-edit/admin-config-edit.component.scss create mode 100644 src/app/admin/admin-config-edit/admin-config-edit.component.ts create mode 100644 src/app/admin/admin-dashboard/admin-dashboard.component.html create mode 100644 src/app/admin/admin-dashboard/admin-dashboard.component.scss create mode 100644 src/app/admin/admin-dashboard/admin-dashboard.component.ts create mode 100644 src/app/admin/admin-userlist-view/admin-userlist-view.component.html create mode 100644 src/app/admin/admin-userlist-view/admin-userlist-view.component.scss create mode 100644 src/app/admin/admin-userlist-view/admin-userlist-view.component.ts create mode 100644 src/app/admin/admin.module.ts create mode 100644 src/app/admin/schema/frontend.config.jsonforms.json create mode 100644 src/app/app-routing/lazy/admin-routing/admin.feature.module.ts create mode 100644 src/app/app-routing/lazy/admin-routing/admin.routing.module.ts create mode 100644 src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.html create mode 100644 src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.scss create mode 100644 src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.ts create mode 100644 src/app/state-management/actions/admin.action.spec.ts create mode 100644 src/app/state-management/actions/admin.action.ts create mode 100644 src/app/state-management/effects/admin.effects.spec.ts create mode 100644 src/app/state-management/effects/admin.effects.ts create mode 100644 src/app/state-management/reducers/admin.reducer.spec.ts create mode 100644 src/app/state-management/reducers/admin.reducer.ts create mode 100644 src/app/state-management/selectors/admin.selectors.spec.ts create mode 100644 src/app/state-management/selectors/admin.selectors.ts create mode 100644 src/app/state-management/state/admin.store.ts diff --git a/angular.json b/angular.json index faf13bc3a5..76629f8393 100644 --- a/angular.json +++ b/angular.json @@ -56,7 +56,7 @@ { "type": "initial", "maximumWarning": "500kb", - "maximumError": "4mb" + "maximumError": "6mb" }, { "type": "anyComponentStyle", diff --git a/src/app/_layout/app-header/app-header.component.html b/src/app/_layout/app-header/app-header.component.html index 1123522112..c1b63813e5 100644 --- a/src/app/_layout/app-header/app-header.component.html +++ b/src/app/_layout/app-header/app-header.component.html @@ -80,6 +80,11 @@ + + + + + +
+ + +
+ +
+ +
+ +
+ +
+ + +
+ + +
+
+
diff --git a/src/app/admin/admin-config-edit/admin-config-edit.component.scss b/src/app/admin/admin-config-edit/admin-config-edit.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/admin/admin-config-edit/admin-config-edit.component.ts b/src/app/admin/admin-config-edit/admin-config-edit.component.ts new file mode 100644 index 0000000000..a52390ee41 --- /dev/null +++ b/src/app/admin/admin-config-edit/admin-config-edit.component.ts @@ -0,0 +1,97 @@ +import { Component, OnInit } from "@angular/core"; +import { Store } from "@ngrx/store"; +import { + loadConfiguration, + updateConfiguration, +} from "state-management/actions/admin.action"; +import { selectConfig } from "state-management/selectors/admin.selectors"; +import schema from "../schema/frontend.config.jsonforms.json"; +import { angularMaterialRenderers } from "@jsonforms/angular-material"; +import { + accordionArrayLayoutRendererTester, + AccordionArrayLayoutRendererComponent, +} from "shared/modules/jsonforms-custom-renderers/expand-panel-renderer/accordion-array-layout-renderer.component"; +import { map } from "rxjs"; +import { + expandGroupTester, + ExpandGroupRendererComponent, +} from "shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer"; +import { + ArrayLayoutRendererCustom, + arrayLayoutRendererTester, +} from "shared/modules/jsonforms-custom-renderers/ingestor-renderer/array-renderer"; + +@Component({ + selector: "admin-config-edit", + templateUrl: "./admin-config-edit.component.html", + styleUrls: ["./admin-config-edit.component.scss"], + standalone: false, +}) +export class AdminConfigEditComponent implements OnInit { + config$ = this.store.select(selectConfig); + data$ = this.config$.pipe( + map((cfg) => { + if (!cfg?.data) return null; + const d = structuredClone(cfg.data); + d.labelsLocalization.dataset = this.toArray(d.labelsLocalization.dataset); + d.labelsLocalization.proposal = this.toArray( + d.labelsLocalization.proposal, + ); + return d; + }), + ); + + showJsonPreview = false; + currentData: any = {}; + schema: any = schema.schema || {}; + uiSchema: any = schema.uiSchema || {}; + renderers = [ + ...angularMaterialRenderers, + { + tester: accordionArrayLayoutRendererTester, + renderer: AccordionArrayLayoutRendererComponent, + }, + { + tester: expandGroupTester, + renderer: ExpandGroupRendererComponent, + }, + { + tester: arrayLayoutRendererTester, + renderer: ArrayLayoutRendererCustom, + }, + ]; + constructor(private store: Store) {} + + ngOnInit(): void { + this.store.dispatch(loadConfiguration()); + } + + toArray(obj: any) { + if (!obj) return []; + return Array.isArray(obj) + ? obj + : Object.entries(obj).map(([key, value]) => ({ key, value })); + } + + toObject(arr: any) { + if (!arr) return {}; + if (!Array.isArray(arr)) return arr; + + return Object.fromEntries(arr.map((i) => [i.key, i.value])); + } + + onChange(event: any) { + this.currentData = event; + } + + save() { + const d = structuredClone(this.currentData); + + d.labelsLocalization = { + dataset: this.toObject(d.labelsLocalization.dataset), + proposal: this.toObject(d.labelsLocalization.proposal), + }; + + this.store.dispatch(updateConfiguration({ config: d })); + } +} diff --git a/src/app/admin/admin-dashboard/admin-dashboard.component.html b/src/app/admin/admin-dashboard/admin-dashboard.component.html new file mode 100644 index 0000000000..b82ca79ed4 --- /dev/null +++ b/src/app/admin/admin-dashboard/admin-dashboard.component.html @@ -0,0 +1,33 @@ + + + + + + + diff --git a/src/app/admin/admin-dashboard/admin-dashboard.component.scss b/src/app/admin/admin-dashboard/admin-dashboard.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/admin/admin-dashboard/admin-dashboard.component.ts b/src/app/admin/admin-dashboard/admin-dashboard.component.ts new file mode 100644 index 0000000000..edb64ba9bd --- /dev/null +++ b/src/app/admin/admin-dashboard/admin-dashboard.component.ts @@ -0,0 +1,82 @@ +import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; +import { MatDialog } from "@angular/material/dialog"; +import { ActivatedRoute, IsActiveMatchOptions } from "@angular/router"; +import { UsersService } from "@scicatproject/scicat-sdk-ts-angular"; +import { AppConfigService } from "app-config.service"; +enum TAB { + configuration = "Configuration", + usersList = "Users List", +} +@Component({ + selector: "app-admin-dashboard", + templateUrl: "./admin-dashboard.component.html", + styleUrls: ["./admin-dashboard.component.scss"], + standalone: false, +}) +export class AdminDashboardComponent implements OnInit { + showError = false; + navLinks: { + location: string; + label: string; + icon: string; + enabled: boolean; + }[] = []; + + routerLinkActiveOptions: IsActiveMatchOptions = { + matrixParams: "ignored", + queryParams: "ignored", + fragment: "ignored", + paths: "exact", + }; + + fetchDataActions: { [tab: string]: { action: any; loaded: boolean } } = { + [TAB.configuration]: { action: "", loaded: false }, + [TAB.usersList]: { action: "", loaded: false }, + }; + + constructor( + public appConfigService: AppConfigService, + private cdRef: ChangeDetectorRef, + private route: ActivatedRoute, + private userService: UsersService, + public dialog: MatDialog, + ) {} + + ngOnInit(): void { + this.navLinks = [ + { + location: "./configuration", + label: TAB.configuration, + icon: "menu", + enabled: true, + }, + { + location: "./usersList", + label: TAB.usersList, + icon: "data_object", + enabled: true, + }, + ]; + } + + onTabSelected(tab: string) { + this.fetchDataForTab(tab); + } + fetchDataForTab(tab: string) { + if (tab in this.fetchDataActions) { + switch (tab) { + case TAB.configuration: + break; + case TAB.usersList: + break; + default: { + // const { action, loaded } = this.fetchDataActions[tab]; + // if (!loaded) { + // this.fetchDataActions[tab].loaded = true; + // this.store.dispatch(action(args)); + // } + } + } + } + } +} diff --git a/src/app/admin/admin-userlist-view/admin-userlist-view.component.html b/src/app/admin/admin-userlist-view/admin-userlist-view.component.html new file mode 100644 index 0000000000..034e3d5761 --- /dev/null +++ b/src/app/admin/admin-userlist-view/admin-userlist-view.component.html @@ -0,0 +1 @@ +
To be implemented
diff --git a/src/app/admin/admin-userlist-view/admin-userlist-view.component.scss b/src/app/admin/admin-userlist-view/admin-userlist-view.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/admin/admin-userlist-view/admin-userlist-view.component.ts b/src/app/admin/admin-userlist-view/admin-userlist-view.component.ts new file mode 100644 index 0000000000..e246459ee8 --- /dev/null +++ b/src/app/admin/admin-userlist-view/admin-userlist-view.component.ts @@ -0,0 +1,11 @@ +import { Component, OnInit } from "@angular/core"; + +@Component({ + selector: "admin-userlist-view", + templateUrl: "./admin-userlist-view.component.html", + styleUrls: ["./admin-userlist-view.component.scss"], + standalone: false, +}) +export class AdminUserlistViewComponent { + constructor() {} +} diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts new file mode 100644 index 0000000000..7ff2c88272 --- /dev/null +++ b/src/app/admin/admin.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { AdminDashboardComponent } from "./admin-dashboard/admin-dashboard.component"; +import { MatDialogModule } from "@angular/material/dialog"; +import { MatIconModule } from "@angular/material/icon"; +import { MatTabsModule } from "@angular/material/tabs"; +import { RouterModule } from "@angular/router"; +import { SharedScicatFrontendModule } from "shared/shared.module"; +import { EffectsModule } from "@ngrx/effects"; +import { AdminEffects } from "state-management/effects/admin.effects"; +import { StoreModule } from "@ngrx/store"; +import { adminReducer } from "state-management/reducers/admin.reducer"; +import { AdminConfigEditComponent } from "./admin-config-edit/admin-config-edit.component"; +import { AdminUserlistViewComponent } from "./admin-userlist-view/admin-userlist-view.component"; +import { NgxJsonViewerModule } from "ngx-json-viewer"; + +@NgModule({ + imports: [ + CommonModule, + RouterModule, + MatTabsModule, + MatIconModule, + MatDialogModule, + NgxJsonViewerModule, + SharedScicatFrontendModule, + EffectsModule.forFeature([AdminEffects]), + StoreModule.forFeature("admin", adminReducer), + ], + declarations: [ + AdminDashboardComponent, + AdminConfigEditComponent, + AdminUserlistViewComponent, + ], + exports: [AdminDashboardComponent], +}) +export class AdminModule {} diff --git a/src/app/admin/schema/frontend.config.jsonforms.json b/src/app/admin/schema/frontend.config.jsonforms.json new file mode 100644 index 0000000000..b3a1d46c3d --- /dev/null +++ b/src/app/admin/schema/frontend.config.jsonforms.json @@ -0,0 +1,718 @@ +{ + "schema": { + "type": "object", + "properties": { + "defaultMainPage": { + "type": "object", + + "properties": { + "nonAuthenticatedUser": { "type": "string" }, + "authenticatedUser": { "type": "string" } + } + }, + + "ingestorComponent": { + "type": "object", + "properties": { + "ingestorEnabled": { "type": "boolean" } + } + }, + + "checkBoxFilterClickTrigger": { "type": "boolean" }, + "accessTokenPrefix": { "type": "string" }, + "addDatasetEnabled": { "type": "boolean" }, + "archiveWorkflowEnabled": { "type": "boolean" }, + "datasetReduceEnabled": { "type": "boolean" }, + "datasetJsonScientificMetadata": { "type": "boolean" }, + "editDatasetEnabled": { "type": "boolean" }, + "editDatasetSampleEnabled": { "type": "boolean" }, + "editMetadataEnabled": { "type": "boolean" }, + "addSampleEnabled": { "type": "boolean" }, + + "externalAuthEndpoint": { "type": "string" }, + "facility": { "type": "string" }, + "siteIcon": { "type": "string" }, + "siteTitle": { "type": "string" }, + "siteSciCatLogo": { "type": "string" }, + + "loginFacilityLabel": { "type": "string" }, + "loginLdapLabel": { "type": "string" }, + "loginLocalLabel": { "type": "string" }, + + "loginFacilityEnabled": { "type": "boolean" }, + "loginLdapEnabled": { "type": "boolean" }, + "loginLocalEnabled": { "type": "boolean" }, + + "fileColorEnabled": { "type": "boolean" }, + "fileDownloadEnabled": { "type": "boolean" }, + + "gettingStarted": {}, + "ingestManual": {}, + + "jobsEnabled": { "type": "boolean" }, + "jsonMetadataEnabled": { "type": "boolean" }, + "jupyterHubUrl": { "type": "string" }, + "landingPage": { "type": "string" }, + "lbBaseURL": { "type": "string" }, + + "logbookEnabled": { "type": "boolean" }, + "loginFormEnabled": { "type": "boolean" }, + + "metadataPreviewEnabled": { "type": "boolean" }, + "metadataStructure": { "type": "string" }, + + "multipleDownloadAction": { "type": "string" }, + "multipleDownloadEnabled": { "type": "boolean" }, + + "oAuth2Endpoints": { + "type": "array", + "items": { + "type": "object", + "properties": { + "authURL": { "type": "string" }, + "displayText": { "type": "string" } + } + } + }, + + "policiesEnabled": { "type": "boolean" }, + "retrieveDestinations": { + "type": "array", + "items": { "type": "string" } + }, + + "riotBaseUrl": { "type": "string" }, + "scienceSearchEnabled": { "type": "boolean" }, + "scienceSearchUnitsEnabled": { "type": "boolean" }, + "searchPublicDataEnabled": { "type": "boolean" }, + "searchSamples": { "type": "boolean" }, + + "sftpHost": { "type": "string" }, + "sourceFolder": { "type": "string" }, + + "maxDirectDownloadSize": { "type": "number" }, + "maxFileSizeWarning": { "type": "string" }, + + "shareEnabled": { "type": "boolean" }, + "shoppingCartEnabled": { "type": "boolean" }, + "shoppingCartOnHeader": { "type": "boolean" }, + + "tableSciDataEnabled": { "type": "boolean" }, + "datasetDetailsShowMissingProposalId": { "type": "boolean" }, + + "notificationInterceptorEnabled": { "type": "boolean" }, + "metadataEditingUnitListDisabled": { "type": "boolean" }, + "hideEmptyMetadataTable": { "type": "boolean" }, + + "datafilesActionsEnabled": { "type": "boolean" }, + + "datafilesActions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "description": { "type": "string" }, + "order": { "type": "number" }, + "label": { "type": "string" }, + "files": { "type": "string" }, + "mat_icon": { "type": "string" }, + "url": { "type": "string" }, + "target": { "type": "string" }, + "enabled": { "type": "string" }, + "authorization": { "type": "array", "items": { "type": "string" } }, + "type": { "type": "string" }, + "icon": { "type": "string" }, + "payload": { "type": "string" }, + "filename": { "type": "string" } + } + } + }, + + "defaultDatasetsListSettings": { + "type": "object", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "order": { "type": "number" }, + "type": { "type": "string" }, + "enabled": { "type": "boolean" } + } + } + }, + "filters": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "label": { "type": "string" }, + "type": { "type": "string" }, + "description": { "type": "string" }, + "enabled": { "type": "boolean" } + } + } + }, + "conditions": { + "type": "array" + } + } + }, + + "defaultProposalsListSettings": { + "type": "object", + "properties": { + "columns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "enabled": { "type": "boolean" }, + "width": { "type": "number" }, + "type": { "type": "string" }, + "format": { "type": "string" } + } + } + }, + "filters": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "label": { "type": "string" }, + "type": { "type": "string" }, + "description": { "type": "string" }, + "enabled": { "type": "boolean" } + } + } + } + } + }, + "labelsLocalization": { + "type": "object", + "properties": { + "dataset": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "value": { "type": "string" } + }, + "required": ["key", "value"] + } + }, + "proposal": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "value": { "type": "string" } + }, + "required": ["key", "value"] + } + } + } + }, + "dateFormat": { "type": "string" }, + "datasetDetailComponent": { + "type": "object", + "properties": { + "enableCustomizedComponent": { "type": "boolean" }, + + "customization": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "label": { "type": "string" }, + "order": { "type": "number" }, + "row": { "type": "number" }, + "col": { "type": "number" }, + "viewMode": { "type": "string" }, + + "options": { + "type": "object", + "properties": { + "limit": { "type": "number" }, + "size": { "type": "string" } + } + }, + + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "element": { "type": "string" }, + "source": { "type": "string" }, + "order": { "type": "number" } + } + } + } + } + } + } + } + }, + "mainMenu": { + "type": "object", + "properties": { + "nonAuthenticatedUser": { + "type": "object", + "properties": { + "datasets": { "type": "boolean" }, + "files": { "type": "boolean" }, + "instruments": { "type": "boolean" }, + "jobs": { "type": "boolean" }, + "policies": { "type": "boolean" }, + "proposals": { "type": "boolean" }, + "publishedData": { "type": "boolean" }, + "samples": { "type": "boolean" } + } + }, + "authenticatedUser": { + "type": "object", + "properties": { + "datasets": { "type": "boolean" }, + "files": { "type": "boolean" }, + "instruments": { "type": "boolean" }, + "jobs": { "type": "boolean" }, + "policies": { "type": "boolean" }, + "proposals": { "type": "boolean" }, + "publishedData": { "type": "boolean" }, + "samples": { "type": "boolean" } + } + } + } + } + } + }, + "uiSchema": { + "type": "VerticalLayout", + "elements": [ + { + "type": "Group", + "label": "Main Page", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "Control", + "scope": "#/properties/defaultMainPage/properties/nonAuthenticatedUser" + }, + { + "type": "Control", + "scope": "#/properties/defaultMainPage/properties/authenticatedUser" + } + ] + }, + { + "type": "Group", + "label": "Ingestor Component", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "Control", + "scope": "#/properties/ingestorComponent/properties/ingestorEnabled" + } + ] + }, + { + "type": "Group", + "label": "General Settings", + "options": { + "expandable": true + }, + "elements": [ + { "type": "Control", "scope": "#/properties/accessTokenPrefix" }, + { + "type": "Control", + "scope": "#/properties/checkBoxFilterClickTrigger" + }, + { "type": "Control", "scope": "#/properties/addDatasetEnabled" }, + { "type": "Control", "scope": "#/properties/archiveWorkflowEnabled" }, + { "type": "Control", "scope": "#/properties/datasetReduceEnabled" }, + { + "type": "Control", + "scope": "#/properties/datasetJsonScientificMetadata" + }, + { "type": "Control", "scope": "#/properties/editDatasetEnabled" }, + { + "type": "Control", + "scope": "#/properties/editDatasetSampleEnabled" + }, + { "type": "Control", "scope": "#/properties/editMetadataEnabled" }, + { "type": "Control", "scope": "#/properties/addSampleEnabled" } + ] + }, + { + "type": "Group", + "options": { + "expandable": true + }, + "label": "Frontend Labels & URLs", + "elements": [ + { + "type": "HorizontalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/lbBaseURL" }, + { + "type": "Control", + "scope": "#/properties/externalAuthEndpoint" + }, + { "type": "Control", "scope": "#/properties/facility" }, + { "type": "Control", "scope": "#/properties/siteIcon" }, + { "type": "Control", "scope": "#/properties/siteTitle" }, + { "type": "Control", "scope": "#/properties/siteSciCatLogo" } + ] + } + ] + }, + { + "type": "Group", + "options": { + "expandable": true + }, + "label": "Login Options", + "elements": [ + { + "type": "HorizontalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/loginFacilityLabel" }, + { "type": "Control", "scope": "#/properties/loginLdapLabel" }, + { "type": "Control", "scope": "#/properties/loginLocalLabel" } + ] + }, + { + "type": "HorizontalLayout", + "elements": [ + { + "type": "Control", + "scope": "#/properties/loginFacilityEnabled" + }, + { "type": "Control", "scope": "#/properties/loginLdapEnabled" }, + { "type": "Control", "scope": "#/properties/loginLocalEnabled" } + ] + } + ] + }, + { + "type": "Group", + "options": { + "expandable": true + }, + "label": "File/Display Flags", + "elements": [ + { + "type": "HorizontalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/jupyterHubUrl" }, + { "type": "Control", "scope": "#/properties/landingPage" }, + { "type": "Control", "scope": "#/properties/metadataStructure" } + ] + }, + + { "type": "Control", "scope": "#/properties/fileColorEnabled" }, + { "type": "Control", "scope": "#/properties/fileDownloadEnabled" }, + { "type": "Control", "scope": "#/properties/jobsEnabled" }, + { "type": "Control", "scope": "#/properties/jsonMetadataEnabled" }, + { "type": "Control", "scope": "#/properties/logbookEnabled" }, + { "type": "Control", "scope": "#/properties/loginFormEnabled" }, + { "type": "Control", "scope": "#/properties/metadataPreviewEnabled" } + ] + }, + { + "type": "Group", + "options": { + "expandable": true + }, + "label": "Download Settings", + "elements": [ + { "type": "Control", "scope": "#/properties/multipleDownloadAction" }, + { "type": "Control", "scope": "#/properties/multipleDownloadEnabled" } + ] + }, + + { + "type": "Group", + "options": { + "expandable": true + }, + "label": "OAuth2 Endpoints", + "elements": [ + { + "type": "Control", + "scope": "#/properties/oAuth2Endpoints" + } + ] + }, + { + "type": "Group", + "label": "SciCat Feature Flags", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "VerticalLayout", + "elements": [ + { + "type": "Control", + "scope": "#/properties/retrieveDestinations" + }, + { "type": "Control", "scope": "#/properties/riotBaseUrl" } + ] + }, + { "type": "Control", "scope": "#/properties/policiesEnabled" }, + { "type": "Control", "scope": "#/properties/scienceSearchEnabled" }, + { + "type": "Control", + "scope": "#/properties/scienceSearchUnitsEnabled" + }, + { + "type": "Control", + "scope": "#/properties/searchPublicDataEnabled" + }, + { "type": "Control", "scope": "#/properties/searchSamples" } + ] + }, + { + "type": "Group", + "label": "SFTP Host & Source Folder", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "HorizontalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/sftpHost" }, + { "type": "Control", "scope": "#/properties/sourceFolder" } + ] + } + ] + }, + { + "type": "Group", + "label": "Download Limits & Warnings", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "HorizontalLayout", + "elements": [ + { + "type": "Control", + "scope": "#/properties/maxDirectDownloadSize" + }, + { "type": "Control", "scope": "#/properties/maxFileSizeWarning" } + ] + } + ] + }, + { + "type": "Group", + "label": "Datafiles Actions", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "Control", + "scope": "#/properties/datafilesActions", + "options": { + "detail": { + "type": "VerticalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/id" }, + { + "type": "Control", + "scope": "#/properties/description" + }, + { "type": "Control", "scope": "#/properties/order" }, + { "type": "Control", "scope": "#/properties/label" }, + { "type": "Control", "scope": "#/properties/files" }, + { "type": "Control", "scope": "#/properties/mat_icon" }, + { "type": "Control", "scope": "#/properties/url" }, + { "type": "Control", "scope": "#/properties/target" }, + { "type": "Control", "scope": "#/properties/enabled" }, + { + "type": "Control", + "scope": "#/properties/authorization" + }, + { "type": "Control", "scope": "#/properties/type" }, + { "type": "Control", "scope": "#/properties/icon" }, + { "type": "Control", "scope": "#/properties/payload" }, + { "type": "Control", "scope": "#/properties/filename" } + ] + } + } + } + ] + }, + { + "type": "Group", + "label": "Localization", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "Group", + "label": "Dataset", + "options": { "expandable": true }, + "elements": [ + { + "type": "Control", + "scope": "#/properties/labelsLocalization/properties/dataset" + } + ] + }, + { + "type": "Group", + "label": "Proposal", + "options": { "expandable": true }, + "elements": [ + { + "type": "Control", + "scope": "#/properties/labelsLocalization/properties/proposal" + } + ] + } + ] + }, + { + "type": "Group", + "label": "Dataset Detail Component", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "Control", + "scope": "#/properties/datasetDetailComponent/properties/enableCustomizedComponent" + }, + { + "type": "Control", + "label": "Customization Configuration", + "scope": "#/properties/datasetDetailComponent/properties/customization", + "options": { + "detail": { + "type": "VerticalLayout", + "elements": [ + { + "type": "HorizontalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/type" }, + { "type": "Control", "scope": "#/properties/label" }, + { "type": "Control", "scope": "#/properties/order" }, + { "type": "Control", "scope": "#/properties/row" }, + { "type": "Control", "scope": "#/properties/col" }, + { "type": "Control", "scope": "#/properties/viewMode" } + ] + }, + { + "type": "Group", + "label": "Attachments Options", + "elements": [ + { + "type": "HorizontalLayout", + "elements": [ + { + "type": "Control", + "scope": "#/properties/options/properties/limit" + }, + { + "type": "Control", + "scope": "#/properties/options/properties/size" + } + ] + } + ] + }, + { + "type": "Control", + "label": "Fields", + "scope": "#/properties/fields", + "options": { + "detail": { + "type": "HorizontalLayout", + "elements": [ + { + "type": "Control", + "scope": "#/properties/element" + }, + { + "type": "Control", + "scope": "#/properties/source" + }, + { + "type": "Control", + "scope": "#/properties/order" + } + ] + } + } + } + ] + } + } + } + ] + }, + { + "type": "Group", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "HorizontalLayout", + "elements": [ + { + "type": "VerticalLayout", + "label": "Non Authenticated User", + "options": { + "expandable": false + }, + "elements": [ + { + "type": "Control", + "scope": "#/properties/mainMenu/properties/nonAuthenticatedUser" + } + ] + }, + { + "type": "VerticalLayout", + "label": "Authenticated User", + "options": { + "expandable": false + }, + "elements": [ + { + "type": "Control", + "scope": "#/properties/mainMenu/properties/authenticatedUser" + } + ] + } + ] + } + ] + }, + { "type": "Control", "scope": "#/properties/dateFormat" } + ] + } +} diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index 65ca265659..d0736dd917 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -1,5 +1,6 @@ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; +import { OutputRuntimeConfigDto } from "@scicatproject/scicat-sdk-ts-angular"; import { mergeWith } from "lodash-es"; import { firstValueFrom, of } from "rxjs"; import { catchError, timeout } from "rxjs/operators"; @@ -204,10 +205,12 @@ export class AppConfigService { async loadAppConfig(): Promise { try { - const config = await this.http - .get("/api/v3/admin/config") + const res = await this.http + .get("/api/v3/runtime-config/data/frontendConfig") .pipe(timeout(2000)) .toPromise(); + + const config = (res as OutputRuntimeConfigDto).data; this.appConfig = Object.assign({}, this.appConfig, config); } catch (err) { console.log("No config available in backend, trying with local config."); diff --git a/src/app/app-routing/admin.guard.ts b/src/app/app-routing/admin.guard.ts index 84ae2dbb6b..44e3fb8b4e 100644 --- a/src/app/app-routing/admin.guard.ts +++ b/src/app/app-routing/admin.guard.ts @@ -7,8 +7,11 @@ import { } from "@angular/router"; import { Store } from "@ngrx/store"; import { Observable } from "rxjs"; -import { map } from "rxjs/operators"; -import { selectIsAdmin } from "state-management/selectors/user.selectors"; +import { filter, map, switchMap, tap } from "rxjs/operators"; +import { + selectIsAdmin, + selectIsLoggedIn, +} from "state-management/selectors/user.selectors"; /** * Ensure that the current user is admin @@ -33,8 +36,10 @@ export class AdminGuard implements CanActivate { route: ActivatedRouteSnapshot, state: RouterStateSnapshot, ): Observable { - return this.store.select(selectIsAdmin).pipe( - map((isAdmin: boolean) => { + return this.store.select(selectIsLoggedIn).pipe( + filter((isLoggedIn) => isLoggedIn), + switchMap(() => this.store.select(selectIsAdmin)), + map((isAdmin) => { if (!isAdmin) { this.router.navigate(["/401"], { skipLocationChange: true, diff --git a/src/app/app-routing/app-routing.module.ts b/src/app/app-routing/app-routing.module.ts index bebe91433f..8a883f8ff6 100644 --- a/src/app/app-routing/app-routing.module.ts +++ b/src/app/app-routing/app-routing.module.ts @@ -8,6 +8,7 @@ import { ServiceGuard } from "./service.guard"; import { IngestorGuard } from "./ingestor.guard"; import { MainPageGuard } from "./main-page"; import { RedirectingComponent } from "./redirecting.component"; +import { AdminGuard } from "./admin.guard"; export const routes: Routes = [ { @@ -87,7 +88,6 @@ export const routes: Routes = [ (m) => m.PoliciesFeatureModule, ), }, - { path: "user", loadChildren: () => @@ -95,6 +95,14 @@ export const routes: Routes = [ (m) => m.UsersFeatureModule, ), }, + { + path: "admin", + loadChildren: () => + import("./lazy/admin-routing/admin.feature.module").then( + (m) => m.AdminFeatureModule, + ), + canActivate: [AdminGuard], + }, { path: "about", loadChildren: () => diff --git a/src/app/app-routing/lazy/admin-routing/admin.feature.module.ts b/src/app/app-routing/lazy/admin-routing/admin.feature.module.ts new file mode 100644 index 0000000000..0470cef2fd --- /dev/null +++ b/src/app/app-routing/lazy/admin-routing/admin.feature.module.ts @@ -0,0 +1,8 @@ +import { NgModule } from "@angular/core"; +import { AdminModule } from "admin/admin.module"; +import { AdminRoutingModule } from "./admin.routing.module"; + +@NgModule({ + imports: [AdminModule, AdminRoutingModule], +}) +export class AdminFeatureModule {} diff --git a/src/app/app-routing/lazy/admin-routing/admin.routing.module.ts b/src/app/app-routing/lazy/admin-routing/admin.routing.module.ts new file mode 100644 index 0000000000..dc5f33bc4b --- /dev/null +++ b/src/app/app-routing/lazy/admin-routing/admin.routing.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { AdminGuard } from "app-routing/admin.guard"; +import { AdminDashboardComponent } from "admin/admin-dashboard/admin-dashboard.component"; +import { AdminConfigEditComponent } from "admin/admin-config-edit/admin-config-edit.component"; +import { AdminUserlistViewComponent } from "admin/admin-userlist-view/admin-userlist-view.component"; + +const routes: Routes = [ + { + path: "", + component: AdminDashboardComponent, // parent with router-outlet + canActivate: [AdminGuard], + children: [ + { path: "", redirectTo: "configuration", pathMatch: "full" }, + { + path: "configuration", + component: AdminConfigEditComponent, + }, + { + path: "usersList", + component: AdminUserlistViewComponent, + }, + ], + }, +]; +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AdminRoutingModule {} diff --git a/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.html b/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.html new file mode 100644 index 0000000000..177c0c968f --- /dev/null +++ b/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.html @@ -0,0 +1,21 @@ + + + {{ uischema?.['label'] || 'Group' }} + + +
+ +
+
+ + +
+

{{ uischema?.['label'] || 'Group' }}

+ +
+ +
+
+
diff --git a/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.scss b/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.ts b/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.ts new file mode 100644 index 0000000000..e33a9ace5d --- /dev/null +++ b/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.ts @@ -0,0 +1,40 @@ +import { Component, ChangeDetectionStrategy } from "@angular/core"; +import { + JsonFormsAngularService, + JsonFormsBaseRenderer, + JsonFormsControl, +} from "@jsonforms/angular"; +import { + GroupLayout, + Layout, + RankedTester, + rankWith, + uiTypeIs, +} from "@jsonforms/core"; + +@Component({ + selector: "app-expand-group-renderer", + templateUrl: "./expand-group-renderer.html", + styleUrls: ["./expand-group-renderer.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, +}) +export class ExpandGroupRendererComponent extends JsonFormsBaseRenderer { + getProps(idx: number) { + return { + uischema: this.uischema.elements[idx], + schema: this.schema, + path: this.path, + }; + } + + trackByFn(index: number) { + return index; + } + + isExpandable() { + return this.uischema.options?.expandable === true; + } +} + +export const expandGroupTester: RankedTester = rankWith(3, uiTypeIs("Group")); diff --git a/src/app/shared/modules/jsonforms-custom-renderers/jsonforms-custom-renderers.module.ts b/src/app/shared/modules/jsonforms-custom-renderers/jsonforms-custom-renderers.module.ts index 46693411d1..9bf0693d19 100644 --- a/src/app/shared/modules/jsonforms-custom-renderers/jsonforms-custom-renderers.module.ts +++ b/src/app/shared/modules/jsonforms-custom-renderers/jsonforms-custom-renderers.module.ts @@ -6,9 +6,13 @@ import { JsonFormsModule } from "@jsonforms/angular"; import { JsonFormsAngularMaterialModule } from "@jsonforms/angular-material"; import { MatTooltipModule } from "@angular/material/tooltip"; import { MatBadgeModule } from "@angular/material/badge"; +import { ExpandGroupRendererComponent } from "./expand-group-renderer/expand-group-renderer"; @NgModule({ - declarations: [AccordionArrayLayoutRendererComponent], + declarations: [ + AccordionArrayLayoutRendererComponent, + ExpandGroupRendererComponent, + ], imports: [ MatExpansionModule, JsonFormsModule, @@ -16,7 +20,10 @@ import { MatBadgeModule } from "@angular/material/badge"; MatTooltipModule, MatBadgeModule, ], - exports: [AccordionArrayLayoutRendererComponent], + exports: [ + AccordionArrayLayoutRendererComponent, + ExpandGroupRendererComponent, + ], providers: [], }) export class JsonFormsCustomRenderersModule {} diff --git a/src/app/state-management/actions/admin.action.spec.ts b/src/app/state-management/actions/admin.action.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/state-management/actions/admin.action.ts b/src/app/state-management/actions/admin.action.ts new file mode 100644 index 0000000000..c8e9a0a49b --- /dev/null +++ b/src/app/state-management/actions/admin.action.ts @@ -0,0 +1,25 @@ +import { createAction, props } from "@ngrx/store"; +import { AdminConfiguration } from "state-management/effects/admin.effects"; + +export const loadConfiguration = createAction("[Admin] Load Configuration"); +export const loadConfigurationSuccess = createAction( + "[Admin] Load Configuration Success", + props<{ config: AdminConfiguration }>(), +); +export const loadConfigurationFailure = createAction( + "[Admin] Load Configuration Failure", + props<{ error: any }>(), +); + +export const updateConfiguration = createAction( + "[Admin] Update Configuration", + props<{ config: Partial }>(), +); +export const updateConfigurationSuccess = createAction( + "[Admin] Update Configuration Success", + props<{ config: AdminConfiguration }>(), +); +export const updateConfigurationFailure = createAction( + "[Admin] Update Configuration Failure", + props<{ error: any }>(), +); diff --git a/src/app/state-management/effects/admin.effects.spec.ts b/src/app/state-management/effects/admin.effects.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/state-management/effects/admin.effects.ts b/src/app/state-management/effects/admin.effects.ts new file mode 100644 index 0000000000..9e30f0fd97 --- /dev/null +++ b/src/app/state-management/effects/admin.effects.ts @@ -0,0 +1,60 @@ +import { Injectable } from "@angular/core"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { of } from "rxjs"; +import { switchMap, map, catchError, exhaustMap } from "rxjs/operators"; +import { + loadConfiguration, + loadConfigurationFailure, + loadConfigurationSuccess, + updateConfiguration, + updateConfigurationFailure, + updateConfigurationSuccess, +} from "state-management/actions/admin.action"; +import { RuntimeConfigService } from "@scicatproject/scicat-sdk-ts-angular"; + +// Types +export interface AdminConfiguration { + [key: string]: any; +} + +@Injectable() +export class AdminEffects { + constructor( + private actions$: Actions, + private runtimeConfigService: RuntimeConfigService, + ) {} + + loadConfiguration$ = createEffect(() => + this.actions$.pipe( + ofType(loadConfiguration), + switchMap(() => { + const configName = "frontendConfig"; + return this.runtimeConfigService + .runtimeConfigControllerGetConfigV3(configName) + .pipe( + map((config: AdminConfiguration) => { + return loadConfigurationSuccess({ config }); + }), + catchError((error) => of(loadConfigurationFailure({ error }))), + ); + }), + ), + ); + + updateConfiguration$ = createEffect(() => + this.actions$.pipe( + ofType(updateConfiguration), + exhaustMap(({ config }) => { + const configName = "frontendConfig"; + return this.runtimeConfigService + .runtimeConfigControllerUpdateConfigV3(configName, config) + .pipe( + map((config: AdminConfiguration) => + updateConfigurationSuccess({ config }), + ), + catchError((error) => of(updateConfigurationFailure({ error }))), + ); + }), + ), + ); +} diff --git a/src/app/state-management/reducers/admin.reducer.spec.ts b/src/app/state-management/reducers/admin.reducer.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/state-management/reducers/admin.reducer.ts b/src/app/state-management/reducers/admin.reducer.ts new file mode 100644 index 0000000000..269ebcfaf9 --- /dev/null +++ b/src/app/state-management/reducers/admin.reducer.ts @@ -0,0 +1,32 @@ +import { createReducer, Action, on } from "@ngrx/store"; +import * as fromActions from "state-management/actions/admin.action"; +import { + AdminState, + initialAdminState, +} from "state-management/state/admin.store"; + +const reducer = createReducer( + initialAdminState, + on( + fromActions.loadConfigurationSuccess, + (state, { config }): AdminState => ({ + ...state, + config, + }), + ), + + on( + fromActions.updateConfigurationSuccess, + (state, { config }): AdminState => ({ + ...state, + config, + }), + ), +); + +export const adminReducer = (state: AdminState | undefined, action: Action) => { + if (action.type.indexOf("[Admin]") !== -1) { + console.log("Action came in! " + action.type); + } + return reducer(state, action); +}; diff --git a/src/app/state-management/selectors/admin.selectors.spec.ts b/src/app/state-management/selectors/admin.selectors.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/state-management/selectors/admin.selectors.ts b/src/app/state-management/selectors/admin.selectors.ts new file mode 100644 index 0000000000..d8a0c77be6 --- /dev/null +++ b/src/app/state-management/selectors/admin.selectors.ts @@ -0,0 +1,9 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; +import { AdminState } from "state-management/state/admin.store"; + +const selectAdminState = createFeatureSelector("admin"); + +export const selectConfig = createSelector( + selectAdminState, + (state) => state.config, +); diff --git a/src/app/state-management/state/admin.store.ts b/src/app/state-management/state/admin.store.ts new file mode 100644 index 0000000000..a788f5cab7 --- /dev/null +++ b/src/app/state-management/state/admin.store.ts @@ -0,0 +1,7 @@ +export interface AdminState { + config: any; +} + +export const initialAdminState: AdminState = { + config: {}, +}; diff --git a/src/styles.scss b/src/styles.scss index d945489c77..940dcfb3bd 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -416,12 +416,27 @@ a:hover { } // Jsonforms customizations -jsonforms .mat-mdc-form-field { - .mat-mdc-form-field-subscript-dynamic-size { - min-height: unset !important; +jsonforms { + .mat-mdc-form-field { + .mat-mdc-form-field-subscript-dynamic-size { + min-height: unset !important; + } + .mat-mdc-form-field-hint-wrapper { + display: none !important; + } + } + + .boolean-control { + mat-error:empty { + display: none !important; + } + .mdc-checkbox { + margin-left: -9px !important; + } } - .mat-mdc-form-field-hint-wrapper { - display: none !important; + + .mat-mdc-card-content { + padding: 0 !important; } } From 8ed2781beac04adbec91327290ef1b2052ff281e Mon Sep 17 00:00:00 2001 From: junjiequan Date: Mon, 5 Jan 2026 15:38:34 +0100 Subject: [PATCH 05/17] set admin guard to check isLogging status --- src/app/app-routing/admin.guard.ts | 37 +++++++++++++++++++----------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/app/app-routing/admin.guard.ts b/src/app/app-routing/admin.guard.ts index 44e3fb8b4e..68a10f25c1 100644 --- a/src/app/app-routing/admin.guard.ts +++ b/src/app/app-routing/admin.guard.ts @@ -4,13 +4,15 @@ import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, + UrlTree, } from "@angular/router"; import { Store } from "@ngrx/store"; -import { Observable } from "rxjs"; -import { filter, map, switchMap, tap } from "rxjs/operators"; +import { combineLatest, Observable } from "rxjs"; +import { filter, map, switchMap, take, tap } from "rxjs/operators"; import { selectIsAdmin, selectIsLoggedIn, + selectIsLoggingIn, } from "state-management/selectors/user.selectors"; /** @@ -35,20 +37,27 @@ export class AdminGuard implements CanActivate { canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot, - ): Observable { - return this.store.select(selectIsLoggedIn).pipe( - filter((isLoggedIn) => isLoggedIn), - switchMap(() => this.store.select(selectIsAdmin)), - map((isAdmin) => { - if (!isAdmin) { - this.router.navigate(["/401"], { - skipLocationChange: true, - queryParams: { - url: state.url, - }, + ): Observable { + return combineLatest([ + this.store.select(selectIsLoggingIn), + this.store.select(selectIsLoggedIn), + this.store.select(selectIsAdmin), + ]).pipe( + filter(([isLoggingIn]) => { + return !isLoggingIn; + }), + take(1), + map(([, isLoggedIn, isAdmin]) => { + if (!isLoggedIn) { + return this.router.createUrlTree(["/login"], { + queryParams: { url: state.url }, }); } - return isAdmin; + return isAdmin + ? true + : this.router.createUrlTree(["/401"], { + queryParams: { url: state.url }, + }); }), ); } From 1b700c6c1c9d5527d4053ec9d1b529d52a787a0b Mon Sep 17 00:00:00 2001 From: junjiequan Date: Mon, 5 Jan 2026 15:38:59 +0100 Subject: [PATCH 06/17] improve frontend config jsonForms UI --- .../schema/frontend.config.jsonforms.json | 228 ++++++++++-------- 1 file changed, 130 insertions(+), 98 deletions(-) diff --git a/src/app/admin/schema/frontend.config.jsonforms.json b/src/app/admin/schema/frontend.config.jsonforms.json index b3a1d46c3d..098225eaec 100644 --- a/src/app/admin/schema/frontend.config.jsonforms.json +++ b/src/app/admin/schema/frontend.config.jsonforms.json @@ -78,7 +78,19 @@ "policiesEnabled": { "type": "boolean" }, "retrieveDestinations": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "object", + "properties": { + "option": { + "type": "string" + }, + "tooltip": { + "type": "string" + } + }, + "required": ["option", "tooltip"], + "additionalProperties": false + } }, "riotBaseUrl": { "type": "string" }, @@ -111,17 +123,17 @@ "items": { "type": "object", "properties": { + "label": { "type": "string" }, "id": { "type": "string" }, "description": { "type": "string" }, "order": { "type": "number" }, - "label": { "type": "string" }, "files": { "type": "string" }, "mat_icon": { "type": "string" }, "url": { "type": "string" }, "target": { "type": "string" }, "enabled": { "type": "string" }, "authorization": { "type": "array", "items": { "type": "string" } }, - "type": { "type": "string" }, + "type": { "type": "string", "enum": ["form", "xhr", "link"] }, "icon": { "type": "string" }, "payload": { "type": "string" }, "filename": { "type": "string" } @@ -226,33 +238,55 @@ "type": "object", "properties": { "enableCustomizedComponent": { "type": "boolean" }, - "customization": { "type": "array", "items": { "type": "object", "properties": { - "type": { "type": "string" }, "label": { "type": "string" }, + "type": { + "type": "string", + "enum": [ + "scientificMetadata", + "attachments", + "datasetJsonView", + "regular" + ] + }, "order": { "type": "number" }, "row": { "type": "number" }, "col": { "type": "number" }, - "viewMode": { "type": "string" }, + "viewMode": { + "type": "string", + "enum": ["table", "json", "tree"] + }, "options": { "type": "object", "properties": { "limit": { "type": "number" }, - "size": { "type": "string" } + "size": { + "type": "string", + "enum": ["small", "medium", "large"] + } } }, - "fields": { "type": "array", "items": { "type": "object", "properties": { - "element": { "type": "string" }, + "element": { + "type": "string", + "enum": [ + "text", + "copy", + "tag", + "linky", + "date", + "internalLink" + ] + }, "source": { "type": "string" }, "order": { "type": "number" } } @@ -301,42 +335,29 @@ "elements": [ { "type": "Group", - "label": "Main Page", + "label": "General Settings", "options": { "expandable": true }, "elements": [ + { "type": "Control", "scope": "#/properties/accessTokenPrefix" }, + { "type": "Control", "scope": "#/properties/landingPage" }, + { "type": "Control", "scope": "#/properties/metadataStructure" }, + { "type": "Control", "scope": "#/properties/accessTokenPrefix" }, + { "type": "Control", "scope": "#/properties/landingPage" }, + { "type": "Control", "scope": "#/properties/metadataStructure" }, + + { "type": "Control", "scope": "#/properties/riotBaseUrl" }, + { "type": "Control", "scope": "#/properties/jupyterHubUrl" }, + { "type": "Control", "scope": "#/properties/lbBaseURL" }, { "type": "Control", - "scope": "#/properties/defaultMainPage/properties/nonAuthenticatedUser" + "scope": "#/properties/externalAuthEndpoint" }, - { - "type": "Control", - "scope": "#/properties/defaultMainPage/properties/authenticatedUser" - } - ] - }, - { - "type": "Group", - "label": "Ingestor Component", - "options": { - "expandable": true - }, - "elements": [ - { - "type": "Control", - "scope": "#/properties/ingestorComponent/properties/ingestorEnabled" - } - ] - }, - { - "type": "Group", - "label": "General Settings", - "options": { - "expandable": true - }, - "elements": [ - { "type": "Control", "scope": "#/properties/accessTokenPrefix" }, + { "type": "Control", "scope": "#/properties/facility" }, + { "type": "Control", "scope": "#/properties/siteIcon" }, + { "type": "Control", "scope": "#/properties/siteTitle" }, + { "type": "Control", "scope": "#/properties/siteSciCatLogo" }, { "type": "Control", "scope": "#/properties/checkBoxFilterClickTrigger" @@ -354,32 +375,52 @@ "scope": "#/properties/editDatasetSampleEnabled" }, { "type": "Control", "scope": "#/properties/editMetadataEnabled" }, - { "type": "Control", "scope": "#/properties/addSampleEnabled" } + { "type": "Control", "scope": "#/properties/addSampleEnabled" }, + { "type": "Control", "scope": "#/properties/fileColorEnabled" }, + { "type": "Control", "scope": "#/properties/fileDownloadEnabled" }, + { "type": "Control", "scope": "#/properties/jobsEnabled" }, + { "type": "Control", "scope": "#/properties/jsonMetadataEnabled" }, + { "type": "Control", "scope": "#/properties/logbookEnabled" }, + { "type": "Control", "scope": "#/properties/loginFormEnabled" }, + { "type": "Control", "scope": "#/properties/metadataPreviewEnabled" } ] }, { "type": "Group", + "label": "Main Page", "options": { "expandable": true }, - "label": "Frontend Labels & URLs", "elements": [ { "type": "HorizontalLayout", "elements": [ - { "type": "Control", "scope": "#/properties/lbBaseURL" }, { "type": "Control", - "scope": "#/properties/externalAuthEndpoint" + "scope": "#/properties/defaultMainPage/properties/nonAuthenticatedUser" }, - { "type": "Control", "scope": "#/properties/facility" }, - { "type": "Control", "scope": "#/properties/siteIcon" }, - { "type": "Control", "scope": "#/properties/siteTitle" }, - { "type": "Control", "scope": "#/properties/siteSciCatLogo" } + { + "type": "Control", + "scope": "#/properties/defaultMainPage/properties/authenticatedUser" + } ] } ] }, + { + "type": "Group", + "label": "Ingestor Component", + "options": { + "expandable": true + }, + "elements": [ + { + "type": "Control", + "scope": "#/properties/ingestorComponent/properties/ingestorEnabled" + } + ] + }, + { "type": "Group", "options": { @@ -408,31 +449,6 @@ } ] }, - { - "type": "Group", - "options": { - "expandable": true - }, - "label": "File/Display Flags", - "elements": [ - { - "type": "HorizontalLayout", - "elements": [ - { "type": "Control", "scope": "#/properties/jupyterHubUrl" }, - { "type": "Control", "scope": "#/properties/landingPage" }, - { "type": "Control", "scope": "#/properties/metadataStructure" } - ] - }, - - { "type": "Control", "scope": "#/properties/fileColorEnabled" }, - { "type": "Control", "scope": "#/properties/fileDownloadEnabled" }, - { "type": "Control", "scope": "#/properties/jobsEnabled" }, - { "type": "Control", "scope": "#/properties/jsonMetadataEnabled" }, - { "type": "Control", "scope": "#/properties/logbookEnabled" }, - { "type": "Control", "scope": "#/properties/loginFormEnabled" }, - { "type": "Control", "scope": "#/properties/metadataPreviewEnabled" } - ] - }, { "type": "Group", "options": { @@ -460,32 +476,18 @@ }, { "type": "Group", - "label": "SciCat Feature Flags", + "label": "Retrieve Destinations", "options": { "expandable": true }, "elements": [ - { - "type": "VerticalLayout", - "elements": [ - { - "type": "Control", - "scope": "#/properties/retrieveDestinations" - }, - { "type": "Control", "scope": "#/properties/riotBaseUrl" } - ] - }, - { "type": "Control", "scope": "#/properties/policiesEnabled" }, - { "type": "Control", "scope": "#/properties/scienceSearchEnabled" }, - { - "type": "Control", - "scope": "#/properties/scienceSearchUnitsEnabled" - }, { "type": "Control", - "scope": "#/properties/searchPublicDataEnabled" - }, - { "type": "Control", "scope": "#/properties/searchSamples" } + "scope": "#/properties/retrieveDestinations", + "options": { + "description": "Retrieve Destinations" + } + } ] }, { @@ -531,19 +533,19 @@ }, "elements": [ { - "type": "Control", + "type": "ListWithDetail", "scope": "#/properties/datafilesActions", "options": { "detail": { "type": "VerticalLayout", "elements": [ + { "type": "Control", "scope": "#/properties/label" }, { "type": "Control", "scope": "#/properties/id" }, { "type": "Control", "scope": "#/properties/description" }, { "type": "Control", "scope": "#/properties/order" }, - { "type": "Control", "scope": "#/properties/label" }, { "type": "Control", "scope": "#/properties/files" }, { "type": "Control", "scope": "#/properties/mat_icon" }, { "type": "Control", "scope": "#/properties/url" }, @@ -606,7 +608,7 @@ "scope": "#/properties/datasetDetailComponent/properties/enableCustomizedComponent" }, { - "type": "Control", + "type": "ListWithDetail", "label": "Customization Configuration", "scope": "#/properties/datasetDetailComponent/properties/customization", "options": { @@ -616,16 +618,28 @@ { "type": "HorizontalLayout", "elements": [ - { "type": "Control", "scope": "#/properties/type" }, { "type": "Control", "scope": "#/properties/label" }, + { "type": "Control", "scope": "#/properties/type" }, { "type": "Control", "scope": "#/properties/order" }, { "type": "Control", "scope": "#/properties/row" }, { "type": "Control", "scope": "#/properties/col" }, - { "type": "Control", "scope": "#/properties/viewMode" } + { + "type": "Control", + "scope": "#/properties/viewMode", + "rule": { + "effect": "SHOW", + "condition": { + "scope": "#/properties/type", + "schema": { + "const": "scientificMetadata" + } + } + } + } ] }, { - "type": "Group", + "type": "VerticalLayout", "label": "Attachments Options", "elements": [ { @@ -641,12 +655,30 @@ } ] } - ] + ], + "rule": { + "effect": "SHOW", + "condition": { + "scope": "#/properties/type", + "schema": { + "const": "attachments" + } + } + } }, { "type": "Control", "label": "Fields", "scope": "#/properties/fields", + "rule": { + "effect": "SHOW", + "condition": { + "scope": "#/properties/type", + "schema": { + "const": "regular" + } + } + }, "options": { "detail": { "type": "HorizontalLayout", From 6ae4e38e127a4e69b6ce394335463b5eb4af4f4f Mon Sep 17 00:00:00 2001 From: junjiequan Date: Mon, 5 Jan 2026 15:39:07 +0100 Subject: [PATCH 07/17] improve jsonForms UI --- .../expand-group-renderer/expand-group-renderer.html | 1 + .../expand-group-renderer/expand-group-renderer.scss | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.html b/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.html index 177c0c968f..f24c351903 100644 --- a/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.html +++ b/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.html @@ -4,6 +4,7 @@
diff --git a/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.scss b/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.scss index e69de29bb2..2a7cfcaedc 100644 --- a/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.scss +++ b/src/app/shared/modules/jsonforms-custom-renderers/expand-group-renderer/expand-group-renderer.scss @@ -0,0 +1,3 @@ +.group-wrapper { + padding-bottom: 10px; +} From d0edf7d3ef8d9ebe9fa5dbf8f8561652fb3d49ca Mon Sep 17 00:00:00 2001 From: junjiequan Date: Tue, 6 Jan 2026 11:51:14 +0100 Subject: [PATCH 08/17] created shared json preview dialog --- .../json-preview-dialog.component.html | 10 ++++++++++ .../json-preview-dialog.component.scss | 0 .../json-preview-dialog.component.ts | 19 +++++++++++++++++++ .../json-preview-dialog.module.ts | 17 +++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.html create mode 100644 src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.scss create mode 100644 src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.ts create mode 100644 src/app/shared/modules/json-preview-dialog/json-preview-dialog.module.ts diff --git a/src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.html b/src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.html new file mode 100644 index 0000000000..18fb667fcf --- /dev/null +++ b/src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.html @@ -0,0 +1,10 @@ +

JSON Preview

+ + + + + + + + + diff --git a/src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.scss b/src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.ts b/src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.ts new file mode 100644 index 0000000000..a0bdefda9c --- /dev/null +++ b/src/app/shared/modules/json-preview-dialog/json-preview-dialog.component.ts @@ -0,0 +1,19 @@ +import { Component, Inject } from "@angular/core"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; + +@Component({ + selector: "app-json-preview-dialog", + templateUrl: "./json-preview-dialog.component.html", + styleUrls: ["./json-preview-dialog.component.scss"], + standalone: false, +}) +export class JsonPreviewDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data, + ) {} + + close() { + this.dialogRef.close(); + } +} diff --git a/src/app/shared/modules/json-preview-dialog/json-preview-dialog.module.ts b/src/app/shared/modules/json-preview-dialog/json-preview-dialog.module.ts new file mode 100644 index 0000000000..3dc01c119f --- /dev/null +++ b/src/app/shared/modules/json-preview-dialog/json-preview-dialog.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { MatDialogModule } from "@angular/material/dialog"; +import { MatButtonModule } from "@angular/material/button"; +import { NgxJsonViewerModule } from "ngx-json-viewer"; +import { JsonPreviewDialogComponent } from "./json-preview-dialog.component"; + +@NgModule({ + declarations: [JsonPreviewDialogComponent], + imports: [ + CommonModule, + MatDialogModule, + MatButtonModule, + NgxJsonViewerModule, + ], +}) +export class JsonPreviewDialogModule {} From 59fe4f8f0f8613e3dbf36f1a798882ad0b9fe701 Mon Sep 17 00:00:00 2001 From: junjiequan Date: Tue, 6 Jan 2026 11:52:00 +0100 Subject: [PATCH 09/17] improve frontend config jsonforms uiSchema --- .../schema/frontend.config.jsonforms.json | 131 ++++++++++++------ 1 file changed, 90 insertions(+), 41 deletions(-) diff --git a/src/app/admin/schema/frontend.config.jsonforms.json b/src/app/admin/schema/frontend.config.jsonforms.json index 098225eaec..e4f4098b7c 100644 --- a/src/app/admin/schema/frontend.config.jsonforms.json +++ b/src/app/admin/schema/frontend.config.jsonforms.json @@ -10,15 +10,14 @@ "authenticatedUser": { "type": "string" } } }, - "ingestorComponent": { "type": "object", "properties": { "ingestorEnabled": { "type": "boolean" } } }, - "checkBoxFilterClickTrigger": { "type": "boolean" }, + "dateFormat": { "type": "string" }, "accessTokenPrefix": { "type": "string" }, "addDatasetEnabled": { "type": "boolean" }, "archiveWorkflowEnabled": { "type": "boolean" }, @@ -28,42 +27,32 @@ "editDatasetSampleEnabled": { "type": "boolean" }, "editMetadataEnabled": { "type": "boolean" }, "addSampleEnabled": { "type": "boolean" }, - "externalAuthEndpoint": { "type": "string" }, "facility": { "type": "string" }, "siteIcon": { "type": "string" }, "siteTitle": { "type": "string" }, "siteSciCatLogo": { "type": "string" }, - "loginFacilityLabel": { "type": "string" }, "loginLdapLabel": { "type": "string" }, "loginLocalLabel": { "type": "string" }, - "loginFacilityEnabled": { "type": "boolean" }, "loginLdapEnabled": { "type": "boolean" }, "loginLocalEnabled": { "type": "boolean" }, - "fileColorEnabled": { "type": "boolean" }, "fileDownloadEnabled": { "type": "boolean" }, - "gettingStarted": {}, "ingestManual": {}, - "jobsEnabled": { "type": "boolean" }, "jsonMetadataEnabled": { "type": "boolean" }, "jupyterHubUrl": { "type": "string" }, "landingPage": { "type": "string" }, "lbBaseURL": { "type": "string" }, - "logbookEnabled": { "type": "boolean" }, "loginFormEnabled": { "type": "boolean" }, - "metadataPreviewEnabled": { "type": "boolean" }, "metadataStructure": { "type": "string" }, - "multipleDownloadAction": { "type": "string" }, "multipleDownloadEnabled": { "type": "boolean" }, - "oAuth2Endpoints": { "type": "array", "items": { @@ -74,7 +63,6 @@ } } }, - "policiesEnabled": { "type": "boolean" }, "retrieveDestinations": { "type": "array", @@ -92,32 +80,24 @@ "additionalProperties": false } }, - "riotBaseUrl": { "type": "string" }, "scienceSearchEnabled": { "type": "boolean" }, "scienceSearchUnitsEnabled": { "type": "boolean" }, "searchPublicDataEnabled": { "type": "boolean" }, "searchSamples": { "type": "boolean" }, - "sftpHost": { "type": "string" }, "sourceFolder": { "type": "string" }, - "maxDirectDownloadSize": { "type": "number" }, "maxFileSizeWarning": { "type": "string" }, - "shareEnabled": { "type": "boolean" }, "shoppingCartEnabled": { "type": "boolean" }, "shoppingCartOnHeader": { "type": "boolean" }, - "tableSciDataEnabled": { "type": "boolean" }, "datasetDetailsShowMissingProposalId": { "type": "boolean" }, - "notificationInterceptorEnabled": { "type": "boolean" }, "metadataEditingUnitListDisabled": { "type": "boolean" }, "hideEmptyMetadataTable": { "type": "boolean" }, - "datafilesActionsEnabled": { "type": "boolean" }, - "datafilesActions": { "type": "array", "items": { @@ -133,7 +113,41 @@ "target": { "type": "string" }, "enabled": { "type": "string" }, "authorization": { "type": "array", "items": { "type": "string" } }, - "type": { "type": "string", "enum": ["form", "xhr", "link"] }, + "inputs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "value": { "type": "string" } + } + } + }, + "variables": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "value": { "type": "string" } + } + } + }, + "headers": { + "type": "object", + "properties": { + "content-type": { "type": "string" }, + "Authorization": { "type": "string" } + } + }, + "method": { + "type": "string", + "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"] + }, + "type": { + "type": "string", + "enum": ["form", "xhr", "link", "json-download"] + }, "icon": { "type": "string" }, "payload": { "type": "string" }, "filename": { "type": "string" } @@ -174,7 +188,6 @@ } } }, - "defaultProposalsListSettings": { "type": "object", "properties": { @@ -233,7 +246,6 @@ } } }, - "dateFormat": { "type": "string" }, "datasetDetailComponent": { "type": "object", "properties": { @@ -340,13 +352,10 @@ "expandable": true }, "elements": [ + { "type": "Control", "scope": "#/properties/dateFormat" }, { "type": "Control", "scope": "#/properties/accessTokenPrefix" }, { "type": "Control", "scope": "#/properties/landingPage" }, { "type": "Control", "scope": "#/properties/metadataStructure" }, - { "type": "Control", "scope": "#/properties/accessTokenPrefix" }, - { "type": "Control", "scope": "#/properties/landingPage" }, - { "type": "Control", "scope": "#/properties/metadataStructure" }, - { "type": "Control", "scope": "#/properties/riotBaseUrl" }, { "type": "Control", "scope": "#/properties/jupyterHubUrl" }, { "type": "Control", "scope": "#/properties/lbBaseURL" }, @@ -553,12 +562,60 @@ { "type": "Control", "scope": "#/properties/enabled" }, { "type": "Control", - "scope": "#/properties/authorization" + "scope": "#/properties/method" }, { "type": "Control", "scope": "#/properties/type" }, { "type": "Control", "scope": "#/properties/icon" }, - { "type": "Control", "scope": "#/properties/payload" }, - { "type": "Control", "scope": "#/properties/filename" } + { + "type": "Control", + "scope": "#/properties/payload", + "options": { "multi": true } + }, + { "type": "Control", "scope": "#/properties/filename" }, + { + "type": "Control", + "scope": "#/properties/authorization" + }, + { + "type": "Control", + "scope": "#/properties/variables", + "options": { + "detail": { + "type": "HorizontalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/key" }, + { "type": "Control", "scope": "#/properties/value" } + ] + } + } + }, + { + "type": "Control", + "scope": "#/properties/inputs", + "options": { + "detail": { + "type": "HorizontalLayout", + "elements": [ + { "type": "Control", "scope": "#/properties/key" }, + { "type": "Control", "scope": "#/properties/value" } + ] + } + } + }, + { + "type": "Group", + "label": "Headers", + "elements": [ + { + "type": "Control", + "scope": "#/properties/headers/properties/content-type" + }, + { + "type": "Control", + "scope": "#/properties/headers/properties/Authorization" + } + ] + } ] } } @@ -707,6 +764,7 @@ }, { "type": "Group", + "label": "Main Menu", "options": { "expandable": true }, @@ -716,10 +774,6 @@ "elements": [ { "type": "VerticalLayout", - "label": "Non Authenticated User", - "options": { - "expandable": false - }, "elements": [ { "type": "Control", @@ -729,10 +783,6 @@ }, { "type": "VerticalLayout", - "label": "Authenticated User", - "options": { - "expandable": false - }, "elements": [ { "type": "Control", @@ -743,8 +793,7 @@ ] } ] - }, - { "type": "Control", "scope": "#/properties/dateFormat" } + } ] } } From c5bd66ae3657a55deaea3eda93dcc60d08ce9267 Mon Sep 17 00:00:00 2001 From: junjiequan Date: Tue, 6 Jan 2026 11:52:29 +0100 Subject: [PATCH 10/17] include json preview & export --- .../admin-config-edit.component.html | 12 +- .../admin-config-edit.component.ts | 122 ++++++++++++++---- .../admin-dashboard.component.html | 10 -- src/app/admin/admin.module.ts | 4 - src/app/shared/shared.module.ts | 2 + 5 files changed, 109 insertions(+), 41 deletions(-) diff --git a/src/app/admin/admin-config-edit/admin-config-edit.component.html b/src/app/admin/admin-config-edit/admin-config-edit.component.html index ff590eadc3..b51aa6125c 100644 --- a/src/app/admin/admin-config-edit/admin-config-edit.component.html +++ b/src/app/admin/admin-config-edit/admin-config-edit.component.html @@ -1,12 +1,14 @@
- +
- +
@@ -21,12 +23,14 @@
- +
- +
diff --git a/src/app/admin/admin-config-edit/admin-config-edit.component.ts b/src/app/admin/admin-config-edit/admin-config-edit.component.ts index a52390ee41..4d88682874 100644 --- a/src/app/admin/admin-config-edit/admin-config-edit.component.ts +++ b/src/app/admin/admin-config-edit/admin-config-edit.component.ts @@ -20,6 +20,8 @@ import { ArrayLayoutRendererCustom, arrayLayoutRendererTester, } from "shared/modules/jsonforms-custom-renderers/ingestor-renderer/array-renderer"; +import { MatDialog } from "@angular/material/dialog"; +import { JsonPreviewDialogComponent } from "shared/modules/json-preview-dialog/json-preview-dialog.component"; @Component({ selector: "admin-config-edit", @@ -30,18 +32,9 @@ import { export class AdminConfigEditComponent implements OnInit { config$ = this.store.select(selectConfig); data$ = this.config$.pipe( - map((cfg) => { - if (!cfg?.data) return null; - const d = structuredClone(cfg.data); - d.labelsLocalization.dataset = this.toArray(d.labelsLocalization.dataset); - d.labelsLocalization.proposal = this.toArray( - d.labelsLocalization.proposal, - ); - return d; - }), + map((cfg) => (cfg.data ? this.toFormData(cfg.data) : null)), ); - showJsonPreview = false; currentData: any = {}; schema: any = schema.schema || {}; uiSchema: any = schema.uiSchema || {}; @@ -60,38 +53,121 @@ export class AdminConfigEditComponent implements OnInit { renderer: ArrayLayoutRendererCustom, }, ]; - constructor(private store: Store) {} + constructor( + private store: Store, + private dialog: MatDialog, + ) {} ngOnInit(): void { this.store.dispatch(loadConfiguration()); } + onChange(event: any) { + this.currentData = event; + } + + save() { + const apiData = this.toApiData(this.currentData); + + this.store.dispatch(updateConfiguration({ config: apiData })); + } + + jsonPreview() { + const apiData = this.toApiData(this.currentData); + + this.dialog.open(JsonPreviewDialogComponent, { + width: "90vw", + maxHeight: "90vh", + data: apiData, + }); + } + + export() { + const apiData = this.toApiData(this.currentData); + const json = JSON.stringify(apiData, null, 2); + + const blob = new Blob([json], { type: "application/json;charset=utf-8" }); + const url = URL.createObjectURL(blob); + + const a = document.createElement("a"); + a.href = url; + a.download = `frontend-config-${new Date().toLocaleString("sv-SE")}.json`; + a.click(); + + URL.revokeObjectURL(url); + } + + // TODO: temp conversion functions, to be removed later toArray(obj: any) { if (!obj) return []; return Array.isArray(obj) ? obj : Object.entries(obj).map(([key, value]) => ({ key, value })); } - + // TODO: temp conversion functions, to be removed later toObject(arr: any) { - if (!arr) return {}; - if (!Array.isArray(arr)) return arr; + if (!arr?.length) return {}; return Object.fromEntries(arr.map((i) => [i.key, i.value])); } - onChange(event: any) { - this.currentData = event; + // TODO: temp conversion functions, to be removed later + private toFormData(data: any) { + const d = structuredClone(data); + + // Convert dynamic object to array with key and value properties + d.labelsLocalization = d.labelsLocalization ?? {}; + if (d.labelsLocalization.dataset) { + d.labelsLocalization.dataset = this.toArray(d.labelsLocalization.dataset); + } + if (d.labelsLocalization.proposal) { + d.labelsLocalization.proposal = this.toArray( + d.labelsLocalization.proposal, + ); + } + if (d.datafilesActions) { + d.datafilesActions = d.datafilesActions.map((a: any) => ({ + ...a, + variables: + a.variables && !Array.isArray(a.variables) + ? this.toArray(a.variables) + : a.variables, + inputs: + a.inputs && !Array.isArray(a.inputs) + ? this.toArray(a.inputs) + : a.inputs, + })); + } + + return d; } - save() { - const d = structuredClone(this.currentData); + // TODO: temp conversion functions, to be removed later + private toApiData(data: any) { + const d = structuredClone(data); - d.labelsLocalization = { - dataset: this.toObject(d.labelsLocalization.dataset), - proposal: this.toObject(d.labelsLocalization.proposal), - }; + // Convert array with key and value properties to dynamic object + d.labelsLocalization = d.labelsLocalization ?? {}; + if (Array.isArray(d.labelsLocalization.dataset)) { + d.labelsLocalization.dataset = this.toObject( + d.labelsLocalization.dataset, + ); + } + if (Array.isArray(d.labelsLocalization.proposal)) { + d.labelsLocalization.proposal = this.toObject( + d.labelsLocalization.proposal, + ); + } + if (Array.isArray(d.datafilesActions)) { + d.datafilesActions = d.datafilesActions.map((a: any) => ({ + ...a, + variables: Array.isArray(a.variables) + ? this.toObject(a.variables) + : a.variables, + inputs: Array.isArray(a.inputs) ? this.toObject(a.inputs) : a.inputs, + })); + } - this.store.dispatch(updateConfiguration({ config: d })); + return d; } } diff --git a/src/app/admin/admin-dashboard/admin-dashboard.component.html b/src/app/admin/admin-dashboard/admin-dashboard.component.html index b82ca79ed4..195794cc1b 100644 --- a/src/app/admin/admin-dashboard/admin-dashboard.component.html +++ b/src/app/admin/admin-dashboard/admin-dashboard.component.html @@ -18,16 +18,6 @@ - diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index 7ff2c88272..5d48358cb5 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -1,7 +1,6 @@ import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { AdminDashboardComponent } from "./admin-dashboard/admin-dashboard.component"; -import { MatDialogModule } from "@angular/material/dialog"; import { MatIconModule } from "@angular/material/icon"; import { MatTabsModule } from "@angular/material/tabs"; import { RouterModule } from "@angular/router"; @@ -12,7 +11,6 @@ import { StoreModule } from "@ngrx/store"; import { adminReducer } from "state-management/reducers/admin.reducer"; import { AdminConfigEditComponent } from "./admin-config-edit/admin-config-edit.component"; import { AdminUserlistViewComponent } from "./admin-userlist-view/admin-userlist-view.component"; -import { NgxJsonViewerModule } from "ngx-json-viewer"; @NgModule({ imports: [ @@ -20,8 +18,6 @@ import { NgxJsonViewerModule } from "ngx-json-viewer"; RouterModule, MatTabsModule, MatIconModule, - MatDialogModule, - NgxJsonViewerModule, SharedScicatFrontendModule, EffectsModule.forFeature([AdminEffects]), StoreModule.forFeature("admin", adminReducer), diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 787e9585de..ed7641f282 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -28,6 +28,7 @@ import { NgxNumericRangeFormFieldModule } from "./modules/numeric-range/ngx-nume import { EmptyContentModule } from "./modules/generic-empty-content/empty-content.module"; import { JsonformsAccordionRendererService } from "./services/jsonforms-accordion-renderer.service"; import { TranslateModule } from "@ngx-translate/core"; +import { JsonPreviewDialogModule } from "./modules/json-preview-dialog/json-preview-dialog.module"; @NgModule({ imports: [ BreadcrumbModule, @@ -54,6 +55,7 @@ import { TranslateModule } from "@ngx-translate/core"; JsonFormsModule, JsonFormsAngularMaterialModule, JsonFormsCustomRenderersModule, + JsonPreviewDialogModule, SharedFilterModule, ], providers: [ From 504b83e23036c4e20ec1e42652d5c25b93774111 Mon Sep 17 00:00:00 2001 From: junjiequan Date: Tue, 6 Jan 2026 12:35:07 +0100 Subject: [PATCH 11/17] eslint fix --- .../admin-config-edit.component.ts | 30 +++++++++++++------ src/app/app-routing/admin.guard.ts | 26 +++++----------- .../state-management/effects/admin.effects.ts | 9 +++--- .../selectors/user.selectors.ts | 11 +++++++ 4 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/app/admin/admin-config-edit/admin-config-edit.component.ts b/src/app/admin/admin-config-edit/admin-config-edit.component.ts index 4d88682874..df4f0f3e2e 100644 --- a/src/app/admin/admin-config-edit/admin-config-edit.component.ts +++ b/src/app/admin/admin-config-edit/admin-config-edit.component.ts @@ -11,7 +11,7 @@ import { accordionArrayLayoutRendererTester, AccordionArrayLayoutRendererComponent, } from "shared/modules/jsonforms-custom-renderers/expand-panel-renderer/accordion-array-layout-renderer.component"; -import { map } from "rxjs"; +import { map, Subscription, take } from "rxjs"; import { expandGroupTester, ExpandGroupRendererComponent, @@ -22,6 +22,8 @@ import { } from "shared/modules/jsonforms-custom-renderers/ingestor-renderer/array-renderer"; import { MatDialog } from "@angular/material/dialog"; import { JsonPreviewDialogComponent } from "shared/modules/json-preview-dialog/json-preview-dialog.component"; +import { JsonSchema, UISchemaElement } from "@jsonforms/core"; +import { AppConfigInterface } from "app-config.service"; @Component({ selector: "admin-config-edit", @@ -30,14 +32,15 @@ import { JsonPreviewDialogComponent } from "shared/modules/json-preview-dialog/j standalone: false, }) export class AdminConfigEditComponent implements OnInit { + private subscriptions: Subscription[] = []; config$ = this.store.select(selectConfig); data$ = this.config$.pipe( map((cfg) => (cfg.data ? this.toFormData(cfg.data) : null)), ); - currentData: any = {}; - schema: any = schema.schema || {}; - uiSchema: any = schema.uiSchema || {}; + currentData: AppConfigInterface; + schema: JsonSchema = schema.schema || {}; + uiSchema: UISchemaElement = schema.uiSchema; renderers = [ ...angularMaterialRenderers, { @@ -60,6 +63,9 @@ export class AdminConfigEditComponent implements OnInit { ngOnInit(): void { this.store.dispatch(loadConfiguration()); + this.subscriptions.push( + this.data$.pipe(take(1)).subscribe((d) => (this.currentData = d)), + ); } onChange(event: any) { @@ -100,9 +106,7 @@ export class AdminConfigEditComponent implements OnInit { // TODO: temp conversion functions, to be removed later toArray(obj: any) { if (!obj) return []; - return Array.isArray(obj) - ? obj - : Object.entries(obj).map(([key, value]) => ({ key, value })); + return Object.entries(obj).map(([key, value]) => ({ key, value })); } // TODO: temp conversion functions, to be removed later toObject(arr: any) { @@ -116,7 +120,7 @@ export class AdminConfigEditComponent implements OnInit { const d = structuredClone(data); // Convert dynamic object to array with key and value properties - d.labelsLocalization = d.labelsLocalization ?? {}; + if (d.labelsLocalization.dataset) { d.labelsLocalization.dataset = this.toArray(d.labelsLocalization.dataset); } @@ -147,7 +151,10 @@ export class AdminConfigEditComponent implements OnInit { const d = structuredClone(data); // Convert array with key and value properties to dynamic object - d.labelsLocalization = d.labelsLocalization ?? {}; + d.labelsLocalization = d.labelsLocalization ?? { + dataset: {}, + proposal: {}, + }; if (Array.isArray(d.labelsLocalization.dataset)) { d.labelsLocalization.dataset = this.toObject( d.labelsLocalization.dataset, @@ -170,4 +177,9 @@ export class AdminConfigEditComponent implements OnInit { return d; } + ngOnDestroy() { + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + } } diff --git a/src/app/app-routing/admin.guard.ts b/src/app/app-routing/admin.guard.ts index 68a10f25c1..de14d1be0e 100644 --- a/src/app/app-routing/admin.guard.ts +++ b/src/app/app-routing/admin.guard.ts @@ -7,13 +7,9 @@ import { UrlTree, } from "@angular/router"; import { Store } from "@ngrx/store"; -import { combineLatest, Observable } from "rxjs"; -import { filter, map, switchMap, take, tap } from "rxjs/operators"; -import { - selectIsAdmin, - selectIsLoggedIn, - selectIsLoggingIn, -} from "state-management/selectors/user.selectors"; +import { Observable } from "rxjs"; +import { filter, map, take } from "rxjs/operators"; +import { selectAdminGuardViewModel } from "state-management/selectors/user.selectors"; /** * Ensure that the current user is admin @@ -38,22 +34,16 @@ export class AdminGuard implements CanActivate { route: ActivatedRouteSnapshot, state: RouterStateSnapshot, ): Observable { - return combineLatest([ - this.store.select(selectIsLoggingIn), - this.store.select(selectIsLoggedIn), - this.store.select(selectIsAdmin), - ]).pipe( - filter(([isLoggingIn]) => { - return !isLoggingIn; - }), + return this.store.select(selectAdminGuardViewModel).pipe( + filter((vm) => !vm.isLoggingIn), take(1), - map(([, isLoggedIn, isAdmin]) => { - if (!isLoggedIn) { + map((vm) => { + if (!vm.isLoggedIn) { return this.router.createUrlTree(["/login"], { queryParams: { url: state.url }, }); } - return isAdmin + return vm.isAdmin ? true : this.router.createUrlTree(["/401"], { queryParams: { url: state.url }, diff --git a/src/app/state-management/effects/admin.effects.ts b/src/app/state-management/effects/admin.effects.ts index 9e30f0fd97..d2217f0f03 100644 --- a/src/app/state-management/effects/admin.effects.ts +++ b/src/app/state-management/effects/admin.effects.ts @@ -19,11 +19,6 @@ export interface AdminConfiguration { @Injectable() export class AdminEffects { - constructor( - private actions$: Actions, - private runtimeConfigService: RuntimeConfigService, - ) {} - loadConfiguration$ = createEffect(() => this.actions$.pipe( ofType(loadConfiguration), @@ -57,4 +52,8 @@ export class AdminEffects { }), ), ); + constructor( + private actions$: Actions, + private runtimeConfigService: RuntimeConfigService, + ) {} } diff --git a/src/app/state-management/selectors/user.selectors.ts b/src/app/state-management/selectors/user.selectors.ts index b32a9e63e3..34013309ae 100644 --- a/src/app/state-management/selectors/user.selectors.ts +++ b/src/app/state-management/selectors/user.selectors.ts @@ -134,6 +134,17 @@ export const selectUserSettingsPageViewModel = createSelector( }), ); +export const selectAdminGuardViewModel = createSelector( + selectIsLoggingIn, + selectIsLoggedIn, + selectIsAdmin, + (isLoggingIn, isLoggedIn, isAdmin) => ({ + isLoggingIn, + isLoggedIn, + isAdmin, + }), +); + export const selectHasFetchedSettings = createSelector( selectUserState, (state) => state.hasFetchedSettings, From 8b360d27c5ecbea0a5a5efbfe22cce483c2e8182 Mon Sep 17 00:00:00 2001 From: junjiequan Date: Tue, 6 Jan 2026 18:07:42 +0100 Subject: [PATCH 12/17] change naming of admin state to runtimeConfig state --- .../admin-config-edit.component.ts | 12 ++--- .../admin-dashboard.component.ts | 47 +++++++++++-------- src/app/admin/admin.module.ts | 8 ++-- .../state-management/actions/admin.action.ts | 25 ---------- ....spec.ts => runtime-config.action.spec.ts} | 0 .../actions/runtime-config.action.ts | 28 +++++++++++ ...spec.ts => runtime-config.effects.spec.ts} | 0 ...n.effects.ts => runtime-config.effects.ts} | 20 ++++---- .../reducers/admin.reducer.spec.ts | 0 .../reducers/admin.reducer.ts | 32 ------------- .../reducers/runtime-config.reducer.spec.ts | 27 +++++++++++ .../reducers/runtime-config.reducer.ts | 35 ++++++++++++++ .../selectors/admin.selectors.spec.ts | 0 .../selectors/admin.selectors.ts | 9 ---- .../runtime-config.selectors.spec.ts | 23 +++++++++ .../selectors/runtime-config.selectors.ts | 10 ++++ src/app/state-management/state/admin.store.ts | 7 --- .../state/runtimeConfig.store.ts | 7 +++ 18 files changed, 176 insertions(+), 114 deletions(-) delete mode 100644 src/app/state-management/actions/admin.action.ts rename src/app/state-management/actions/{admin.action.spec.ts => runtime-config.action.spec.ts} (100%) create mode 100644 src/app/state-management/actions/runtime-config.action.ts rename src/app/state-management/effects/{admin.effects.spec.ts => runtime-config.effects.spec.ts} (100%) rename src/app/state-management/effects/{admin.effects.ts => runtime-config.effects.ts} (72%) delete mode 100644 src/app/state-management/reducers/admin.reducer.spec.ts delete mode 100644 src/app/state-management/reducers/admin.reducer.ts create mode 100644 src/app/state-management/reducers/runtime-config.reducer.spec.ts create mode 100644 src/app/state-management/reducers/runtime-config.reducer.ts delete mode 100644 src/app/state-management/selectors/admin.selectors.spec.ts delete mode 100644 src/app/state-management/selectors/admin.selectors.ts create mode 100644 src/app/state-management/selectors/runtime-config.selectors.spec.ts create mode 100644 src/app/state-management/selectors/runtime-config.selectors.ts delete mode 100644 src/app/state-management/state/admin.store.ts create mode 100644 src/app/state-management/state/runtimeConfig.store.ts diff --git a/src/app/admin/admin-config-edit/admin-config-edit.component.ts b/src/app/admin/admin-config-edit/admin-config-edit.component.ts index df4f0f3e2e..70eb557eed 100644 --- a/src/app/admin/admin-config-edit/admin-config-edit.component.ts +++ b/src/app/admin/admin-config-edit/admin-config-edit.component.ts @@ -1,10 +1,7 @@ import { Component, OnInit } from "@angular/core"; import { Store } from "@ngrx/store"; -import { - loadConfiguration, - updateConfiguration, -} from "state-management/actions/admin.action"; -import { selectConfig } from "state-management/selectors/admin.selectors"; +import { updateConfiguration } from "state-management/actions/runtime-config.action"; +import { selectConfig } from "state-management/selectors/runtime-config.selectors"; import schema from "../schema/frontend.config.jsonforms.json"; import { angularMaterialRenderers } from "@jsonforms/angular-material"; import { @@ -62,7 +59,6 @@ export class AdminConfigEditComponent implements OnInit { ) {} ngOnInit(): void { - this.store.dispatch(loadConfiguration()); this.subscriptions.push( this.data$.pipe(take(1)).subscribe((d) => (this.currentData = d)), ); @@ -75,7 +71,9 @@ export class AdminConfigEditComponent implements OnInit { save() { const apiData = this.toApiData(this.currentData); - this.store.dispatch(updateConfiguration({ config: apiData })); + this.store.dispatch( + updateConfiguration({ id: "frontendConfig", config: apiData }), + ); } jsonPreview() { diff --git a/src/app/admin/admin-dashboard/admin-dashboard.component.ts b/src/app/admin/admin-dashboard/admin-dashboard.component.ts index edb64ba9bd..48de8e8bc5 100644 --- a/src/app/admin/admin-dashboard/admin-dashboard.component.ts +++ b/src/app/admin/admin-dashboard/admin-dashboard.component.ts @@ -1,8 +1,9 @@ import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; import { ActivatedRoute, IsActiveMatchOptions } from "@angular/router"; -import { UsersService } from "@scicatproject/scicat-sdk-ts-angular"; +import { Store } from "@ngrx/store"; import { AppConfigService } from "app-config.service"; +import { loadConfiguration } from "state-management/actions/runtime-config.action"; enum TAB { configuration = "Configuration", usersList = "Users List", @@ -14,7 +15,6 @@ enum TAB { standalone: false, }) export class AdminDashboardComponent implements OnInit { - showError = false; navLinks: { location: string; label: string; @@ -30,15 +30,16 @@ export class AdminDashboardComponent implements OnInit { }; fetchDataActions: { [tab: string]: { action: any; loaded: boolean } } = { - [TAB.configuration]: { action: "", loaded: false }, - [TAB.usersList]: { action: "", loaded: false }, + [TAB.configuration]: { action: loadConfiguration, loaded: false }, + // [TAB.usersList]: { action: "", loaded: false }, }; constructor( public appConfigService: AppConfigService, private cdRef: ChangeDetectorRef, private route: ActivatedRoute, - private userService: UsersService, + private store: Store, + public dialog: MatDialog, ) {} @@ -47,35 +48,43 @@ export class AdminDashboardComponent implements OnInit { { location: "./configuration", label: TAB.configuration, - icon: "menu", - enabled: true, - }, - { - location: "./usersList", - label: TAB.usersList, - icon: "data_object", + icon: "settings", enabled: true, }, + // { + // location: "./usersList", + // label: TAB.usersList, + // icon: "people", + // enabled: true, + // }, ]; + + this.route.firstChild?.url + .subscribe((childUrl) => { + const tab = childUrl.length === 1 ? childUrl[0].path : "configuration"; + this.fetchDataForTab(TAB[tab]); + }) + .unsubscribe(); } onTabSelected(tab: string) { this.fetchDataForTab(tab); } + fetchDataForTab(tab: string) { if (tab in this.fetchDataActions) { switch (tab) { case TAB.configuration: + const { action, loaded } = this.fetchDataActions[tab]; + if (!loaded) { + this.fetchDataActions[tab].loaded = true; + this.store.dispatch(action({ id: "frontendConfig" })); + } break; case TAB.usersList: break; - default: { - // const { action, loaded } = this.fetchDataActions[tab]; - // if (!loaded) { - // this.fetchDataActions[tab].loaded = true; - // this.store.dispatch(action(args)); - // } - } + default: + break; } } } diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index 5d48358cb5..ac1898ddbd 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -6,9 +6,9 @@ import { MatTabsModule } from "@angular/material/tabs"; import { RouterModule } from "@angular/router"; import { SharedScicatFrontendModule } from "shared/shared.module"; import { EffectsModule } from "@ngrx/effects"; -import { AdminEffects } from "state-management/effects/admin.effects"; +import { RunTimeConfigEffects } from "state-management/effects/runtime-config.effects"; import { StoreModule } from "@ngrx/store"; -import { adminReducer } from "state-management/reducers/admin.reducer"; +import { runtimeConfigReducer } from "state-management/reducers/runtime-config.reducer"; import { AdminConfigEditComponent } from "./admin-config-edit/admin-config-edit.component"; import { AdminUserlistViewComponent } from "./admin-userlist-view/admin-userlist-view.component"; @@ -19,8 +19,8 @@ import { AdminUserlistViewComponent } from "./admin-userlist-view/admin-userlist MatTabsModule, MatIconModule, SharedScicatFrontendModule, - EffectsModule.forFeature([AdminEffects]), - StoreModule.forFeature("admin", adminReducer), + EffectsModule.forFeature([RunTimeConfigEffects]), + StoreModule.forFeature("runtimeConfig", runtimeConfigReducer), ], declarations: [ AdminDashboardComponent, diff --git a/src/app/state-management/actions/admin.action.ts b/src/app/state-management/actions/admin.action.ts deleted file mode 100644 index c8e9a0a49b..0000000000 --- a/src/app/state-management/actions/admin.action.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createAction, props } from "@ngrx/store"; -import { AdminConfiguration } from "state-management/effects/admin.effects"; - -export const loadConfiguration = createAction("[Admin] Load Configuration"); -export const loadConfigurationSuccess = createAction( - "[Admin] Load Configuration Success", - props<{ config: AdminConfiguration }>(), -); -export const loadConfigurationFailure = createAction( - "[Admin] Load Configuration Failure", - props<{ error: any }>(), -); - -export const updateConfiguration = createAction( - "[Admin] Update Configuration", - props<{ config: Partial }>(), -); -export const updateConfigurationSuccess = createAction( - "[Admin] Update Configuration Success", - props<{ config: AdminConfiguration }>(), -); -export const updateConfigurationFailure = createAction( - "[Admin] Update Configuration Failure", - props<{ error: any }>(), -); diff --git a/src/app/state-management/actions/admin.action.spec.ts b/src/app/state-management/actions/runtime-config.action.spec.ts similarity index 100% rename from src/app/state-management/actions/admin.action.spec.ts rename to src/app/state-management/actions/runtime-config.action.spec.ts diff --git a/src/app/state-management/actions/runtime-config.action.ts b/src/app/state-management/actions/runtime-config.action.ts new file mode 100644 index 0000000000..5f83df5c71 --- /dev/null +++ b/src/app/state-management/actions/runtime-config.action.ts @@ -0,0 +1,28 @@ +import { createAction, props } from "@ngrx/store"; +import { Configuration } from "state-management/effects/runtime-config.effects"; + +export const loadConfiguration = createAction( + "[RunTimeConfig] Load Configuration", + props<{ id: string }>(), +); +export const loadConfigurationSuccess = createAction( + "[RunTimeConfig] Load Configuration Success", + props<{ config: Configuration }>(), +); +export const loadConfigurationFailure = createAction( + "[RunTimeConfig] Load Configuration Failure", + props<{ error: any }>(), +); + +export const updateConfiguration = createAction( + "[RunTimeConfig] Update Configuration", + props<{ id: string; config: Partial }>(), +); +export const updateConfigurationSuccess = createAction( + "[RunTimeConfig] Update Configuration Success", + props<{ config: Configuration }>(), +); +export const updateConfigurationFailure = createAction( + "[RunTimeConfig] Update Configuration Failure", + props<{ error: any }>(), +); diff --git a/src/app/state-management/effects/admin.effects.spec.ts b/src/app/state-management/effects/runtime-config.effects.spec.ts similarity index 100% rename from src/app/state-management/effects/admin.effects.spec.ts rename to src/app/state-management/effects/runtime-config.effects.spec.ts diff --git a/src/app/state-management/effects/admin.effects.ts b/src/app/state-management/effects/runtime-config.effects.ts similarity index 72% rename from src/app/state-management/effects/admin.effects.ts rename to src/app/state-management/effects/runtime-config.effects.ts index d2217f0f03..becb6421a3 100644 --- a/src/app/state-management/effects/admin.effects.ts +++ b/src/app/state-management/effects/runtime-config.effects.ts @@ -9,25 +9,24 @@ import { updateConfiguration, updateConfigurationFailure, updateConfigurationSuccess, -} from "state-management/actions/admin.action"; +} from "state-management/actions/runtime-config.action"; import { RuntimeConfigService } from "@scicatproject/scicat-sdk-ts-angular"; // Types -export interface AdminConfiguration { +export interface Configuration { [key: string]: any; } @Injectable() -export class AdminEffects { +export class RunTimeConfigEffects { loadConfiguration$ = createEffect(() => this.actions$.pipe( ofType(loadConfiguration), - switchMap(() => { - const configName = "frontendConfig"; + switchMap(({ id }) => { return this.runtimeConfigService - .runtimeConfigControllerGetConfigV3(configName) + .runtimeConfigControllerGetConfigV3(id) .pipe( - map((config: AdminConfiguration) => { + map((config: Configuration) => { return loadConfigurationSuccess({ config }); }), catchError((error) => of(loadConfigurationFailure({ error }))), @@ -39,12 +38,11 @@ export class AdminEffects { updateConfiguration$ = createEffect(() => this.actions$.pipe( ofType(updateConfiguration), - exhaustMap(({ config }) => { - const configName = "frontendConfig"; + exhaustMap(({ id, config }) => { return this.runtimeConfigService - .runtimeConfigControllerUpdateConfigV3(configName, config) + .runtimeConfigControllerUpdateConfigV3(id, config) .pipe( - map((config: AdminConfiguration) => + map((config: Configuration) => updateConfigurationSuccess({ config }), ), catchError((error) => of(updateConfigurationFailure({ error }))), diff --git a/src/app/state-management/reducers/admin.reducer.spec.ts b/src/app/state-management/reducers/admin.reducer.spec.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/state-management/reducers/admin.reducer.ts b/src/app/state-management/reducers/admin.reducer.ts deleted file mode 100644 index 269ebcfaf9..0000000000 --- a/src/app/state-management/reducers/admin.reducer.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createReducer, Action, on } from "@ngrx/store"; -import * as fromActions from "state-management/actions/admin.action"; -import { - AdminState, - initialAdminState, -} from "state-management/state/admin.store"; - -const reducer = createReducer( - initialAdminState, - on( - fromActions.loadConfigurationSuccess, - (state, { config }): AdminState => ({ - ...state, - config, - }), - ), - - on( - fromActions.updateConfigurationSuccess, - (state, { config }): AdminState => ({ - ...state, - config, - }), - ), -); - -export const adminReducer = (state: AdminState | undefined, action: Action) => { - if (action.type.indexOf("[Admin]") !== -1) { - console.log("Action came in! " + action.type); - } - return reducer(state, action); -}; diff --git a/src/app/state-management/reducers/runtime-config.reducer.spec.ts b/src/app/state-management/reducers/runtime-config.reducer.spec.ts new file mode 100644 index 0000000000..4cef58bef9 --- /dev/null +++ b/src/app/state-management/reducers/runtime-config.reducer.spec.ts @@ -0,0 +1,27 @@ +import { runtimeConfigReducer } from "./runtime-config.reducer"; +import { initialRuntimeConfigState } from "state-management/state/runtimeConfig.store"; +import * as fromActions from "state-management/actions/runtime-config.action"; + +describe("RuntimeConfigReducer", () => { + describe("on loadConfigurationSuccess", () => { + it("should set config", () => { + const config = { siteTitle: "SciCat", policiesEnabled: true } as any; + + const action = fromActions.loadConfigurationSuccess({ config }); + const state = runtimeConfigReducer(initialRuntimeConfigState, action); + + expect(state.config).toEqual(config); + }); + }); + + describe("on updateConfigurationSuccess", () => { + it("should set config", () => { + const config = { siteTitle: "Updated", jobsEnabled: false } as any; + + const action = fromActions.updateConfigurationSuccess({ config }); + const state = runtimeConfigReducer(initialRuntimeConfigState, action); + + expect(state.config).toEqual(config); + }); + }); +}); diff --git a/src/app/state-management/reducers/runtime-config.reducer.ts b/src/app/state-management/reducers/runtime-config.reducer.ts new file mode 100644 index 0000000000..02c8cf12fa --- /dev/null +++ b/src/app/state-management/reducers/runtime-config.reducer.ts @@ -0,0 +1,35 @@ +import { createReducer, Action, on } from "@ngrx/store"; +import * as fromActions from "state-management/actions/runtime-config.action"; +import { + RuntimeConfigState, + initialRuntimeConfigState, +} from "state-management/state/runtimeConfig.store"; + +const reducer = createReducer( + initialRuntimeConfigState, + on( + fromActions.loadConfigurationSuccess, + (state, { config }): RuntimeConfigState => ({ + ...state, + config, + }), + ), + + on( + fromActions.updateConfigurationSuccess, + (state, { config }): RuntimeConfigState => ({ + ...state, + config, + }), + ), +); + +export const runtimeConfigReducer = ( + state: RuntimeConfigState | undefined, + action: Action, +) => { + if (action.type.indexOf("[RunTimeConfig]") !== -1) { + console.log("Action came in! " + action.type); + } + return reducer(state, action); +}; diff --git a/src/app/state-management/selectors/admin.selectors.spec.ts b/src/app/state-management/selectors/admin.selectors.spec.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/state-management/selectors/admin.selectors.ts b/src/app/state-management/selectors/admin.selectors.ts deleted file mode 100644 index d8a0c77be6..0000000000 --- a/src/app/state-management/selectors/admin.selectors.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createFeatureSelector, createSelector } from "@ngrx/store"; -import { AdminState } from "state-management/state/admin.store"; - -const selectAdminState = createFeatureSelector("admin"); - -export const selectConfig = createSelector( - selectAdminState, - (state) => state.config, -); diff --git a/src/app/state-management/selectors/runtime-config.selectors.spec.ts b/src/app/state-management/selectors/runtime-config.selectors.spec.ts new file mode 100644 index 0000000000..0d890f3bf9 --- /dev/null +++ b/src/app/state-management/selectors/runtime-config.selectors.spec.ts @@ -0,0 +1,23 @@ +import * as fromSelectors from "./runtime-config.selectors"; +import { + initialRuntimeConfigState, + RuntimeConfigState, +} from "state-management/state/runtimeConfig.store"; + +describe("selectConfig", () => { + it("should return config object", () => { + const state: RuntimeConfigState = { + config: { siteTitle: "SciCat" }, + }; + + expect(fromSelectors.selectConfig.projector(state)).toEqual({ + siteTitle: "SciCat", + }); + }); + + it("should return empty object for initial state", () => { + expect( + fromSelectors.selectConfig.projector(initialRuntimeConfigState), + ).toEqual({}); + }); +}); diff --git a/src/app/state-management/selectors/runtime-config.selectors.ts b/src/app/state-management/selectors/runtime-config.selectors.ts new file mode 100644 index 0000000000..6e5f6731f0 --- /dev/null +++ b/src/app/state-management/selectors/runtime-config.selectors.ts @@ -0,0 +1,10 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; +import { RuntimeConfigState } from "state-management/state/runtimeConfig.store"; + +const selectRunTimeConfigState = + createFeatureSelector("runtimeConfig"); + +export const selectConfig = createSelector( + selectRunTimeConfigState, + (state) => state.config, +); diff --git a/src/app/state-management/state/admin.store.ts b/src/app/state-management/state/admin.store.ts deleted file mode 100644 index a788f5cab7..0000000000 --- a/src/app/state-management/state/admin.store.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface AdminState { - config: any; -} - -export const initialAdminState: AdminState = { - config: {}, -}; diff --git a/src/app/state-management/state/runtimeConfig.store.ts b/src/app/state-management/state/runtimeConfig.store.ts new file mode 100644 index 0000000000..628d71c41a --- /dev/null +++ b/src/app/state-management/state/runtimeConfig.store.ts @@ -0,0 +1,7 @@ +export interface RuntimeConfigState { + config: Record; +} + +export const initialRuntimeConfigState: RuntimeConfigState = { + config: {}, +}; From ea319ab0ebfd5d95ff9c3f4fc98e2b5a9292ecfa Mon Sep 17 00:00:00 2001 From: junjiequan Date: Tue, 6 Jan 2026 18:22:56 +0100 Subject: [PATCH 13/17] fix es lint --- .../admin-config-edit.component.ts | 4 +-- .../admin-dashboard.component.ts | 3 +- .../admin-userlist-view.component.ts | 2 +- .../effects/runtime-config.effects.ts | 33 +++++++++---------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/app/admin/admin-config-edit/admin-config-edit.component.ts b/src/app/admin/admin-config-edit/admin-config-edit.component.ts index 70eb557eed..3fffd993f1 100644 --- a/src/app/admin/admin-config-edit/admin-config-edit.component.ts +++ b/src/app/admin/admin-config-edit/admin-config-edit.component.ts @@ -114,7 +114,7 @@ export class AdminConfigEditComponent implements OnInit { } // TODO: temp conversion functions, to be removed later - private toFormData(data: any) { + toFormData(data: any) { const d = structuredClone(data); // Convert dynamic object to array with key and value properties @@ -145,7 +145,7 @@ export class AdminConfigEditComponent implements OnInit { } // TODO: temp conversion functions, to be removed later - private toApiData(data: any) { + toApiData(data: any) { const d = structuredClone(data); // Convert array with key and value properties to dynamic object diff --git a/src/app/admin/admin-dashboard/admin-dashboard.component.ts b/src/app/admin/admin-dashboard/admin-dashboard.component.ts index 48de8e8bc5..8b23d5ddcb 100644 --- a/src/app/admin/admin-dashboard/admin-dashboard.component.ts +++ b/src/app/admin/admin-dashboard/admin-dashboard.component.ts @@ -74,13 +74,14 @@ export class AdminDashboardComponent implements OnInit { fetchDataForTab(tab: string) { if (tab in this.fetchDataActions) { switch (tab) { - case TAB.configuration: + case TAB.configuration: { const { action, loaded } = this.fetchDataActions[tab]; if (!loaded) { this.fetchDataActions[tab].loaded = true; this.store.dispatch(action({ id: "frontendConfig" })); } break; + } case TAB.usersList: break; default: diff --git a/src/app/admin/admin-userlist-view/admin-userlist-view.component.ts b/src/app/admin/admin-userlist-view/admin-userlist-view.component.ts index e246459ee8..a44f0be4d0 100644 --- a/src/app/admin/admin-userlist-view/admin-userlist-view.component.ts +++ b/src/app/admin/admin-userlist-view/admin-userlist-view.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; @Component({ selector: "admin-userlist-view", diff --git a/src/app/state-management/effects/runtime-config.effects.ts b/src/app/state-management/effects/runtime-config.effects.ts index becb6421a3..b3ca9577be 100644 --- a/src/app/state-management/effects/runtime-config.effects.ts +++ b/src/app/state-management/effects/runtime-config.effects.ts @@ -14,29 +14,26 @@ import { RuntimeConfigService } from "@scicatproject/scicat-sdk-ts-angular"; // Types export interface Configuration { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @Injectable() export class RunTimeConfigEffects { - loadConfiguration$ = createEffect(() => - this.actions$.pipe( + loadConfiguration$ = createEffect(() => { + return this.actions$.pipe( ofType(loadConfiguration), - switchMap(({ id }) => { - return this.runtimeConfigService - .runtimeConfigControllerGetConfigV3(id) - .pipe( - map((config: Configuration) => { - return loadConfigurationSuccess({ config }); - }), - catchError((error) => of(loadConfigurationFailure({ error }))), - ); - }), - ), - ); + switchMap(({ id }) => + this.runtimeConfigService.runtimeConfigControllerGetConfigV3(id).pipe( + map((config: Configuration) => loadConfigurationSuccess({ config })), + catchError((error) => of(loadConfigurationFailure({ error }))), + ), + ), + ); + }); - updateConfiguration$ = createEffect(() => - this.actions$.pipe( + updateConfiguration$ = createEffect(() => { + return this.actions$.pipe( ofType(updateConfiguration), exhaustMap(({ id, config }) => { return this.runtimeConfigService @@ -48,8 +45,8 @@ export class RunTimeConfigEffects { catchError((error) => of(updateConfigurationFailure({ error }))), ); }), - ), - ); + ); + }); constructor( private actions$: Actions, private runtimeConfigService: RuntimeConfigService, From 34d61fb74f6972d4aa2aea16eb91e6e0312ccd1a Mon Sep 17 00:00:00 2001 From: junjiequan Date: Tue, 6 Jan 2026 18:28:16 +0100 Subject: [PATCH 14/17] update comments for temporary conversion functions --- .../admin-config-edit.component.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/app/admin/admin-config-edit/admin-config-edit.component.ts b/src/app/admin/admin-config-edit/admin-config-edit.component.ts index 3fffd993f1..5511f0bf44 100644 --- a/src/app/admin/admin-config-edit/admin-config-edit.component.ts +++ b/src/app/admin/admin-config-edit/admin-config-edit.component.ts @@ -101,24 +101,28 @@ export class AdminConfigEditComponent implements OnInit { URL.revokeObjectURL(url); } - // TODO: temp conversion functions, to be removed later + /////////////////////////////////////////////////////// + // NOTE: below are temporary conversion functions + // it converts dynamic object to array with key and value properties + // and vice versa for jsonforms compatibility + // Should be removed when config values are fixed to use only arrays + /////////////////////////////////////////////////////// + toArray(obj: any) { if (!obj) return []; return Object.entries(obj).map(([key, value]) => ({ key, value })); } - // TODO: temp conversion functions, to be removed later + // TODO: to be removed toObject(arr: any) { if (!arr?.length) return {}; return Object.fromEntries(arr.map((i) => [i.key, i.value])); } - // TODO: temp conversion functions, to be removed later + // TODO: to be removed toFormData(data: any) { const d = structuredClone(data); - // Convert dynamic object to array with key and value properties - if (d.labelsLocalization.dataset) { d.labelsLocalization.dataset = this.toArray(d.labelsLocalization.dataset); } @@ -144,11 +148,10 @@ export class AdminConfigEditComponent implements OnInit { return d; } - // TODO: temp conversion functions, to be removed later + // TODO: to be removed toApiData(data: any) { const d = structuredClone(data); - // Convert array with key and value properties to dynamic object d.labelsLocalization = d.labelsLocalization ?? { dataset: {}, proposal: {}, @@ -175,6 +178,7 @@ export class AdminConfigEditComponent implements OnInit { return d; } + ngOnDestroy() { this.subscriptions.forEach((subscription) => { subscription.unsubscribe(); From fd95985d774f538e288ff0aabbd782bc1cc033e8 Mon Sep 17 00:00:00 2001 From: junjiequan Date: Wed, 7 Jan 2026 10:57:35 +0100 Subject: [PATCH 15/17] show admin settings button only to admin user --- src/app/_layout/app-header/app-header.component.html | 7 ++++++- src/app/_layout/app-header/app-header.component.ts | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/_layout/app-header/app-header.component.html b/src/app/_layout/app-header/app-header.component.html index c1b63813e5..547bcc3851 100644 --- a/src/app/_layout/app-header/app-header.component.html +++ b/src/app/_layout/app-header/app-header.component.html @@ -80,7 +80,12 @@ - diff --git a/src/app/_layout/app-header/app-header.component.ts b/src/app/_layout/app-header/app-header.component.ts index 4b8a5a7a0e..836b9f07ca 100644 --- a/src/app/_layout/app-header/app-header.component.ts +++ b/src/app/_layout/app-header/app-header.component.ts @@ -7,6 +7,7 @@ import { selectIsLoggedIn, selectCurrentUserName, selectThumbnailPhoto, + selectIsAdmin, } from "state-management/selectors/user.selectors"; import { selectDatasetsInBatchIndicator } from "state-management/selectors/datasets.selectors"; import { @@ -42,6 +43,8 @@ export class AppHeaderComponent implements OnInit { profileImage$ = this.store.select(selectThumbnailPhoto); inBatchIndicator$ = this.store.select(selectDatasetsInBatchIndicator); isLoggedIn$ = this.store.select(selectIsLoggedIn); + isAdmin$ = this.store.select(selectIsAdmin); + mainMenuConfig$: Observable; defaultMainPage$: Observable; siteHeaderLogoUrl$: Observable; From e27df8b84ef0c64ed663945f7e039d160550ed83 Mon Sep 17 00:00:00 2001 From: junjiequan Date: Thu, 8 Jan 2026 15:43:41 +0100 Subject: [PATCH 16/17] remove additionalProperties from jsonforms schema --- src/app/admin/schema/frontend.config.jsonforms.json | 3 +-- src/app/app-config.service.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/admin/schema/frontend.config.jsonforms.json b/src/app/admin/schema/frontend.config.jsonforms.json index e4f4098b7c..876f794600 100644 --- a/src/app/admin/schema/frontend.config.jsonforms.json +++ b/src/app/admin/schema/frontend.config.jsonforms.json @@ -76,8 +76,7 @@ "type": "string" } }, - "required": ["option", "tooltip"], - "additionalProperties": false + "required": ["option", "tooltip"] } }, "riotBaseUrl": { "type": "string" }, diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index 1ac1f7b6d9..aa8bb4f124 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -220,7 +220,7 @@ export class AppConfigService { async loadAppConfig(): Promise { try { const res = await this.http - .get("/api/v3/runtime-config/data/frontendConfig") + .get("/api/v3/runtime-config/frontendConfig") .pipe(timeout(2000)) .toPromise(); From 0b4114d89dcae30243b5376d73eab568ba66ccfc Mon Sep 17 00:00:00 2001 From: junjiequan Date: Fri, 9 Jan 2026 10:02:16 +0100 Subject: [PATCH 17/17] replace deprecated toPromise() with firstValueFrom --- .../schema/frontend.config.jsonforms.json | 39 ++++++------------- src/app/app-config.service.ts | 9 ++--- src/app/app-theme.service.ts | 16 ++++---- 3 files changed, 23 insertions(+), 41 deletions(-) diff --git a/src/app/admin/schema/frontend.config.jsonforms.json b/src/app/admin/schema/frontend.config.jsonforms.json index 876f794600..d2d53dcf79 100644 --- a/src/app/admin/schema/frontend.config.jsonforms.json +++ b/src/app/admin/schema/frontend.config.jsonforms.json @@ -401,17 +401,12 @@ }, "elements": [ { - "type": "HorizontalLayout", - "elements": [ - { - "type": "Control", - "scope": "#/properties/defaultMainPage/properties/nonAuthenticatedUser" - }, - { - "type": "Control", - "scope": "#/properties/defaultMainPage/properties/authenticatedUser" - } - ] + "type": "Control", + "scope": "#/properties/defaultMainPage/properties/nonAuthenticatedUser" + }, + { + "type": "Control", + "scope": "#/properties/defaultMainPage/properties/authenticatedUser" } ] }, @@ -505,13 +500,8 @@ "expandable": true }, "elements": [ - { - "type": "HorizontalLayout", - "elements": [ - { "type": "Control", "scope": "#/properties/sftpHost" }, - { "type": "Control", "scope": "#/properties/sourceFolder" } - ] - } + { "type": "Control", "scope": "#/properties/sftpHost" }, + { "type": "Control", "scope": "#/properties/sourceFolder" } ] }, { @@ -522,15 +512,10 @@ }, "elements": [ { - "type": "HorizontalLayout", - "elements": [ - { - "type": "Control", - "scope": "#/properties/maxDirectDownloadSize" - }, - { "type": "Control", "scope": "#/properties/maxFileSizeWarning" } - ] - } + "type": "Control", + "scope": "#/properties/maxDirectDownloadSize" + }, + { "type": "Control", "scope": "#/properties/maxFileSizeWarning" } ] }, { diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index aa8bb4f124..7902279180 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -1,6 +1,5 @@ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; -import { OutputRuntimeConfigDto } from "@scicatproject/scicat-sdk-ts-angular"; import { mergeWith } from "lodash-es"; import { firstValueFrom, of } from "rxjs"; import { catchError, timeout } from "rxjs/operators"; @@ -219,12 +218,10 @@ export class AppConfigService { async loadAppConfig(): Promise { try { - const res = await this.http - .get("/api/v3/runtime-config/frontendConfig") - .pipe(timeout(2000)) - .toPromise(); + const config = await firstValueFrom( + this.http.get("/api/v3/admin/config").pipe(timeout(2000)), + ); - const config = (res as OutputRuntimeConfigDto).data; this.appConfig = Object.assign({}, this.appConfig, config); } catch (err) { console.log("No config available in backend, trying with local config."); diff --git a/src/app/app-theme.service.ts b/src/app/app-theme.service.ts index 9276f02998..b4fbddeea8 100644 --- a/src/app/app-theme.service.ts +++ b/src/app/app-theme.service.ts @@ -1,5 +1,6 @@ import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { timeout } from "rxjs/operators"; import { light, Theme } from "theme"; @@ -11,16 +12,15 @@ export class AppThemeService { async loadTheme(): Promise { try { - this.activeTheme = (await this.http - .get("/api/v3/admin/theme") - .pipe(timeout(2000)) - .toPromise()) as Theme; + this.activeTheme = await firstValueFrom( + this.http.get("/api/v3/admin/theme").pipe(timeout(2000)), + ); } catch (err) { - console.log("No theme available in backend, using local theme."); - this.activeTheme = (await this.http - .get("/assets/theme.json") - .toPromise()) as Theme; + this.activeTheme = await firstValueFrom( + this.http.get("/assets/theme.json").pipe(timeout(2000)), + ); } + this.setActiveTheme(); }