diff --git a/CI/e2e/docker-compose.e2e.yaml b/CI/e2e/docker-compose.e2e.yaml index b56cd346f1..3311b5ae81 100644 --- a/CI/e2e/docker-compose.e2e.yaml +++ b/CI/e2e/docker-compose.e2e.yaml @@ -33,9 +33,11 @@ services: - "./CI/e2e/frontend.config.e2e.json:/home/node/app/dist/config/frontend.config.json" - "./CI/e2e/frontend.theme.e2e.json:/home/node/app/dist/config/frontend.theme.json" - "./CI/e2e/publishedDataConfig.e2e.json:/home/node/app/publishedDataConfig.json" + - "./CI/e2e/jobconfig.e2e.yaml:/home/node/app/jobconfig.yaml" environment: - DOI_USERNAME=${DOI_USERNAME} - DOI_PASSWORD=${DOI_PASSWORD} + - JOB_CONFIGURATION_FILE=/home/node/app/jobconfig.yaml depends_on: mongodb: condition: service_healthy diff --git a/CI/e2e/jobconfig.e2e.yaml b/CI/e2e/jobconfig.e2e.yaml new file mode 100644 index 0000000000..089a1fa02d --- /dev/null +++ b/CI/e2e/jobconfig.e2e.yaml @@ -0,0 +1,111 @@ +configVersion: v1.0 2024-03-01 6f3f38 +jobs: + - jobType: all_access + create: + auth: "#all" + actions: + - actionType: log + update: + auth: "#all" + - jobType: public_access + create: + auth: "#datasetPublic" + update: + auth: "#all" + - jobType: authenticated_access + create: + auth: "#authenticated" + update: + auth: "#all" + - jobType: dataset_access + create: + auth: "#datasetAccess" + update: + auth: "#jobOwnerGroup" + - jobType: owner_access + create: + auth: "#datasetOwner" + update: + auth: "#jobOwnerUser" + - jobType: user_access + create: + auth: user5.1 + update: + auth: user5.1 + - jobType: group_access + create: + auth: "@group5" + update: + auth: "@group5" + - jobType: job_admin + create: + auth: "#jobAdmin" + update: + auth: "#jobAdmin" + - jobType: archive + create: + auth: "#all" + actions: + - actionType: validate + datasets: + datasetlifecycle.archivable: + const: true + update: + auth: "#all" + - jobType: retrieve + create: + auth: "#all" + actions: + - actionType: validate + datasets: + datasetlifecycle.retrievable: + const: true + update: + auth: "#all" + - jobType: public + create: + auth: "#all" + actions: + - actionType: validate + datasets: + isPublished: + const: true + update: + auth: "#all" + - jobType: validate + create: + auth: admin + actions: + - actionType: validate + request: + jobParams.requiredParam: + type: string + jobParams.arrayOfStrings: + type: array + items: + type: string + datasets: + datasetlifecycle.archivable: + const: true + update: + auth: admin + actions: + - actionType: validate + request: + $: + $schema: http://json-schema.org/draft-07/schema# + required: + - jobResultObject + properties: + jobResultObject: + type: object + required: + - requiredParam + - arrayOfStrings + properties: + requiredParam: + type: string + arrayOfStrings: + type: array + items: + type: string diff --git a/cypress/e2e/jobs/jobs-general.cy.js b/cypress/e2e/jobs/jobs-general.cy.js new file mode 100644 index 0000000000..299c0f0f30 --- /dev/null +++ b/cypress/e2e/jobs/jobs-general.cy.js @@ -0,0 +1,94 @@ +import { testData } from "../../fixtures/testData"; + +describe("Jobs general", () => { + beforeEach(() => { + cy.login(Cypress.env("username"), Cypress.env("password")); + cy.createDataset({ + type: "raw", + dataFileSize: "small", + datasetName: "Jobs Dataset", + pid: "6ED35C17-EDD4-4CD4-B917-4E49698F7532", + isPublished: true, + }); + }); + + afterEach(() => { + cy.removeJobs(); + cy.removeDatasets(); + }); + + describe("Jobs dynamic material table", () => { + it("should be able to search for job in the global search", () => { + cy.createJob(); + + cy.visit("/user/jobs"); + + cy.get('[data-cy="text-search"]').type("all_access"); + cy.get('[data-cy="search-button"]').click(); + + cy.get("mat-table mat-row").first().should("contain", "all_access"); + }); + + it("should be able to change page and page size in the job table", () => { + cy.createJob({ emailJobInitiator: "test1@example.com" }); + cy.createJob({ emailJobInitiator: "test2@example.com" }); + cy.createJob({ emailJobInitiator: "test3@example.com" }); + cy.createJob({ emailJobInitiator: "test4@example.com" }); + cy.createJob({ emailJobInitiator: "test5@example.com" }); + cy.createJob({ emailJobInitiator: "test6@example.com" }); + + cy.visit("/user/jobs"); + + cy.get("mat-paginator").first().find("mat-select").click({ force: true }); + cy.get("mat-option").contains("5").click({ force: true }); + + cy.get("mat-paginator .mat-mdc-paginator-range-actions").contains( + "1 – 5", + ); + + cy.get("mat-paginator").first().find("[aria-label='Next page']").click(); + + cy.get("mat-paginator .mat-mdc-paginator-range-actions").contains( + "6 – 6", + ); + }); + + it("should be able to change visible columns settings in the table", () => { + cy.createJob(); + + cy.visit("/user/jobs"); + + cy.get("dynamic-mat-table mat-header-row.header").should("exist"); + + cy.get("dynamic-mat-table table-menu button").click(); + cy.get('[role="menu"] button').contains("Default setting").click(); + cy.get("body").type("{esc}"); + + cy.get("dynamic-mat-table") + .scrollTo("right", { ensureScrollable: false }) + .get("mat-header-row") + .should("contain", "Initiator"); + + cy.get("dynamic-mat-table table-menu button").click(); + cy.get('[role="menu"] button').contains("Column setting").click(); + + cy.get('[role="menu"]') + .contains("Initiator") + .parent() + .find("input[type=checkbox]") + .uncheck(); + + cy.contains(".column-config-apply button.done-setting", "done").click(); + + cy.get("dynamic-mat-table table-menu button").click(); + cy.get('[role="menu"] button').contains("Save table setting").click(); + + cy.reload(); + + cy.get("dynamic-mat-table") + .scrollTo("right", { ensureScrollable: false }) + .get("mat-header-row") + .should("not.contain", "Initiator"); + }); + }); +}); diff --git a/cypress/fixtures/testData.js b/cypress/fixtures/testData.js index e854094679..323bc20f8d 100644 --- a/cypress/fixtures/testData.js +++ b/cypress/fixtures/testData.js @@ -175,6 +175,16 @@ export const testData = { }, ], }, + job: { + emailJobInitiator: "user@example.com", + type: "all_access", + jobParams: { dataset: "6ED35C17-EDD4-4CD4-B917-4E49698F7532" }, + datasetList: [ + { pid: "6ED35C17-EDD4-4CD4-B917-4E49698F7532", files: [] }, + ], + jobStatusMessage: "jobSubmitted", + jobResultObject: {}, + }, }; export const testConfig = { diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 363158e084..5963238d8b 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -244,6 +244,34 @@ Cypress.Commands.add("createSample", (sample) => { }); }); +Cypress.Commands.add("createJob", (overwrites = {}) => { + return cy.getCookie("user").then((userCookie) => { + const user = JSON.parse(decodeURIComponent(userCookie.value)); + + cy.getToken().then((token) => { + cy.log("Job: " + JSON.stringify(overwrites, null, 2)); + cy.log("User: " + JSON.stringify(user, null, 2)); + + const job = { + ...testData.job, + emailJobInitiator: user.email, + ...overwrites, + }; + + cy.request({ + method: "POST", + url: lbBaseUrl + "/jobs", + headers: { + Authorization: token, + Accept: "application/json", + "Content-Type": "application/json", + }, + body: job, + }); + }); + }); +}); + Cypress.Commands.add("updateProposal", (proposalId, updateProposalDto) => { return cy.getCookie("user").then((userCookie) => { const user = JSON.parse(decodeURIComponent(userCookie.value)); @@ -445,6 +473,51 @@ Cypress.Commands.add("removeSamples", () => { }); }); +Cypress.Commands.add("removeJobs", () => { + cy.login(Cypress.env("username"), Cypress.env("password")); + cy.getToken().then((token) => { + const fields = { type: "all_access" }; + const limits = { limit: 10, skip: 0, sort: { type: "asc" } }; + + cy.request({ + method: "GET", + url: + lbBaseUrl + + "/jobs/fullquery?fields=" + + encodeURIComponent(JSON.stringify(fields)) + + "&limits=" + + encodeURIComponent(JSON.stringify(limits)), + headers: { + Authorization: token, + Accept: "application/json", + "Content-Type": "application/json", + }, + }) + .its("body") + .as("jobs"); + + cy.login( + Cypress.env("secondaryUsername"), + Cypress.env("secondaryPassword"), + ); + cy.getToken().then((token) => { + cy.get("@jobs").then((jobs) => { + jobs.forEach((job) => { + cy.request({ + method: "DELETE", + url: lbBaseUrl + `/jobs/${encodeURIComponent(job.id)}`, + headers: { + Authorization: token, + Accept: "application/json", + "Content-Type": "application/json", + }, + }); + }); + }); + }); + }); +}); + Cypress.Commands.add("initializeElasticSearch", (index) => { cy.login(Cypress.env("username"), Cypress.env("password")); cy.getToken().then((token) => { diff --git a/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.html b/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.html index 8cb997d2b3..aee744ff25 100644 --- a/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.html +++ b/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.html @@ -1,7 +1,22 @@ - - + diff --git a/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.spec.ts b/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.spec.ts index 92f0a07eee..3028c8aab6 100644 --- a/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.spec.ts +++ b/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.spec.ts @@ -1,49 +1,50 @@ -import { NO_ERRORS_SCHEMA } from "@angular/compiler"; import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; -import { ActivatedRoute, Router } from "@angular/router"; +import { JobsDashboardNewComponent } from "./jobs-dashboard-new.component"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; +import { Router } from "@angular/router"; import { AppConfigService } from "app-config.service"; -import { - MockActivatedRoute, - MockAppConfigService, - MockAuthService, - MockHttp, - MockRouter, -} from "shared/MockStubs"; +import { ScicatDataService } from "shared/services/scicat-data-service"; import { ExportExcelService } from "shared/services/export-excel.service"; -import { JobsDashboardNewComponent } from "./jobs-dashboard-new.component"; -import { SharedTableModule } from "shared/modules/shared-table/shared-table.module"; -import { SharedScicatFrontendModule } from "shared/shared.module"; -import { HttpClient } from "@angular/common/http"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { InternalStorage } from "shared/services/auth/base.storage"; -import { AuthService } from "shared/services/auth/auth.service"; -import { provideLuxonDateAdapter } from "@angular/material-luxon-adapter"; +import { RowEventType } from "shared/modules/dynamic-material-table/models/table-row.model"; +import { provideMockStore } from "@ngrx/store/testing"; +import { selectJobsDashboardPageViewModel } from "state-management/selectors/jobs.selectors"; + +const getConfig = () => ({}); describe("JobsDashboardNewComponent", () => { let component: JobsDashboardNewComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - const appconfig = new MockAppConfigService(null); - const authService = new MockAuthService(); + const router = { + navigateByUrl: jasmine.createSpy("navigateByUrl"), + }; + + const jobsVm = { + jobs: [], + count: 0, + filters: { + skip: 0, + limit: 5, + sortField: "creationTime:desc", + mode: undefined, + }, + tableSettings: { columns: [] }, + }; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ schemas: [NO_ERRORS_SCHEMA], declarations: [JobsDashboardNewComponent], - imports: [ - SharedTableModule, - SharedScicatFrontendModule, - BrowserAnimationsModule, - ], providers: [ - { provide: ActivatedRoute, useClass: MockActivatedRoute }, + { provide: Router, useValue: router }, + { provide: AppConfigService, useValue: { getConfig } }, + { provide: ScicatDataService, useValue: {} }, { provide: ExportExcelService, useValue: {} }, - { provide: Router, useClass: MockRouter }, - { provide: HttpClient, useClass: MockHttp }, - { provide: AppConfigService, useValue: appconfig }, - { provide: AuthService, useValue: authService }, - { provide: InternalStorage }, - provideLuxonDateAdapter(), + provideMockStore({ + selectors: [ + { selector: selectJobsDashboardPageViewModel, value: jobsVm }, + ], + }), ], }).compileComponents(); })); @@ -61,4 +62,19 @@ describe("JobsDashboardNewComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + + describe("#onRowEvent", () => { + it("should navigate to a Job detail", () => { + const job = { jobId: "job-1" }; + const id = encodeURIComponent(job.jobId); + + component.onRowEvent({ + event: RowEventType.RowClick, + sender: { row: job }, + } as any); + + expect(router.navigateByUrl).toHaveBeenCalledTimes(1); + expect(router.navigateByUrl).toHaveBeenCalledWith("/user/jobs/" + id); + }); + }); }); diff --git a/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.ts b/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.ts index 8a325b1046..7054c36044 100644 --- a/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.ts +++ b/src/app/jobs/jobs-dashboard-new/jobs-dashboard-new.component.ts @@ -1,16 +1,29 @@ -import { - AfterViewChecked, - ChangeDetectorRef, - Component, - OnDestroy, -} from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { SciCatDataSource } from "../../shared/services/scicat.datasource"; -import { ScicatDataService } from "../../shared/services/scicat-data-service"; -import { ExportExcelService } from "../../shared/services/export-excel.service"; -import { Column } from "shared/modules/shared-table/shared-table.module"; +import { Store } from "@ngrx/store"; +import { OutputJobV3Dto } from "@scicatproject/scicat-sdk-ts-angular"; +import { BehaviorSubject, Subscription, take, filter } from "rxjs"; import { AppConfigService } from "app-config.service"; -import { JobsTableData } from "jobs/jobs-dashboard/jobs-dashboard.component"; +import { ScicatDataService } from "shared/services/scicat-data-service"; +import { ExportExcelService } from "shared/services/export-excel.service"; +import { SciCatDataSource } from "shared/services/scicat.datasource"; +import { TableField } from "shared/modules/dynamic-material-table/models/table-field.model"; +import { + TablePagination, + TablePaginationMode, +} from "shared/modules/dynamic-material-table/models/table-pagination.model"; +import { + ITableSetting, + TableSettingEventType, +} from "shared/modules/dynamic-material-table/models/table-setting.model"; +import { actionMenu } from "shared/modules/dynamic-material-table/utilizes/default-table-settings"; +import { + IRowEvent, + RowEventType, +} from "shared/modules/dynamic-material-table/models/table-row.model"; +import { TableConfigService } from "shared/services/table-config.service"; +import { updateUserSettingsAction } from "state-management/actions/user.actions"; +import { selectJobsDashboardPageViewModel } from "state-management/selectors/jobs.selectors"; @Component({ selector: "app-jobs-new-dashboard", @@ -18,98 +31,101 @@ import { JobsTableData } from "jobs/jobs-dashboard/jobs-dashboard.component"; styleUrls: ["./jobs-dashboard-new.component.scss"], standalone: false, }) -export class JobsDashboardNewComponent implements OnDestroy, AfterViewChecked { - // not needed, date by default is shown in local time and using the locale of the browser (if installed, see app.module.ts) - // tz = Intl.DateTimeFormat().resolvedOptions().timeZone; - - columns: Column[] = [ - { - id: "id", - label: "ID", - canSort: true, - icon: "perm_device_information", - matchMode: "contains", - hideOrder: 0, - }, - { - id: "emailJobInitiator", - label: "Initiator", - icon: "person", - canSort: true, - matchMode: "contains", - hideOrder: 1, - }, - { - id: "type", - label: "Type", - icon: "unarchive", - canSort: true, - matchMode: "is", - hideOrder: 2, - }, - { - id: "creationTime", - icon: "schedule", - label: "Created at local time", - format: "date medium ", - canSort: true, - matchMode: "between", - hideOrder: 3, - sortDefault: "desc", - }, - { - id: "jobParams", - icon: "work", - label: "Parameters", - format: "json", - canSort: false, - matchMode: "contains", - hideOrder: 4, +export class JobsDashboardNewComponent implements OnInit, OnDestroy { + public vm$ = this.store.select(selectJobsDashboardPageViewModel); + + columns: TableField[] = []; + setting: ITableSetting = {}; + + tableName = "jobsTable"; + rowSelectionMode: "single" | "multi" | "none" = "none"; + paginationMode: TablePaginationMode = "server-side"; + pending = false; + globalTextSearch = ""; + + tableDefaultSettingsConfig: ITableSetting = { + visibleActionMenu: actionMenu, + settingList: [ + { + visibleActionMenu: actionMenu, + isDefaultSetting: true, + isCurrentSetting: true, + columnSetting: [ + { name: "jobId", header: "ID", index: 0 }, + { name: "emailJobInitiator", header: "Initiator", index: 1 }, + { name: "type", header: "Type", index: 2 }, + { + name: "creationTime", + header: "Created at local time", + index: 3, + type: "date", + format: "medium", + }, + { + name: "jobParams", + header: "Parameters", + index: 4, + customRender: (_, row) => JSON.stringify(row.jobParams), + }, + { name: "jobStatusMessage", header: "Status", index: 5 }, + { + name: "datasetList", + header: "Datasets", + index: 6, + customRender: (_, row) => JSON.stringify(row.datasetList), + }, + { + name: "jobResultObject", + header: "Result", + index: 7, + customRender: (_, row) => JSON.stringify(row.jobResultObject), + }, + ], + }, + ], + rowStyle: { + "border-bottom": "1px solid #d2d2d2", }, - { - id: "jobStatusMessage", - icon: "traffic", - label: "Status", - format: "json", + }; + + pagination: TablePagination = { + pageSize: 5, + pageIndex: 0, + pageSizeOptions: [5, 10, 25, 100], + length: 0, + }; + + scicatColumnsDef = + this.tableDefaultSettingsConfig.settingList[0]?.columnSetting?.map((c) => ({ + id: c.name, + label: c.header ?? c.name, + hideOrder: c.index ?? 0, canSort: true, - matchMode: "contains", - hideOrder: 5, - }, - { - id: "datasetList", - icon: "list", - label: "Datasets", - format: "json", - canSort: false, - matchMode: "contains", - hideOrder: 6, - }, - { - id: "jobResultObject", - icon: "work_outline", - label: "Result", - format: "json", - canSort: false, - matchMode: "contains", - hideOrder: 7, - }, - ]; + matchMode: c.type === "date" ? "between" : "contains", + })) ?? []; tableDefinition = { collection: "Jobs", - columns: this.columns, + columns: this.scicatColumnsDef, }; - dataSource: SciCatDataSource; + dataSource: BehaviorSubject = new BehaviorSubject< + OutputJobV3Dto[] + >([]); + scicatDataSource: SciCatDataSource; + + subscriptions: Subscription[] = []; + currentFilters: any = {}; constructor( + private router: Router, + private store: Store, private appConfigService: AppConfigService, - private cdRef: ChangeDetectorRef, private dataService: ScicatDataService, private exportService: ExportExcelService, - private router: Router, + private tableConfigService: TableConfigService, ) { - this.dataSource = new SciCatDataSource( + this.scicatDataSource = new SciCatDataSource( this.appConfigService, this.dataService, this.exportService, @@ -117,16 +133,138 @@ export class JobsDashboardNewComponent implements OnDestroy, AfterViewChecked { ); } - ngAfterViewChecked() { - this.cdRef.detectChanges(); + ngOnInit() { + this.subscriptions.push( + this.vm$ + .pipe( + filter((vm) => vm.hasFetchedSettings), + take(1), + ) + .subscribe((vm) => { + this.currentFilters = vm.filters; + const { skip, limit } = vm.filters; + const pageIndex = skip / limit; + + this.loadData(vm.filters, pageIndex, limit); + + const tableSettingsConfig = + this.tableConfigService.getTableSettingsConfig( + this.tableName, + this.tableDefaultSettingsConfig, + vm.tableSettings?.columns || [], + ); + + const currentColumnSetting = + tableSettingsConfig.settingList.find((s) => s.isCurrentSetting) + ?.columnSetting ?? []; + + this.columns = currentColumnSetting; + this.setting = tableSettingsConfig; + + this.pagination = { + ...this.pagination, + length: vm.count, + pageIndex, + pageSize: limit, + }; + }), + ); + + this.subscriptions.push( + this.scicatDataSource.connect().subscribe((data) => { + const mapped = data.map((job) => ({ + ...job, + jobId: job.id, + })); + this.dataSource.next(mapped); + }), + ); + + this.subscriptions.push( + this.scicatDataSource.count$.subscribe((count) => { + this.pagination = { ...this.pagination, length: count }; + }), + ); + } + + onRowEvent(event: IRowEvent) { + if (event?.event === RowEventType.RowClick) { + const id = encodeURIComponent(event.sender.row.jobId); + this.router.navigateByUrl("/user/jobs/" + id); + } } - ngOnDestroy() { - this.dataSource.disconnectExportData(); + onPaginationChange(pagination: TablePagination) { + const pageIndex = pagination.pageIndex; + const pageSize = pagination.pageSize; + const newFilters = { + ...this.currentFilters, + skip: pageIndex * pageSize, + limit: pageSize, + }; + + this.currentFilters = newFilters; + this.loadData(newFilters, pageIndex, pageSize); + } + + saveTableSettings(setting: ITableSetting) { + const columnsSetting = setting.columnSetting.map((column, index) => { + const { name, display, width } = column; + + return { name, display, order: index, width }; + }); + + this.store.dispatch( + updateUserSettingsAction({ + property: { fe_job_table_columns: columnsSetting }, + }), + ); + } + + onSettingChange(event: { + type: TableSettingEventType; + setting: ITableSetting; + }) { + if ( + event.type === TableSettingEventType.save || + event.type === TableSettingEventType.create + ) { + this.saveTableSettings(event.setting); + } + } + + loadData(filters: any, pageIndex: number, pageSize: number) { + const sortField = filters?.sortField; + const [field, direction] = sortField ? sortField.split(":") : ["", "asc"]; + + this.scicatDataSource.loadAllData( + filters, + field, + direction, + pageIndex, + pageSize, + ); + } + + onGlobalTextSearchChange(text: string) { + this.globalTextSearch = text; } - onRowClick(job: JobsTableData) { - const id = encodeURIComponent(job.id); - this.router.navigateByUrl("/user/jobs/" + id); + onGlobalTextSearchAction() { + const newFilters = { + ...this.currentFilters, + skip: 0, + limit: this.pagination.pageSize, + globalSearch: this.globalTextSearch || undefined, + }; + + this.loadData(newFilters, 0, this.pagination.pageSize); + this.currentFilters = newFilters; + this.pagination = { ...this.pagination, pageIndex: 0 }; + } + + ngOnDestroy() { + this.scicatDataSource.disconnectExportData(); + this.subscriptions.forEach((sub) => sub.unsubscribe()); } } diff --git a/src/app/jobs/jobs-detail/jobs-detail.component.ts b/src/app/jobs/jobs-detail/jobs-detail.component.ts index 7f2c393549..277b07150f 100644 --- a/src/app/jobs/jobs-detail/jobs-detail.component.ts +++ b/src/app/jobs/jobs-detail/jobs-detail.component.ts @@ -35,7 +35,7 @@ export class JobsDetailComponent implements OnInit, OnDestroy { }); this.job$.subscribe((job) => { this.hasJobResultObject = - Object.keys(job.jobResultObject || {}).length > 0; + Object.keys(job?.jobResultObject || {}).length > 0; }); } diff --git a/src/app/state-management/effects/user.effects.spec.ts b/src/app/state-management/effects/user.effects.spec.ts index 6e2df86840..e5d1555335 100644 --- a/src/app/state-management/effects/user.effects.spec.ts +++ b/src/app/state-management/effects/user.effects.spec.ts @@ -619,6 +619,7 @@ describe("UserEffects", () => { fe_sample_table_conditions: [], fe_instrument_table_columns: [], fe_file_table_columns: [], + fe_job_table_columns: [], }, } as unknown as UserSettings; const action = fromActions.fetchUserSettingsAction({ id }); diff --git a/src/app/state-management/models/index.ts b/src/app/state-management/models/index.ts index 16ce256ff8..7c99f62e1c 100644 --- a/src/app/state-management/models/index.ts +++ b/src/app/state-management/models/index.ts @@ -19,6 +19,7 @@ export interface Settings { fe_sample_table_conditions?: ConditionConfig[]; fe_instrument_table_columns?: TableColumn[]; fe_file_table_columns?: TableColumn[]; + fe_job_table_columns?: TableColumn[]; } export interface TableColumn { @@ -220,6 +221,7 @@ export const SETTINGS_CONFIG = [ configKey: "columns", }, { key: "fe_file_table_columns", scope: "file", configKey: "columns" }, + { key: "fe_job_table_columns", scope: "job", configKey: "columns" }, ]; export type SettingScope = @@ -227,7 +229,8 @@ export type SettingScope = | "proposal" | "sample" | "instrument" - | "file"; + | "file" + | "job"; export type SettingKind = "columns" | "filters" | "conditions"; export const getSettingKey = ( diff --git a/src/app/state-management/selectors/jobs.selectors.ts b/src/app/state-management/selectors/jobs.selectors.ts index 7d319b6322..448c7f878e 100644 --- a/src/app/state-management/selectors/jobs.selectors.ts +++ b/src/app/state-management/selectors/jobs.selectors.ts @@ -1,5 +1,6 @@ import { createFeatureSelector, createSelector } from "@ngrx/store"; import { JobsState } from "state-management/state/jobs.store"; +import { selectHasFetchedSettings, selectSettings } from "./user.selectors"; const selectJobState = createFeatureSelector("jobs"); @@ -48,3 +49,32 @@ export const selectQueryParams = createSelector(selectFilters, (filters) => { return { order: sortField, skip, limit }; } }); + +export const selectJobsDashboardPageViewModel = createSelector( + selectJobs, + selectJobsCount, + selectPage, + selectJobsPerPage, + selectFilters, + selectSettings, + selectHasFetchedSettings, + ( + jobs, + count, + currentPage, + jobsPerPage, + filters, + settings, + hasFetchedSettings, + ) => ({ + jobs, + count, + currentPage, + jobsPerPage, + filters, + hasFetchedSettings, + tableSettings: { + columns: settings.fe_job_table_columns, + }, + }), +); diff --git a/src/app/state-management/state/user.store.ts b/src/app/state-management/state/user.store.ts index 9e01a3c661..63ea8e3b0d 100644 --- a/src/app/state-management/state/user.store.ts +++ b/src/app/state-management/state/user.store.ts @@ -79,6 +79,7 @@ export const initialUserState: UserState = { fe_sample_table_conditions: [], fe_instrument_table_columns: [], fe_file_table_columns: [], + fe_job_table_columns: [], }, // TODO sync with server settings? message: undefined,