From 39401da393211d567e4ed5dda424c04cdfe6f57d Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Mon, 10 Feb 2025 11:27:49 +0200 Subject: [PATCH 1/4] fix: change the way the original devfile content is stored Signed-off-by: Oleksii Orel --- .../__snapshots__/index.spec.tsx.snap | 135 ---------------- .../EditorTools/__tests__/index.spec.tsx | 86 +++-------- .../src/components/EditorTools/index.tsx | 71 ++------- .../Apply/Devfile/__tests__/index.spec.tsx | 12 ++ .../CreatingSteps/Fetch/Devfile/index.tsx | 4 + .../__snapshots__/index.spec.tsx.snap | 4 +- .../DevfileEditorTab/__tests__/index.spec.tsx | 87 +---------- .../DevfileEditorTab/index.tsx | 80 +++++----- .../Toolbar/__tests__/index.spec.tsx | 6 +- .../services/devfileApi/devfile/metadata.ts | 3 +- .../src/services/devfileApi/typeguards.ts | 6 +- .../workspace-adapter/__tests__/index.spec.ts | 34 ----- .../src/services/workspace-adapter/index.ts | 38 +---- .../converters/__tests__/converter.spec.ts | 113 -------------- .../fixtures/sample-devfile-plugins.yaml | 44 ------ .../__tests__/fixtures/sample-devfile.yaml | 31 ---- .../fixtures/sample-devworkspace.yaml | 53 ------- .../fixtures/test-devfile-ephemeral.yaml | 10 -- .../test-devfile-metadata-description.yaml | 10 -- .../fixtures/test-devfile-parent.yaml | 12 -- ...devworkspace-devfile-annotation-error.yaml | 23 --- .../test-devworkspace-devfile-annotation.yaml | 24 --- .../fixtures/test-devworkspace-ephemeral.yaml | 19 --- .../fixtures/test-devworkspace-parent.yaml | 21 --- .../devworkspace/converters/index.ts | 144 ------------------ .../devworkspace/devWorkspaceClient.ts | 9 +- .../__tests__/selectors.spec.ts | 24 ++- .../src/store/DevfileRegistries/selectors.ts | 15 +- .../FactoryResolver/__tests__/actions.spec.ts | 2 + .../helpers.normalizeDevfileV2.spec.ts | 11 +- .../src/store/FactoryResolver/actions.ts | 35 +++-- .../src/store/FactoryResolver/helpers.ts | 37 +++-- .../createWorkspaceFromDevfile.ts | 15 ++ .../store/__mocks__/devWorkspaceBuilder.ts | 5 +- 34 files changed, 190 insertions(+), 1033 deletions(-) delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/converter.spec.ts delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devfile-plugins.yaml delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devfile.yaml delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devworkspace.yaml delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-ephemeral.yaml delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-metadata-description.yaml delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-parent.yaml delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-devfile-annotation-error.yaml delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-devfile-annotation.yaml delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-ephemeral.yaml delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-parent.yaml delete mode 100644 packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/index.ts diff --git a/packages/dashboard-frontend/src/components/EditorTools/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/components/EditorTools/__tests__/__snapshots__/index.spec.tsx.snap index c8b649b68..793b038ba 100644 --- a/packages/dashboard-frontend/src/components/EditorTools/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/dashboard-frontend/src/components/EditorTools/__tests__/__snapshots__/index.spec.tsx.snap @@ -1,140 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EditorTools DevWorkspace snapshot 1`] = ` -
-
-
- - - - - - - Cluster console - -
-
-
- -
-
-
- - - - - Download - -
-
-
- -
-
-
-`; - exports[`EditorTools Devfile snapshot 1`] = `
{ - const clusterConsole = { - id: ApplicationId.CLUSTER_CONSOLE, - url: 'https://console-url', - icon: 'https://console-icon-url', - title: 'Cluster console', - }; - beforeEach(() => { - store = new MockStoreBuilder() - .withClusterInfo({ - applications: [clusterConsole], - }) - .build(); + store = new MockStoreBuilder().build(); jest.useFakeTimers(); }); @@ -72,26 +62,22 @@ describe('EditorTools', () => { }); describe('Devfile', () => { - let devfile: devfileApi.Devfile; - - beforeEach(() => { - devfile = { - schemaVersion: '2.1.0', - metadata: { - name: 'my-project', - namespace: 'user-che', - }, - }; + const name = 'my-project'; + const devfileContent = dump({ + schemaVersion: '2.1.0', + metadata: { + name, + }, }); test('snapshot', () => { - const snapshot = createSnapshot(devfile); + const snapshot = createSnapshot(devfileContent, name); expect(snapshot.toJSON()).toMatchSnapshot(); }); test('expand and compress', async () => { const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); - renderComponent(devfile); + renderComponent(devfileContent, name); /* expand the editor */ @@ -120,7 +106,7 @@ describe('EditorTools', () => { const mockCreateObjectURL = jest.fn().mockReturnValue('blob-url'); URL.createObjectURL = mockCreateObjectURL; - renderComponent(devfile); + renderComponent(devfileContent, name); const copyButtonName = 'Copy to clipboard'; expect(screen.queryByRole('button', { name: copyButtonName })).toBeTruthy; @@ -129,7 +115,7 @@ describe('EditorTools', () => { await user.click(copyButton); expect(mockClipboard).toHaveBeenCalledWith( - 'schemaVersion: 2.1.0\nmetadata:\n name: my-project\n namespace: user-che\n', + 'schemaVersion: 2.1.0\nmetadata:\n name: my-project\n', ); /* 'Copy to clipboard' should be hidden for a while */ @@ -146,46 +132,16 @@ describe('EditorTools', () => { expect(screen.queryByRole('button', { name: copyButtonNameAfter })).toBeFalsy; }); }); - - describe('DevWorkspace', () => { - let devWorkspace: devfileApi.DevWorkspace; - - beforeEach(() => { - devWorkspace = { - apiVersion: '1.0.0', - metadata: { - name: 'my-project', - namespace: 'user-che', - labels: {}, - uid: '123', - }, - kind: 'DevWorkspace', - spec: { - template: {}, - started: true, - }, - }; - }); - - test('snapshot', () => { - const snapshot = createSnapshot(devWorkspace); - expect(snapshot.toJSON()).toMatchSnapshot(); - }); - - test('Cluster Console', () => { - renderComponent(devWorkspace); - - const clusterConsoleButton = screen.getByRole('link', { name: clusterConsole.title }); - - expect(clusterConsoleButton.textContent).toEqual(clusterConsole.title); - }); - }); }); -function getComponent(devfileOrDevWorkspace: devfileApi.Devfile | devfileApi.DevWorkspace) { +function getComponent(contentText: string, workspaceName: string) { return ( - + ); } diff --git a/packages/dashboard-frontend/src/components/EditorTools/index.tsx b/packages/dashboard-frontend/src/components/EditorTools/index.tsx index c092519f0..eeca40033 100644 --- a/packages/dashboard-frontend/src/components/EditorTools/index.tsx +++ b/packages/dashboard-frontend/src/components/EditorTools/index.tsx @@ -10,15 +10,9 @@ * Red Hat, Inc. - initial API and implementation */ -import { ApplicationId, helpers } from '@eclipse-che/common'; +import { helpers } from '@eclipse-che/common'; import { AlertVariant, Button, Divider, Flex, FlexItem } from '@patternfly/react-core'; -import { - CompressIcon, - CopyIcon, - DownloadIcon, - ExpandIcon, - ExternalLinkSquareAltIcon, -} from '@patternfly/react-icons'; +import { CompressIcon, CopyIcon, DownloadIcon, ExpandIcon } from '@patternfly/react-icons'; import React from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import { connect, ConnectedProps } from 'react-redux'; @@ -27,16 +21,14 @@ import styles from '@/components/EditorTools/index.module.css'; import { ToggleBarsContext } from '@/contexts/ToggleBars'; import { lazyInject } from '@/inversify.config'; import { AppAlerts } from '@/services/alerts/appAlerts'; -import devfileApi, { isDevfileV2, isDevWorkspace } from '@/services/devfileApi'; -import stringify from '@/services/helpers/editor'; import { AlertItem } from '@/services/helpers/types'; -import { WorkspaceAdapter } from '@/services/workspace-adapter'; import { RootState } from '@/store'; import { bannerAlertActionCreators } from '@/store/BannerAlert'; import { selectApplications } from '@/store/ClusterInfo/selectors'; export type Props = MappedProps & { - devfileOrDevWorkspace: devfileApi.DevWorkspace | devfileApi.Devfile; + contentText: string; + workspaceName: string; handleExpand: (isExpand: boolean) => void; }; @@ -75,9 +67,8 @@ class EditorTools extends React.PureComponent { } private init() { - const { devfileOrDevWorkspace } = this.props; + const { contentText } = this.props; try { - const contentText = stringify(devfileOrDevWorkspace); if (contentText !== this.state.contentText) { const contentBlobUrl = URL.createObjectURL( new Blob([contentText], { type: 'application/x-yaml' }), @@ -121,55 +112,13 @@ class EditorTools extends React.PureComponent { }, 3000); } - private buildOpenShiftConsoleItem(): React.ReactElement | undefined { - const { applications, devfileOrDevWorkspace } = this.props; - const clusterConsole = applications.find(app => app.id === ApplicationId.CLUSTER_CONSOLE); - - if (isDevfileV2(devfileOrDevWorkspace)) { - return; - } - const devWorkspace = devfileOrDevWorkspace; - - if (!clusterConsole) { - return; - } - - const devWorkspaceOpenShiftConsoleUrl = WorkspaceAdapter.buildClusterConsoleUrl( - devWorkspace, - clusterConsole.url, - ); - - return ( - <> - - - - - - ); - } - public render(): React.ReactElement { - const { devfileOrDevWorkspace } = this.props; + const { workspaceName } = this.props; const { contentText, contentBlobUrl, isExpanded, copied } = this.state; - const { name } = devfileOrDevWorkspace.metadata; - - const openshiftConsoleItem = this.buildOpenShiftConsoleItem(); - return (
- {openshiftConsoleItem} this.onCopyToClipboard()}> - + Download - +
diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/__tests__/index.spec.tsx index 32c711c3f..080117f19 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/__tests__/index.spec.tsx @@ -11,18 +11,11 @@ */ import userEvent from '@testing-library/user-event'; -import { dump } from 'js-yaml'; -import { cloneDeep } from 'lodash'; import React from 'react'; -import DevfileEditorTab, { prepareDevfile } from '@/pages/WorkspaceDetails/DevfileEditorTab'; +import DevfileEditorTab from '@/pages/WorkspaceDetails/DevfileEditorTab'; import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; -import devfileApi from '@/services/devfileApi'; import { constructWorkspace, Workspace } from '@/services/workspace-adapter'; -import { - DEVWORKSPACE_DEVFILE, - DEVWORKSPACE_METADATA_ANNOTATION, -} from '@/services/workspace-client/devworkspace/devWorkspaceClient'; import { DevWorkspaceBuilder } from '@/store/__mocks__/devWorkspaceBuilder'; jest.mock('@/components/EditorTools'); @@ -54,84 +47,6 @@ describe('DevfileEditorTab', () => { expect(isExpanded).toHaveTextContent('true'); }); }); - - describe('prepareDevfile', () => { - describe('devWorkspace with the DEVWORKSPACE_DEVFILE annotation', () => { - test('devfile without DEVWORKSPACE_METADATA_ANNOTATION', () => { - const expectedDevfile = { - schemaVersion: '2.1.0', - metadata: { - name: 'wksp', - namespace: 'user-che', - tags: ['tag1', 'tag2'], - }, - } as devfileApi.Devfile; - const devWorkspace = new DevWorkspaceBuilder() - .withName('wksp') - .withNamespace('user-che') - .withMetadata({ - annotations: { - [DEVWORKSPACE_DEVFILE]: dump(expectedDevfile), - }, - }) - .build(); - const workspace = constructWorkspace(devWorkspace); - - const devfile = prepareDevfile(workspace); - expect(devfile).toEqual(expectedDevfile); - }); - - test('devfile with DEVWORKSPACE_METADATA_ANNOTATION', () => { - const origDevfile = { - schemaVersion: '2.1.0', - metadata: { - name: 'wksp', - namespace: 'user-che', - tags: ['tag1', 'tag2'], - }, - attributes: { - [DEVWORKSPACE_METADATA_ANNOTATION]: dump({ url: 'devfile-source-location' }), - }, - } as devfileApi.Devfile; - const devWorkspace = new DevWorkspaceBuilder() - .withName('wksp') - .withNamespace('user-che') - .withMetadata({ - annotations: { - [DEVWORKSPACE_DEVFILE]: dump(origDevfile), - }, - }) - .build(); - const workspace = constructWorkspace(devWorkspace); - - const devfile = prepareDevfile(workspace); - - const expectedDevfile = cloneDeep(origDevfile); - delete expectedDevfile.attributes; - - expect(devfile).toEqual(expectedDevfile); - }); - }); - - test('devWorkspace without DEVWORKSPACE_DEVFILE annotation', () => { - const expectedDevfile = { - schemaVersion: '2.2.0', - metadata: { - name: 'wksp', - namespace: 'user-che', - }, - components: [], - } as devfileApi.Devfile; - const devWorkspace = new DevWorkspaceBuilder() - .withName('wksp') - .withNamespace('user-che') - .build(); - const workspace = constructWorkspace(devWorkspace); - - const devfile = prepareDevfile(workspace); - expect(devfile).toEqual(expectedDevfile); - }); - }); }); function getComponent(isActive: boolean, workspace: Workspace) { diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/index.tsx index 8264d56d2..2f38d00d2 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/index.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/index.tsx @@ -10,22 +10,21 @@ * Red Hat, Inc. - initial API and implementation */ -import { TextContent } from '@patternfly/react-core'; -import { load } from 'js-yaml'; +import { + EmptyState, + EmptyStateIcon, + EmptyStateVariant, + TextContent, + Title, +} from '@patternfly/react-core'; +import { CogIcon } from '@patternfly/react-icons'; import React from 'react'; import { DevfileViewer } from '@/components/DevfileViewer'; import EditorTools from '@/components/EditorTools'; import styles from '@/pages/WorkspaceDetails/DevfileEditorTab/index.module.css'; -import { DevfileAdapter } from '@/services/devfile/adapter'; -import devfileApi from '@/services/devfileApi'; -import stringify from '@/services/helpers/editor'; import { Workspace } from '@/services/workspace-adapter'; -import { - DEVWORKSPACE_BOOTSTRAP, - DEVWORKSPACE_DEVFILE, - DEVWORKSPACE_METADATA_ANNOTATION, -} from '@/services/workspace-client/devworkspace/devWorkspaceClient'; +import { DEVWORKSPACE_DEVFILE } from '@/services/workspace-client/devworkspace/devWorkspaceClient'; export type Props = { workspace: Workspace; @@ -49,47 +48,40 @@ export default class DevfileEditorTab extends React.PureComponent public render(): React.ReactElement { const { isExpanded } = this.state; const editorTabStyle = isExpanded ? styles.editorTabExpanded : styles.editorTab; - - const devfile = prepareDevfile(this.props.workspace); - const devfileStr = stringify(devfile); + const devfileStr = this.props.workspace.ref.metadata?.annotations?.[DEVWORKSPACE_DEVFILE] || ''; + const name = this.props.workspace.name; return (
- { - this.setState({ isExpanded }); - }} - /> - + {devfileStr && ( + <> + { + this.setState({ isExpanded }); + }} + /> + + + )} + {!devfileStr && ( + + + + No Data Found + + + )}
); } } - -export function prepareDevfile(workspace: Workspace): devfileApi.Devfile { - const devfileStr = workspace.ref.metadata?.annotations?.[DEVWORKSPACE_DEVFILE]; - const devfile = devfileStr ? (load(devfileStr) as devfileApi.Devfile) : workspace.devfile; - - const attrs = DevfileAdapter.getAttributes(devfile); - if (attrs?.[DEVWORKSPACE_METADATA_ANNOTATION]) { - delete attrs[DEVWORKSPACE_METADATA_ANNOTATION]; - } - if (attrs?.[DEVWORKSPACE_BOOTSTRAP]) { - delete attrs[DEVWORKSPACE_BOOTSTRAP]; - } - if (Object.keys(attrs).length === 0) { - delete devfile.attributes; - delete devfile.metadata.attributes; - } - - return devfile; -} diff --git a/packages/dashboard-frontend/src/pages/WorkspacesList/Toolbar/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/WorkspacesList/Toolbar/__tests__/index.spec.tsx index b5856c648..44f4dd935 100644 --- a/packages/dashboard-frontend/src/pages/WorkspacesList/Toolbar/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspacesList/Toolbar/__tests__/index.spec.tsx @@ -100,7 +100,7 @@ describe('Workspaces List Toolbar', () => { const searchbox = screen.getByRole('searchbox'); const searchButton = screen.getByRole('button', { name: /filter workspaces/i }); - await userEvent.type(searchbox, workspaces[0].devfile.metadata.name!); + await userEvent.type(searchbox, workspaces[0].name!); await userEvent.click(searchButton); expect(mockOnFilter).toHaveBeenCalledWith([workspaces[0]]); @@ -132,7 +132,7 @@ describe('Workspaces List Toolbar', () => { const searchbox = screen.getByRole('searchbox'); - await userEvent.type(searchbox, workspaces[2].devfile.metadata.name!); + await userEvent.type(searchbox, workspaces[2].name!); const enterKeydown = new KeyboardEvent('keydown', { code: 'Enter', key: 'a' }); fireEvent(searchbox, enterKeydown); @@ -144,7 +144,7 @@ describe('Workspaces List Toolbar', () => { const searchbox = screen.getByRole('searchbox'); - await userEvent.type(searchbox, workspaces[2].devfile.metadata.name!); + await userEvent.type(searchbox, workspaces[2].name!); const escapeKeydown = new KeyboardEvent('keydown', { code: 'Escape', key: 'a' }); fireEvent(searchbox, escapeKeydown); diff --git a/packages/dashboard-frontend/src/services/devfileApi/devfile/metadata.ts b/packages/dashboard-frontend/src/services/devfileApi/devfile/metadata.ts index 002b10f28..77953cb7b 100644 --- a/packages/dashboard-frontend/src/services/devfileApi/devfile/metadata.ts +++ b/packages/dashboard-frontend/src/services/devfileApi/devfile/metadata.ts @@ -17,5 +17,4 @@ export type DevfileMetadataLike = V230DevfileMetadata & { generateName?: string; }; -export type DevfileMetadata = DevfileMetadataLike & - Required>; +export type DevfileMetadata = DevfileMetadataLike & Required>; diff --git a/packages/dashboard-frontend/src/services/devfileApi/typeguards.ts b/packages/dashboard-frontend/src/services/devfileApi/typeguards.ts index 64cb53744..9bd20bae1 100644 --- a/packages/dashboard-frontend/src/services/devfileApi/typeguards.ts +++ b/packages/dashboard-frontend/src/services/devfileApi/typeguards.ts @@ -13,20 +13,20 @@ import devfileApi from '.'; export function isDevfileV2Like(devfile: unknown): devfile is devfileApi.DevfileLike { - return (devfile as devfileApi.DevfileLike).schemaVersion !== undefined; + return (devfile as devfileApi.DevfileLike)?.schemaVersion !== undefined; } const schemaVersionRe = /^([2-9])\.([0-9]+)\.([0-9]+)(-[0-9a-z-]+(\.[0-9a-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/; export function isDevfileV2(devfile: unknown): devfile is devfileApi.Devfile { return ( - (devfile as devfileApi.Devfile).schemaVersion !== undefined && + (devfile as devfileApi.Devfile)?.schemaVersion !== undefined && schemaVersionRe.test((devfile as devfileApi.Devfile).schemaVersion) ); } export function isDevWorkspaceLike(workspace: unknown): workspace is devfileApi.DevWorkspaceLike { - return (workspace as devfileApi.DevWorkspaceLike).kind === 'DevWorkspace'; + return (workspace as devfileApi.DevWorkspaceLike)?.kind === 'DevWorkspace'; } export function isDevWorkspace(workspace: unknown): workspace is devfileApi.DevWorkspace { diff --git a/packages/dashboard-frontend/src/services/workspace-adapter/__tests__/index.spec.ts b/packages/dashboard-frontend/src/services/workspace-adapter/__tests__/index.spec.ts index 8971fe830..c9b9a9b27 100644 --- a/packages/dashboard-frontend/src/services/workspace-adapter/__tests__/index.spec.ts +++ b/packages/dashboard-frontend/src/services/workspace-adapter/__tests__/index.spec.ts @@ -46,16 +46,10 @@ describe('for DevWorkspace', () => { const workspace = constructWorkspace(devWorkspace); expect(workspace.storageType).toEqual('per-workspace'); - expect(workspace.devfile.attributes).toEqual({ - [DEVWORKSPACE_STORAGE_TYPE_ATTR]: 'per-workspace', - }); workspace.storageType = 'ephemeral'; expect(workspace.storageType).toEqual('ephemeral'); - expect(workspace.devfile.attributes).toEqual({ - [DEVWORKSPACE_STORAGE_TYPE_ATTR]: 'ephemeral', - }); }); it('should set "per-workspace" storage type', () => { @@ -66,16 +60,10 @@ describe('for DevWorkspace', () => { const workspace = constructWorkspace(devWorkspace); expect(workspace.storageType).toEqual('ephemeral'); - expect(workspace.devfile.attributes).toEqual({ - [DEVWORKSPACE_STORAGE_TYPE_ATTR]: 'ephemeral', - }); workspace.storageType = 'per-workspace'; expect(workspace.storageType).toEqual('per-workspace'); - expect(workspace.devfile.attributes).toEqual({ - [DEVWORKSPACE_STORAGE_TYPE_ATTR]: 'per-workspace', - }); }); it('should set "per-user" storage type', () => { @@ -86,16 +74,10 @@ describe('for DevWorkspace', () => { const workspace = constructWorkspace(devWorkspace); expect(workspace.storageType).toEqual('per-workspace'); - expect(workspace.devfile.attributes).toEqual({ - [DEVWORKSPACE_STORAGE_TYPE_ATTR]: 'per-workspace', - }); workspace.storageType = 'per-user'; expect(workspace.storageType).toEqual('per-user'); - expect(workspace.devfile.attributes).toEqual({ - [DEVWORKSPACE_STORAGE_TYPE_ATTR]: 'per-user', - }); }); it('should return reference to the workspace', () => { @@ -207,22 +189,6 @@ describe('for DevWorkspace', () => { expect(StorageTypeTitle[workspace.storageType as 'per-workspace']).toEqual('Per-workspace'); }); - it('should return devfile', () => { - const devfile = { - schemaVersion: '2.2.0', - metadata: { - name: 'my-wksp', - namespace: 'my-namespace', - }, - }; - const devWorkspace = new DevWorkspaceBuilder() - .withName('my-wksp') - .withNamespace('my-namespace') - .build(); - const workspace = constructWorkspace(devWorkspace); - expect(workspace.devfile).toMatchObject(devfile); - }); - it('should return list of project names', () => { const projects = [ { diff --git a/packages/dashboard-frontend/src/services/workspace-adapter/index.ts b/packages/dashboard-frontend/src/services/workspace-adapter/index.ts index cb245d52e..9b94d6876 100644 --- a/packages/dashboard-frontend/src/services/workspace-adapter/index.ts +++ b/packages/dashboard-frontend/src/services/workspace-adapter/index.ts @@ -10,7 +10,7 @@ * Red Hat, Inc. - initial API and implementation */ -import devfileApi, { isDevfileV2, isDevWorkspace } from '@/services/devfileApi'; +import devfileApi, { isDevWorkspace } from '@/services/devfileApi'; import { DEVWORKSPACE_UPDATING_TIMESTAMP_ANNOTATION } from '@/services/devfileApi/devWorkspace/metadata'; import { DEVWORKSPACE_STORAGE_TYPE_ATTR } from '@/services/devfileApi/devWorkspace/spec/template'; import { @@ -19,11 +19,6 @@ import { WorkspaceStatus, } from '@/services/helpers/types'; import { che } from '@/services/models'; -import { - devfileToDevWorkspace, - devWorkspaceToDevfile, -} from '@/services/workspace-client/devworkspace/converters'; -import { DEVWORKSPACE_NEXT_START_ANNOTATION } from '@/services/workspace-client/devworkspace/devWorkspaceClient'; export interface Workspace { readonly ref: devfileApi.DevWorkspace; @@ -37,7 +32,6 @@ export interface Workspace { readonly updated: number; status: WorkspaceStatus | DevWorkspaceStatus | DeprecatedWorkspaceStatus; readonly ideUrl?: string; - devfile: devfileApi.Devfile; storageType: che.WorkspaceStorageType; readonly projects: string[]; readonly isStarting: boolean; @@ -236,36 +230,6 @@ export class WorkspaceAdapter implements Work } } - get devfile(): devfileApi.Devfile { - if ( - this.workspace.metadata.annotations && - this.workspace.metadata.annotations[DEVWORKSPACE_NEXT_START_ANNOTATION] - ) { - const devfile = devWorkspaceToDevfile( - JSON.parse(this.workspace.metadata.annotations[DEVWORKSPACE_NEXT_START_ANNOTATION]), - ); - if (isDevfileV2(devfile)) { - return devfile; - } - } - return devWorkspaceToDevfile(this.workspace) as devfileApi.Devfile; - } - - set devfile(devfile: devfileApi.Devfile) { - const plugins = this.workspace.spec.contributions || []; - const converted = devfileToDevWorkspace( - devfile as devfileApi.Devfile, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.workspace.spec.routingClass!, - this.workspace.spec.started, - ); - if (converted.spec.contributions === undefined) { - converted.spec.contributions = []; - } - converted.spec.contributions.push(...plugins); - (this.workspace as devfileApi.DevWorkspace) = converted; - } - get projects(): string[] { return (this.workspace.spec.template.projects || []).map(project => project.name); } diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/converter.spec.ts b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/converter.spec.ts deleted file mode 100644 index 32d75473d..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/converter.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2018-2024 Red Hat, Inc. - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - */ - -import * as fs from 'fs'; -import * as yaml from 'js-yaml'; - -import { devfileSchemaVersion, devfileToDevWorkspace, devWorkspaceToDevfile } from '..'; - -describe('testing sample conversions', () => { - describe('devfile to devworkspace', () => { - test('the sample-devfile-plugins fixture should convert into sample-devworkspace fixture', () => { - const input: any = yaml.load( - fs.readFileSync(__dirname + '/fixtures/sample-devfile-plugins.yaml', 'utf-8'), - ); - const output = yaml.load( - fs.readFileSync(__dirname + '/fixtures/sample-devworkspace.yaml', 'utf-8'), - ); - expect(devfileToDevWorkspace(input, 'che', true)).toStrictEqual(output); - }); - }); - describe('devworkspace to devfile', () => { - // mute the outputs - console.debug = jest.fn(); - - test('the sample-devworkspace fixture should convert into sample-devfile fixture', () => { - const input: any = yaml.load( - fs.readFileSync(__dirname + '/fixtures/sample-devworkspace.yaml', 'utf-8'), - ); - const output = yaml.load( - fs.readFileSync(__dirname + '/fixtures/sample-devfile.yaml', 'utf-8'), - ); - delete (output as any).metadata.attributes; - expect(devWorkspaceToDevfile(input)).toStrictEqual(output); - }); - - test('the test-devworkspace-devfile-annotation fixture should convert into test-devfile-metadata-description fixture', () => { - const input: any = yaml.load( - fs.readFileSync(__dirname + '/fixtures/test-devworkspace-devfile-annotation.yaml', 'utf-8'), - ); - const output = yaml.load( - fs.readFileSync(__dirname + '/fixtures/test-devfile-metadata-description.yaml', 'utf-8'), - ); - expect(devWorkspaceToDevfile(input)).toStrictEqual(output); - }); - - test('should eliminate the devworkspace devfile annotation in the case with an error', () => { - const input: any = yaml.load( - fs.readFileSync( - __dirname + '/fixtures/test-devworkspace-devfile-annotation-error.yaml', - 'utf-8', - ), - ); - const devfile = devWorkspaceToDevfile(input); - expect(console.debug).toHaveBeenCalledWith( - 'Failed to parse the origin devfile. The target object is not devfile V2.', - ); - expect(devfile.schemaVersion).toEqual(devfileSchemaVersion); - }); - }); - describe('parent section', () => { - test('the test-devfile-parent fixture should convert into test-devworkspace-parent fixture', () => { - const input: any = yaml.load( - fs.readFileSync(__dirname + '/fixtures/test-devfile-parent.yaml', 'utf-8'), - ); - const output = yaml.load( - fs.readFileSync(__dirname + '/fixtures/test-devworkspace-parent.yaml', 'utf-8'), - ); - expect(devfileToDevWorkspace(input, 'che', true)).toStrictEqual(output); - }); - - test('the test-devworkspace-parent fixture should convert into test-devfile-parent fixture', () => { - const input: any = yaml.load( - fs.readFileSync(__dirname + '/fixtures/test-devworkspace-parent.yaml', 'utf-8'), - ); - const output = yaml.load( - fs.readFileSync(__dirname + '/fixtures/test-devfile-parent.yaml', 'utf-8'), - ); - delete (output as any).metadata.attributes; - expect(devWorkspaceToDevfile(input)).toStrictEqual(output); - }); - }); - describe('storage-type section', () => { - test('the test-devfile-ephemeral fixture should convert into test-devworkspace-ephemeral fixture', () => { - const input: any = yaml.load( - fs.readFileSync(__dirname + '/fixtures/test-devfile-ephemeral.yaml', 'utf-8'), - ); - const output = yaml.load( - fs.readFileSync(__dirname + '/fixtures/test-devworkspace-ephemeral.yaml', 'utf-8'), - ); - expect(devfileToDevWorkspace(input, 'che', true)).toStrictEqual(output); - }); - - test('the test-devworkspace-ephemeral fixture should convert into test-devfile-ephemeral fixture', () => { - const input: any = yaml.load( - fs.readFileSync(__dirname + '/fixtures/test-devworkspace-ephemeral.yaml', 'utf-8'), - ); - const output = yaml.load( - fs.readFileSync(__dirname + '/fixtures/test-devfile-ephemeral.yaml', 'utf-8'), - ); - delete (output as any).metadata.attributes; - expect(devWorkspaceToDevfile(input)).toStrictEqual(output); - }); - }); -}); diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devfile-plugins.yaml b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devfile-plugins.yaml deleted file mode 100644 index 3ac527212..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devfile-plugins.yaml +++ /dev/null @@ -1,44 +0,0 @@ -schemaVersion: 2.2.0 -attributes: - author: Somebody - dw.metadata.annotations: - any.custom.settings: 'true' -metadata: - name: nodejs-stack - namespace: dwclient-test -projects: - - name: project - git: - remotes: - origin: 'https://github.com/che-samples/web-nodejs-sample.git' -components: - - name: nodejs - container: - image: quay.io/eclipse/che-nodejs10-ubi:nightly - memoryLimit: 512Mi - endpoints: - - name: nodejs - protocol: http - targetPort: 3000 - mountSources: true - - name: editor - plugin: - id: eclipse/che-theia/7.1.0 - - name: terminal - plugin: - id: eclipse/che-machine-exec-plugin/7.1.0 - - name: typescript-plugin - plugin: - id: che-incubator/typescript/1.30.2 - components: - - name: 'somename' - container: - memoryLimit: 512Mi -commands: - - id: download-dependencies - exec: - component: nodejs - commandLine: npm install - workingDir: ${PROJECTS_ROOT}/project/app - group: - kind: build diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devfile.yaml b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devfile.yaml deleted file mode 100644 index 09eecf809..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devfile.yaml +++ /dev/null @@ -1,31 +0,0 @@ -schemaVersion: 2.2.0 -attributes: - author: Somebody - dw.metadata.annotations: - any.custom.settings: 'true' -metadata: - name: nodejs-stack - namespace: dwclient-test -projects: - - name: project - git: - remotes: - origin: 'https://github.com/che-samples/web-nodejs-sample.git' -components: - - name: nodejs - container: - image: quay.io/eclipse/che-nodejs10-ubi:nightly - memoryLimit: 512Mi - endpoints: - - name: nodejs - protocol: http - targetPort: 3000 - mountSources: true -commands: - - id: download-dependencies - exec: - component: nodejs - commandLine: npm install - workingDir: ${PROJECTS_ROOT}/project/app - group: - kind: build diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devworkspace.yaml b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devworkspace.yaml deleted file mode 100644 index 87d3a72a8..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/sample-devworkspace.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: workspace.devfile.io/v1alpha2 -kind: DevWorkspace -metadata: - labels: {} - name: nodejs-stack - namespace: dwclient-test - annotations: - any.custom.settings: 'true' - uid: '' -spec: - routingClass: che - started: true - template: - attributes: - author: Somebody - dw.metadata.annotations: - any.custom.settings: 'true' - projects: - - name: project - git: - remotes: - origin: 'https://github.com/che-samples/web-nodejs-sample.git' - components: - - name: nodejs - container: - image: quay.io/eclipse/che-nodejs10-ubi:nightly - memoryLimit: 512Mi - endpoints: - - name: nodejs - protocol: http - targetPort: 3000 - mountSources: true - - name: editor - plugin: - id: eclipse/che-theia/7.1.0 - - name: terminal - plugin: - id: eclipse/che-machine-exec-plugin/7.1.0 - - name: typescript-plugin - plugin: - id: che-incubator/typescript/1.30.2 - components: - - name: 'somename' - container: - memoryLimit: 512Mi - commands: - - id: download-dependencies - exec: - component: nodejs - commandLine: npm install - workingDir: ${PROJECTS_ROOT}/project/app - group: - kind: build diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-ephemeral.yaml b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-ephemeral.yaml deleted file mode 100644 index 431dbfbe8..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-ephemeral.yaml +++ /dev/null @@ -1,10 +0,0 @@ -schemaVersion: 2.2.0 -attributes: - controller.devfile.io/storage-type: ephemeral - author: Somebody - dw.metadata.annotations: - any.custom.settings: 'true' -metadata: - name: nodejs-stack - namespace: dwclient-test -components: [] diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-metadata-description.yaml b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-metadata-description.yaml deleted file mode 100644 index ab953902b..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-metadata-description.yaml +++ /dev/null @@ -1,10 +0,0 @@ -schemaVersion: 2.0.0 -attributes: - author: Somebody - dw.metadata.annotations: - any.custom.settings: 'true' -metadata: - description: nodejs-stack sample - name: nodejs-stack - namespace: dwclient-test -components: [] diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-parent.yaml b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-parent.yaml deleted file mode 100644 index f1e8a9d98..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devfile-parent.yaml +++ /dev/null @@ -1,12 +0,0 @@ -schemaVersion: 2.2.0 -attributes: - author: Somebody - dw.metadata.annotations: - any.custom.settings: 'true' -metadata: - name: nodejs-stack - namespace: dwclient-test -parent: - id: nodejs - registryUrl: 'https://registry.devfile.io' -components: [] diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-devfile-annotation-error.yaml b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-devfile-annotation-error.yaml deleted file mode 100644 index 6d6a9aac4..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-devfile-annotation-error.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: workspace.devfile.io/v1alpha2 -kind: DevWorkspace -metadata: - labels: {} - name: nodejs-stack - namespace: dwclient-test - annotations: - any.custom.settings: 'true' - che.eclipse.org/devfile: | - metadata: - description: nodejs-stack sample - name: nodejs-stack - namespace: dwclient-test - uid: '' -spec: - routingClass: che - started: true - template: - attributes: - author: Somebody - dw.metadata.annotations: - any.custom.settings: 'true' - components: [] diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-devfile-annotation.yaml b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-devfile-annotation.yaml deleted file mode 100644 index 18a71260a..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-devfile-annotation.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: workspace.devfile.io/v1alpha2 -kind: DevWorkspace -metadata: - labels: {} - name: nodejs-stack - namespace: dwclient-test - annotations: - any.custom.settings: 'true' - che.eclipse.org/devfile: | - schemaVersion: 2.0.0 - metadata: - description: nodejs-stack sample - name: nodejs-stack - namespace: dwclient-test - uid: '' -spec: - routingClass: che - started: true - template: - attributes: - author: Somebody - dw.metadata.annotations: - any.custom.settings: 'true' - components: [] diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-ephemeral.yaml b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-ephemeral.yaml deleted file mode 100644 index 0568363eb..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-ephemeral.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: workspace.devfile.io/v1alpha2 -kind: DevWorkspace -metadata: - labels: {} - name: nodejs-stack - namespace: dwclient-test - annotations: - any.custom.settings: 'true' - uid: '' -spec: - routingClass: che - started: true - template: - attributes: - author: Somebody - controller.devfile.io/storage-type: ephemeral - dw.metadata.annotations: - any.custom.settings: 'true' - components: [] diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-parent.yaml b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-parent.yaml deleted file mode 100644 index 5da9228b7..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/__tests__/fixtures/test-devworkspace-parent.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: workspace.devfile.io/v1alpha2 -kind: DevWorkspace -metadata: - labels: {} - name: nodejs-stack - namespace: dwclient-test - annotations: - any.custom.settings: 'true' - uid: '' -spec: - routingClass: che - started: true - template: - attributes: - author: Somebody - dw.metadata.annotations: - any.custom.settings: 'true' - parent: - id: nodejs - registryUrl: 'https://registry.devfile.io' - components: [] diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/index.ts b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/index.ts deleted file mode 100644 index f108c46d0..000000000 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/converters/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2018-2024 Red Hat, Inc. - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - */ - -import { V1alpha2DevWorkspaceSpecTemplateComponents } from '@devfile/api'; -import { helpers } from '@eclipse-che/common'; -import { load } from 'js-yaml'; - -import devfileApi, { isDevfileV2 } from '@/services/devfileApi'; -import { DevWorkspaceSpecTemplateAttribute } from '@/services/devfileApi/devWorkspace/spec/template'; -import { - DEVWORKSPACE_DEVFILE, - DEVWORKSPACE_METADATA_ANNOTATION, -} from '@/services/workspace-client/devworkspace/devWorkspaceClient'; - -export const devfileSchemaVersion = '2.2.0'; - -export const devWorkspaceVersion = 'v1alpha2'; -export const devWorkspaceApiGroup = 'workspace.devfile.io'; -export const devWorkspaceSingularSubresource = 'devworkspace'; - -export function devfileToDevWorkspace( - devfile: devfileApi.Devfile, - routingClass: string, - started: boolean, -): devfileApi.DevWorkspace { - const devfileAttributes = devfile.metadata.attributes || {}; - if (devfile.attributes) { - Object.assign(devfileAttributes, devfile.attributes); - } - const devWorkspaceAnnotations = devfileAttributes[DEVWORKSPACE_METADATA_ANNOTATION] || {}; - - const devWorkspaceAttributes: DevWorkspaceSpecTemplateAttribute = {}; - Object.keys(devfileAttributes).forEach(key => { - devWorkspaceAttributes[key] = devfileAttributes[key]; - }); - - const devWorkspace: devfileApi.DevWorkspace = { - apiVersion: `${devWorkspaceApiGroup}/${devWorkspaceVersion}`, - kind: 'DevWorkspace', - metadata: { - name: devfile.metadata.name, - namespace: devfile.metadata.namespace, - annotations: devWorkspaceAnnotations, - labels: {}, - uid: '', - }, - spec: { - started, - routingClass, - template: { - components: [], - }, - }, - }; - if (Object.keys(devWorkspaceAttributes).length > 0) { - devWorkspace.spec.template.attributes = devWorkspaceAttributes; - } - if (devfile.parent) { - devWorkspace.spec.template.parent = devfile.parent; - } - if (devfile.projects) { - devWorkspace.spec.template.projects = devfile.projects; - } - if (devfile.components) { - devWorkspace.spec.template.components = devfile.components; - } - if (devfile.commands) { - devWorkspace.spec.template.commands = devfile.commands; - } - if (devfile.events) { - devWorkspace.spec.template.events = devfile.events; - } - return devWorkspace; -} - -export function devWorkspaceToDevfile(devworkspace: devfileApi.DevWorkspace): devfileApi.Devfile { - let originDevfile: devfileApi.Devfile | undefined; - if (devworkspace.metadata?.annotations?.[DEVWORKSPACE_DEVFILE]) { - try { - const loadedObject = load(devworkspace.metadata.annotations[DEVWORKSPACE_DEVFILE]); - if (isDevfileV2(loadedObject)) { - originDevfile = loadedObject; - } else { - throw new Error('The target object is not devfile V2.'); - } - } catch (e) { - const errorMessage = helpers.errors.getMessage(e); - console.debug(`Failed to parse the origin devfile. ${errorMessage}`); - } - } - - const schemaVersion = originDevfile?.schemaVersion || devfileSchemaVersion; - const metadata = originDevfile?.metadata || { - name: devworkspace.metadata.name, - namespace: devworkspace.metadata.namespace, - }; - - const template = { - schemaVersion, - metadata, - components: [], - } as devfileApi.Devfile; - if (devworkspace.spec.template.parent) { - template.parent = devworkspace.spec.template.parent; - } - if (devworkspace.spec.template.projects) { - template.projects = devworkspace.spec.template.projects; - } - if (devworkspace.spec.template.components) { - template.components = filterPluginComponents(devworkspace.spec.template.components); - } - if (devworkspace.spec.template.commands) { - template.commands = devworkspace.spec.template.commands; - } - if (devworkspace.spec.template.events) { - template.events = devworkspace.spec.template.events; - } - if (devworkspace.spec.template.attributes) { - const devWorkspaceAttributes: DevWorkspaceSpecTemplateAttribute = {}; - Object.keys(devworkspace.spec.template.attributes).forEach(key => { - devWorkspaceAttributes[key] = devworkspace.spec.template.attributes?.[key]; - }); - if (Object.keys(devWorkspaceAttributes).length > 0) { - template.attributes = devWorkspaceAttributes; - } - } - return template; -} - -// Filter plugins from components -function filterPluginComponents( - components: V1alpha2DevWorkspaceSpecTemplateComponents[], -): V1alpha2DevWorkspaceSpecTemplateComponents[] { - return components.filter(comp => !('plugin' in comp)); -} diff --git a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts index 340904716..471f4b0c3 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/devworkspace/devWorkspaceClient.ts @@ -37,14 +37,13 @@ import { delay } from '@/services/helpers/delay'; import { isWebTerminal } from '@/services/helpers/devworkspace'; import { DevWorkspaceStatus } from '@/services/helpers/types'; import { WorkspaceAdapter } from '@/services/workspace-adapter'; -import { - devWorkspaceApiGroup, - devWorkspaceSingularSubresource, - devWorkspaceVersion, -} from '@/services/workspace-client/devworkspace/converters'; import { DevWorkspaceDefaultPluginsHandler } from '@/services/workspace-client/devworkspace/DevWorkspaceDefaultPluginsHandler'; import { WorkspacesDefaultPlugins } from '@/store/Plugins/devWorkspacePlugins'; +export const devWorkspaceVersion = 'v1alpha2'; +export const devWorkspaceApiGroup = 'workspace.devfile.io'; +export const devWorkspaceSingularSubresource = 'devworkspace'; + export const COMPONENT_UPDATE_POLICY = 'che.eclipse.org/components-update-policy'; export const REGISTRY_URL = 'che.eclipse.org/plugin-registry-url'; diff --git a/packages/dashboard-frontend/src/store/DevfileRegistries/__tests__/selectors.spec.ts b/packages/dashboard-frontend/src/store/DevfileRegistries/__tests__/selectors.spec.ts index 94b7dc923..0a70de442 100644 --- a/packages/dashboard-frontend/src/store/DevfileRegistries/__tests__/selectors.spec.ts +++ b/packages/dashboard-frontend/src/store/DevfileRegistries/__tests__/selectors.spec.ts @@ -10,7 +10,7 @@ * Red Hat, Inc. - initial API and implementation */ -import { load } from 'js-yaml'; +import { dump, load } from 'js-yaml'; import devfileApi from '@/services/devfileApi'; import { che } from '@/services/models'; @@ -28,6 +28,7 @@ import { jest.mock('js-yaml', () => ({ load: jest.fn(), + dump: jest.fn(), })); jest.mock('@/store/ServerConfig/selectors', () => { return { @@ -180,11 +181,28 @@ describe('DevfileRegistries, selectors', () => { describe('selectDefaultDevfile', () => { it('should select default devfile', () => { - const mockDevfile = { components: [] }; + const mockDevfile = { + schemaVersion: '2.2.0', + metadata: { + name: 'empty', + }, + }; + const mockDevfileStr = 'schemaVersion: 2.2.0\nmetadata:\n name: empty\n'; + (load as jest.Mock).mockReturnValue(mockDevfile); + (dump as jest.Mock).mockReturnValue(mockDevfileStr); const result = selectDefaultDevfile(mockState); - expect(result).toEqual(mockDevfile); + expect(result).toEqual( + Object.assign({}, mockDevfile, { + attributes: { + 'dw.metadata.annotations': { + 'che.eclipse.org/devfile': mockDevfileStr, + }, + }, + components: [], + }), + ); }); it('should return undefined if the empty workspace URL is not found', () => { diff --git a/packages/dashboard-frontend/src/store/DevfileRegistries/selectors.ts b/packages/dashboard-frontend/src/store/DevfileRegistries/selectors.ts index 7509e9fee..c7138754a 100644 --- a/packages/dashboard-frontend/src/store/DevfileRegistries/selectors.ts +++ b/packages/dashboard-frontend/src/store/DevfileRegistries/selectors.ts @@ -12,10 +12,17 @@ import { createSelector } from '@reduxjs/toolkit'; import { load } from 'js-yaml'; +import cloneDeep from 'lodash/cloneDeep'; +import { DevfileAdapter } from '@/services/devfile/adapter'; import devfileApi from '@/services/devfileApi'; +import stringify from '@/services/helpers/editor'; import match from '@/services/helpers/filter'; import { che } from '@/services/models'; +import { + DEVWORKSPACE_DEVFILE, + DEVWORKSPACE_METADATA_ANNOTATION, +} from '@/services/workspace-client/devworkspace/devWorkspaceClient'; import { RootState } from '@/store'; import { selectDefaultComponents } from '@/store/ServerConfig/selectors'; @@ -94,7 +101,13 @@ export const selectDefaultDevfile = createSelector( const devfileContent = state.devfiles[devfileLocation]?.content; if (devfileContent) { try { - const devfile = load(devfileContent) as devfileApi.Devfile; + const _devfile = load(devfileContent) as devfileApi.Devfile; + const devfile = cloneDeep(_devfile); + const devfileAttr = DevfileAdapter.getAttributes(devfile); + if (!devfileAttr[DEVWORKSPACE_METADATA_ANNOTATION]) { + devfileAttr[DEVWORKSPACE_METADATA_ANNOTATION] = {}; + } + devfileAttr[DEVWORKSPACE_METADATA_ANNOTATION][DEVWORKSPACE_DEVFILE] = stringify(_devfile); // propagate default components if (!devfile.components || devfile.components.length === 0) { devfile.components = defaultComponents; diff --git a/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/actions.spec.ts b/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/actions.spec.ts index 61f8cbb92..4fb90bef3 100644 --- a/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/actions.spec.ts +++ b/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/actions.spec.ts @@ -196,6 +196,7 @@ describe('FactoryResolver, actions', () => { (verifyAuthorized as jest.Mock).mockResolvedValue(true); (isDevfileRegistryLocation as jest.Mock).mockReturnValue(false); (getFactoryResolver as jest.Mock).mockRejectedValue(new Error(errorMessage)); + (getYamlResolver as jest.Mock).mockResolvedValue(new Error(errorMessage)); (common.helpers.errors.getMessage as jest.Mock).mockReturnValue(errorMessage); await expect( @@ -227,6 +228,7 @@ describe('FactoryResolver, actions', () => { (verifyAuthorized as jest.Mock).mockResolvedValue(true); (isDevfileRegistryLocation as jest.Mock).mockReturnValue(false); (getFactoryResolver as jest.Mock).mockRejectedValue(mockError); + (getYamlResolver as jest.Mock).mockResolvedValue(mockError); (common.helpers.errors.includesAxiosResponse as unknown as jest.Mock).mockReturnValue(true); (common.helpers.errors.getMessage as jest.Mock).mockReturnValue('Unauthorized'); (isOAuthResponse as unknown as jest.Mock).mockImplementation(() => true); diff --git a/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/helpers.normalizeDevfileV2.spec.ts b/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/helpers.normalizeDevfileV2.spec.ts index 67ac77bc0..48dfc1204 100644 --- a/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/helpers.normalizeDevfileV2.spec.ts +++ b/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/helpers.normalizeDevfileV2.spec.ts @@ -102,7 +102,6 @@ describe('Normalize Devfile V2', () => { } as FactoryResolver, 'http://dummy-registry/devfiles/empty.yaml', defaultComponents, - 'che', {}, ); @@ -138,7 +137,6 @@ describe('Normalize Devfile V2', () => { } as FactoryResolver, 'http://dummy-registry/devfiles/empty.yaml', defaultComponents, - 'che', {}, ); @@ -154,7 +152,7 @@ describe('Normalize Devfile V2', () => { ); }); - it('should apply metadata name and namespace', () => { + it('should apply metadata name', () => { const devfile = { schemaVersion: '2.2.2', } as V230Devfile; @@ -165,12 +163,10 @@ describe('Normalize Devfile V2', () => { } as FactoryResolver, 'http://dummy-registry/devfiles/empty.yaml', [], - 'che', {}, ); expect(targetDevfile.metadata.name).toEqual(expect.stringContaining('empty-yaml')); - expect(targetDevfile.metadata.namespace).toEqual(expect.stringContaining('che')); }); it('should apply defaultComponents', () => { @@ -188,7 +184,6 @@ describe('Normalize Devfile V2', () => { } as FactoryResolver, 'http://dummy-registry/devfiles/empty.yaml', defaultComponents, - 'che', {}, ); @@ -229,7 +224,6 @@ describe('Normalize Devfile V2', () => { } as FactoryResolver, 'http://dummy-registry/devfiles/empty.yaml', defaultComponents, - 'che', factoryParams, ); @@ -273,7 +267,6 @@ describe('Normalize Devfile V2', () => { } as FactoryResolver, 'http://dummy-registry/devfiles/empty.yaml', defaultComponents, - 'che', factoryParams, ); @@ -317,7 +310,6 @@ describe('Normalize Devfile V2', () => { } as FactoryResolver, 'http://dummy-registry/devfiles/empty.yaml', defaultComponents, - 'che', factoryParams, ); @@ -352,7 +344,6 @@ describe('Normalize Devfile V2', () => { } as FactoryResolver, 'http://dummy-registry/devfiles/empty.yaml', defaultComponents, - 'che', factoryParams, ); diff --git a/packages/dashboard-frontend/src/store/FactoryResolver/actions.ts b/packages/dashboard-frontend/src/store/FactoryResolver/actions.ts index b8170f245..517f8cb0a 100644 --- a/packages/dashboard-frontend/src/store/FactoryResolver/actions.ts +++ b/packages/dashboard-frontend/src/store/FactoryResolver/actions.ts @@ -26,7 +26,6 @@ import { isDevfileRegistryLocation, normalizeDevfile, } from '@/store/FactoryResolver/helpers'; -import { selectDefaultNamespace } from '@/store/InfrastructureNamespaces/selectors'; import { verifyAuthorized } from '@/store/SanityCheck'; import { selectDefaultComponents } from '@/store/ServerConfig/selectors'; @@ -40,7 +39,6 @@ export const actionCreators = { (location: string, factoryParams: Partial = {}): AppThunk => async (dispatch, getState): Promise => { const state = getState(); - const namespace = selectDefaultNamespace(state).name; const optionalFilesContent = {}; const overrideParams = factoryParams @@ -54,29 +52,36 @@ export const actionCreators = { dispatch(factoryResolverRequestAction()); - let data: FactoryResolver; + let data: FactoryResolver | undefined = undefined; if (isDevfileRegistryLocation(location, state.dwServerConfig.config)) { data = await getYamlResolver(location); } else { - data = await getFactoryResolver(location, overrideParams); - const cheEditor = await grabLink(data.links || [], CHE_EDITOR_YAML_PATH); - if (cheEditor) { - optionalFilesContent[CHE_EDITOR_YAML_PATH] = cheEditor; + try { + data = await getFactoryResolver(location, overrideParams); + const cheEditor = await grabLink(data.links || [], CHE_EDITOR_YAML_PATH); + if (cheEditor) { + optionalFilesContent[CHE_EDITOR_YAML_PATH] = cheEditor; + } + } catch (error) { + if (location.endsWith('.yaml') || location.endsWith('.yml')) { + try { + data = await getYamlResolver(location); + } catch (e) { + console.log(e); + } + } + if (!data?.devfile) { + throw error; + } } } - if (!data.devfile) { + if (!data?.devfile) { throw new Error('The specified link does not contain any Devfile.'); } const defaultComponents = selectDefaultComponents(state); - const devfile = normalizeDevfile( - data, - location, - defaultComponents, - namespace, - factoryParams, - ); + const devfile = normalizeDevfile(data, location, defaultComponents, factoryParams); const resolver = { ...data, diff --git a/packages/dashboard-frontend/src/store/FactoryResolver/helpers.ts b/packages/dashboard-frontend/src/store/FactoryResolver/helpers.ts index e462a4a61..211589337 100644 --- a/packages/dashboard-frontend/src/store/FactoryResolver/helpers.ts +++ b/packages/dashboard-frontend/src/store/FactoryResolver/helpers.ts @@ -18,12 +18,14 @@ import cloneDeep from 'lodash/cloneDeep'; import { DevfileAdapter } from '@/services/devfile/adapter'; import devfileApi, { isDevfileV2 } from '@/services/devfileApi'; +import stringify from '@/services/helpers/editor'; import { FactoryParams } from '@/services/helpers/factoryFlow/buildFactoryParams'; import { generateWorkspaceName } from '@/services/helpers/generateName'; import { getProjectName } from '@/services/helpers/getProjectName'; import { DevfileV2ProjectSource, FactoryResolver } from '@/services/helpers/types'; import { che } from '@/services/models'; import { + DEVWORKSPACE_DEVFILE, DEVWORKSPACE_DEVFILE_SOURCE, DEVWORKSPACE_METADATA_ANNOTATION, } from '@/services/workspace-client/devworkspace/devWorkspaceClient'; @@ -63,7 +65,10 @@ export async function grabLink( return response.data; } catch (error) { // content may not be there - if (common.helpers.errors.includesAxiosResponse(error) && error.response?.status == 404) { + if ( + (common.helpers.errors.includesAxiosResponse(error) && error.response?.status == 404) || + error?.['code'] === 'ERR_NETWORK' + ) { return undefined; } throw error; @@ -155,7 +160,6 @@ export function buildDevfileV2( * @param location a source location. * @param defaultComponents Default components. These default components * are meant to be used when a Devfile does not contain any components. - * @param namespace the namespace where the pod lives. * @param factoryParams a Partial object. */ @@ -163,23 +167,15 @@ export function normalizeDevfile( data: FactoryResolver, location: string, defaultComponents: V230DevfileComponents[], - namespace: string, factoryParams: Partial, ): devfileApi.Devfile { /* Validate object */ - - let _devfile: unknown = data.devfile; - if (isDevfileFoundInRepo(data) === true) { - _devfile = data.devfile; - } else if (!isDevfileV2(data.devfile)) { - _devfile = buildDevfileV2(data.devfile); - } - if (!isDevfileV2(_devfile)) { + if (!isDevfileV2(data.devfile)) { throw new Error('Received object is not a Devfile V2.'); } const scmInfo = data['scm_info']; - const devfile = cloneDeep(_devfile); + const devfile = cloneDeep(data.devfile); /* Devfile Metadata */ @@ -187,13 +183,14 @@ export function normalizeDevfile( const namePrefix = devfile.metadata?.generateName ? devfile.metadata?.generateName : projectName; const name = devfile.metadata?.name || generateWorkspaceName(namePrefix); - const metadata: devfileApi.DevfileMetadata = { - name, - namespace, - }; - - devfile.metadata = Object.assign({}, metadata, devfile.metadata); - delete devfile.metadata.generateName; + if (!devfile.metadata) { + devfile.metadata = { name }; + } else { + devfile.metadata.name = devfile.metadata?.name || generateWorkspaceName(namePrefix); + if (devfile.metadata.generateName) { + delete devfile.metadata.generateName; + } + } /* Devfile Components */ @@ -272,6 +269,6 @@ export function normalizeDevfile( attributes[DEVWORKSPACE_METADATA_ANNOTATION] = {}; } attributes[DEVWORKSPACE_METADATA_ANNOTATION][DEVWORKSPACE_DEVFILE_SOURCE] = devfileSource; - + attributes[DEVWORKSPACE_METADATA_ANNOTATION][DEVWORKSPACE_DEVFILE] = stringify(data.devfile); return devfile; } diff --git a/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/actions/actionCreators/createWorkspaceFromDevfile.ts b/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/actions/actionCreators/createWorkspaceFromDevfile.ts index af32bf937..9a60d37b3 100644 --- a/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/actions/actionCreators/createWorkspaceFromDevfile.ts +++ b/packages/dashboard-frontend/src/store/Workspaces/devWorkspaces/actions/actionCreators/createWorkspaceFromDevfile.ts @@ -14,11 +14,14 @@ import common from '@eclipse-che/common'; import { dump } from 'js-yaml'; import { fetchResources } from '@/services/backend-client/devworkspaceResourcesApi'; +import { DevfileAdapter } from '@/services/devfile/adapter'; import devfileApi from '@/services/devfileApi'; import { FactoryParams } from '@/services/helpers/factoryFlow/buildFactoryParams'; import { loadResourcesContent } from '@/services/registry/resources'; import { COMPONENT_UPDATE_POLICY, + DEVWORKSPACE_DEVFILE, + DEVWORKSPACE_METADATA_ANNOTATION, REGISTRY_URL, } from '@/services/workspace-client/devworkspace/devWorkspaceClient'; import { AppThunk } from '@/store'; @@ -125,6 +128,18 @@ export const createWorkspaceFromDevfile = throw e; } + // TODO: remove this after merge of https://github.com/devfile/devworkspace-generator/pull/118 + if (!devWorkspaceResource.metadata.annotations) { + devWorkspaceResource.metadata.annotations = {}; + } + const originDevfileStr = + DevfileAdapter.getAttributes(devfile)?.[DEVWORKSPACE_METADATA_ANNOTATION]?.[ + DEVWORKSPACE_DEVFILE + ]; + if (originDevfileStr) { + devWorkspaceResource.metadata.annotations[DEVWORKSPACE_DEVFILE] = originDevfileStr; + } + await dispatch( actionCreators.createWorkspaceFromResources( devWorkspaceResource, diff --git a/packages/dashboard-frontend/src/store/__mocks__/devWorkspaceBuilder.ts b/packages/dashboard-frontend/src/store/__mocks__/devWorkspaceBuilder.ts index 0a61e61f2..2dcecaa38 100644 --- a/packages/dashboard-frontend/src/store/__mocks__/devWorkspaceBuilder.ts +++ b/packages/dashboard-frontend/src/store/__mocks__/devWorkspaceBuilder.ts @@ -19,11 +19,14 @@ import getRandomString from '@/services/helpers/random'; import { DevWorkspaceStatus } from '@/services/helpers/types'; export class DevWorkspaceBuilder { + private name = 'dev-wksp-' + getRandomString(4); private workspace: any = { kind: 'DevWorkspace', apiVersion: 'workspace.devfile.io/v1alpha2', metadata: { - annotations: {}, + annotations: { + 'che.eclipse.org/devfile': `schemaVersion: 2.2.0\nmetadata:\n generateName: dev-wksp\n`, + }, labels: {}, name: 'dev-wksp-' + getRandomString(4), namespace: '', From fb10759d4d20bd1b96a6128eb894815277d9639d Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Thu, 13 Feb 2025 03:22:40 +0200 Subject: [PATCH 2/4] fixup! fix: change the way the original devfile content is stored Signed-off-by: Oleksii Orel --- .../SshKeys/List/Entry/index.tsx | 25 ++- .../OverviewTab/GitRepo/__mocks__/index.tsx | 19 ++ .../__snapshots__/index.spec.tsx.snap | 51 ++++++ .../GitRepo/__tests__/index.spec.tsx | 130 ++++++++++++++ .../OverviewTab/GitRepo/index.tsx | 162 ++++++++++++++++++ .../OverviewTab/Projects/index.tsx | 3 + .../__snapshots__/index.spec.tsx.snap | 6 + .../OverviewTab/__tests__/index.spec.tsx | 1 + .../WorkspaceDetails/OverviewTab/index.tsx | 2 + .../src/services/workspace-adapter/index.ts | 20 ++- 10 files changed, 403 insertions(+), 16 deletions(-) create mode 100644 packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/__mocks__/index.tsx create mode 100644 packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/__tests__/__snapshots__/index.spec.tsx.snap create mode 100644 packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/__tests__/index.spec.tsx create mode 100644 packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/index.tsx diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/SshKeys/List/Entry/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/SshKeys/List/Entry/index.tsx index 754c26820..f7a1fbd9e 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/SshKeys/List/Entry/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/SshKeys/List/Entry/index.tsx @@ -37,7 +37,7 @@ export type Props = { onDeleteSshKey: (sshKey: api.SshKey) => void; }; export type State = { - isCopyTimerId: number | undefined; + timerId: number | undefined; isOpenDropdown: boolean; }; @@ -46,7 +46,7 @@ export class SshKeysListEntry extends React.PureComponent { super(props); this.state = { - isCopyTimerId: undefined, + timerId: undefined, isOpenDropdown: false, }; } @@ -60,26 +60,21 @@ export class SshKeysListEntry extends React.PureComponent { } private handleCopyToClipboard(): void { - const { isCopyTimerId } = this.state; - - if (isCopyTimerId !== undefined) { - clearTimeout(isCopyTimerId); + let { timerId } = this.state; + if (timerId !== undefined) { + window.clearTimeout(timerId); } - - const nextTimerId = window.setTimeout(() => { + timerId = window.setTimeout(() => { this.setState({ - isCopyTimerId: undefined, + timerId: undefined, }); }, 3000); - - this.setState({ - isCopyTimerId: nextTimerId, - }); + this.setState({ timerId }); } render(): React.ReactNode { const { sshKey } = this.props; - const { isCopyTimerId, isOpenDropdown } = this.state; + const { timerId, isOpenDropdown } = this.state; const publicKey = atob(sshKey.keyPub); const addedOn = getFormattedDate(sshKey.creationTimestamp); @@ -95,7 +90,7 @@ export class SshKeysListEntry extends React.PureComponent { {sshKey.name} - + this.handleCopyToClipboard()}>
; + } +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 000000000..5e4559683 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GitRepoURL screenshot when factory params is available 1`] = ` +
+
+ + +
+
+ + eclipse-che/che-dashboard + +
+ + Copy to clipboard +
+
+
+`; + +exports[`GitRepoURL screenshot when factory params is not available 1`] = `null`; diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/__tests__/index.spec.tsx new file mode 100644 index 000000000..1d3aae45b --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/__tests__/index.spec.tsx @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2018-2024 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import userEvent from '@testing-library/user-event'; +import { dump } from 'js-yaml'; +import React from 'react'; + +import GitRepoFormGroup from '@/pages/WorkspaceDetails/OverviewTab/GitRepo'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; +import { constructWorkspace, Workspace } from '@/services/workspace-adapter'; +import { DevWorkspaceBuilder } from '@/store/__mocks__/devWorkspaceBuilder'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +// mute the outputs +console.error = jest.fn(); + +const mockClipboard = jest.fn(); +jest.mock('react-copy-to-clipboard', () => { + return { + __esModule: true, + default: (props: any) => { + return ( + + ); + }, + }; +}); + +describe('GitRepoURL', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + test('screenshot when factory params is not available', () => { + const devWorkspace = new DevWorkspaceBuilder().build(); + + const snapshot = createSnapshot(constructWorkspace(devWorkspace)); + + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('screenshot when factory params is available', () => { + const devWorkspace = new DevWorkspaceBuilder() + .withTemplateAttributes({ + 'dw.metadata.annotations': { + 'che.eclipse.org/devfile-source': dump({ + factory: { + params: + 'editor-image=test-images/che-code:tag&url=https://github.com/eclipse-che/che-dashboard', + }, + }), + }, + }) + .build(); + + const snapshot = createSnapshot(constructWorkspace(devWorkspace)); + + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('copy to clipboard', async () => { + const devWorkspace = new DevWorkspaceBuilder() + .withTemplateAttributes({ + 'dw.metadata.annotations': { + 'che.eclipse.org/devfile-source': dump({ + factory: { + params: + 'editor-image=test-images/che-code:tag&url=https://github.com/eclipse-che/che-dashboard', + }, + }), + }, + }) + .build(); + + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + const mockCreateObjectURL = jest.fn().mockReturnValue('blob-url'); + URL.createObjectURL = mockCreateObjectURL; + + renderComponent(constructWorkspace(devWorkspace)); + + const copyButtonName = 'Copy to clipboard'; + expect(screen.queryByRole('button', { name: copyButtonName })).toBeTruthy; + + const copyButton = screen.getByRole('button', { name: copyButtonName }); + await user.click(copyButton); + + expect(mockClipboard).toHaveBeenCalledWith( + 'https://github.com/eclipse-che/che-dashboard?editor-image=test-images/che-code:tag', + ); + + /* 'Copy to clipboard' should be hidden for a while */ + + expect(screen.queryByRole('button', { name: copyButtonName })).toBeFalsy; + + const copyButtonNameAfter = 'Copied'; + expect(screen.queryByRole('button', { name: copyButtonNameAfter })).toBeTruthy; + + /* 'Copy to clipboard' should re-appear after 3000ms */ + + jest.advanceTimersByTime(4000); + expect(screen.queryByRole('button', { name: copyButtonName })).toBeTruthy; + expect(screen.queryByRole('button', { name: copyButtonNameAfter })).toBeFalsy; + }); +}); + +function getComponent(workspace: Workspace) { + return ; +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/index.tsx new file mode 100644 index 000000000..4ec7b70aa --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/GitRepo/index.tsx @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2018-2024 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { Button, FormGroup } from '@patternfly/react-core'; +import { CopyIcon } from '@patternfly/react-icons'; +import { load } from 'js-yaml'; +import React from 'react'; +import CopyToClipboard from 'react-copy-to-clipboard'; + +import { CheTooltip } from '@/components/CheTooltip'; +import overviewStyles from '@/pages/WorkspaceDetails/OverviewTab/index.module.css'; +import devfileApi from '@/services/devfileApi'; +import { Workspace } from '@/services/workspace-adapter'; +import { + DEVWORKSPACE_DEVFILE_SOURCE, + DEVWORKSPACE_METADATA_ANNOTATION, +} from '@/services/workspace-client/devworkspace/devWorkspaceClient'; + +export type Props = { + workspace: Workspace; +}; + +export type State = { + timerId: number | undefined; +}; + +class GitRepoFormGroup extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + timerId: undefined, + }; + } + + private handleCopyToClipboard(): void { + let { timerId } = this.state; + if (timerId !== undefined) { + window.clearTimeout(timerId); + } + timerId = window.setTimeout(() => { + this.setState({ + timerId: undefined, + }); + }, 3000); + this.setState({ timerId }); + } + + public getSource(workspace: devfileApi.DevWorkspace): { + isUrl: boolean; + gitRepo: string; + fieldName: string; + } { + const source = { + isUrl: false, + gitRepo: '', + fieldName: '', + }; + + let devfileSourseStr = + workspace.metadata.annotations?.[DEVWORKSPACE_METADATA_ANNOTATION]?.[ + DEVWORKSPACE_DEVFILE_SOURCE + ]; + + if (devfileSourseStr === undefined) { + devfileSourseStr = + workspace.spec?.template?.attributes?.[DEVWORKSPACE_METADATA_ANNOTATION]?.[ + DEVWORKSPACE_DEVFILE_SOURCE + ]; + } + + if (devfileSourseStr === undefined) { + return source; + } + + const devfileSourse = load(devfileSourseStr) as { + scm?: { + repo?: string; + fileName?: string; + }; + factory?: { + params?: string; + }; + }; + + const factoryParams = devfileSourse?.factory?.params; + if (factoryParams === undefined) { + return source; + } + + const paramsArr = factoryParams.split('&'); + if (paramsArr.length === 0) { + return source; + } + + paramsArr.forEach(param => { + const [key, value] = param.split('='); + if (key === 'url') { + if (!source.gitRepo && paramsArr.length === 1) { + source.gitRepo = value; + } else { + source.gitRepo = value + '?' + source.gitRepo; + } + source.isUrl = new RegExp('^http[s]?://').test(value); + source.fieldName = source.isUrl ? new URL(value).pathname.replace(/^\//, '') : value; + if (source.fieldName.length > 50) { + source.fieldName = source.fieldName.substring(0, 50) + '...'; + } + } else { + if (source.gitRepo.length !== 0 && !source.gitRepo.endsWith('?')) { + source.gitRepo = source.gitRepo + '&'; + } + source.gitRepo = source.gitRepo + key + '=' + value; + } + }); + + return source; + } + + public render(): React.ReactNode { + const { workspace } = this.props; + const { timerId } = this.state; + + const { gitRepo, fieldName, isUrl } = this.getSource(workspace.ref); + if (!gitRepo || !fieldName) { + return <>; + } + + return ( + + {isUrl ? ( + + ) : ( + {fieldName} + )} + + this.handleCopyToClipboard()}> +