diff --git a/Dockerfile b/Dockerfile index f688c1d..42f0b11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ### STAGE 1: Build ### -FROM node AS builder +FROM quay.io/sartography/node:latest AS builder RUN mkdir /app WORKDIR /app ADD package.json /app/ @@ -11,7 +11,7 @@ RUN npm install && \ ### STAGE 2: Run ### -FROM nginx:alpine +FROM quay.io/sartography/nginx:alpine RUN set -x && apk add --update --no-cache bash libintl gettext curl COPY --from=builder /app/dist/* /etc/nginx/html/ diff --git a/package-lock.json b/package-lock.json index 9a33a5b..ea2077a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -802,6 +802,12 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, "inquirer": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", @@ -2562,6 +2568,12 @@ "semver-intersect": "1.4.0" }, "dependencies": { + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, "rxjs": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", @@ -4113,9 +4125,9 @@ "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==" }, "clipboard": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz", - "integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.7.tgz", + "integrity": "sha512-8M8WEZcIvs0hgOma+wAPkrUxpv0PMY1L6VsAJh/2DOKARIMpyWe6ZLcEoe1qktl6/ced5ceYHs+oGedSbgZ3sg==", "optional": true, "requires": { "good-listener": "^1.2.2", @@ -5661,24 +5673,24 @@ "dev": true }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -7308,8 +7320,8 @@ }, "ini": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "resolved": "", + "dev": true }, "inquirer": { "version": "3.0.6", @@ -8848,9 +8860,10 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true }, "minipass": { "version": "3.1.3", @@ -9141,9 +9154,9 @@ } }, "node-forge": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", - "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", "dev": true }, "node-libs-browser": { @@ -9568,6 +9581,13 @@ "minimist": "1.2.0", "node-fetch": "1.6.3", "opn": "4.0.2" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } } }, "opn": { @@ -10911,9 +10931,9 @@ "dev": true }, "prismjs": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.20.0.tgz", - "integrity": "sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz", + "integrity": "sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==", "requires": { "clipboard": "^2.0.0" } @@ -11900,9 +11920,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sartography-workflow-lib": { - "version": "0.0.396", - "resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.396.tgz", - "integrity": "sha512-gfdlq7sFpoX2nzigkQtgzY9kapHUeYar5FI80AqsOEkQU0Rh6gKO++hG3NN6zlqmYtQHFtJd+shSXOgbS6xEsQ==" + "version": "0.0.415", + "resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.415.tgz", + "integrity": "sha512-dc/JObsAq0TqGra/NuHG23EpmXrfnQqxbhGWXXkTfySDcA9beRNxJ5nd7xlc8EZzbUiWXIKxVM3xiyBCBQ+88A==" }, "sass": { "version": "1.26.3", @@ -12062,12 +12082,12 @@ } }, "selfsigned": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", - "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz", + "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==", "dev": true, "requires": { - "node-forge": "0.9.0" + "node-forge": "^0.10.0" } }, "semver": { diff --git a/package.json b/package.json index 17df051..5864dcc 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "ngx-markdown": "^9.1.1", "protractor": "^7.0.0", "rxjs": "~6.5.4", - "sartography-workflow-lib": "0.0.396", + "sartography-workflow-lib": "0.0.415", "tslib": "^1.13.0", "uuid": "^7.0.2", "zone.js": "^0.10.3" diff --git a/src/app/workflow-spec-list/workflow-spec-list.component.html b/src/app/workflow-spec-list/workflow-spec-list.component.html index e0aef2b..c1251c1 100644 --- a/src/app/workflow-spec-list/workflow-spec-list.component.html +++ b/src/app/workflow-spec-list/workflow-spec-list.component.html @@ -1,11 +1,11 @@

Workflow Specifications

