From 0a3310c0a8e7e14ef7f0d1c9bdda82f5170edfcf Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Wed, 8 May 2024 12:51:54 +0200 Subject: [PATCH] test: Add Stories for file browser and trigger pipeline dialog --- frontend/.storybook/preview.ts | 4 + .../trigger-pipeline.docs.mdx | 56 ++++++++ .../trigger-pipeline.stories.ts | 128 +++++++++++++++++ .../model-diagram-dialog.stories.ts | 5 +- .../file-browser-dialog.docs.mdx | 67 +++++++++ .../file-browser-dialog.stories.ts | 130 ++++++++++++++++++ .../create-readonly-session-dialog.stories.ts | 16 +-- frontend/src/storybook/decorators.ts | 10 ++ frontend/src/storybook/session.ts | 2 + frontend/src/storybook/t4c.ts | 12 ++ frontend/src/storybook/user.ts | 19 ++- 11 files changed, 437 insertions(+), 12 deletions(-) create mode 100644 frontend/src/app/projects/models/backup-settings/trigger-pipeline/trigger-pipeline.docs.mdx create mode 100644 frontend/src/app/projects/models/backup-settings/trigger-pipeline/trigger-pipeline.stories.ts create mode 100644 frontend/src/app/sessions/user-sessions-wrapper/active-sessions/file-browser-dialog/file-browser-dialog.docs.mdx create mode 100644 frontend/src/app/sessions/user-sessions-wrapper/active-sessions/file-browser-dialog/file-browser-dialog.stories.ts create mode 100644 frontend/src/storybook/decorators.ts create mode 100644 frontend/src/storybook/t4c.ts diff --git a/frontend/.storybook/preview.ts b/frontend/.storybook/preview.ts index 32404cbac..d75b3fd67 100644 --- a/frontend/.storybook/preview.ts +++ b/frontend/.storybook/preview.ts @@ -13,6 +13,8 @@ import { ToastrModule } from 'ngx-toastr'; import { RouterModule } from '@angular/router'; import { CookieModule } from 'ngx-cookie'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MatDialogRef } from '@angular/material/dialog'; +import { DialogRef } from '@angular/cdk/dialog'; setCompodocJson(docJson); @@ -44,6 +46,8 @@ const preview: Preview = { RouterModule.forRoot([]), ), importProvidersFrom(BrowserAnimationsModule), + { provide: MatDialogRef, useValue: {} }, + { provide: DialogRef, useValue: {} }, ], }), ], diff --git a/frontend/src/app/projects/models/backup-settings/trigger-pipeline/trigger-pipeline.docs.mdx b/frontend/src/app/projects/models/backup-settings/trigger-pipeline/trigger-pipeline.docs.mdx new file mode 100644 index 000000000..eb88d46a1 --- /dev/null +++ b/frontend/src/app/projects/models/backup-settings/trigger-pipeline/trigger-pipeline.docs.mdx @@ -0,0 +1,56 @@ +{/* + SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + SPDX-License-Identifier: Apache-2.0 +*/} + +import * as TriggerPipelineComponent from './trigger-pipeline.stories.ts' +import { Meta, Title, Story, Canvas, Unstyled } from '@storybook/blocks' + + + + + +The `TriggerPipeline` component is used to trigger and manage pipelines for a project. +Pipelines are used to synchronize TeamForCapella models to Git. + +When no pipeline is configured, a message is displayed: + +<Unstyled> + <div style={{ width: '600px' }}> + <Story of={TriggerPipelineComponent.NoPipelineFound} /> + </div> +</Unstyled> + + +While the pipelines are loading, they are not displayed: + +<Unstyled> + <div style={{ width: '600px' }}> + <Story of={TriggerPipelineComponent.LoadingPipelines} /> + </div> +</Unstyled> + +When the pipelines are loaded, an overview of all pipelines is displayed: + +<Unstyled> + <div style={{ width: '670px' }}> + <Story of={TriggerPipelineComponent.PipelineOverview} /> + </div> +</Unstyled> + +The user can select a pipeline to see more details and actions: + +<Unstyled> + <div style={{ width: '800px' }}> + <Story of={TriggerPipelineComponent.OnePipelineSelected} /> + </div> +</Unstyled> + +Pipelines can only be deleted when the TeamForCapella server is reachable. +Administrators can also force-delete a pipeline when the T4C server is not running: + +<Unstyled> + <div style={{ width: '800px' }}> + <Story of={TriggerPipelineComponent.ForcePipelineDeletion} /> + </div> +</Unstyled> diff --git a/frontend/src/app/projects/models/backup-settings/trigger-pipeline/trigger-pipeline.stories.ts b/frontend/src/app/projects/models/backup-settings/trigger-pipeline/trigger-pipeline.stories.ts new file mode 100644 index 000000000..791b0c785 --- /dev/null +++ b/frontend/src/app/projects/models/backup-settings/trigger-pipeline/trigger-pipeline.stories.ts @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { Observable, of } from 'rxjs'; +import { + Pipeline, + PipelineService, +} from 'src/app/projects/models/backup-settings/service/pipeline.service'; +import { TriggerPipelineComponent } from 'src/app/projects/models/backup-settings/trigger-pipeline/trigger-pipeline.component'; +import { UserService } from 'src/app/services/user/user.service'; +import { dialogWrapper } from 'src/storybook/decorators'; +import { mockPrimaryGitModel } from 'src/storybook/git'; +import { mockTeamForCapellaRepository } from 'src/storybook/t4c'; +import { MockUserService } from 'src/storybook/user'; + +const meta: Meta<TriggerPipelineComponent> = { + title: 'Pipeline Components / Trigger Pipeline', + component: TriggerPipelineComponent, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: { projectSlug: 'test', modelSlug: 'test' }, + }, + ], + }), + dialogWrapper, + ], +}; + +export default meta; +type Story = StoryObj<TriggerPipelineComponent>; + +class MockPipelineService implements Partial<PipelineService> { + public readonly pipelines$: Observable<Pipeline[] | undefined> = + of(undefined); + + constructor(pipelines?: Pipeline[] | undefined) { + this.pipelines$ = of(pipelines); + } +} + +export const NoPipelineFound: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: PipelineService, + useFactory: () => new MockPipelineService([]), + }, + ], + }), + ], +}; + +export const LoadingPipelines: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: PipelineService, + useFactory: () => new MockPipelineService(undefined), + }, + ], + }), + ], +}; + +const pipeline = { + id: 1, + t4c_model: mockTeamForCapellaRepository, + git_model: mockPrimaryGitModel, + run_nightly: false, + include_commit_history: false, +}; + +export const PipelineOverview: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: PipelineService, + useFactory: () => new MockPipelineService([pipeline, pipeline]), + }, + ], + }), + ], +}; + +export const OnePipelineSelected: Story = { + args: { selectedPipeline: pipeline }, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: PipelineService, + useFactory: () => new MockPipelineService([pipeline]), + }, + ], + }), + ], +}; + +export const ForcePipelineDeletion: Story = { + args: { selectedPipeline: pipeline }, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: PipelineService, + useFactory: () => new MockPipelineService([pipeline]), + }, + { + provide: UserService, + useFactory: () => new MockUserService('administrator'), + }, + ], + }), + ], +}; diff --git a/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-dialog.stories.ts b/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-dialog.stories.ts index 3a6d5e667..62a1c147c 100644 --- a/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-dialog.stories.ts +++ b/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-dialog.stories.ts @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { dialogWrapper } from 'src/storybook/decorators'; import { base64ModelDiagram } from 'src/storybook/diagram'; import { mockModel } from 'src/storybook/model'; import { mockProject } from 'src/storybook/project'; @@ -24,9 +25,9 @@ const meta: Meta<ModelDiagramDialogComponent> = { provide: MAT_DIALOG_DATA, useValue: { model: mockModel, project: mockProject }, }, - { provide: MatDialogRef, useValue: {} }, ], }), + dialogWrapper, ], }; diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/file-browser-dialog/file-browser-dialog.docs.mdx b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/file-browser-dialog/file-browser-dialog.docs.mdx new file mode 100644 index 000000000..aa7bd4cd3 --- /dev/null +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/file-browser-dialog/file-browser-dialog.docs.mdx @@ -0,0 +1,67 @@ +{/* + SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + SPDX-License-Identifier: Apache-2.0 +*/} + +import * as FileBrowserDialog from './file-browser-dialog.stories.ts' +import { Meta, Title, Story, Canvas, Unstyled } from '@storybook/blocks' + +<Meta of={FileBrowserDialog} /> + +<Title /> + +The file explorer allows users to browse, download and upload files from their workspace. + +While the files are loading, a progress bar is displayed: + +<Unstyled> + <div style={{ width: '400px' }}> + <Story of={FileBrowserDialog.LoadingFiles} /> + </div> +</Unstyled> + +When the files are loaded, a tree is displayed. +Users can expand directories by clicking on the folder icon. + +<Unstyled> + <div style={{ width: '400px' }}> + <Story of={FileBrowserDialog.Files} /> + </div> +</Unstyled> + +## Uploads + +When uploading a file, the upload button on the specific directory has to be selected. +The file is then staged for upload. In this story, expand the workspace directory to see the staged file1. + +<Unstyled> + <div style={{ width: '400px' }}> + <Story of={FileBrowserDialog.UploadNewFile} /> + </div> +</Unstyled> + +When the upload is confirmed with the Submit button, it's uploaded to the server: + +<Unstyled> + <div style={{ width: '400px' }}> + <Story of={FileBrowserDialog.UploadInProgress} /> + </div> +</Unstyled> + +When the upload is finished, it's processed by the backend: + +<Unstyled> + <div style={{ width: '400px' }}> + <Story of={FileBrowserDialog.UploadProcessedByBackend} /> + </div> +</Unstyled> + +## Downloads + +When downloading a directory, the download button on the specific directory has to be selected. +The file is then downloaded: +<Unstyled> + <div style={{ width: '400px' }}> + <Story of={FileBrowserDialog.DownloadPreparation} /> + </div> +</Unstyled> diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/file-browser-dialog/file-browser-dialog.stories.ts b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/file-browser-dialog/file-browser-dialog.stories.ts new file mode 100644 index 000000000..4c56cd1ae --- /dev/null +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/file-browser-dialog/file-browser-dialog.stories.ts @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { BehaviorSubject } from 'rxjs'; +import { PathNode } from 'src/app/sessions/service/session.service'; +import { FileBrowserDialogComponent } from 'src/app/sessions/user-sessions-wrapper/active-sessions/file-browser-dialog/file-browser-dialog.component'; +import { dialogWrapper } from 'src/storybook/decorators'; +import { startedSession } from 'src/storybook/session'; + +const meta: Meta<FileBrowserDialogComponent> = { + title: 'Session Components / File Browser', + component: FileBrowserDialogComponent, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: { session: startedSession }, + }, + ], + }), + dialogWrapper, + ], +}; + +export default meta; +type Story = StoryObj<FileBrowserDialogComponent>; + +export const LoadingFiles: Story = { + args: { + loadingFiles: true, + }, +}; + +export const Files: Story = { + args: { + loadingFiles: false, + dataSource: new BehaviorSubject<PathNode[]>([ + { + path: '/workspace', + name: 'workspace', + type: 'directory', + isNew: false, + children: [ + { + path: '/workspace/file1', + name: 'file1', + type: 'file', + isNew: false, + children: null, + }, + { + path: '/workspace/file2', + name: 'file2', + type: 'file', + isNew: false, + children: null, + }, + ], + }, + ]), + }, +}; + +export const UploadNewFile: Story = { + args: { + loadingFiles: false, + dataSource: new BehaviorSubject<PathNode[]>([ + { + path: '/workspace', + name: 'workspace', + type: 'directory', + isNew: false, + children: [ + { + path: '/workspace/file1', + name: 'file1', + type: 'file', + isNew: true, + children: null, + }, + { + path: '/workspace/file2', + name: 'file2', + type: 'file', + isNew: false, + children: null, + }, + ], + }, + ]), + }, +}; + +export const UploadInProgress: Story = { + args: { + loadingFiles: false, + uploadProgress: 30, + }, +}; + +export const UploadProcessedByBackend: Story = { + args: { + loadingFiles: false, + uploadProgress: 100, + }, +}; + +export const DownloadPreparation: Story = { + args: { + loadingFiles: false, + }, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: MAT_DIALOG_DATA, + useValue: { + ...startedSession, + download_in_progress: true, + }, + }, + ], + }), + ], +}; diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog.stories.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog.stories.ts index cfa4303dc..a83132104 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog.stories.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog.stories.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DialogRef } from '@angular/cdk/dialog'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { SessionService } from 'src/app/sessions/service/session.service'; import { Tool } from 'src/app/settings/core/tools-settings/tool.service'; +import { dialogWrapper } from 'src/storybook/decorators'; import { mockPrimaryGitModel } from 'src/storybook/git'; import { createModelWithId } from 'src/storybook/model'; import { mockTool, mockToolVersion } from 'src/storybook/tool'; @@ -18,6 +18,12 @@ class MockSessionService implements Partial<SessionService> {} const meta: Meta<CreateReadonlySessionDialogComponent> = { title: 'Session Components / Create Readonly Session Dialog', component: CreateReadonlySessionDialogComponent, + decorators: [ + moduleMetadata({ + providers: [{ provide: MAT_DIALOG_DATA, useValue: {} }], + }), + dialogWrapper, + ], }; const tool: Tool = { ...mockTool }; @@ -53,8 +59,6 @@ export const ModelSelectedAndStartSessionPossible: Story = { provide: SessionService, useFactory: () => new MockSessionService(), }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: DialogRef, useValue: {} }, ], }), ], @@ -69,8 +73,6 @@ export const NoModelsToShow: Story = { provide: SessionService, useFactory: () => new MockSessionService(), }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: DialogRef, useValue: {} }, ], }), ], @@ -104,8 +106,6 @@ export const MaxNumberOfModelsExceeded: Story = { provide: SessionService, useFactory: () => new MockSessionService(), }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: DialogRef, useValue: {} }, ], }), ], @@ -136,8 +136,6 @@ export const ShowNoteForCompatibleSession: Story = { provide: SessionService, useFactory: () => new MockSessionService(), }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: DialogRef, useValue: {} }, ], }), ], diff --git a/frontend/src/storybook/decorators.ts b/frontend/src/storybook/decorators.ts new file mode 100644 index 000000000..b6a34c9be --- /dev/null +++ b/frontend/src/storybook/decorators.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { componentWrapperDecorator } from '@storybook/angular'; + +export const dialogWrapper = componentWrapperDecorator( + (story) => `<div class="rounded-md border shadow">${story}</div>`, +); diff --git a/frontend/src/storybook/session.ts b/frontend/src/storybook/session.ts index f8714e232..8ef5c8b22 100644 --- a/frontend/src/storybook/session.ts +++ b/frontend/src/storybook/session.ts @@ -10,6 +10,8 @@ import { import { mockHttpConnectionMethod, mockToolVersionWithTool } from './tool'; import { mockUser } from './user'; +export const startedSession = createPersistentSessionWithState('Started'); + export function createPersistentSessionWithState(state: string): Session { return { id: '1', diff --git a/frontend/src/storybook/t4c.ts b/frontend/src/storybook/t4c.ts new file mode 100644 index 000000000..a630e44cf --- /dev/null +++ b/frontend/src/storybook/t4c.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SimpleT4CModel } from 'src/app/projects/models/model-source/t4c/service/t4c-model.service'; + +export const mockTeamForCapellaRepository: Readonly<SimpleT4CModel> = { + project_name: 'project', + repository_name: 'repository', + instance_name: 'instance', +}; diff --git a/frontend/src/storybook/user.ts b/frontend/src/storybook/user.ts index 0041ab169..4e92a15a3 100644 --- a/frontend/src/storybook/user.ts +++ b/frontend/src/storybook/user.ts @@ -3,7 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { User } from 'src/app/services/user/user.service'; +import { + User, + UserRole, + UserService, +} from 'src/app/services/user/user.service'; export const mockUser: Readonly<User> = { id: 1, @@ -12,3 +16,16 @@ export const mockUser: Readonly<User> = { created: '2024-04-29T14:00:00Z', last_login: '2024-04-29T14:59:00Z', }; + +export class MockUserService implements Partial<UserService> { + role: UserRole; + + constructor(role: UserRole) { + this.role = role; + } + + validateUserRole(requiredRole: UserRole): boolean { + const roles = ['user', 'administrator']; + return roles.indexOf(requiredRole) <= roles.indexOf(this.role); + } +}