diff --git a/packages/dashboard-frontend/src/Routes/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/Routes/__tests__/index.spec.tsx index c4fff6df0..6e134e898 100644 --- a/packages/dashboard-frontend/src/Routes/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/Routes/__tests__/index.spec.tsx @@ -138,8 +138,17 @@ describe('Routes', () => { expect(screen.queryByTestId('fallback-spinner')).not.toBeInTheDocument(); }); - it('should handle "/workspace/namespace/name?tab=Devworkspace"', async () => { - const location = buildDetailsLocation(workspace, WorkspaceDetailsTab.DEVWORKSPACE); + it('should handle "/workspace/namespace/name?tab=Logs"', async () => { + const location = buildDetailsLocation(workspace, WorkspaceDetailsTab.LOGS); + render(getComponent(location)); + + await waitFor(() => expect(screen.queryByText('Workspace Details')).toBeTruthy()); + + expect(screen.queryByTestId('fallback-spinner')).not.toBeInTheDocument(); + }); + + it('should handle "/workspace/namespace/name?tab=Events"', async () => { + const location = buildDetailsLocation(workspace, WorkspaceDetailsTab.EVENTS); render(getComponent(location)); await waitFor(() => expect(screen.queryByText('Workspace Details')).toBeTruthy()); diff --git a/packages/dashboard-frontend/src/components/DevfileViewer/__mocks__/index.tsx b/packages/dashboard-frontend/src/components/DevfileViewer/__mocks__/index.tsx new file mode 100644 index 000000000..163489b67 --- /dev/null +++ b/packages/dashboard-frontend/src/components/DevfileViewer/__mocks__/index.tsx @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2018-2023 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 React from 'react'; + +import { Props } from '@/components/DevfileViewer'; + +export class DevfileViewer extends React.PureComponent { + public render(): React.ReactNode { + const { isActive, isExpanded, value } = this.props; + + return ( +
+ Mock Devfile Viewer + {isActive.toString()} + {isExpanded.toString()} + {value} +
+ ); + } +} diff --git a/packages/dashboard-frontend/src/components/DevfileViewer/index.tsx b/packages/dashboard-frontend/src/components/DevfileViewer/index.tsx index 601d9cbfe..a2f8f5f52 100644 --- a/packages/dashboard-frontend/src/components/DevfileViewer/index.tsx +++ b/packages/dashboard-frontend/src/components/DevfileViewer/index.tsx @@ -19,7 +19,7 @@ import React from 'react'; import styles from '@/components/DevfileViewer/index.module.css'; -type Props = { +export type Props = { isActive: boolean; isExpanded: boolean; value: string; @@ -71,5 +71,3 @@ export class DevfileViewer extends React.PureComponent { ); } } - -export default DevfileViewer; diff --git a/packages/dashboard-frontend/src/components/EditorTools/__mocks__/index.tsx b/packages/dashboard-frontend/src/components/EditorTools/__mocks__/index.tsx new file mode 100644 index 000000000..b6efb0f01 --- /dev/null +++ b/packages/dashboard-frontend/src/components/EditorTools/__mocks__/index.tsx @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2018-2023 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 + */ + +// mock for the EditorTools component +import React from 'react'; + +import { Props } from '@/components/EditorTools'; + +export default class EditorTools extends React.PureComponent { + public render(): React.ReactNode { + const { handleExpand } = this.props; + return ( +
+ Mock Editor Tools + +
+ ); + } +} diff --git a/packages/dashboard-frontend/src/components/EditorTools/index.tsx b/packages/dashboard-frontend/src/components/EditorTools/index.tsx index 500702a51..cd4555f46 100644 --- a/packages/dashboard-frontend/src/components/EditorTools/index.tsx +++ b/packages/dashboard-frontend/src/components/EditorTools/index.tsx @@ -35,7 +35,7 @@ import { AppState } from '@/store'; import { actionCreators } from '@/store/BannerAlert'; import { selectApplications } from '@/store/ClusterInfo/selectors'; -type Props = MappedProps & { +export type Props = MappedProps & { devfileOrDevWorkspace: devfileApi.DevWorkspace | devfileApi.Devfile; handleExpand: (isExpand: boolean) => void; }; diff --git a/packages/dashboard-frontend/src/contexts/WorkspaceActions/Provider.tsx b/packages/dashboard-frontend/src/contexts/WorkspaceActions/Provider.tsx index e1bbc2208..7fbcb8196 100644 --- a/packages/dashboard-frontend/src/contexts/WorkspaceActions/Provider.tsx +++ b/packages/dashboard-frontend/src/contexts/WorkspaceActions/Provider.tsx @@ -123,7 +123,7 @@ export class WorkspaceActionsProvider extends React.Component { return this.handleLocation(buildIdeLoaderLocation(workspace), workspace); } case WorkspaceAction.WORKSPACE_DETAILS: { - return buildDetailsLocation(workspace, WorkspaceDetailsTab.DEVFILE); + return buildDetailsLocation(workspace); } case WorkspaceAction.START_DEBUG_AND_OPEN_LOGS: { await this.props.startWorkspace(workspace, { diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 000000000..689b22943 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DevfileEditorTab component snapshot 1`] = ` +[ +
, +
+
+ Mock Editor Tools + +
+
+ Mock Devfile Viewer + + true + + + false + + + schemaVersion: 2.2.0 +metadata: + name: wksp + namespace: '' +components: [] + + +
+
, +] +`; 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 new file mode 100644 index 000000000..ea3d0a7d0 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/__tests__/index.spec.tsx @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2018-2023 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 { cloneDeep } from 'lodash'; +import React from 'react'; + +import { DevfileEditorTab, prepareDevfile } 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'); +jest.mock('@/components/DevfileViewer'); + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +describe('DevfileEditorTab', () => { + let workspace: Workspace; + + beforeEach(() => { + const devWorkspace = new DevWorkspaceBuilder().withName('wksp').build(); + workspace = constructWorkspace(devWorkspace); + }); + + describe('component', () => { + test('snapshot', () => { + const snapshot = createSnapshot(true, workspace); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('expanded state', () => { + renderComponent(true, workspace); + + const buttonExpand = screen.getByRole('button', { name: 'Expand Editor' }); + userEvent.click(buttonExpand); + + const isExpanded = screen.getByTestId('devfile-viewer-is-expanded'); + 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) { + return ; +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/index.tsx index 7245931f8..fbd783a87 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/index.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevfileEditorTab/index.tsx @@ -14,7 +14,7 @@ import { TextContent } from '@patternfly/react-core'; import { load } from 'js-yaml'; import React from 'react'; -import DevfileViewer from '@/components/DevfileViewer'; +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'; @@ -49,20 +49,8 @@ export class DevfileEditorTab extends React.PureComponent { const { isExpanded } = this.state; const editorTabStyle = isExpanded ? styles.editorTabExpanded : styles.editorTab; - let originDevfileStr = this.props.workspace.ref.metadata?.annotations?.[DEVWORKSPACE_DEVFILE]; - if (!originDevfileStr) { - originDevfileStr = stringify(this.props.workspace.devfile); - } - const devfile = load(originDevfileStr) as devfileApi.Devfile; - const attrs = DevfileAdapter.getAttributesFromDevfileV2(devfile); - if (attrs?.[DEVWORKSPACE_METADATA_ANNOTATION]) { - delete attrs[DEVWORKSPACE_METADATA_ANNOTATION]; - if (Object.keys(attrs).length === 0) { - delete devfile.attributes; - delete devfile.metadata.attributes; - } - originDevfileStr = stringify(devfile); - } + const devfile = prepareDevfile(this.props.workspace); + const devfileStr = stringify(devfile); return ( @@ -77,7 +65,7 @@ export class DevfileEditorTab extends React.PureComponent { @@ -86,4 +74,18 @@ export class DevfileEditorTab extends React.PureComponent { } } -export default DevfileEditorTab; +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.getAttributesFromDevfileV2(devfile); + if (attrs?.[DEVWORKSPACE_METADATA_ANNOTATION]) { + delete attrs[DEVWORKSPACE_METADATA_ANNOTATION]; + } + if (Object.keys(attrs).length === 0) { + delete devfile.attributes; + delete devfile.metadata.attributes; + } + + return devfile; +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevworkspaceEditorTab/index.module.css b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevworkspaceEditorTab/index.module.css deleted file mode 100644 index 318b743f5..000000000 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevworkspaceEditorTab/index.module.css +++ /dev/null @@ -1,28 +0,0 @@ -/* -* Copyright (c) 2018-2023 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 -*/ - -.editorTab { - width: calc(100% - 10px); - height: 50vh; -} - -.editorTabExpanded { - position: absolute; - top: 0; - right: 0; - left: 0; - - height: calc(100vh - 55px); - padding: 0 20px; - - background-color: var(--pf-global--BackgroundColor--100); -} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevworkspaceEditorTab/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevworkspaceEditorTab/index.tsx deleted file mode 100644 index 396b2f731..000000000 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/DevworkspaceEditorTab/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2018-2023 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 { TextContent } from '@patternfly/react-core'; -import { dump } from 'js-yaml'; -import React from 'react'; - -import DevfileViewer from '@/components/DevfileViewer'; -import EditorTools from '@/components/EditorTools'; -import styles from '@/pages/WorkspaceDetails/DevworkspaceEditorTab/index.module.css'; -import { Workspace } from '@/services/workspace-adapter'; - -export type Props = { - workspace: Workspace; - isActive: boolean; -}; - -export type State = { - isExpanded: boolean; - copied?: boolean; -}; - -export class DevworkspaceEditorTab extends React.PureComponent { - constructor(props: Props) { - super(props); - - this.state = { - isExpanded: false, - }; - } - - public render(): React.ReactElement { - const { isExpanded } = this.state; - const { workspace } = this.props; - const editorTabStyle = isExpanded ? styles.editorTabExpanded : styles.editorTab; - const devWorkspaceStr = dump(workspace.ref); - - return ( - <> -
- - { - this.setState({ isExpanded }); - }} - /> - - - - ); - } -} - -export default DevworkspaceEditorTab; diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/__mocks__/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/__mocks__/index.tsx new file mode 100644 index 000000000..dcf57bb35 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/__mocks__/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018-2023 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 React from 'react'; + +export class InfrastructureNamespaceFormGroup extends React.PureComponent { + public render(): React.ReactElement { + return
Mock Infrastructure Namespace Form
; + } +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 000000000..6927d7a58 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InfrastructureNamespaceFormGroup screenshot 1`] = ` +
+
+ + +
+
+
+ user-namespace +
+ +
+
+`; diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/__tests__/index.spec.tsx new file mode 100644 index 000000000..6cc174e76 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/__tests__/index.spec.tsx @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018-2023 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 React from 'react'; + +import { InfrastructureNamespaceFormGroup } from '@/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +describe('InfrastructureNamespaceFormGroup', () => { + test('screenshot', () => { + const snapshot = createSnapshot('user-namespace'); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('namespace is displayed', () => { + renderComponent('user-namespace'); + expect(screen.queryByText('user-namespace')).not.toBeNull(); + }); +}); + +function getComponent(namespace: string) { + return ; +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/index.tsx similarity index 86% rename from packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace.tsx rename to packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/index.tsx index 1a34120bb..b4475406b 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace/index.tsx @@ -19,7 +19,7 @@ type Props = { namespace: string; }; -class InfrastructureNamespaceFormGroup extends React.PureComponent { +export class InfrastructureNamespaceFormGroup extends React.PureComponent { public render(): React.ReactElement { return ( @@ -28,5 +28,3 @@ class InfrastructureNamespaceFormGroup extends React.PureComponent { ); } } - -export default InfrastructureNamespaceFormGroup; diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/__mocks__/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/__mocks__/index.tsx new file mode 100644 index 000000000..a2bfcbd1c --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/__mocks__/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018-2023 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 React from 'react'; + +export class ProjectsFormGroup extends React.PureComponent { + public render(): React.ReactElement { + return
Mock Projects Form
; + } +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 000000000..57171be29 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProjectsFormGroup screenshot 1`] = ` +
+
+ + +
+
+
+ project1, project2 +
+ +
+
+`; diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/__tests__/index.spec.tsx new file mode 100644 index 000000000..f81b19e2b --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/__tests__/index.spec.tsx @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018-2023 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 React from 'react'; + +import { ProjectsFormGroup } from '@/pages/WorkspaceDetails/OverviewTab/Projects'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +describe('ProjectsFormGroup', () => { + test('screenshot', () => { + const snapshot = createSnapshot(['project1', 'project2']); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('all projects are displayed', () => { + renderComponent(['project1', 'project2']); + expect(screen.queryByText('project1, project2')).not.toBeNull(); + }); +}); + +function getComponent(projects: string[]) { + return ; +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/index.tsx similarity index 89% rename from packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects.tsx rename to packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/index.tsx index d02f3d5f9..63d8683aa 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/Projects/index.tsx @@ -19,7 +19,7 @@ type Props = { projects: string[]; }; -class ProjectsFormGroup extends React.PureComponent { +export class ProjectsFormGroup extends React.PureComponent { public render(): React.ReactElement { const projects = this.props.projects.join(', '); return ( @@ -29,5 +29,3 @@ class ProjectsFormGroup extends React.PureComponent { ); } } - -export default ProjectsFormGroup; diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/__mocks__/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/__mocks__/index.tsx index 075fbcc36..1bb790a1c 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/__mocks__/index.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/__mocks__/index.tsx @@ -12,10 +12,15 @@ import React from 'react'; -import { Props, State } from '..'; +import { Props } from '@/pages/WorkspaceDetails/OverviewTab/StorageType'; -export default class StorageTypeFormGroup extends React.PureComponent { +export default class StorageTypeFormGroup extends React.PureComponent { render() { - return
Fake Storage Type Form
; + return ( +
+ Mock Storage Type Form + +
+ ); } } diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 000000000..f73ffcaf9 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,162 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StorageTypeFormGroup editable storage type screenshot 1`] = ` +
+
+ + + +
+
+ + per-workspace + + + +
+
+`; + +exports[`StorageTypeFormGroup readonly storage type screenshot 1`] = ` +
+
+ + + +
+
+ + per-workspace + + +
+
+`; diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/__tests__/index.spec.tsx new file mode 100644 index 000000000..c987074a9 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/__tests__/index.spec.tsx @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2018-2023 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 { StateMock } from '@react-mock/state'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; + +import StorageTypeFormGroup, { State } from '@/pages/WorkspaceDetails/OverviewTab/StorageType'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; +import { BrandingData } from '@/services/bootstrap/branding.constant'; +import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +const mockOnSave = jest.fn(); + +describe('StorageTypeFormGroup', () => { + let store: Store; + + beforeEach(() => { + store = new FakeStoreBuilder() + .withBranding({ + docs: { + storageTypes: 'storage-types-docs', + }, + } as BrandingData) + .withDwServerConfig({ + defaults: { + pvcStrategy: 'per-workspace', + components: [], + editor: undefined, + plugins: [], + }, + }) + .build(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('readonly storage type', () => { + const readonly = true; + + test('screenshot', () => { + const snapshot = createSnapshot(store, { readonly }); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + }); + + describe('editable storage type', () => { + const readonly = false; + + test('screenshot', () => { + const snapshot = createSnapshot(store, { readonly }); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + describe('storage type info modal dialog', () => { + test('show modal', () => { + renderComponent(store, { readonly }); + + const button = screen.queryByRole('button', { name: 'Storage Type Info' }); + expect(button).not.toBeNull(); + + userEvent.click(button!); + + const modal = screen.queryByRole('dialog', { name: 'Storage Type Info' }); + const buttonClose = screen.queryByRole('button', { name: 'Close' }); + const documentationLink = screen.queryByRole('link', { name: 'Open documentation page' }); + + expect(modal).not.toBeNull(); + expect(buttonClose).not.toBeNull(); + expect(documentationLink).not.toBeNull(); + }); + + test('close modal dialog', () => { + renderComponent(store, { readonly }, { isInfoOpen: true }); + + // modal is opened + expect(screen.queryByRole('dialog', { name: 'Storage Type Info' })).not.toBeNull(); + + const buttonClose = screen.getByRole('button', { name: 'Close' }); + + userEvent.click(buttonClose!); + + // modal is closed + expect(screen.queryByRole('dialog', { name: 'Storage Type Info' })).toBeNull(); + }); + }); + + describe('change storage type modal dialog', () => { + test('show modal', () => { + renderComponent(store, { readonly }); + + const button = screen.queryByRole('button', { name: 'Change Storage Type' }); + expect(button).not.toBeNull(); + + userEvent.click(button!); + + const modal = screen.queryByRole('dialog', { name: 'Change Storage Type' }); + const buttonSave = screen.queryByRole('button', { name: 'Save' }); + const buttonClose = screen.queryByRole('button', { name: 'Close' }); + const buttonCancel = screen.queryByRole('button', { name: 'Cancel' }); + + expect(modal).not.toBeNull(); + expect(buttonSave).not.toBeNull(); + expect(buttonClose).not.toBeNull(); + expect(buttonCancel).not.toBeNull(); + }); + + test('close modal dialog', () => { + renderComponent(store, { readonly }, { isSelectorOpen: true }); + + // modal is opened + expect(screen.queryByRole('dialog', { name: 'Change Storage Type' })).not.toBeNull(); + + const buttonClose = screen.getByRole('button', { name: 'Close' }); + + userEvent.click(buttonClose!); + + // modal is closed + expect(screen.queryByRole('dialog', { name: 'Change Storage Type' })).toBeNull(); + }); + + test('change storage type', () => { + renderComponent(store, { readonly, storageType: 'ephemeral' }, { isSelectorOpen: true }); + + const radioPerWorkspace = screen.getByRole('radio', { name: 'Per-workspace' }); + const buttonSave = screen.getByRole('button', { name: 'Save' }); + + userEvent.click(radioPerWorkspace); + userEvent.click(buttonSave); + + // modal is closed + expect(screen.queryByRole('dialog', { name: 'Change Storage Type' })).toBeNull(); + + expect(mockOnSave).toHaveBeenCalledWith('per-workspace'); + }); + }); + }); +}); + +function getComponent( + store: Store, + props: { readonly: boolean; storageType?: che.WorkspaceStorageType }, + state?: Partial, +) { + if (state) { + return ( + + + + + + ); + } else { + return ( + + + + ); + } +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/index.tsx index bf1fb75db..7a90f2a0f 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/index.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/StorageType/index.tsx @@ -36,7 +36,7 @@ import { selectPvcStrategy } from '@/store/ServerConfig/selectors'; export type Props = MappedProps & { readonly: boolean; storageType?: che.WorkspaceStorageType; - onSave?: (storageType: che.WorkspaceStorageType) => void; + onSave: (storageType: che.WorkspaceStorageType) => void; }; export type State = { isSelectorOpen?: boolean; @@ -44,7 +44,7 @@ export type State = { isInfoOpen?: boolean; }; -export class StorageTypeFormGroup extends React.PureComponent { +class StorageTypeFormGroup extends React.PureComponent { storageTypes: che.WorkspaceStorageType[] = []; options: string[] = []; preferredType: che.WorkspaceStorageType; @@ -257,7 +257,7 @@ export class StorageTypeFormGroup extends React.PureComponent { variant={ModalVariant.small} isOpen={isSelectorOpen} className={styles.modalEditStorageType} - title="Edit Storage Type" + title="Change Storage Type" onClose={() => this.handleCancelChanges()} actions={[ @@ -334,6 +333,7 @@ export class StorageTypeFormGroup extends React.PureComponent { data-testid="overview-storage-edit-toggle" variant="plain" onClick={() => this.handleEditToggle(true)} + title="Change Storage Type" > @@ -341,7 +341,7 @@ export class StorageTypeFormGroup extends React.PureComponent { )} {this.getSelectorModal()} { @@ -360,7 +360,10 @@ const mapStateToProps = (state: AppState) => ({ preferredStorageType: selectPvcStrategy(state), }); -const connector = connect(mapStateToProps); +const connector = connect(mapStateToProps, null, null, { + // forwardRef is mandatory for using `@react-mock/state` in unit tests + forwardRef: true, +}); type MappedProps = ConnectedProps; export default connector(StorageTypeFormGroup); diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/__mocks__/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/__mocks__/index.tsx new file mode 100644 index 000000000..21c845d38 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/__mocks__/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018-2023 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 React from 'react'; + +export default class WorkspaceNameFormGroups extends React.PureComponent { + render() { + return
Mock Workspace Name Form
; + } +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 000000000..08ba08e87 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WorkspaceNameLink screenshot when cluster console is NOT available 1`] = ` +
+
+ + +
+
+ + my-project + + +
+
+`; + +exports[`WorkspaceNameLink screenshot when cluster console is available 1`] = ` +
+
+ + +
+ +
+`; diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/__tests__/index.spec.tsx index b7a7d0e87..3aded1d8d 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/__tests__/index.spec.tsx @@ -10,180 +10,59 @@ * Red Hat, Inc. - initial API and implementation */ -import { fireEvent, render, RenderResult, screen } from '@testing-library/react'; +import { ApplicationId } from '@eclipse-che/common'; import React from 'react'; - -import { WorkspaceNameFormGroup } from '..'; - -describe('Overview Tab Workspace Name Input', () => { - const mockOnSave = jest.fn(); - - function renderInput(name: string): RenderResult { - return render(); - } - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should show default placeholder', () => { - renderInput('new-workspace'); - - screen.getByTestId('overview-name-edit-toggle').click(); - - const placeholder = screen.getByPlaceholderText('Enter a workspace name'); - expect(placeholder).toBeTruthy(); +import { Provider } from 'react-redux'; +import { Store } from 'redux'; + +import WorkspaceNameLink from '@/pages/WorkspaceDetails/OverviewTab/WorkspaceName'; +import getComponentRenderer from '@/services/__mocks__/getComponentRenderer'; +import devfileApi from '@/services/devfileApi'; +import { constructWorkspace, Workspace } from '@/services/workspace-adapter'; +import { DevWorkspaceBuilder } from '@/store/__mocks__/devWorkspaceBuilder'; +import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; + +const { createSnapshot } = getComponentRenderer(getComponent); + +describe('WorkspaceNameLink', () => { + let storeBuilder: FakeStoreBuilder; + let devWorkspace: devfileApi.DevWorkspace; + let workspace: Workspace; + + beforeEach(() => { + devWorkspace = new DevWorkspaceBuilder().withName('my-project').build(); + workspace = constructWorkspace(devWorkspace); + storeBuilder = new FakeStoreBuilder().withDevWorkspaces({ workspaces: [devWorkspace] }); }); - it('should show placeholder with generated name', () => { - renderInput(''); - - screen.getByTestId('overview-name-edit-toggle').click(); - - const textbox = screen.getByRole('textbox'); - expect(textbox).toBeTruthy(); - - const placeholder = textbox.getAttribute('placeholder'); - expect(placeholder).toMatch('Enter a workspace name'); - }); - - it('should correctly render the component', () => { - renderInput('new-workspace'); - - screen.getByTestId('overview-name-edit-toggle').click(); - - const input = screen.getByRole('textbox'); - expect(input).toHaveValue('new-workspace'); + test('screenshot when cluster console is available', () => { + const store = storeBuilder + .withClusterInfo({ + applications: [ + { + id: ApplicationId.CLUSTER_CONSOLE, + title: 'Cluster Console', + url: 'https://console-openshift-console.apps-crc.testing', + icon: 'icon', + }, + ], + }) + .build(); + const snapshot = createSnapshot(store, workspace); + expect(snapshot.toJSON()).toMatchSnapshot(); }); - it('should make form readonly', () => { - render(); - - expect(screen.queryByTestId('overview-name-edit-toggle')).not.toBeInTheDocument(); - }); - - it('should correctly re-render the component', () => { - const { rerender } = renderInput('name'); - - screen.getByTestId('overview-name-edit-toggle').click(); - - const input = screen.getByRole('textbox'); - expect(input).toHaveValue('name'); - - rerender(); - - expect(input).toHaveValue('new-name'); - }); - - it('should fire onChange event', () => { - renderInput(''); - - screen.getByTestId('overview-name-edit-toggle').click(); - - const input = screen.getByRole('textbox'); - - fireEvent.change(input, { target: { value: 'new-workspace' } }); - - screen.getByTestId('handle-on-save').click(); - - expect(mockOnSave).toHaveBeenCalledWith('new-workspace'); - }); - - describe('Overview Tab Workspace Name Validation', () => { - let textbox: HTMLInputElement; - - beforeEach(() => { - renderInput('new-workspace'); - - screen.getByTestId('overview-name-edit-toggle').click(); - - textbox = screen.getByRole('textbox') as HTMLInputElement; - }); - - it('should handle empty value', () => { - fireEvent.change(textbox, { target: { value: '' } }); - const label = screen.getByText('A value is required.'); - expect(label).toBeTruthy(); - expect(textbox).toBeInvalid(); - }); - - it('should handle minimal value length', () => { - let label: HTMLElement | null; - const message = 'The name has to be at least 3 characters long.'; - - const disallowedName1 = 'a'; - - fireEvent.change(textbox, { target: { value: disallowedName1 } }); - label = screen.queryByText(message); - expect(label).toBeTruthy(); - expect(textbox).toBeInvalid(); - - const disallowedName2 = 'ab'; - - fireEvent.change(textbox, { target: { value: disallowedName2 } }); - label = screen.queryByText(message); - expect(label).toBeTruthy(); - expect(textbox).toBeInvalid(); - - const allowedName = 'abc'; - - fireEvent.change(textbox, { target: { value: allowedName } }); - label = screen.queryByText(message); - expect(label).not.toBeTruthy(); - expect(textbox).toBeValid(); - }); - - it('should handle maximum value length', () => { - let label: HTMLElement | null; - const message = 'The name is too long. The maximum length is 100 characters.'; - - const allowedName = 'a'.repeat(100); - - fireEvent.change(textbox, { target: { value: allowedName } }); - label = screen.queryByText(message); - expect(label).toBeFalsy(); - expect(textbox).toBeValid(); - - const disallowedName = 'a'.repeat(101); - - fireEvent.change(textbox, { target: { value: disallowedName } }); - label = screen.queryByText(message); - expect(label).toBeTruthy(); - expect(textbox).toBeInvalid(); - }); - - it('should handle pattern mismatch', () => { - let label: HTMLElement | null; - const message = - 'The name can contain digits, latin letters, underscores and it should not contain special characters like space, dollar, etc. It should start and end only with digit or latin letter.'; - - const allowedName = 'new-name'; - - fireEvent.change(textbox, { target: { value: allowedName } }); - label = screen.queryByText(message); - expect(label).toBeFalsy(); - expect(textbox).toBeValid(); - - const disallowedName1 = 'new*name'; - - fireEvent.change(textbox, { target: { value: disallowedName1 } }); - label = screen.queryByText(message); - expect(label).toBeTruthy(); - expect(textbox).toBeInvalid(); - - const disallowedName2 = '-new-name'; - - fireEvent.change(textbox, { target: { value: disallowedName2 } }); - label = screen.queryByText(message); - expect(label).toBeTruthy(); - expect(textbox).toBeInvalid(); - - const disallowedName3 = 'new-name-'; - - fireEvent.change(textbox, { target: { value: disallowedName3 } }); - label = screen.queryByText(message); - expect(label).toBeTruthy(); - expect(textbox).toBeInvalid(); - }); + test('screenshot when cluster console is NOT available', () => { + const store = storeBuilder.build(); + const snapshot = createSnapshot(store, workspace); + expect(snapshot.toJSON()).toMatchSnapshot(); }); }); + +function getComponent(store: Store, workspace: Workspace) { + return ( + + + + ); +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/index.module.css b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/index.module.css deleted file mode 100644 index 532606351..000000000 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/index.module.css +++ /dev/null @@ -1,15 +0,0 @@ -/* -* Copyright (c) 2018-2023 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 -*/ - -.nameInput { - max-width: 450px; -} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/index.tsx index 354c9d8a5..95e92ec97 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/index.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/WorkspaceName/index.tsx @@ -10,213 +10,67 @@ * Red Hat, Inc. - initial API and implementation */ -import { Button, FormGroup, InputGroup, TextInput, ValidatedOptions } from '@patternfly/react-core'; -import { - CheckIcon, - ExclamationCircleIcon, - PencilAltIcon, - TimesIcon, -} from '@patternfly/react-icons'; +import { ApplicationId } from '@eclipse-che/common'; +import { Button, FormGroup } from '@patternfly/react-core'; +import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons'; import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; import overviewStyles from '@/pages/WorkspaceDetails/OverviewTab/index.module.css'; -import workspaceNameStyles from '@/pages/WorkspaceDetails/OverviewTab/WorkspaceName/index.module.css'; +import { Workspace, WorkspaceAdapter } from '@/services/workspace-adapter'; +import { AppState } from '@/store'; +import { actionCreators } from '@/store/BannerAlert'; +import { selectApplications } from '@/store/ClusterInfo/selectors'; -const MIN_LENGTH = 3; -const MAX_LENGTH = 100; -const PATTERN = `^(?:[a-zA-Z0-9][-_.a-zA-Z0-9]{1,${MAX_LENGTH - 2}}[a-zA-Z0-9])?$`; -const ERROR_REQUIRED_VALUE = 'A value is required.'; -const ERROR_MIN_LENGTH = `The name has to be at least ${MIN_LENGTH} characters long.`; -const ERROR_MAX_LENGTH = `The name is too long. The maximum length is ${MAX_LENGTH} characters.`; -const ERROR_PATTERN_MISMATCH = - 'The name can contain digits, latin letters, underscores and it should not contain special characters like space, dollar, etc. It should start and end only with digit or latin letter.'; - -type Props = { - name: string; - readonly: boolean; - onSave: (name: string) => Promise; - onChange?: (name: string) => void; - callbacks?: { cancelChanges?: () => void }; -}; - -type State = { - errorMessage?: string; - name?: string; - validated?: ValidatedOptions; - isEditMode: boolean; - hasChanges: boolean; +export type Props = MappedProps & { + workspace: Workspace; }; -export class WorkspaceNameFormGroup extends React.PureComponent { - constructor(props: Props) { - super(props); - - this.state = { - name: this.props.name, - validated: ValidatedOptions.default, - isEditMode: false, - hasChanges: false, - }; - - if (this.props.callbacks && !this.props.callbacks.cancelChanges) { - this.props.callbacks.cancelChanges = () => this.handleCancel(); - } - } - - public componentDidUpdate(prevProps: Props): void { - if (prevProps.name !== this.props.name) { - this.validate(this.props.name); - this.setState({ - name: this.props.name, - }); - if (this.props.onChange) { - this.props.onChange(this.props.name); - } - } - } - - private handleEditModeToggle(): void { - this.setState(({ isEditMode }) => ({ - isEditMode: !isEditMode, - })); - } +class WorkspaceNameFormGroup extends React.PureComponent { + private buildOpenShiftConsoleLink(): React.ReactElement | undefined { + const { applications, workspace } = this.props; + const clusterConsole = applications.find(app => app.id === ApplicationId.CLUSTER_CONSOLE); - private handleChange(name: string): void { - const hasChanges = name !== this.state.name; - this.setState({ - name, - hasChanges, - }); - this.validate(name); - if (this.props.onChange) { - this.props.onChange(name); - } - } - - private validate(name: string): void { - if (name.length === 0) { - this.setState({ - errorMessage: ERROR_REQUIRED_VALUE, - validated: ValidatedOptions.error, - }); - return; - } else if (name.length !== 0 && name.length < MIN_LENGTH) { - this.setState({ - errorMessage: ERROR_MIN_LENGTH, - validated: ValidatedOptions.error, - }); - return; - } else if (name.length > MAX_LENGTH) { - this.setState({ - errorMessage: ERROR_MAX_LENGTH, - validated: ValidatedOptions.error, - }); - return; - } - if (new RegExp(PATTERN).test(name) === false) { - this.setState({ - errorMessage: ERROR_PATTERN_MISMATCH, - validated: ValidatedOptions.error, - }); + if (!clusterConsole) { return; } - this.setState({ - errorMessage: undefined, - validated: ValidatedOptions.success, - }); - } - - private async handleSave(): Promise { - if (this.state.validated === ValidatedOptions.error) { - return; - } - await this.props.onSave(this.state.name as string); - this.setState({ - validated: ValidatedOptions.default, - hasChanges: false, - }); - this.handleEditModeToggle(); - if (this.props.onChange) { - this.props.onChange(this.props.name); - } - } + const devWorkspaceOpenShiftConsoleUrl = WorkspaceAdapter.buildClusterConsoleUrl( + workspace.ref, + clusterConsole.url, + ); - private handleCancel(): void { - this.setState({ - name: this.props.name, - errorMessage: '', - validated: ValidatedOptions.default, - hasChanges: false, - }); - this.handleEditModeToggle(); - if (this.props.onChange) { - this.props.onChange(this.props.name); - } + return ( + + ); } - public render(): React.ReactElement { - const { readonly } = this.props; - const { name, errorMessage, validated, isEditMode } = this.state; - const isSaveButtonDisable = - this.state.validated === ValidatedOptions.error || !this.state.hasChanges; - const fieldId = 'workspace-name'; - + public render(): React.ReactNode { + const workspaceName = this.buildOpenShiftConsoleLink() || this.props.workspace.name; return ( - } - validated={validated} - > - {readonly && {name}} - {!readonly && !isEditMode && ( - - {name} - - - )} - {isEditMode && ( - - this.handleChange(_name)} - minLength={MIN_LENGTH} - maxLength={MAX_LENGTH} - placeholder="Enter a workspace name" - /> - - - - )} + + {workspaceName} ); } } + +const mapStateToProps = (state: AppState) => ({ + applications: selectApplications(state), +}); + +const connector = connect(mapStateToProps, actionCreators); + +type MappedProps = ConnectedProps; + +export default connector(WorkspaceNameFormGroup); diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/__mocks__/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/__mocks__/index.tsx new file mode 100644 index 000000000..0bd17bb6d --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/__mocks__/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018-2023 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 React from 'react'; + +import { Props } from '@/pages/WorkspaceDetails/OverviewTab'; + +export class OverviewTab extends React.PureComponent { + public render(): React.ReactElement { + const { workspace, onSave } = this.props; + return ( +
+ Mock Overview Tab + +
+ ); + } +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 000000000..307db8373 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OverviewTab screenshot 1`] = ` +
+
+
+ Mock Workspace Name Form +
+
+ Mock Infrastructure Namespace Form +
+
+ Mock Storage Type Form + +
+
+ Mock Projects Form +
+
+
+`; diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/__tests__/index.spec.tsx new file mode 100644 index 000000000..106505418 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/__tests__/index.spec.tsx @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2018-2023 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 React from 'react'; +import { Provider } from 'react-redux'; + +import { OverviewTab } from '@/pages/WorkspaceDetails/OverviewTab'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; +import { constructWorkspace, Workspace } from '@/services/workspace-adapter'; +import { DevWorkspaceBuilder } from '@/store/__mocks__/devWorkspaceBuilder'; +import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; + +jest.mock('@/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace'); +jest.mock('@/pages/WorkspaceDetails/OverviewTab/Projects'); +jest.mock('@/pages/WorkspaceDetails/OverviewTab/StorageType'); +jest.mock('@/pages/WorkspaceDetails/OverviewTab/WorkspaceName'); + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +const mockOnSave = jest.fn(); + +describe('OverviewTab', () => { + let workspace: Workspace; + + beforeEach(() => { + const devWorkspace = new DevWorkspaceBuilder().withName('my-project').build(); + workspace = constructWorkspace(devWorkspace); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('screenshot', () => { + const snapshot = createSnapshot(workspace); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('change storage type', () => { + renderComponent(workspace); + expect(mockOnSave).not.toHaveBeenCalled(); + + const changeStorageType = screen.getByRole('button', { name: 'Change storage type' }); + + userEvent.click(changeStorageType); + + expect(mockOnSave).toHaveBeenCalledWith( + expect.objectContaining({ storageType: 'per-workspace' }), + ); + }); +}); + +function getComponent(workspace: Workspace) { + const store = new FakeStoreBuilder().build(); + return ( + + + + ); +} diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/index.tsx index e61d4fdfd..90de621b0 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/index.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/OverviewTab/index.tsx @@ -14,27 +14,23 @@ import { Form, PageSection, PageSectionVariants } from '@patternfly/react-core'; import { cloneDeep } from 'lodash'; import React from 'react'; -import InfrastructureNamespaceFormGroup from '@/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace'; -import ProjectsFormGroup from '@/pages/WorkspaceDetails/OverviewTab/Projects'; +import { InfrastructureNamespaceFormGroup } from '@/pages/WorkspaceDetails/OverviewTab/InfrastructureNamespace'; +import { ProjectsFormGroup } from '@/pages/WorkspaceDetails/OverviewTab/Projects'; import StorageTypeFormGroup from '@/pages/WorkspaceDetails/OverviewTab/StorageType'; -import { WorkspaceNameFormGroup } from '@/pages/WorkspaceDetails/OverviewTab/WorkspaceName'; +import WorkspaceNameFormGroup from '@/pages/WorkspaceDetails/OverviewTab/WorkspaceName'; import { DevWorkspaceStatus } from '@/services/helpers/types'; import { constructWorkspace, Workspace } from '@/services/workspace-adapter'; -type Props = { +export type Props = { onSave: (workspace: Workspace) => Promise; workspace: Workspace; }; export type State = { storageType: che.WorkspaceStorageType; - workspaceName: string; }; export class OverviewTab extends React.Component { - private readonly workspaceNameCallbacks: { cancelChanges?: () => void }; - private isWorkspaceNameChanged: boolean; - constructor(props: Props) { super(props); @@ -42,45 +38,20 @@ export class OverviewTab extends React.Component { this.state = { storageType: workspace.storageType, - workspaceName: workspace.name, }; - - this.isWorkspaceNameChanged = false; - this.workspaceNameCallbacks = {}; } public componentDidUpdate(): void { - const { storageType, workspaceName } = this.state; + const { storageType } = this.state; const workspace = this.props.workspace; - if (storageType !== workspace.storageType || workspaceName !== workspace.name) { + if (storageType !== workspace.storageType) { this.setState({ storageType: workspace.storageType, - workspaceName: workspace.name, }); } } - public get hasChanges() { - return this.isWorkspaceNameChanged; - } - - public cancelChanges(): void { - if (this.workspaceNameCallbacks.cancelChanges) { - this.workspaceNameCallbacks.cancelChanges(); - } - } - - private async handleWorkspaceNameSave(workspaceName: string): Promise { - if (this.props.workspace.isDevWorkspace) { - return; - } - const workspaceClone = constructWorkspace(cloneDeep(this.props.workspace.ref)); - workspaceClone.name = workspaceName; - await this.props.onSave(workspaceClone); - this.setState({ workspaceName }); - } - private async handleStorageSave(storageType: che.WorkspaceStorageType): Promise { const workspaceClone = constructWorkspace(cloneDeep(this.props.workspace.ref)); workspaceClone.storageType = storageType; @@ -89,7 +60,7 @@ export class OverviewTab extends React.Component { } public render(): React.ReactElement { - const { workspaceName, storageType } = this.state; + const { storageType } = this.state; const { workspace } = this.props; const namespace = workspace.namespace; const projects = workspace.projects; @@ -99,20 +70,12 @@ export class OverviewTab extends React.Component {
e.preventDefault()}> - this.handleWorkspaceNameSave(_workspaceName)} - onChange={_workspaceName => { - this.isWorkspaceNameChanged = workspaceName !== _workspaceName; - }} - callbacks={this.workspaceNameCallbacks} - /> + this.handleStorageSave(_storageType)} + onSave={storageType => this.handleStorageSave(storageType)} /> @@ -121,5 +84,3 @@ export class OverviewTab extends React.Component { ); } } - -export default OverviewTab; diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/__tests__/index.spec.tsx index 6f578389c..df9a3e977 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/__tests__/index.spec.tsx @@ -11,26 +11,24 @@ */ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { createHashHistory, History, Location } from 'history'; import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; +import { Props, WorkspaceDetails } from '@/pages/WorkspaceDetails'; import devfileApi from '@/services/devfileApi'; import { constructWorkspace } from '@/services/workspace-adapter'; import { DevWorkspaceBuilder } from '@/store/__mocks__/devWorkspaceBuilder'; import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; -import { Props, WorkspaceDetails } from '..'; - const mockOnSave = jest.fn(); -jest.mock('../DevfileEditorTab'); -jest.mock('../OverviewTab/StorageType'); - -jest.mock('../DevworkspaceEditorTab', () => { - return () => ''; -}); +jest.mock('@/pages/WorkspaceDetails/DevfileEditorTab'); +jest.mock('@/pages/WorkspaceDetails/OverviewTab'); +jest.mock('@/components/WorkspaceLogs'); +jest.mock('@/components/WorkspaceEvents'); let history: History; @@ -64,20 +62,40 @@ describe('Workspace Details page', () => { }); const tabpanel = screen.queryByRole('tabpanel', { name: 'Overview' }); - expect(tabpanel).toBeTruthy(); + expect(tabpanel).not.toBeNull(); }); - it('should have two tabs visible', () => { + it('should have four tabs visible', () => { const workspace = constructWorkspace(devWorkspaceBuilder.build()); renderComponent({ workspace, }); + const allTabs = screen.getAllByRole('tab'); + expect(allTabs.length).toBe(4); + const overviewTab = screen.queryByRole('tab', { name: 'Overview' }); const devfileTab = screen.queryByRole('tab', { name: 'Devfile' }); + const logsTab = screen.queryByRole('tab', { name: 'Logs' }); + const eventsTab = screen.queryByRole('tab', { name: 'Events' }); - expect(overviewTab).toBeTruthy(); - expect(devfileTab).toBeTruthy(); + expect(overviewTab).not.toBeNull(); + expect(devfileTab).not.toBeNull(); + expect(logsTab).not.toBeNull(); + expect(eventsTab).not.toBeNull(); + }); + + it('should switch to the Devfile tab', () => { + const workspace = constructWorkspace(devWorkspaceBuilder.build()); + renderComponent({ + workspace, + }); + + const devfileTab = screen.getByRole('tab', { name: 'Devfile' }); + userEvent.click(devfileTab); + + const tabpanel = screen.getByRole('tabpanel', { name: 'Devfile' }); + expect(tabpanel).not.toBeNull(); }); }); @@ -105,6 +123,18 @@ describe('Workspace Details page', () => { expect(screen.queryByRole('link', { name: 'Show Original Devfile' })).toBeTruthy(); }); }); + + it('should handle the onSave event', () => { + const workspace = constructWorkspace(devWorkspaceBuilder.build()); + renderComponent({ + workspace, + }); + + const saveButton = screen.getByRole('button', { name: 'Update workspace' }); + userEvent.click(saveButton); + + expect(mockOnSave).toHaveBeenCalledTimes(1); + }); }); function renderComponent(props?: Partial): void { diff --git a/packages/dashboard-frontend/src/pages/WorkspaceDetails/index.tsx b/packages/dashboard-frontend/src/pages/WorkspaceDetails/index.tsx index f147f6ccd..94c63be14 100644 --- a/packages/dashboard-frontend/src/pages/WorkspaceDetails/index.tsx +++ b/packages/dashboard-frontend/src/pages/WorkspaceDetails/index.tsx @@ -25,18 +25,14 @@ import { Link } from 'react-router-dom'; import Head from '@/components/Head'; import ProgressIndicator from '@/components/Progress'; -import UnsavedChangesModal from '@/components/UnsavedChangesModal'; import WorkspaceEvents from '@/components/WorkspaceEvents'; import WorkspaceLogs from '@/components/WorkspaceLogs'; import { lazyInject } from '@/inversify.config'; -import DevfileEditorTab, { - DevfileEditorTab as Editor, -} from '@/pages/WorkspaceDetails/DevfileEditorTab'; -import DevworkspaceEditorTab from '@/pages/WorkspaceDetails/DevworkspaceEditorTab'; +import { DevfileEditorTab } from '@/pages/WorkspaceDetails/DevfileEditorTab'; import Header from '@/pages/WorkspaceDetails/Header'; import { HeaderActionSelect } from '@/pages/WorkspaceDetails/Header/Actions'; import styles from '@/pages/WorkspaceDetails/index.module.css'; -import OverviewTab, { OverviewTab as Overview } from '@/pages/WorkspaceDetails/OverviewTab'; +import { OverviewTab } from '@/pages/WorkspaceDetails/OverviewTab'; import { AppAlerts } from '@/services/alerts/appAlerts'; import { buildDetailsLocation } from '@/services/helpers/location'; import { WorkspaceDetailsTab } from '@/services/helpers/types'; @@ -69,17 +65,15 @@ export class WorkspaceDetails extends React.PureComponent { public showAlert: (variant: AlertVariant, title: string) => void; private readonly handleTabClick: ( event: React.MouseEvent, - tabIndex: React.ReactText, + tabIndex: string | number, ) => void; - private readonly editorTabPageRef: React.RefObject; - private readonly overviewTabPageRef: React.RefObject; + private readonly overviewTabPageRef: React.RefObject; constructor(props: Props) { super(props); - this.editorTabPageRef = React.createRef(); - this.overviewTabPageRef = React.createRef(); + this.overviewTabPageRef = React.createRef(); this.state = { activeTabKey: this.getActiveTabKey(this.props.history.location.search), @@ -88,16 +82,13 @@ export class WorkspaceDetails extends React.PureComponent { // Toggle currently active tab this.handleTabClick = ( - event: React.MouseEvent, + _event: React.MouseEvent, tabIndex: React.ReactText, ): void => { const searchParams = new window.URLSearchParams(this.props.history.location.search); - const { clickedTabIndex } = this.state; this.setState({ clickedTabIndex: tabIndex as WorkspaceDetailsTab }); - const tab = - clickedTabIndex && this.hasUnsavedChanges() - ? clickedTabIndex - : (tabIndex as WorkspaceDetailsTab); + + const tab = tabIndex as WorkspaceDetailsTab; searchParams.set('tab', tab); this.props.history.location.search = searchParams.toString(); this.props.history.push(this.props.history.location); @@ -111,24 +102,6 @@ export class WorkspaceDetails extends React.PureComponent { }; } - private showConversionAlert(errorMessage: string): void { - this.setState({ - inlineAlertConversionError: errorMessage, - }); - } - - private closeConversionAlert(): void { - this.setState({ - inlineAlertConversionError: undefined, - }); - } - - private handleRestartWarning(): void { - this.setState({ - showInlineAlertRestartWarning: true, - }); - } - private handleCloseRestartWarning(): void { this.setState({ showInlineAlertRestartWarning: false, @@ -144,8 +117,6 @@ export class WorkspaceDetails extends React.PureComponent { return WorkspaceDetailsTab.OVERVIEW; case WorkspaceDetailsTab.DEVFILE: return WorkspaceDetailsTab.DEVFILE; - case WorkspaceDetailsTab.DEVWORKSPACE: - return WorkspaceDetailsTab.DEVWORKSPACE; case WorkspaceDetailsTab.EVENTS: return WorkspaceDetailsTab.EVENTS; case WorkspaceDetailsTab.LOGS: @@ -176,31 +147,8 @@ export class WorkspaceDetails extends React.PureComponent { } } - private handleDiscardChanges(pathname: string): void { - if (this.state.activeTabKey === WorkspaceDetailsTab.OVERVIEW) { - this.overviewTabPageRef.current?.cancelChanges(); - } - - if (pathname.startsWith('/workspace/')) { - const tabIndex = this.state.clickedTabIndex; - const searchParams = new window.URLSearchParams(this.props.history.location.search); - searchParams.set('tab', tabIndex as WorkspaceDetailsTab); - this.props.history.location.search = searchParams.toString(); - this.props.history.push(this.props.history.location); - } else { - this.props.history.push(pathname); - } - } - - private hasUnsavedChanges(): boolean { - if (this.state.activeTabKey === WorkspaceDetailsTab.OVERVIEW) { - return this.overviewTabPageRef.current?.hasChanges === true; - } - return false; - } - public render(): React.ReactElement { - const { history, oldWorkspaceLocation, workspace, workspacesLink } = this.props; + const { oldWorkspaceLocation, workspace, workspacesLink } = this.props; if (!workspace) { return
Workspace not found.
; @@ -248,16 +196,6 @@ export class WorkspaceDetails extends React.PureComponent { isActive={WorkspaceDetailsTab.DEVFILE === this.state.activeTabKey} /> - - - - @@ -265,11 +203,6 @@ export class WorkspaceDetails extends React.PureComponent { - this.hasUnsavedChanges()} - onDiscardChanges={pathname => this.handleDiscardChanges(pathname)} - history={history} - />
); diff --git a/packages/dashboard-frontend/src/services/helpers/types.ts b/packages/dashboard-frontend/src/services/helpers/types.ts index 71fa04489..ae2dcf1bf 100644 --- a/packages/dashboard-frontend/src/services/helpers/types.ts +++ b/packages/dashboard-frontend/src/services/helpers/types.ts @@ -89,7 +89,6 @@ export enum LoaderTab { export enum WorkspaceDetailsTab { OVERVIEW = 'Overview', DEVFILE = 'Devfile', - DEVWORKSPACE = 'DevWorkspace', EVENTS = 'Events', LOGS = 'Logs', }