Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/app/_layout/app-header/app-header.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@
</mat-menu>

<mat-menu #userMenu="matMenu">
<button
*ngIf="isAdmin$ | async"
mat-menu-item
routerLink="/admin"
data-cy="admin-button"
>
<mat-icon> build</mat-icon>
<span> Admin Settings <mat-chip class="beta-badge">Beta</mat-chip></span>
</button>

<button mat-menu-item routerLink="/user/" data-cy="setting-button">
<mat-icon> settings</mat-icon>
<span>Settings</span>
Expand Down
7 changes: 7 additions & 0 deletions src/app/_layout/app-header/app-header.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@
}
}

.beta-badge {
height: 1.25rem;
font-size: 0.75rem;
margin-left: 0.5rem;
background-color: var(--theme-header-3-default);
}

@media only screen and (max-width: 1279px) {
.large-screen-text {
display: none;
Expand Down
3 changes: 3 additions & 0 deletions src/app/_layout/app-header/app-header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
selectIsLoggedIn,
selectCurrentUserName,
selectThumbnailPhoto,
selectIsAdmin,
} from "state-management/selectors/user.selectors";
import { selectDatasetsInBatchIndicator } from "state-management/selectors/datasets.selectors";
import {
Expand Down Expand Up @@ -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<MainMenuOptions>;
defaultMainPage$: Observable<string>;
siteHeaderLogoUrl$: Observable<string>;
Expand Down
2 changes: 2 additions & 0 deletions src/app/_layout/layout.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AppMainLayoutComponent } from "./app-main-layout/app-main-layout.compon
import { BatchCardModule } from "datasets/batch-card/batch-card.module";
import { BreadcrumbModule } from "shared/modules/breadcrumb/breadcrumb.module";
import { UsersModule } from "../users/users.module";
import { MatChipsModule } from "@angular/material/chips";

@NgModule({
declarations: [
Expand All @@ -26,6 +27,7 @@ import { UsersModule } from "../users/users.module";
MatButtonModule,
MatIconModule,
MatMenuModule,
MatChipsModule,
MatToolbarModule,
RouterModule,
BreadcrumbModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div style="display: flex; justify-content: space-between; margin-bottom: 10px">
<!-- LEFT -->
<div>
<button mat-button color="primary" (click)="jsonPreview()">
Json Preview
</button>
</div>

<!-- RIGHT -->
<div>
<button mat-button color="primary" (click)="export()">Export</button>
<button mat-button color="primary" (click)="save()">Save</button>
</div>
</div>
<div>
<jsonforms
[data]="data$ | async"
[schema]="schema"
[uischema]="uiSchema"
[renderers]="renderers"
(dataChange)="onChange($event)"
></jsonforms>
<div style="display: flex; justify-content: space-between; margin-top: 10px">
<!-- LEFT -->
<div>
<button mat-button color="primary" (click)="jsonPreview()">
Json Preview
</button>
</div>

<!-- RIGHT -->
<div>
<button mat-button color="primary" (click)="export()">Export</button>
<button mat-button color="primary" (click)="save()">Save</button>
</div>
</div>
</div>
Empty file.
187 changes: 187 additions & 0 deletions src/app/admin/admin-config-edit/admin-config-edit.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
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 {
accordionArrayLayoutRendererTester,
AccordionArrayLayoutRendererComponent,
} from "shared/modules/jsonforms-custom-renderers/expand-panel-renderer/accordion-array-layout-renderer.component";
import { map, Subscription, take } 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";
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",
templateUrl: "./admin-config-edit.component.html",
styleUrls: ["./admin-config-edit.component.scss"],
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: AppConfigInterface;
schema: JsonSchema = schema.schema || {};
uiSchema: UISchemaElement = schema.uiSchema;
renderers = [
...angularMaterialRenderers,
{
tester: accordionArrayLayoutRendererTester,
renderer: AccordionArrayLayoutRendererComponent,
},
{
tester: expandGroupTester,
renderer: ExpandGroupRendererComponent,
},
{
tester: arrayLayoutRendererTester,
renderer: ArrayLayoutRendererCustom,
},
];
constructor(
private store: Store,
private dialog: MatDialog,
) {}

ngOnInit(): void {
this.subscriptions.push(
this.data$.pipe(take(1)).subscribe((d) => (this.currentData = d)),
);
}

onChange(event: any) {

Check warning on line 67 in src/app/admin/admin-config-edit/admin-config-edit.component.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
this.currentData = event;
}

save() {
const apiData = this.toApiData(this.currentData);

this.store.dispatch(
updateConfiguration({ id: "frontendConfig", 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);
}

///////////////////////////////////////////////////////
// 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) {

Check warning on line 111 in src/app/admin/admin-config-edit/admin-config-edit.component.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
if (!obj) return [];
return Object.entries(obj).map(([key, value]) => ({ key, value }));
}
// TODO: to be removed
toObject(arr: any) {

Check warning on line 116 in src/app/admin/admin-config-edit/admin-config-edit.component.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
if (!arr?.length) return {};

return Object.fromEntries(arr.map((i) => [i.key, i.value]));
}

// TODO: to be removed
toFormData(data: any) {

Check warning on line 123 in src/app/admin/admin-config-edit/admin-config-edit.component.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
const d = structuredClone(data);

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) => ({

Check warning on line 135 in src/app/admin/admin-config-edit/admin-config-edit.component.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
...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;
}

// TODO: to be removed
toApiData(data: any) {

Check warning on line 152 in src/app/admin/admin-config-edit/admin-config-edit.component.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
const d = structuredClone(data);

d.labelsLocalization = d.labelsLocalization ?? {
dataset: {},
proposal: {},
};
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) => ({

Check warning on line 170 in src/app/admin/admin-config-edit/admin-config-edit.component.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
...a,
variables: Array.isArray(a.variables)
? this.toObject(a.variables)
: a.variables,
inputs: Array.isArray(a.inputs) ? this.toObject(a.inputs) : a.inputs,
}));
}

return d;
}

ngOnDestroy() {

Check warning on line 182 in src/app/admin/admin-config-edit/admin-config-edit.component.ts

View workflow job for this annotation

GitHub Actions / eslint

Lifecycle interface 'OnDestroy' should be implemented for method 'ngOnDestroy'. (https://angular.dev/style-guide#style-09-01)
this.subscriptions.forEach((subscription) => {
subscription.unsubscribe();
});
}
}
23 changes: 23 additions & 0 deletions src/app/admin/admin-dashboard/admin-dashboard.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!-- src/app/admin/admin-dashboard/admin-dashboard.component.html -->

<nav mat-tab-nav-bar [tabPanel]="tabPanel">
<ng-container *ngFor="let link of navLinks">
<a
mat-tab-link
*ngIf="link.enabled"
routerLink="{{ link.location }}"
routerLinkActive
#rla="routerLinkActive"
[routerLinkActiveOptions]="routerLinkActiveOptions"
[active]="rla.isActive"
[replaceUrl]="true"
(click)="onTabSelected(link.label)"
>
<mat-icon>{{ link.icon }}</mat-icon>
<span>{{ link.label }}</span>
</a>
</ng-container>
</nav>
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
Empty file.
Loading
Loading