- - diff --git a/src/app/workflow-spec-list/workflow-spec-list.component.spec.ts b/src/app/workflow-spec-list/workflow-spec-list.component.spec.ts index f6794bc..51a4773 100644 --- a/src/app/workflow-spec-list/workflow-spec-list.component.spec.ts +++ b/src/app/workflow-spec-list/workflow-spec-list.component.spec.ts @@ -1,9 +1,9 @@ import {APP_BASE_HREF} from '@angular/common'; import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {async, ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing'; import {MAT_BOTTOM_SHEET_DATA, MatBottomSheetModule, MatBottomSheetRef} from '@angular/material/bottom-sheet'; import {MatCardModule} from '@angular/material/card'; -import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog'; +import {MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef} from '@angular/material/dialog'; import {MatIconModule} from '@angular/material/icon'; import {MatListModule} from '@angular/material/list'; import {MatSnackBarModule} from '@angular/material/snack-bar'; @@ -11,7 +11,7 @@ import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/tes import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {RouterTestingModule} from '@angular/router/testing'; import createClone from 'rfdc'; -import {of} from 'rxjs'; +import {Observable, of} from 'rxjs'; import { ApiErrorsComponent, ApiService, @@ -37,11 +37,26 @@ import { import {GetIconCodePipe} from '../_pipes/get-icon-code.pipe'; import {FileListComponent} from '../file-list/file-list.component'; import {WorkflowSpecListComponent} from './workflow-spec-list.component'; +import {WorkflowSpecDialogComponent} from '../_dialogs/workflow-spec-dialog/workflow-spec-dialog.component'; + +export class MdDialogMock { + // When the component calls this.dialog.open(...) we'll return an object + // with an afterClosed method that allows to subscribe to the dialog result observable. + open() { + return { + afterClosed: () => of([ + {} + ]) + }; + } +} + describe('WorkflowSpecListComponent', () => { let httpMock: HttpTestingController; let component: WorkflowSpecListComponent; let fixture: ComponentFixture; + let dialog: MatDialog; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -68,11 +83,7 @@ describe('WorkflowSpecListComponent', () => { {provide: 'APP_ENVIRONMENT', useClass: MockEnvironment}, {provide: APP_BASE_HREF, useValue: ''}, { - provide: MatDialogRef, - useValue: { - close: (dialogResult: any) => { - }, - } + provide: MatDialogRef, useClass: MdDialogMock, }, {provide: MAT_DIALOG_DATA, useValue: []}, { @@ -100,6 +111,7 @@ describe('WorkflowSpecListComponent', () => { fixture = TestBed.createComponent(WorkflowSpecListComponent); component = fixture.componentInstance; fixture.detectChanges(); + dialog = TestBed.inject(MatDialog); const catReq = httpMock.expectOne('apiRoot/workflow-specification-category'); expect(catReq.request.method).toEqual('GET'); @@ -151,7 +163,7 @@ describe('WorkflowSpecListComponent', () => { const _updateWorkflowSpecSpy = spyOn((component as any), '_updateWorkflowSpec').and.stub(); component.selectedSpec = undefined; - (component as any)._upsertWorkflowSpecification(mockWorkflowSpec1 as WorkflowSpecDialogData); + (component as any)._upsertWorkflowSpecification(true, mockWorkflowSpec1 as WorkflowSpecDialogData); expect(_addWorkflowSpecSpy).toHaveBeenCalled(); expect(_updateWorkflowSpecSpy).not.toHaveBeenCalled(); @@ -161,7 +173,7 @@ describe('WorkflowSpecListComponent', () => { component.selectedSpec = mockWorkflowSpec0; const modifiedData: WorkflowSpecDialogData = createClone({circles: true})(mockWorkflowSpec0); modifiedData.display_name = 'Modified'; - (component as any)._upsertWorkflowSpecification(modifiedData); + (component as any)._upsertWorkflowSpecification(false, modifiedData); expect(_addWorkflowSpecSpy).not.toHaveBeenCalled(); expect(_updateWorkflowSpecSpy).toHaveBeenCalled(); }); @@ -454,6 +466,8 @@ describe('WorkflowSpecListComponent', () => { expect(_loadWorkflowSpecCategoriesSpy).toHaveBeenCalled(); }); + + it('should load master workflow spec', () => { const mockMasterSpec: WorkflowSpec = { id: 'master_status_spec', @@ -480,4 +494,14 @@ describe('WorkflowSpecListComponent', () => { expect(component.masterStatusSpec).toEqual(mockMasterSpec); }); + + it('should call editWorkflowSpec, open Dialog & call _upsertWorkflowSpecification when Edit button is clicked', fakeAsync(() => { + spyOn(dialog, 'open').and.callThrough(); + const _upsertWorkflowSpecification = spyOn((component as any), '_upsertWorkflowSpecification').and.stub(); + const button = fixture.debugElement.nativeElement.querySelector('#add_spec'); + button.click(); + const req = httpMock.expectOne(`apiRoot/workflow-specification-category`); + expect(dialog.open).toHaveBeenCalled(); + } + )); }); diff --git a/src/app/workflow-spec-list/workflow-spec-list.component.ts b/src/app/workflow-spec-list/workflow-spec-list.component.ts index f200ab7..0a623bb 100644 --- a/src/app/workflow-spec-list/workflow-spec-list.component.ts +++ b/src/app/workflow-spec-list/workflow-spec-list.component.ts @@ -97,16 +97,20 @@ export class WorkflowSpecListComponent implements OnInit { this.location.replaceState(environment.homeRoute + '/' + selectedSpec.name); } + categoryExpanded(cat: WorkflowSpecCategory) { + return this.selectedSpec != null && this.selectedSpec.category_id === cat.id; + } + editWorkflowSpec(selectedSpec?: WorkflowSpec) { - this.selectedSpec = selectedSpec; - const hasDisplayOrder = this.selectedSpec && isNumberDefined(this.selectedSpec.display_order); + + const hasDisplayOrder = selectedSpec && isNumberDefined(selectedSpec.display_order); const dialogData: WorkflowSpecDialogData = { - id: this.selectedSpec ? this.selectedSpec.id : '', - name: this.selectedSpec ? this.selectedSpec.name || this.selectedSpec.id : '', - display_name: this.selectedSpec ? this.selectedSpec.display_name : '', - description: this.selectedSpec ? this.selectedSpec.description : '', - category_id: this.selectedSpec ? this.selectedSpec.category_id : null, - display_order: hasDisplayOrder ? this.selectedSpec.display_order : 0, + id: selectedSpec ? selectedSpec.id : '', + name: selectedSpec ? selectedSpec.name || selectedSpec.id : '', + display_name: selectedSpec ? selectedSpec.display_name : '', + description: selectedSpec ? selectedSpec.description : '', + category_id: selectedSpec ? selectedSpec.category_id : null, + display_order: hasDisplayOrder ? selectedSpec.display_order : 0, }; // Open new filename/workflow spec dialog @@ -118,7 +122,7 @@ export class WorkflowSpecListComponent implements OnInit { dialogRef.afterClosed().subscribe((data: WorkflowSpecDialogData) => { if (data && data.id && data.name && data.display_name && data.description) { - this._upsertWorkflowSpecification(data); + this._upsertWorkflowSpecification(selectedSpec == null, data); } }); } @@ -171,6 +175,7 @@ export class WorkflowSpecListComponent implements OnInit { dialogRef.afterClosed().subscribe((data: DeleteWorkflowSpecDialogData) => { if (data && data.confirm && data.workflowSpec) { this._deleteWorkflowSpec(data.workflowSpec); + this.selectedSpec = this.masterStatusSpec; } }); } @@ -211,7 +216,6 @@ export class WorkflowSpecListComponent implements OnInit { this.api.getWorkflowSpecList().subscribe(wfs => { this.workflowSpecs = wfs; - this.workflowSpecsByCategory.forEach(cat => { cat.workflow_specs = this.workflowSpecs .filter(wf => { @@ -244,12 +248,9 @@ export class WorkflowSpecListComponent implements OnInit { }); } - private _upsertWorkflowSpecification(data: WorkflowSpecDialogData) { + private _upsertWorkflowSpecification(isNew: boolean, data: WorkflowSpecDialogData) { if (data.id && data.name && data.display_name && data.description) { - // Save old workflow spec id, in case it's changed - const specId = this.selectedSpec ? this.selectedSpec.id : undefined; - const newSpec: WorkflowSpec = { id: data.id, name: data.name, @@ -259,10 +260,10 @@ export class WorkflowSpecListComponent implements OnInit { display_order: data.display_order, }; - if (specId) { - this._updateWorkflowSpec(specId, newSpec); - } else { + if (isNew) { this._addWorkflowSpec(newSpec); + } else { + this._updateWorkflowSpec(data.id, newSpec); } } } @@ -389,5 +390,7 @@ export class WorkflowSpecListComponent implements OnInit { }); }); } + + } diff --git a/src/assets/images/squirrels.png b/src/assets/images/squirrels.png new file mode 100644 index 0000000..16d46ba Binary files /dev/null and b/src/assets/images/squirrels.png differ