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
new file mode 100644
index 000000000..c8b649b68
--- /dev/null
+++ b/packages/dashboard-frontend/src/components/EditorTools/__tests__/__snapshots__/index.spec.tsx.snap
@@ -0,0 +1,227 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EditorTools DevWorkspace snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`EditorTools Devfile snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/dashboard-frontend/src/components/EditorTools/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/EditorTools/__tests__/index.spec.tsx
new file mode 100644
index 000000000..f1a66f51e
--- /dev/null
+++ b/packages/dashboard-frontend/src/components/EditorTools/__tests__/index.spec.tsx
@@ -0,0 +1,180 @@
+/*
+ * 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 getComponentRenderer, { screen } from '../../../services/__mocks__/getComponentRenderer';
+import { Provider } from 'react-redux';
+import EditorTools from '..';
+import { Store } from 'redux';
+import { FakeStoreBuilder } from '../../../store/__mocks__/storeBuilder';
+import devfileApi from '../../../services/devfileApi';
+import { ApplicationId } from '@eclipse-che/common';
+
+jest.mock('../../../contexts/ToggleBars');
+
+const mockClipboard = jest.fn();
+jest.mock('react-copy-to-clipboard', () => {
+ return {
+ __esModule: true,
+ default: (props: any) => {
+ return (
+
+ );
+ },
+ };
+});
+
+const { renderComponent, createSnapshot } = getComponentRenderer(getComponent);
+
+const mockOnExpand = jest.fn();
+let store: Store;
+
+describe('EditorTools', () => {
+ const clusterConsole = {
+ id: ApplicationId.CLUSTER_CONSOLE,
+ url: 'https://console-url',
+ icon: 'https://console-icon-url',
+ title: 'Cluster console',
+ };
+
+ beforeEach(() => {
+ store = new FakeStoreBuilder()
+ .withClusterInfo({
+ applications: [clusterConsole],
+ })
+ .build();
+
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ jest.clearAllTimers();
+ });
+
+ describe('Devfile', () => {
+ let devfile: devfileApi.Devfile;
+
+ beforeEach(() => {
+ devfile = {
+ schemaVersion: '2.1.0',
+ metadata: {
+ name: 'my-project',
+ namespace: 'user-che',
+ },
+ };
+ });
+
+ test('snapshot', () => {
+ const snapshot = createSnapshot(devfile);
+ expect(snapshot.toJSON()).toMatchSnapshot();
+ });
+
+ test('expand and compress', () => {
+ renderComponent(devfile);
+
+ /* expand the editor */
+
+ const expandButtonName = 'Expand';
+ expect(screen.getByRole('button', { name: expandButtonName })).toBeTruthy;
+
+ screen.getByRole('button', { name: expandButtonName }).click();
+ expect(mockOnExpand).toHaveBeenCalledWith(true);
+
+ /* compress the editor */
+
+ const compressButtonName = 'Compress';
+ expect(screen.getByRole('button', { name: compressButtonName })).toBeTruthy;
+
+ screen.getByRole('button', { name: compressButtonName }).click();
+ expect(mockOnExpand).toHaveBeenCalledWith(false);
+ });
+
+ test('copy to clipboard', () => {
+ const mockCreateObjectURL = jest.fn().mockReturnValue('blob-url');
+ URL.createObjectURL = mockCreateObjectURL;
+
+ renderComponent(devfile);
+
+ const copyButtonName = 'Copy to clipboard';
+ expect(screen.queryByRole('button', { name: copyButtonName })).toBeTruthy;
+
+ screen.getByRole('button', { name: copyButtonName }).click();
+
+ expect(mockClipboard).toHaveBeenCalledWith(
+ 'schemaVersion: 2.1.0\nmetadata:\n name: my-project\n namespace: user-che\n',
+ );
+
+ /* '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;
+ });
+ });
+
+ 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) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/dashboard-frontend/src/components/EditorTools/index.module.css b/packages/dashboard-frontend/src/components/EditorTools/index.module.css
index ab2f270f3..e767e357e 100644
--- a/packages/dashboard-frontend/src/components/EditorTools/index.module.css
+++ b/packages/dashboard-frontend/src/components/EditorTools/index.module.css
@@ -26,7 +26,7 @@
text-decoration: none;
}
-.editorTools button {
+.editorTools .button {
padding: 0;
font-size: inherit;
outline: none;
diff --git a/packages/dashboard-frontend/src/components/EditorTools/index.tsx b/packages/dashboard-frontend/src/components/EditorTools/index.tsx
index ea7cc00ea..488d3e26b 100644
--- a/packages/dashboard-frontend/src/components/EditorTools/index.tsx
+++ b/packages/dashboard-frontend/src/components/EditorTools/index.tsx
@@ -10,21 +10,32 @@
* Red Hat, Inc. - initial API and implementation
*/
+import { ApplicationId, helpers } from '@eclipse-che/common';
import { AlertVariant, Button, Divider, Flex, FlexItem } from '@patternfly/react-core';
-import { CompressIcon, CopyIcon, DownloadIcon, ExpandIcon } from '@patternfly/react-icons';
+import {
+ CompressIcon,
+ CopyIcon,
+ DownloadIcon,
+ ExpandIcon,
+ ExternalLinkSquareAltIcon,
+} from '@patternfly/react-icons';
import React from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
-import stringify from '../../services/helpers/editor';
-import styles from './index.module.css';
-import devfileApi from '../../services/devfileApi';
-import { AppAlerts } from '../../services/alerts/appAlerts';
+import { ConnectedProps, connect } from 'react-redux';
+import { ToggleBarsContext } from '../../contexts/ToggleBars';
import { lazyInject } from '../../inversify.config';
+import { AppAlerts } from '../../services/alerts/appAlerts';
+import devfileApi, { isDevWorkspace, isDevfileV2 } from '../../services/devfileApi';
+import stringify from '../../services/helpers/editor';
import { AlertItem } from '../../services/helpers/types';
-import { helpers } from '@eclipse-che/common';
-import { ToggleBarsContext } from '../../contexts/ToggleBars';
+import { AppState } from '../../store';
+import { actionCreators } from '../../store/BannerAlert';
+import { selectApplications } from '../../store/ClusterInfo/selectors';
+import styles from './index.module.css';
+import { WorkspaceAdapter } from '../../services/workspace-adapter';
-type Props = {
- content: devfileApi.DevWorkspace | devfileApi.Devfile;
+type Props = MappedProps & {
+ devfileOrDevWorkspace: devfileApi.DevWorkspace | devfileApi.Devfile;
handleExpand: (isExpand: boolean) => void;
};
@@ -32,7 +43,6 @@ type State = {
copied?: boolean;
isExpanded: boolean;
contentText: string;
- isWorkspace: boolean;
contentBlobUrl: string;
};
@@ -51,39 +61,27 @@ class EditorTools extends React.PureComponent {
this.state = {
isExpanded: false,
contentText: '',
- isWorkspace: false,
contentBlobUrl: '',
};
}
public componentDidMount(): void {
- const { content } = this.props;
- try {
- const contentText = stringify(content);
- const isWorkspace = this.isWorkspace(content);
- const contentBlobUrl = URL.createObjectURL(
- new Blob([contentText], { type: 'application/x-yaml' }),
- );
- this.setState({ contentText, isWorkspace, contentBlobUrl });
- } catch (e) {
- this.showAlert({
- key: 'editor-tools-create blob-url-fails',
- variant: AlertVariant.danger,
- title: helpers.errors.getMessage(e),
- });
- }
+ this.init();
}
public componentDidUpdate(): void {
- const { content } = this.props;
+ this.init();
+ }
+
+ private init() {
+ const { devfileOrDevWorkspace } = this.props;
try {
- const contentText = stringify(content);
+ const contentText = stringify(devfileOrDevWorkspace);
if (contentText !== this.state.contentText) {
- const isWorkspace = this.isWorkspace(content);
const contentBlobUrl = URL.createObjectURL(
new Blob([contentText], { type: 'application/x-yaml' }),
);
- this.setState({ contentText, isWorkspace, contentBlobUrl });
+ this.setState({ contentText, contentBlobUrl });
}
} catch (e) {
this.showAlert({
@@ -112,22 +110,6 @@ class EditorTools extends React.PureComponent {
}
}
- private getName(content: devfileApi.DevWorkspace | devfileApi.Devfile): string | undefined {
- if ((content as devfileApi.DevWorkspace)?.kind === 'DevWorkspace') {
- return (content as devfileApi.DevWorkspace).metadata.name;
- } else {
- return (content as devfileApi.Devfile).metadata.name;
- }
- return undefined;
- }
-
- private isWorkspace(content: devfileApi.DevWorkspace | devfileApi.Devfile): boolean {
- if ((content as devfileApi.DevWorkspace)?.kind === 'DevWorkspace') {
- return true;
- }
- return false;
- }
-
private onCopyToClipboard(): void {
this.setState({ copied: true });
if (this.copiedTimer) {
@@ -138,16 +120,58 @@ 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 (
+ <>
+
+ }
+ >
+ {clusterConsole.title}
+
+
+
+ >
+ );
+ }
+
public render(): React.ReactElement {
- const { contentText, isWorkspace, contentBlobUrl, isExpanded, copied } = this.state;
- const name = this.getName(this.props.content);
+ const { devfileOrDevWorkspace } = this.props;
+ const { contentText, contentBlobUrl, isExpanded, copied } = this.state;
+
+ const { name } = devfileOrDevWorkspace.metadata;
+
+ const openshiftConsoleItem = this.buildOpenShiftConsoleItem();
return (
+ {openshiftConsoleItem}
this.onCopyToClipboard()}>
-