diff --git a/devspaces-dashboard/devfile.yaml b/devspaces-dashboard/devfile.yaml index c42285bd4d..def5d3aea3 100644 --- a/devspaces-dashboard/devfile.yaml +++ b/devspaces-dashboard/devfile.yaml @@ -5,7 +5,7 @@ metadata: components: - name: tools container: - image: quay.io/devfile/universal-developer-image:ubi8-2554ed2 + image: quay.io/devfile/universal-developer-image:ubi8-latest memoryLimit: 10G memoryRequest: 512Mi cpuRequest: 1000m diff --git a/devspaces-dashboard/packages/dashboard-frontend/assets/branding/product.json b/devspaces-dashboard/packages/dashboard-frontend/assets/branding/product.json index f5d59dab03..a86dbefd8a 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/assets/branding/product.json +++ b/devspaces-dashboard/packages/dashboard-frontend/assets/branding/product.json @@ -1,7 +1,7 @@ { "title": "Red Hat OpenShift Dev Spaces ", "name": "Red Hat OpenShift Dev Spaces", - "productVersion": "3.16 @ fca48 #6 :: Eclipse Che Dashboard 7.90.0 @ 32c7", + "productVersion": "3.16 @ d9616 #7 :: Eclipse Che Dashboard 7.90.0 @ 86bc2", "logoFile": "che-logo.svg", "logoTextFile": "che-logo-text.svg", "links": [ diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/__snapshots__/index.spec.tsx.snap deleted file mode 100644 index 9b0393d703..0000000000 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/__snapshots__/index.spec.tsx.snap +++ /dev/null @@ -1,517 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AdditionalGitRemotesField snapshot 1`] = ` -
-
-
- Additional Git Remotes -
-
-
-
-
-
-
- - -
-
- - -
-
-
-
-
-
- - -
-
- - -
-
-
-
-
- - Remove Remote -
-
-
-
-
-
-
- - -
-
- - -
-
-
-
-
-
- - -
-
- - -
-
-
-
-
- - Remove Remote -
-
-
-
-
-
-
- - -
-
- - -
-
-
-
-
-
- - -
-
- - -
-
-
-
-
- - Remove Remote -
-
-
-
-
-
- -
-
-
-
-
-
-
-`; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/__mocks__/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/__mocks__/index.tsx new file mode 100644 index 0000000000..14890d7e16 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/__mocks__/index.tsx @@ -0,0 +1,29 @@ +/* + * 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 React from 'react'; + +import { Props } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField'; + +export class ContainerImageField extends React.PureComponent { + public render() { + const { containerImage, onChange } = this.props; + + return ( +
+
Container Image
+
{containerImage}
+ +
+ ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..c172752104 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ContainerImageField snapshot 1`] = ` +
+
+ + +
+
+ + +
+
+`; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/__tests__/index.spec.tsx new file mode 100644 index 0000000000..186d450551 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/__tests__/index.spec.tsx @@ -0,0 +1,58 @@ +/* + * 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 React from 'react'; + +import { ContainerImageField } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +const mockOnChange = jest.fn(); + +describe('ContainerImageField', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('snapshot', () => { + const snapshot = createSnapshot(); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('container image preset value', () => { + renderComponent('preset-container-image'); + + const input = screen.getByRole('textbox'); + + expect(input).toHaveValue('preset-container-image'); + }); + + test('container image change', () => { + renderComponent(); + + const input = screen.getByRole('textbox'); + + const containerImage = 'new-container-image'; + userEvent.paste(input, containerImage); + + expect(mockOnChange).toHaveBeenNthCalledWith(1, containerImage); + + userEvent.clear(input); + expect(mockOnChange).toHaveBeenNthCalledWith(2, undefined); + }); +}); + +function getComponent(containerImage?: string) { + return ; +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/index.tsx new file mode 100644 index 0000000000..396f47f723 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField/index.tsx @@ -0,0 +1,63 @@ +/* + * 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 { FormGroup, TextInput } from '@patternfly/react-core'; +import React from 'react'; + +export type Props = { + onChange: (definition: string | undefined) => void; + containerImage: string | undefined; +}; +export type State = { + containerImage: string | undefined; +}; + +export class ContainerImageField extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + containerImage: this.props.containerImage, + }; + } + + public componentDidUpdate(prevProps: Readonly): void { + const { containerImage } = this.props; + if (prevProps.containerImage !== containerImage) { + this.setState({ containerImage }); + } + } + + private handleChange(value: string) { + let containerImage: string | undefined = value.trim(); + containerImage = containerImage !== '' ? containerImage : undefined; + if (containerImage !== this.state.containerImage) { + this.setState({ containerImage }); + this.props.onChange(containerImage); + } + } + + public render() { + const containerImage = this.state.containerImage || ''; + + return ( + + this.handleChange(value)} + value={containerImage} + /> + + ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/__mocks__/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/__mocks__/index.tsx new file mode 100644 index 0000000000..9752e10867 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/__mocks__/index.tsx @@ -0,0 +1,29 @@ +/* + * 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 React from 'react'; + +import { Props } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField'; + +export class CpuLimitField extends React.PureComponent { + public render() { + const { cpuLimit, onChange } = this.props; + + return ( +
+
Cpu Limit
+
{cpuLimit.toString()}
+ +
+ ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..62641b878e --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CpuLimitField 2Gi snapshot 1`] = ` +
+
+ + +
+
+ + +
+
+`; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/__tests__/index.spec.tsx new file mode 100644 index 0000000000..8aaefd791d --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/__tests__/index.spec.tsx @@ -0,0 +1,69 @@ +/* + * 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 { SliderProps } from '@patternfly/react-core'; +import { fireEvent, screen } from '@testing-library/react'; +import React from 'react'; + +import { CpuLimitField } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField'; +import getComponentRenderer from '@/services/__mocks__/getComponentRenderer'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +jest.mock('@patternfly/react-core', () => { + return { + ...jest.requireActual('@patternfly/react-core'), + Slider: (obj: SliderProps) => ( + { + if (obj.onChange) { + obj.onChange(event.target.value ? parseInt(event.target.value) : 0); + } + }} + /> + ), + }; +}); + +const mockOnChange = jest.fn(); + +describe('CpuLimitField', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('2Gi snapshot', () => { + const snapshot = createSnapshot(2); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + it('should be init with 2Gi and switched to 8Gi', () => { + renderComponent(2); + const slider = screen.getByTestId('cpu-limit-slider') as HTMLInputElement; + const getVal = () => parseInt(slider.value); + + expect(slider).toBeDefined(); + expect(getVal()).toEqual(2); + + fireEvent.change(slider, { target: { value: 8 } }); + + expect(getVal()).toEqual(8); + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); +}); + +function getComponent(cpuLimit: number) { + return ; +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/index.tsx new file mode 100644 index 0000000000..0c1a3da375 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField/index.tsx @@ -0,0 +1,86 @@ +/* + * 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 { FormGroup, Slider } from '@patternfly/react-core'; +import React from 'react'; + +const steps = [ + { value: 0, label: 'default' }, + { value: 1, label: '1' }, + { value: 2, label: '2', isLabelHidden: true }, + { value: 3, label: '3', isLabelHidden: true }, + { value: 4, label: '4' }, + { value: 5, label: '5', isLabelHidden: true }, + { value: 6, label: '6', isLabelHidden: true }, + { value: 7, label: '7', isLabelHidden: true }, + { value: 8, label: '8' }, +]; + +export type Props = { + onChange: (cpuLimit: number) => void; + cpuLimit: number; +}; +export type State = { + cpuLimit: number; +}; + +export class CpuLimitField extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + cpuLimit: this.props.cpuLimit, + }; + } + + public componentDidUpdate(prevProps: Readonly): void { + const { cpuLimit } = this.props; + if (prevProps.cpuLimit !== cpuLimit && cpuLimit !== this.state.cpuLimit) { + this.setState({ cpuLimit }); + } + } + + private handleChange(cpuLimit: number) { + if (cpuLimit !== this.state.cpuLimit) { + this.setState({ cpuLimit }); + this.props.onChange(cpuLimit); + } + } + + private getLabel(cpuLimit: number): string { + const label = 'CPU Limit'; + if (cpuLimit === 0) { + return label; + } else if (cpuLimit === 1) { + return `${label} (1 core)`; + } + + return `${label} (${cpuLimit} cores)`; + } + + public render() { + const cpuLimit = this.state.cpuLimit; + const label = this.getLabel(cpuLimit); + + return ( + + this.handleChange(value)} + max={steps[steps.length - 1].value} + customSteps={steps} + /> + + ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/__mocks__/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/__mocks__/index.tsx new file mode 100644 index 0000000000..b6918a6856 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/__mocks__/index.tsx @@ -0,0 +1,33 @@ +/* + * 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 React from 'react'; + +import { Props } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField'; + +export class CreateNewIfExistingField extends React.PureComponent { + public render() { + const { createNewIfExisting, onChange } = this.props; + + return ( +
+
Create New If Existing
+
+ {createNewIfExisting !== undefined ? createNewIfExisting.toString() : 'undefined'} +
+ +
+ ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..2fc7b72689 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,135 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TemporaryStorageField switched off snapshot 1`] = ` +
+
+ + +
+
+ + +
+
+`; + +exports[`TemporaryStorageField switched on snapshot 1`] = ` +
+
+ + +
+
+ + +
+
+`; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/__tests__/index.spec.tsx new file mode 100644 index 0000000000..225fd44dee --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/__tests__/index.spec.tsx @@ -0,0 +1,66 @@ +/* + * 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 React from 'react'; + +import { TemporaryStorageField } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +const mockOnChange = jest.fn(); + +describe('TemporaryStorageField', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('switched off snapshot', () => { + const snapshot = createSnapshot(false); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('switched on snapshot', () => { + const snapshot = createSnapshot(true); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + it('should be initially switched off', () => { + renderComponent(undefined); + const switchInput = screen.getByRole('checkbox') as HTMLInputElement; + expect(switchInput.checked).toBeFalsy(); + }); + + it('should be switched off', () => { + renderComponent(false); + const switchInput = screen.getByRole('checkbox') as HTMLInputElement; + expect(switchInput.checked).toBeFalsy(); + + switchInput.click(); + expect(switchInput.checked).toBeTruthy(); + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); + + it('should be initially switched on', () => { + renderComponent(true); + const switchInput = screen.getByRole('checkbox') as HTMLInputElement; + expect(switchInput.checked).toBeTruthy(); + + switchInput.click(); + expect(switchInput.checked).toBeFalsy(); + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); +}); + +function getComponent(isTemporary: boolean | undefined) { + return ; +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/index.tsx new file mode 100644 index 0000000000..aeff663c8d --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField/index.tsx @@ -0,0 +1,59 @@ +/* + * 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 { FormGroup, Switch } from '@patternfly/react-core'; +import React from 'react'; + +export type Props = { + onChange: (createNewIfExisting: boolean | undefined) => void; + createNewIfExisting: boolean | undefined; +}; +export type State = { + createNewIfExisting: boolean; +}; + +export class CreateNewIfExistingField extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + createNewIfExisting: this.props.createNewIfExisting || false, + }; + } + + public componentDidUpdate(prevProps: Readonly): void { + const createNewIfExisting = this.props.createNewIfExisting || false; + if (prevProps.createNewIfExisting !== createNewIfExisting) { + this.setState({ createNewIfExisting }); + } + } + + private handleChange(createNewIfExisting: boolean) { + this.setState({ createNewIfExisting }); + this.props.onChange(createNewIfExisting); + } + + public render() { + const { createNewIfExisting } = this.state; + + return ( + + this.handleChange(value)} + /> + + ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/__mocks__/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/__mocks__/index.tsx new file mode 100644 index 0000000000..77deb008c7 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/__mocks__/index.tsx @@ -0,0 +1,29 @@ +/* + * 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 React from 'react'; + +import { Props } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField'; + +export class MemoryLimitField extends React.PureComponent { + public render() { + const { memoryLimit, onChange } = this.props; + + return ( +
+
Memory Limit
+
{memoryLimit.toString()}
+ +
+ ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..57bc1dfb89 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MemoryLimitField 8Gi snapshot 1`] = ` +
+
+ + +
+
+ + +
+
+`; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/__tests__/index.spec.tsx new file mode 100644 index 0000000000..b243e00d1f --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/__tests__/index.spec.tsx @@ -0,0 +1,72 @@ +/* + * 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 { SliderProps } from '@patternfly/react-core'; +import { fireEvent, screen } from '@testing-library/react'; +import React from 'react'; + +import { + MemoryLimitField, + STEP, +} from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField'; +import getComponentRenderer from '@/services/__mocks__/getComponentRenderer'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +jest.mock('@patternfly/react-core', () => { + return { + ...jest.requireActual('@patternfly/react-core'), + Slider: (obj: SliderProps) => ( + { + if (obj.onChange) { + obj.onChange(event.target.value ? parseInt(event.target.value) : 0); + } + }} + /> + ), + }; +}); + +const mockOnChange = jest.fn(); + +describe('MemoryLimitField', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('8Gi snapshot', () => { + const snapshot = createSnapshot(8); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + it('should be init with 8Gi and switched to 32Gi', () => { + renderComponent(8 * STEP); + const slider = screen.getByTestId('memory-limit-slider') as HTMLInputElement; + const getVal = () => parseInt(slider.value); + + expect(slider).toBeDefined(); + expect(getVal()).toEqual(8); + + fireEvent.change(slider, { target: { value: 32 } }); + + expect(getVal()).toEqual(32); + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); +}); + +function getComponent(memoryLimit: number) { + return ; +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/index.tsx new file mode 100644 index 0000000000..e4ac544f32 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField/index.tsx @@ -0,0 +1,103 @@ +/* + * 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 { FormGroup, Slider } from '@patternfly/react-core'; +import React from 'react'; + +import { formatBytes } from '@/components/ImportFromGit/helpers'; + +export const STEP = 1073741824; + +const steps = [ + { value: 0, label: 'default' }, + { value: 1, label: '1', isLabelHidden: true }, + { value: 2, label: '2', isLabelHidden: true }, + { value: 3, label: '4', isLabelHidden: true }, + { value: 4, label: '4' }, + { value: 6, label: '6', isLabelHidden: true }, + { value: 8, label: '8', isLabelHidden: true }, + { value: 12, label: '12', isLabelHidden: true }, + { value: 16, label: '16' }, + { value: 20, label: '20', isLabelHidden: true }, + { value: 24, label: '24', isLabelHidden: true }, + { value: 28, label: '28', isLabelHidden: true }, + { value: 32, label: '32' }, +]; + +export type Props = { + onChange: (memoryLimit: number) => void; + memoryLimit: number; +}; +export type State = { + memoryLimit: number; +}; + +export class MemoryLimitField extends React.PureComponent { + constructor(props: Props) { + super(props); + + const memoryLimit = this.getMemoryLimit(); + + this.state = { + memoryLimit, + }; + } + + public componentDidUpdate(prevProps: Readonly): void { + if (prevProps.memoryLimit !== this.props.memoryLimit) { + const memoryLimit = this.getMemoryLimit(); + if (memoryLimit !== this.state.memoryLimit) { + this.setState({ memoryLimit }); + } + } + } + + private handleChange(memoryLimit: number) { + if (memoryLimit !== this.state.memoryLimit) { + this.setState({ memoryLimit }); + this.props.onChange(memoryLimit * STEP); + } + } + + private getMemoryLimit(): number { + const memoryLimit = this.props.memoryLimit; + if (memoryLimit <= STEP) { + return 0; + } + return memoryLimit / STEP; + } + + private getLabel(memoryLimit: number): string { + if (memoryLimit > 0) { + return `Memory Limit (${formatBytes(memoryLimit * STEP)})`; + } + + return 'Memory Limit'; + } + + public render() { + const memoryLimit = this.state.memoryLimit; + const label = this.getLabel(memoryLimit); + + return ( + + this.handleChange(value)} + max={steps[steps.length - 1].value} + customSteps={steps} + /> + + ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/__mocks__/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/__mocks__/index.tsx new file mode 100644 index 0000000000..4301186634 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/__mocks__/index.tsx @@ -0,0 +1,31 @@ +/* + * 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 React from 'react'; + +import { Props } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField'; + +export class TemporaryStorageField extends React.PureComponent { + public render() { + const { isTemporary, onChange } = this.props; + + return ( +
+
Temporary Storage
+
+ {isTemporary !== undefined ? isTemporary.toString() : 'undefined'} +
+ +
+ ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..2fc7b72689 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,135 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TemporaryStorageField switched off snapshot 1`] = ` +
+
+ + +
+
+ + +
+
+`; + +exports[`TemporaryStorageField switched on snapshot 1`] = ` +
+
+ + +
+
+ + +
+
+`; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/__tests__/index.spec.tsx new file mode 100644 index 0000000000..225fd44dee --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/__tests__/index.spec.tsx @@ -0,0 +1,66 @@ +/* + * 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 React from 'react'; + +import { TemporaryStorageField } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +const mockOnChange = jest.fn(); + +describe('TemporaryStorageField', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('switched off snapshot', () => { + const snapshot = createSnapshot(false); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('switched on snapshot', () => { + const snapshot = createSnapshot(true); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + it('should be initially switched off', () => { + renderComponent(undefined); + const switchInput = screen.getByRole('checkbox') as HTMLInputElement; + expect(switchInput.checked).toBeFalsy(); + }); + + it('should be switched off', () => { + renderComponent(false); + const switchInput = screen.getByRole('checkbox') as HTMLInputElement; + expect(switchInput.checked).toBeFalsy(); + + switchInput.click(); + expect(switchInput.checked).toBeTruthy(); + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); + + it('should be initially switched on', () => { + renderComponent(true); + const switchInput = screen.getByRole('checkbox') as HTMLInputElement; + expect(switchInput.checked).toBeTruthy(); + + switchInput.click(); + expect(switchInput.checked).toBeFalsy(); + expect(mockOnChange).toHaveBeenCalledTimes(1); + }); +}); + +function getComponent(isTemporary: boolean | undefined) { + return ; +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/index.tsx new file mode 100644 index 0000000000..429e3f6b69 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField/index.tsx @@ -0,0 +1,59 @@ +/* + * 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 { FormGroup, Switch } from '@patternfly/react-core'; +import React from 'react'; + +export type Props = { + onChange: (isTemporary: boolean | undefined) => void; + isTemporary: boolean | undefined; +}; +export type State = { + isTemporary: boolean; +}; + +export class TemporaryStorageField extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + isTemporary: this.props.isTemporary || false, + }; + } + + public componentDidUpdate(prevProps: Readonly): void { + const isTemporary = this.props.isTemporary || false; + if (prevProps.isTemporary !== isTemporary) { + this.setState({ isTemporary }); + } + } + + private handleChange(isTemporary: boolean) { + this.setState({ isTemporary }); + this.props.onChange(isTemporary); + } + + public render() { + const { isTemporary } = this.state; + + return ( + + this.handleChange(value)} + /> + + ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/__mocks__/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/__mocks__/index.tsx new file mode 100644 index 0000000000..5be0511710 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/__mocks__/index.tsx @@ -0,0 +1,42 @@ +/* + * 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 React from 'react'; + +import { Props } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions'; + +export class AdvancedOptions extends React.PureComponent { + public render() { + const { + containerImage, + temporaryStorage, + createNewIfExisting, + memoryLimit, + cpuLimit, + onChange, + } = this.props; + + return ( +
+
Advanced Options
+
{`${containerImage}, ${temporaryStorage}, ${createNewIfExisting}, ${memoryLimit}, ${cpuLimit}`}
+ +
+ ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..e30eb7df53 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,167 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AdvancedOptions snapshot with all values 1`] = ` +
+
+
+ Container Image +
+
+ testimage +
+ +
+
+
+ Temporary Storage +
+
+ true +
+ +
+
+
+ Create New If Existing +
+
+ true +
+ +
+
+
+ Memory Limit +
+
+ 4718592 +
+ +
+
+
+ Cpu Limit +
+
+ 2 +
+ +
+
+`; + +exports[`AdvancedOptions snapshot with default values 1`] = ` +
+
+
+ Container Image +
+
+ +
+
+
+ Temporary Storage +
+
+ undefined +
+ +
+
+
+ Create New If Existing +
+
+ undefined +
+ +
+
+
+ Memory Limit +
+
+ 0 +
+ +
+
+
+ Cpu Limit +
+
+ 0 +
+ +
+ +`; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/__tests__/index.spec.tsx new file mode 100644 index 0000000000..9f0bab2ef9 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/__tests__/index.spec.tsx @@ -0,0 +1,156 @@ +/* + * 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 React from 'react'; + +import { AdvancedOptions } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +jest.mock('@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField'); +jest.mock('@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField'); +jest.mock('@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField'); +jest.mock('@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField'); +jest.mock( + '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField', +); + +const mockOnChange = jest.fn(); + +describe('AdvancedOptions', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('snapshot with default values', () => { + const snapshot = createSnapshot(); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('snapshot with all values', () => { + const snapshot = createSnapshot('testimage', true, true, 4718592, 2); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('update Container Image', () => { + renderComponent('testimage'); + + const containerImage = screen.getByTestId('container-image'); + + expect(containerImage).toHaveTextContent('testimage'); + + const updateContainerImage = screen.getByRole('button', { + name: 'Container Image Change', + }); + + userEvent.click(updateContainerImage); + + expect(mockOnChange).toHaveBeenCalledWith( + 'new-container-image', + undefined, + undefined, + undefined, + undefined, + ); + }); + + test('update Cpu Limit', () => { + renderComponent(undefined, undefined, undefined, undefined, 8); + + const cpuLimit = screen.getByTestId('cpu-limit'); + + expect(cpuLimit).toHaveTextContent('8'); + + const updateCpuLimit = screen.getByRole('button', { + name: 'Cpu Limit Change', + }); + + userEvent.click(updateCpuLimit); + + expect(mockOnChange).toHaveBeenCalledWith(undefined, undefined, undefined, undefined, 1); + }); + + test('update CreateNewIfExisting', () => { + renderComponent(undefined, undefined, true); + + const createNewIfExisting = screen.getByTestId('create-new-if-existing'); + + expect(createNewIfExisting).toHaveTextContent('true'); + + const updateCreateNewIfExisting = screen.getByRole('button', { + name: 'Create New If Existing Change', + }); + + userEvent.click(updateCreateNewIfExisting); + + expect(mockOnChange).toHaveBeenCalledWith(undefined, undefined, false, undefined, undefined); + }); + + test('update Memory Limit', () => { + renderComponent(undefined, undefined, undefined, 4718592); + + const memoryLimit = screen.getByTestId('memory-limit'); + + expect(memoryLimit).toHaveTextContent('4718592'); + + const updateMemoryLimit = screen.getByRole('button', { + name: 'Memory Limit Change', + }); + + userEvent.click(updateMemoryLimit); + + expect(mockOnChange).toHaveBeenCalledWith( + undefined, + undefined, + undefined, + 1073741824, + undefined, + ); + }); + + test('update Temporary Storage', () => { + renderComponent(undefined, true); + + const temporaryStorage = screen.getByTestId('temporary-storage'); + + expect(temporaryStorage).toHaveTextContent('true'); + + const updateTemporaryStorage = screen.getByRole('button', { + name: 'Temporary Storage Change', + }); + + userEvent.click(updateTemporaryStorage); + + expect(mockOnChange).toHaveBeenCalledWith(undefined, false, undefined, undefined, undefined); + }); +}); + +function getComponent( + containerImage?: string | undefined, + temporaryStorage?: boolean | undefined, + createNewIfExisting?: boolean | undefined, + memoryLimit?: number | undefined, + cpuLimit?: number | undefined, +) { + return ( + + ); +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/index.tsx new file mode 100644 index 0000000000..f5840e9f23 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/index.tsx @@ -0,0 +1,176 @@ +/* + * 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 { Form } from '@patternfly/react-core'; +import React from 'react'; + +import { ContainerImageField } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/ContainerImageField'; +import { CpuLimitField } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CpuLimitField'; +import { CreateNewIfExistingField } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/CreateNewIfExistingField'; +import { MemoryLimitField } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/MemoryLimitField'; +import { TemporaryStorageField } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions/TemporaryStorageField'; + +export type Props = { + containerImage: string | undefined; + temporaryStorage: boolean | undefined; + createNewIfExisting: boolean | undefined; + memoryLimit: number | undefined; + cpuLimit: number | undefined; + onChange: ( + containerImage: string | undefined, + temporaryStorage: boolean | undefined, + createNewIfExisting: boolean | undefined, + memoryLimit: number | undefined, + cpuLimit: number | undefined, + ) => void; +}; + +export type State = { + containerImage: string | undefined; + temporaryStorage: boolean | undefined; + createNewIfExisting: boolean | undefined; + memoryLimit: number | undefined; + cpuLimit: number | undefined; +}; + +export class AdvancedOptions extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + containerImage: props.containerImage, + temporaryStorage: props.temporaryStorage, + createNewIfExisting: props.createNewIfExisting, + memoryLimit: props.memoryLimit, + cpuLimit: props.cpuLimit, + }; + } + + public componentDidUpdate(prevProps: Readonly): void { + const { containerImage, temporaryStorage, createNewIfExisting, memoryLimit, cpuLimit } = + this.props; + + if (containerImage !== prevProps.containerImage) { + this.setState({ containerImage }); + } + + if (temporaryStorage !== prevProps.temporaryStorage) { + this.setState({ temporaryStorage }); + } + + if (createNewIfExisting !== prevProps.createNewIfExisting) { + this.setState({ createNewIfExisting }); + } + + if (memoryLimit !== prevProps.memoryLimit) { + this.setState({ memoryLimit }); + } + + if (cpuLimit !== prevProps.cpuLimit) { + this.setState({ cpuLimit }); + } + } + + private handleContainerImage(containerImage: string | undefined) { + const { temporaryStorage, createNewIfExisting, memoryLimit, cpuLimit } = this.state; + + this.setState({ containerImage }); + this.props.onChange( + containerImage, + temporaryStorage, + createNewIfExisting, + memoryLimit, + cpuLimit, + ); + } + + private handleTemporaryStorage(temporaryStorage: boolean | undefined) { + const { containerImage, createNewIfExisting, memoryLimit, cpuLimit } = this.state; + + this.setState({ temporaryStorage }); + this.props.onChange( + containerImage, + temporaryStorage, + createNewIfExisting, + memoryLimit, + cpuLimit, + ); + } + + private handleCreateNewIfExisting(createNewIfExisting: boolean | undefined) { + const { containerImage, temporaryStorage, memoryLimit, cpuLimit } = this.state; + + this.setState({ createNewIfExisting }); + this.props.onChange( + containerImage, + temporaryStorage, + createNewIfExisting, + memoryLimit, + cpuLimit, + ); + } + + private handleMemoryLimit(memoryLimit: number | undefined) { + const { containerImage, temporaryStorage, createNewIfExisting, cpuLimit } = this.state; + + this.setState({ memoryLimit }); + this.props.onChange( + containerImage, + temporaryStorage, + createNewIfExisting, + memoryLimit, + cpuLimit, + ); + } + + private handleCpuLimit(cpuLimit: number | undefined) { + const { containerImage, temporaryStorage, createNewIfExisting, memoryLimit } = this.state; + + this.setState({ cpuLimit }); + this.props.onChange( + containerImage, + temporaryStorage, + createNewIfExisting, + memoryLimit, + cpuLimit, + ); + } + + public render() { + const { containerImage, temporaryStorage, createNewIfExisting, memoryLimit, cpuLimit } = + this.state; + return ( +
e.preventDefault()}> + this.handleContainerImage(containerImage)} + containerImage={containerImage} + /> + this.handleTemporaryStorage(temporaryStorage)} + isTemporary={temporaryStorage} + /> + this.handleCreateNewIfExisting(createNewIfExisting)} + createNewIfExisting={createNewIfExisting} + /> + this.handleMemoryLimit(memoryLimit)} + memoryLimit={memoryLimit || 0} + /> + this.handleCpuLimit(cpuLimit)} + cpuLimit={cpuLimit || 0} + /> + + ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__mocks__/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__mocks__/index.tsx new file mode 100644 index 0000000000..78e221ddfa --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__mocks__/index.tsx @@ -0,0 +1,31 @@ +/* + * 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 React from 'react'; + +import { Props } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes'; + +export class AdditionalGitRemotes extends React.PureComponent { + public render() { + const { remotes, onChange } = this.props; + + return ( +
+
Git Remotes
+
{remotes ? JSON.stringify(remotes) : 'undefined'}
+ +
+ ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/__snapshots__/gitRemote.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/__snapshots__/gitRemote.spec.tsx.snap similarity index 100% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/__snapshots__/gitRemote.spec.tsx.snap rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/__snapshots__/gitRemote.spec.tsx.snap diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..a57990150f --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,512 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AdditionalGitRemotesField snapshot 1`] = ` +
+
+ Additional Git Remotes +
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+ + Remove Remote +
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+ + Remove Remote +
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+ + Remove Remote +
+
+
+
+
+
+ +
+
+
+
+
+
+`; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/gitRemote.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/gitRemote.spec.tsx similarity index 97% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/gitRemote.spec.tsx rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/gitRemote.spec.tsx index 7785da5d8f..4a8c674cac 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/gitRemote.spec.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/gitRemote.spec.tsx @@ -15,7 +15,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { Provider } from 'react-redux'; -import AdditionalGitRemote from '@/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/gitRemote'; +import AdditionalGitRemote from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/gitRemote'; import { GitRemote } from '@/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes'; import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/index.spec.tsx similarity index 91% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/index.spec.tsx rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/index.spec.tsx index b63b400da2..edf736206c 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/__tests__/index.spec.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/__tests__/index.spec.tsx @@ -15,7 +15,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { Provider } from 'react-redux'; -import { AdditionalGitRemotes } from '@/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes'; +import { AdditionalGitRemotes } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes'; import { GitRemote } from '@/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes'; import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; @@ -51,20 +51,30 @@ describe('AdditionalGitRemotesField', () => { expect(inputNames[0]).toHaveValue(''); }); - test('valid remotes', async () => { - renderComponent([ + test('remotes rerendering with different values', async () => { + const { reRenderComponent } = renderComponent(); + + let inputNames = await screen.findAllByPlaceholderText('origin'); + expect(inputNames.length).toBe(1); + expect(inputNames[0]).toHaveValue(''); + + let inputURLs = await screen.findAllByPlaceholderText('HTTP or SSH URL'); + expect(inputURLs.length).toBe(1); + expect(inputURLs[0]).toHaveValue(''); + + reRenderComponent([ { name: 'test-1', url: 'https://test-1.repo.git' }, { name: 'test-2', url: 'https://test-2.repo.git' }, { name: 'test-3', url: 'https://test-3.repo.git' }, ]); - const inputNames = await screen.findAllByPlaceholderText('origin'); + inputNames = await screen.findAllByPlaceholderText('origin'); expect(inputNames.length).toBe(3); expect(inputNames[0]).toHaveValue('test-1'); expect(inputNames[1]).toHaveValue('test-2'); expect(inputNames[2]).toHaveValue('test-3'); - const inputURLs = await screen.findAllByPlaceholderText('HTTP or SSH URL'); + inputURLs = await screen.findAllByPlaceholderText('HTTP or SSH URL'); expect(inputURLs.length).toBe(3); expect(inputURLs[0]).toHaveValue('https://test-1.repo.git'); expect(inputURLs[1]).toHaveValue('https://test-2.repo.git'); diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/gitRemote.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/gitRemote.tsx similarity index 98% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/gitRemote.tsx rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/gitRemote.tsx index e577edff05..a66ea157d0 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/gitRemote.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/gitRemote.tsx @@ -25,8 +25,8 @@ import React from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { CheTooltip } from '@/components/CheTooltip'; -import styles from '@/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/index.module.css'; import { validateBrName, validateLocation } from '@/components/ImportFromGit/helpers'; +import styles from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/index.module.css'; import { GitRemote } from '@/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes'; import { ROUTE } from '@/Routes/routes'; import { FactoryLocationAdapter } from '@/services/factory-location-adapter'; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/index.module.css b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/index.module.css similarity index 100% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/index.module.css rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/index.module.css diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/index.tsx similarity index 75% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/index.tsx rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/index.tsx index 4d0ffdfa5a..64178844bb 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/index.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/index.tsx @@ -14,7 +14,6 @@ import { ActionGroup, Button, ButtonVariant, - Form, FormFieldGroup, FormSection, } from '@patternfly/react-core'; @@ -22,8 +21,8 @@ import { PlusCircleIcon } from '@patternfly/react-icons'; import { isEqual } from 'lodash'; import React from 'react'; -import AdditionalGitRemote from '@/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/gitRemote'; -import styles from '@/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes/index.module.css'; +import AdditionalGitRemote from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/gitRemote'; +import styles from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes/index.module.css'; import { GitRemote } from '@/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes'; export type Props = { @@ -40,7 +39,7 @@ export class AdditionalGitRemotes extends React.PureComponent { constructor(props: Props) { super(props); - const remotes = this.props.remotes ? [...this.props.remotes] : []; + const remotes = this.props.remotes || []; if (remotes.length === 0) { remotes.push({ name: '', url: '' }); } @@ -61,7 +60,13 @@ export class AdditionalGitRemotes extends React.PureComponent { const remotes = this.props.remotes || []; const prevRemotes = prevProps.remotes || []; - if (!isEqual(remotes, prevRemotes) && !isEqual(remotes, this.state.remotes)) { + if ( + !isEqual(remotes, prevRemotes) && + !isEqual( + remotes, + this.state.remotes.filter(remote => remote.name !== '' && remote.url !== ''), + ) + ) { this.setState({ remotes, }); @@ -126,24 +131,22 @@ export class AdditionalGitRemotes extends React.PureComponent { public render() { return ( -
- - - {this.buildGitRemotes()} - - - - - -
+ + + {this.buildGitRemotes()} + + + + + ); } } diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/__mocks__/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/__mocks__/index.tsx new file mode 100644 index 0000000000..5e2a041fab --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/__mocks__/index.tsx @@ -0,0 +1,29 @@ +/* + * 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 React from 'react'; + +import { Props } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField'; + +export class GitBranchField extends React.PureComponent { + public render() { + const { gitBranch, onChange } = this.props; + + return ( +
+
Git Branch
+
{gitBranch}
+ +
+ ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/GitBranchField/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/__tests__/__snapshots__/index.spec.tsx.snap similarity index 100% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/GitBranchField/__tests__/__snapshots__/index.spec.tsx.snap rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/__tests__/__snapshots__/index.spec.tsx.snap diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/GitBranchField/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/__tests__/index.spec.tsx similarity index 93% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/GitBranchField/__tests__/index.spec.tsx rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/__tests__/index.spec.tsx index fdc678b68a..4d7659ce95 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/GitBranchField/__tests__/index.spec.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/__tests__/index.spec.tsx @@ -13,7 +13,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; -import { GitBranchField } from '@/components/ImportFromGit/GitRepoOptions/GitBranchField'; +import { GitBranchField } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField'; import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/GitBranchField/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/index.tsx similarity index 88% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/GitBranchField/index.tsx rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/index.tsx index 629ecef4ad..126c38825a 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/GitBranchField/index.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField/index.tsx @@ -30,12 +30,6 @@ export class GitBranchField extends React.PureComponent { }; } - public shouldComponentUpdate(nextProps: Readonly, nextState: Readonly): boolean { - return ( - this.state.gitBranch !== nextState.gitBranch || this.props.gitBranch !== nextProps.gitBranch - ); - } - public componentDidUpdate(prevProps: Readonly): void { const { gitBranch } = this.props; if (prevProps.gitBranch !== gitBranch) { diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/__mocks__/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/__mocks__/index.tsx new file mode 100644 index 0000000000..1d6d58e626 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/__mocks__/index.tsx @@ -0,0 +1,29 @@ +/* + * 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 React from 'react'; + +import { Props } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField'; + +export class PathToDevfileField extends React.PureComponent { + public render() { + const { devfilePath, onChange } = this.props; + + return ( +
+
Devfile Path
+
{devfilePath}
+ +
+ ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/PathToDevfileField/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/__tests__/__snapshots__/index.spec.tsx.snap similarity index 100% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/PathToDevfileField/__tests__/__snapshots__/index.spec.tsx.snap rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/__tests__/__snapshots__/index.spec.tsx.snap diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/PathToDevfileField/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/__tests__/index.spec.tsx similarity index 92% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/PathToDevfileField/__tests__/index.spec.tsx rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/__tests__/index.spec.tsx index 2ddf1b5f8a..b5724988d1 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/PathToDevfileField/__tests__/index.spec.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/__tests__/index.spec.tsx @@ -13,7 +13,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; -import { PathToDevfileField } from '@/components/ImportFromGit/GitRepoOptions/PathToDevfileField'; +import { PathToDevfileField } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField'; import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/PathToDevfileField/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/index.tsx similarity index 87% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/PathToDevfileField/index.tsx rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/index.tsx index e11c947a66..a005930eef 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/PathToDevfileField/index.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField/index.tsx @@ -30,13 +30,6 @@ export class PathToDevfileField extends React.PureComponent { }; } - public shouldComponentUpdate(nextProps: Readonly, nextState: Readonly): boolean { - return ( - this.state.devfilePath !== nextState.devfilePath || - this.props.devfilePath !== nextProps.devfilePath - ); - } - public componentDidUpdate(prevProps: Readonly): void { const { devfilePath } = this.props; if (prevProps.devfilePath !== devfilePath) { diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/__mocks__/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/__mocks__/index.tsx new file mode 100644 index 0000000000..325d1c2981 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/__mocks__/index.tsx @@ -0,0 +1,40 @@ +/* + * 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 React from 'react'; + +import { Props } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions'; + +export class GitRepoOptions extends React.PureComponent { + public render() { + const { gitBranch, remotes, devfilePath, hasSupportedGitService, onChange } = this.props; + + return ( +
+
Git Repo Options
+
{`${gitBranch}, ${JSON.stringify(remotes)}, ${devfilePath}, ${hasSupportedGitService}`}
+ +
+ ); + } +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..1bba4e61c0 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GitRepoOptions snapshot with all values 1`] = ` +
+
+
+ Git Branch +
+
+ test-git-branch +
+ +
+
+
+ Git Remotes +
+
+ [{"name":"test","url":"http://test"}] +
+ +
+
+
+ Devfile Path +
+
+ test-devfile-path +
+ +
+
+`; + +exports[`GitRepoOptions snapshot with default values 1`] = ` +
+
+
+ Git Branch +
+
+ +
+
+
+ Git Remotes +
+
+ undefined +
+ +
+
+
+ Devfile Path +
+
+ +
+ +`; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/__tests__/index.spec.tsx new file mode 100644 index 0000000000..5a5bab3748 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/__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 React from 'react'; + +import { GitRepoOptions } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions'; +import { GitRemote } from '@/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +jest.mock('@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes'); +jest.mock('@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField'); +jest.mock('@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField'); + +const mockOnChange = jest.fn(); + +describe('GitRepoOptions', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('snapshot with default values', () => { + const snapshot = createSnapshot(); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('snapshot with all values', () => { + const snapshot = createSnapshot( + 'test-git-branch', + [{ name: 'test', url: 'http://test' }], + 'test-devfile-path', + ); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + it('should remove "Git Branch" component when it is not supported', () => { + const { reRenderComponent } = renderComponent( + 'test-git-branch', + [{ name: 'test', url: 'http://test' }], + 'test-devfile-path', + ); + + expect(screen.queryByTestId('git-branch-component')).not.toBeNull(); + + reRenderComponent(undefined, undefined, undefined, false); + + expect(screen.queryByTestId('git-branch-component')).toBeNull(); + }); + + test('update Git Branch', () => { + renderComponent('test-git-branch'); + + const gitBranch = screen.getByTestId('git-branch'); + + expect(gitBranch).toHaveTextContent('test-git-branch'); + + const updateGitBranch = screen.getByRole('button', { + name: 'Git Branch Change', + }); + + userEvent.click(updateGitBranch); + + expect(mockOnChange).toHaveBeenCalledWith('new-branch', undefined, undefined, true); + }); + + test('update Remotes', () => { + renderComponent(undefined, [{ name: 'test', url: 'http://test' }]); + + const gitRemotes = screen.getByTestId('git-remotes'); + + expect(gitRemotes).toHaveTextContent('[{"name":"test","url":"http://test"}]'); + + const updateGitRemotes = screen.getByRole('button', { + name: 'Git Remotes Change', + }); + + userEvent.click(updateGitRemotes); + + expect(mockOnChange).toHaveBeenCalledWith( + undefined, + [{ name: 'test-updated', url: 'http://test' }], + undefined, + true, + ); + }); + + test('update PathToDevfile', () => { + renderComponent(undefined, undefined, 'test-devfile-path'); + + const pathToDevfile = screen.getByTestId('devfile-path'); + + expect(pathToDevfile).toHaveTextContent('test-devfile-path'); + + const updatePathToDevfile = screen.getByRole('button', { + name: 'Devfile Path Change', + }); + + userEvent.click(updatePathToDevfile); + + expect(mockOnChange).toHaveBeenCalledWith(undefined, undefined, 'new-devfile-path', true); + }); +}); + +function getComponent( + gitBranch?: string | undefined, + remotes?: GitRemote[] | undefined, + devfilePath?: string | undefined, + hasSupportedGitService: boolean = true, +) { + return ( + + ); +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/index.tsx similarity index 91% rename from devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/index.tsx rename to devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/index.tsx index f63015939d..fe594bd906 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/GitRepoOptions/index.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/index.tsx @@ -14,9 +14,9 @@ import { Form } from '@patternfly/react-core'; import { isEqual } from 'lodash'; import React from 'react'; -import { AdditionalGitRemotes } from '@/components/ImportFromGit/GitRepoOptions/AdditionalGitRemotes'; -import { GitBranchField } from '@/components/ImportFromGit/GitRepoOptions/GitBranchField'; -import { PathToDevfileField } from '@/components/ImportFromGit/GitRepoOptions/PathToDevfileField'; +import { AdditionalGitRemotes } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/AdditionalGitRemotes'; +import { GitBranchField } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/GitBranchField'; +import { PathToDevfileField } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions/PathToDevfileField'; import { GitRemote } from '@/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes'; export type Props = { diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/__tests__/__snapshots__/index.spec.tsx.snap b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 0000000000..d47880bc82 --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RepoOptionsAccordion snapshot with default values 1`] = ` +
+

+ +

+ +

+ +

+ +
+`; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/__tests__/index.spec.tsx new file mode 100644 index 0000000000..9c55c0ecac --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/__tests__/index.spec.tsx @@ -0,0 +1,159 @@ +/* + * 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 { createMemoryHistory } from 'history'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { Store } from 'redux'; + +import RepoOptionsAccordion from '@/components/ImportFromGit/RepoOptionsAccordion'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; +import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + +const history = createMemoryHistory({ + initialEntries: ['/'], +}); + +jest.mock('@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions'); +jest.mock('@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions'); + +const mockOnChange = jest.fn(); + +describe('RepoOptionsAccordion', () => { + let store: Store; + + beforeEach(() => { + store = new FakeStoreBuilder() + .withSshKeys({ + keys: [{ name: 'key1', keyPub: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD' }], + }) + .build(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('snapshot with default values', () => { + const snapshot = createSnapshot(store, 'testlocation'); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('update Advanced Options', () => { + renderComponent(store, 'https://testlocation'); + + let updateAdvancedOptions = screen.queryByRole('button', { + name: 'Advanced Options Change', + }); + + expect(updateAdvancedOptions).toBeNull(); + + const accordionItemAdvancedOptions = screen.getByTestId('accordion-item-advanced-options'); + + userEvent.click(accordionItemAdvancedOptions); + + const advancedOptions = screen.queryByTestId('advanced-options'); + + expect(advancedOptions).not.toBeNull(); + expect(advancedOptions).toHaveTextContent( + 'undefined, undefined, undefined, undefined, undefined', + ); + + updateAdvancedOptions = screen.queryByRole('button', { + name: 'Advanced Options Change', + }); + + expect(updateAdvancedOptions).not.toBeNull(); + + userEvent.click(updateAdvancedOptions as HTMLElement); + + expect(mockOnChange).toHaveBeenCalledWith( + 'https://testlocation?image=newContainerImage&storageType=ephemeral&policies.create=perclick&memoryLimit=1Gi&cpuLimit=1', + undefined, + ); + }); + + test('update Git Repo Options without a supported git service', () => { + renderComponent(store, 'https://testlocation'); + + let updateGitRepoOptions = screen.queryByRole('button', { + name: 'Git Repo Options Change', + }); + + expect(updateGitRepoOptions).toBeNull(); + + const accordionItemGitRepoOptions = screen.getByTestId('accordion-item-git-repo-options'); + + userEvent.click(accordionItemGitRepoOptions); + + const gitRepoOptions = screen.queryByTestId('git-repo-options'); + + expect(gitRepoOptions).not.toBeNull(); + expect(gitRepoOptions).toHaveTextContent('undefined, [], undefined, false'); + + updateGitRepoOptions = screen.queryByRole('button', { + name: 'Git Repo Options Change', + }); + + expect(updateGitRepoOptions).not.toBeNull(); + + userEvent.click(updateGitRepoOptions as HTMLElement); + + expect(mockOnChange).toHaveBeenCalledWith( + 'https://testlocation?remotes={{test-updated,http://test}}&devfilePath=newDevfilePath', + 'success', + ); + }); + + test('update Git Repo Options wit a supported git service', () => { + renderComponent(store, 'https://github.com/testlocation'); + + let updateGitRepoOptions = screen.queryByRole('button', { + name: 'Git Repo Options Change', + }); + + expect(updateGitRepoOptions).toBeNull(); + + const accordionItemGitRepoOptions = screen.getByTestId('accordion-item-git-repo-options'); + + userEvent.click(accordionItemGitRepoOptions); + + const gitRepoOptions = screen.queryByTestId('git-repo-options'); + + expect(gitRepoOptions).not.toBeNull(); + expect(gitRepoOptions).toHaveTextContent('undefined, [], undefined, true'); + + updateGitRepoOptions = screen.queryByRole('button', { + name: 'Git Repo Options Change', + }); + + expect(updateGitRepoOptions).not.toBeNull(); + + userEvent.click(updateGitRepoOptions as HTMLElement); + + expect(mockOnChange).toHaveBeenCalledWith( + 'https://github.com/testlocation/undefined/tree/newBranch?remotes={{test-updated,http://test}}&devfilePath=newDevfilePath', + 'success', + ); + }); +}); + +function getComponent(store: Store, location: string) { + return ( + + + + ); +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/index.tsx new file mode 100644 index 0000000000..e236279b3a --- /dev/null +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/RepoOptionsAccordion/index.tsx @@ -0,0 +1,276 @@ +/* + * 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 { + Accordion, + AccordionContent, + AccordionItem, + AccordionToggle, + Panel, + PanelMain, + PanelMainBody, + ValidatedOptions, +} from '@patternfly/react-core'; +import { History } from 'history'; +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { + getAdvancedOptionsFromLocation, + getGitRepoOptionsFromLocation, + setAdvancedOptionsToLocation, + setGitRepoOptionsToLocation, + validateLocation, +} from '@/components/ImportFromGit/helpers'; +import { AdvancedOptions } from '@/components/ImportFromGit/RepoOptionsAccordion/AdvancedOptions'; +import { GitRepoOptions } from '@/components/ImportFromGit/RepoOptionsAccordion/GitRepoOptions'; +import { GitRemote } from '@/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes'; +import { AppState } from '@/store'; +import { selectSshKeys } from '@/store/SshKeys/selectors'; + +type AccordionId = 'git-repo-options' | 'advanced-options'; + +export type Props = MappedProps & { + location: string; + onChange: (location: string, remotesValidated: ValidatedOptions) => void; + history: History; +}; +export type State = { + location: string; + hasSshKeys: boolean; + expanded: AccordionId[]; + gitBranch: string | undefined; + remotes: GitRemote[] | undefined; + remotesValidated: ValidatedOptions; + devfilePath: string | undefined; + containerImage: string | undefined; + temporaryStorage: boolean | undefined; + createNewIfExisting: boolean | undefined; + memoryLimit: number | undefined; + cpuLimit: number | undefined; + hasSupportedGitService: boolean; +}; + +class RepoOptionsAccordion extends React.PureComponent { + constructor(props: Props) { + super(props); + + const { location } = props; + + this.state = { + hasSupportedGitService: false, + location, + hasSshKeys: props.sshKeys.length > 0, + expanded: [], + gitBranch: undefined, + remotes: undefined, + remotesValidated: ValidatedOptions.default, + devfilePath: undefined, + containerImage: undefined, + temporaryStorage: undefined, + createNewIfExisting: undefined, + memoryLimit: undefined, + cpuLimit: undefined, + }; + } + + private updateStateFromLocation(): void { + const { location } = this.props; + + const validated = validateLocation(location, this.state.hasSshKeys); + if (validated !== ValidatedOptions.success) { + return; + } + const gitRepoOptions = getGitRepoOptionsFromLocation(location); + if (!gitRepoOptions.location) { + return; + } + const advancedOptions = getAdvancedOptionsFromLocation(gitRepoOptions.location); + + const state = Object.assign(gitRepoOptions, advancedOptions) as State; + + this.setState(state); + } + + public componentDidMount() { + this.updateStateFromLocation(); + } + + public componentDidUpdate(prevProps: Readonly) { + const location = this.props.location.trim(); + if (location === prevProps.location || location === this.state.location) { + return; + } + this.updateStateFromLocation(); + } + + private handleToggle(id: AccordionId): void { + const { expanded } = this.state; + const index = expanded.indexOf(id); + const newExpanded: AccordionId[] = + index >= 0 + ? [...expanded.slice(0, index), ...expanded.slice(index + 1, expanded.length)] + : [...expanded, id]; + + this.setState({ + expanded: newExpanded, + }); + } + + private handleGitRepoOptionsChange( + gitBranch: string | undefined, + remotes: GitRemote[] | undefined, + devfilePath: string | undefined, + isValid: boolean, + ): void { + const state = setGitRepoOptionsToLocation( + { gitBranch, remotes, devfilePath }, + { + location: this.state.location, + gitBranch: this.state.gitBranch, + remotes: this.state.remotes, + devfilePath: this.state.devfilePath, + }, + ) as State; + state.remotesValidated = isValid ? ValidatedOptions.success : ValidatedOptions.error; + this.setState(state); + this.props.onChange(state.location, state.remotesValidated); + } + + private handleAdvancedOptionsOptionsChange( + containerImage: string | undefined, + temporaryStorage: boolean | undefined, + createNewIfExisting: boolean | undefined, + memoryLimit: number | undefined, + cpuLimit: number | undefined, + ) { + const state = setAdvancedOptionsToLocation( + { + containerImage, + temporaryStorage, + createNewIfExisting, + memoryLimit, + cpuLimit, + }, + { + location: this.state.location, + containerImage: this.state.containerImage, + temporaryStorage: this.state.temporaryStorage, + createNewIfExisting: this.state.createNewIfExisting, + memoryLimit: this.state.memoryLimit, + cpuLimit: this.state.cpuLimit, + }, + ) as State; + + this.setState(state); + this.props.onChange(state.location, state.remotesValidated); + } + + public render() { + const { hasSupportedGitService } = this.state; + const { expanded, remotes, devfilePath, gitBranch } = this.state; + const { containerImage, temporaryStorage, createNewIfExisting, memoryLimit, cpuLimit } = + this.state; + return ( + + + { + this.handleToggle('git-repo-options'); + }} + isExpanded={expanded.includes('git-repo-options')} + id="accordion-item-git-repo-options" + data-testid="accordion-item-git-repo-options" + > + Git Repo Options + + + + + + + + this.handleGitRepoOptionsChange(gitBranch, remotes, devfilePath, isValid) + } + /> + + + + + + + { + this.handleToggle('advanced-options'); + }} + isExpanded={expanded.includes('advanced-options')} + id="accordion-item-advanced-options" + data-testid="accordion-item-advanced-options" + > + Advanced Options + + + + + + + + this.handleAdvancedOptionsOptionsChange( + containerImage, + temporaryStorage, + createNewIfExisting, + memoryLimit, + cpuLimit, + ) + } + /> + + + + + + + ); + } +} + +const mapStateToProps = (state: AppState) => ({ + sshKeys: selectSshKeys(state), +}); + +const connector = connect(mapStateToProps); + +type MappedProps = ConnectedProps; +export default connector(RepoOptionsAccordion); diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/__tests__/helpers.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/__tests__/helpers.spec.tsx index 9e7c35b57e..9e28074cc0 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/__tests__/helpers.spec.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/__tests__/helpers.spec.tsx @@ -14,6 +14,7 @@ import common from '@eclipse-che/common'; import { ValidatedOptions } from '@patternfly/react-core'; import * as helpers from '@/components/ImportFromGit/helpers'; +import { formatBytes, getBytes } from '@/components/ImportFromGit/helpers'; describe('helpers', () => { afterEach(() => { @@ -57,7 +58,12 @@ describe('helpers', () => { describe('supportedProviders', () => { test('should includes "github", "gitlab" and "bitbucket"', () => { - expect(helpers.supportedProviders).toEqual(['github', 'gitlab', 'bitbucket']); + expect(helpers.supportedProviders).toEqual([ + 'github', + 'gitlab', + 'bitbucket-server', + 'azure-devops', + ]); }); }); @@ -65,7 +71,8 @@ describe('helpers', () => { test('should return provider', () => { expect(helpers.getSupportedGitService('https://github.com')).toBe('github'); expect(helpers.getSupportedGitService('https://gitlab.com')).toBe('gitlab'); - expect(helpers.getSupportedGitService('https://bitbucket.org')).toBe('bitbucket'); + expect(helpers.getSupportedGitService('https://bitbucket.org')).toBe('bitbucket-server'); + expect(helpers.getSupportedGitService('https://dev.azure.com')).toBe('azure-devops'); }); test('should throw error when provider is not supported', () => { @@ -80,6 +87,7 @@ describe('helpers', () => { expect(helpers.isSupportedGitService('https://github.com')).toBe(true); expect(helpers.isSupportedGitService('https://gitlab.com')).toBe(true); expect(helpers.isSupportedGitService('https://bitbucket.org')).toBe(true); + expect(helpers.isSupportedGitService('https://dev.azure.com')).toBe(true); }); test('should return false when provider is not supported', () => { @@ -118,7 +126,7 @@ describe('helpers', () => { ).toBe(branch); }); }); - describe('bitbucket', () => { + describe('Bitbucket', () => { test('should return the empty value', () => { expect( helpers.getBranchFromLocation('https://bitbucket.org/eclipse-che/che-dashboard.git'), @@ -132,6 +140,22 @@ describe('helpers', () => { ).toBe(branch); }); }); + describe('Azure', () => { + test('should return the empty value', () => { + expect( + helpers.getBranchFromLocation( + 'https://dev.azure.com/marioloriedo/publicproject/_git/public-repo', + ), + ).toBeUndefined(); + }); + test('should return the branch', () => { + expect( + helpers.getBranchFromLocation( + `https://dev.azure.com/testuser/publicproject/_git/public-repo%3Fversion=GB${branch}`, + ), + ).toBe(branch); + }); + }); }); describe('unsupported Git provider', () => { @@ -201,6 +225,26 @@ describe('helpers', () => { ).toBe(`https://bitbucket.org/eclipse-che/che-dashboard.git/src/${branch}`); }); }); + describe('Azure', () => { + test('should return the location without branch', () => { + expect( + helpers.setBranchToLocation( + `https://dev.azure.com/testuser/publicproject/_git/public-repo%3Fpath=%2F&version=GB${branch}`, + undefined, + ), + ).toBe('https://dev.azure.com/testuser/publicproject/_git/public-repo%3Fpath%3D%252F'); + }); + test('should return the location with branch', () => { + expect( + helpers.setBranchToLocation( + 'https://dev.azure.com/testuser/publicproject/_git/public-repo', + branch, + ), + ).toBe( + `https://dev.azure.com/testuser/publicproject/_git/public-repo%3Fversion%3DGB${branch}`, + ); + }); + }); }); describe('unsupported Git provider', () => { test('should throw the error', () => { @@ -231,6 +275,30 @@ describe('helpers', () => { devfilePath: undefined, }); }); + test('should return options from location with params with empty values', () => { + let options = helpers.getGitRepoOptionsFromLocation( + 'https://github.com/eclipse-che/che-dashboard.git?remotes&devfilePath', + ); + expect(options).toEqual({ + location: 'https://github.com/eclipse-che/che-dashboard.git', + hasSupportedGitService: true, + gitBranch: undefined, + remotes: undefined, + devfilePath: undefined, + }); + + options = helpers.getGitRepoOptionsFromLocation( + 'https://github.com/eclipse-che/che-dashboard.git?remotes={}&df', + ); + + expect(options).toEqual({ + location: 'https://github.com/eclipse-che/che-dashboard.git', + hasSupportedGitService: true, + gitBranch: undefined, + remotes: undefined, + devfilePath: undefined, + }); + }); test('should return all supported options', () => { const location = 'https://github.com/eclipse-che/che-dashboard/tree/main?remotes={{test-1,http://test-1.git}}&df=devfile2.yaml'; @@ -284,6 +352,83 @@ describe('helpers', () => { gitBranch: undefined, remotes: [{ name: 'test-1', url: 'http://test-1.git' }], devfilePath: 'devfile2.yaml', + containerImage: undefined, + temporaryStorage: undefined, + createNewIfExisting: undefined, + memoryLimit: undefined, + cpuLimit: undefined, + }); + }); + }); + }); + + describe('getAdvancedOptionsFromLocation', () => { + describe('HTTP', () => { + test('should return options from location without parameters', () => { + const location = 'https://github.com/eclipse-che/che-dashboard.git'; + const options = helpers.getAdvancedOptionsFromLocation(location); + expect(options).toEqual({ + location: 'https://github.com/eclipse-che/che-dashboard.git', + containerImage: undefined, + temporaryStorage: undefined, + createNewIfExisting: undefined, + memoryLimit: undefined, + cpuLimit: undefined, + }); + }); + test('should return options from location with params with empty values', () => { + const location = + 'https://github.com/eclipse-che/che-dashboard.git?image&policies.create&memoryLimit&storageType'; + const options = helpers.getAdvancedOptionsFromLocation(location); + expect(options).toEqual({ + location: 'https://github.com/eclipse-che/che-dashboard.git', + containerImage: undefined, + temporaryStorage: undefined, + createNewIfExisting: undefined, + memoryLimit: undefined, + cpuLimit: undefined, + }); + }); + test('should return all supported options', () => { + const location = + 'https://github.com/eclipse-che/che-dashboard/tree/main?image=custom-image&new&memoryLimit=2Gi&cpuLimit=1&storageType=ephemeral'; + const options = helpers.getAdvancedOptionsFromLocation(location); + expect(options).toEqual({ + location: + 'https://github.com/eclipse-che/che-dashboard/tree/main?storageType=ephemeral&image=custom-image&cpuLimit=1&memoryLimit=2Gi&policies.create=perclick', + containerImage: 'custom-image', + temporaryStorage: true, + createNewIfExisting: true, + memoryLimit: 2147483648, + cpuLimit: 1, + }); + }); + }); + describe('SSH', () => { + test('should return options from location without parameters', () => { + const location = 'git@github.com:eclipse-che/che-dashboard.git'; + const options = helpers.getAdvancedOptionsFromLocation(location); + expect(options).toEqual({ + location: 'git@github.com:eclipse-che/che-dashboard.git', + containerImage: undefined, + temporaryStorage: undefined, + createNewIfExisting: undefined, + memoryLimit: undefined, + cpuLimit: undefined, + }); + }); + test('should return all supported options', () => { + const location = + 'git@github.com:eclipse-che/che-dashboard.git?image=custom-image&new&memoryLimit=2Gi&cpuLimit=1&storageType=ephemeral'; + const options = helpers.getAdvancedOptionsFromLocation(location); + expect(options).toEqual({ + location: + 'git@github.com:eclipse-che/che-dashboard.git?storageType=ephemeral&image=custom-image&cpuLimit=1&memoryLimit=2Gi&policies.create=perclick', + containerImage: 'custom-image', + temporaryStorage: true, + createNewIfExisting: true, + memoryLimit: 2147483648, + cpuLimit: 1, }); }); }); @@ -292,12 +437,33 @@ describe('helpers', () => { describe('setGitRepoOptionsToLocation', () => { describe('supported Git services', () => { describe('HTTP', () => { - test('should return options with updated location', () => { + test('should return options with updated location(set search params)', () => { const newOptions = { gitBranch: 'test-branch', remotes: [{ name: 'test-2', url: 'http://test-2.git' }], devfilePath: 'devfile3.yaml', }; + const currentOptions = { + location: 'https://github.com/eclipse-che/che-dashboard/tree/main', + gitBranch: undefined, + remotes: undefined, + devfilePath: undefined, + }; + const options = helpers.setGitRepoOptionsToLocation(newOptions, currentOptions); + expect(options).toEqual({ + location: + 'https://github.com/eclipse-che/che-dashboard/tree/test-branch?remotes={{test-2,http://test-2.git}}&devfilePath=devfile3.yaml', + gitBranch: 'test-branch', + remotes: [{ name: 'test-2', url: 'http://test-2.git' }], + devfilePath: 'devfile3.yaml', + }); + }); + test('should return options with updated location(reset search params)', () => { + const newOptions = { + gitBranch: undefined, + remotes: undefined, + devfilePath: undefined, + }; const currentOptions = { location: 'https://github.com/eclipse-che/che-dashboard/tree/main?remotes={{test-1,http://test-1.git}}&df=devfile2.yaml', @@ -307,11 +473,10 @@ describe('helpers', () => { }; const options = helpers.setGitRepoOptionsToLocation(newOptions, currentOptions); expect(options).toEqual({ - location: - 'https://github.com/eclipse-che/che-dashboard/tree/test-branch?remotes=%7B%7Btest-2%2Chttp%3A%2F%2Ftest-2.git%7D%7D&devfilePath=devfile3.yaml', - gitBranch: 'test-branch', - remotes: [{ name: 'test-2', url: 'http://test-2.git' }], - devfilePath: 'devfile3.yaml', + location: 'https://github.com/eclipse-che/che-dashboard', + gitBranch: undefined, + remotes: undefined, + devfilePath: undefined, }); }); }); @@ -332,7 +497,7 @@ describe('helpers', () => { const options = helpers.setGitRepoOptionsToLocation(newOptions, currentOptions); expect(options).toEqual({ location: - 'git@github.com:eclipse-che/che-dashboard.git?remotes=%7B%7Btest-2%2Chttp%3A%2F%2Ftest-2.git%7D%7D&devfilePath=devfile3.yaml', + 'git@github.com:eclipse-che/che-dashboard.git?remotes={{test-2,http://test-2.git}}&devfilePath=devfile3.yaml', gitBranch: undefined, remotes: [{ name: 'test-2', url: 'http://test-2.git' }], devfilePath: 'devfile3.yaml', @@ -356,7 +521,7 @@ describe('helpers', () => { const options = helpers.setGitRepoOptionsToLocation(newOptions, currentOptions); expect(options).toEqual({ location: - 'http://not-supported.com?remotes=%7B%7Btest-2%2Chttp%3A%2F%2Ftest-2.git%7D%7D&devfilePath=devfile3.yaml', + 'http://not-supported.com?remotes={{test-2,http://test-2.git}}&devfilePath=devfile3.yaml', gitBranch: undefined, remotes: [{ name: 'test-2', url: 'http://test-2.git' }], devfilePath: 'devfile3.yaml', @@ -365,4 +530,159 @@ describe('helpers', () => { }); }); }); + describe('setAdvancedOptionsToLocation', () => { + describe('supported Git services', () => { + describe('HTTP', () => { + test('should return options with updated location(set search params)', () => { + const newOptions = { + containerImage: 'custom-image', + temporaryStorage: true, + createNewIfExisting: true, + memoryLimit: 2147483648, + cpuLimit: 1, + }; + const currentOptions = { + location: 'https://github.com/eclipse-che/che-dashboard.git', + containerImage: undefined, + temporaryStorage: undefined, + createNewIfExisting: undefined, + memoryLimit: undefined, + cpuLimit: undefined, + }; + const options = helpers.setAdvancedOptionsToLocation(newOptions, currentOptions); + expect(options).toEqual({ + location: + 'https://github.com/eclipse-che/che-dashboard.git?image=custom-image&storageType=ephemeral&policies.create=perclick&memoryLimit=2Gi&cpuLimit=1', + containerImage: 'custom-image', + temporaryStorage: true, + createNewIfExisting: true, + memoryLimit: 2147483648, + cpuLimit: 1, + }); + }); + test('should return options with updated location(reset search params)', () => { + const newOptions = { + containerImage: undefined, + temporaryStorage: undefined, + createNewIfExisting: undefined, + memoryLimit: undefined, + cpuLimit: undefined, + }; + const currentOptions = { + location: 'https://github.com/eclipse-che/che-dashboard.git', + containerImage: 'custom-image', + temporaryStorage: true, + createNewIfExisting: true, + memoryLimit: 2147483648, + cpuLimit: undefined, + }; + const options = helpers.setAdvancedOptionsToLocation(newOptions, currentOptions); + expect(options).toEqual({ + location: 'https://github.com/eclipse-che/che-dashboard.git', + containerImage: undefined, + temporaryStorage: undefined, + createNewIfExisting: undefined, + memoryLimit: undefined, + cpuLimit: undefined, + }); + }); + }); + describe('SSH', () => { + test('should return options with updated location', () => { + const newOptions = { + containerImage: 'custom-image', + temporaryStorage: true, + createNewIfExisting: true, + memoryLimit: 2147483648, + cpuLimit: 1, + }; + const currentOptions = { + location: 'git@github.com:eclipse-che/che-dashboard.git', + containerImage: undefined, + temporaryStorage: undefined, + createNewIfExisting: undefined, + memoryLimit: undefined, + cpuLimit: undefined, + }; + const options = helpers.setAdvancedOptionsToLocation(newOptions, currentOptions); + expect(options).toEqual({ + location: + 'git@github.com:eclipse-che/che-dashboard.git?image=custom-image&storageType=ephemeral&policies.create=perclick&memoryLimit=2Gi&cpuLimit=1', + containerImage: 'custom-image', + temporaryStorage: true, + createNewIfExisting: true, + memoryLimit: 2147483648, + cpuLimit: 1, + }); + }); + }); + }); + }); + describe('units of memory measurements', () => { + describe('getBytes', () => { + test('should return Bytes depends on measurements', () => { + const values = [ + '', // error value + '534', + '1 KB', + '4.5Mi', + '10Gi', + '1.5 TiB', + '2.5 PiB', + '2QQQ', // error value + 'undefined', // error value + ].map(val => getBytes(val)); + + expect(values).toEqual([ + undefined, + 534, + 1000, + 4718592, + 10737418240, + 1649267441664, + 2814749767106560, + undefined, + undefined, + ]); + }); + }); + describe('formatBytes', () => { + test('should return formated memory measurements', () => { + const values = [ + 0, + 534, + 4718592, + 10737418240, + 1649267441664, + 2814749767106560, + undefined, + ].map(val => formatBytes(val, 2, false)); + + expect(values).toEqual([undefined, '534', '4.72M', '10.74G', '1.65T', '2.81P', undefined]); + }); + test('should return formated memory measurements(binaryUnits)', () => { + const values = [ + 0, + 534, + 1000, + 4718592, + 10737418240, + 1649267441664, + 2814749767106560, + undefined, + ].map(val => formatBytes(val)); + + expect(values).toEqual([ + undefined, + '534', + '1000', + '4.5Mi', + '10Gi', + '1.5Ti', + '2.5Pi', + undefined, + ]); + }); + }); + }); }); diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/__tests__/index.spec.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/__tests__/index.spec.tsx index c9242db10a..d4c3229c10 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/__tests__/index.spec.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/__tests__/index.spec.tsx @@ -32,6 +32,9 @@ const defaultEditorId = 'che-incubator/che-code/next'; const editorId = 'che-incubator/che-code/insiders'; const editorImage = 'custom-editor-image'; +// mute the outputs +console.error = jest.fn(); + describe('GitRepoLocationInput', () => { let store: Store; diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/helpers.ts b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/helpers.ts index d2b9bbb59a..eba0b23d1a 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/helpers.ts +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/helpers.ts @@ -10,7 +10,7 @@ * Red Hat, Inc. - initial API and implementation */ -import { api } from '@eclipse-che/common'; +import common, { api } from '@eclipse-che/common'; import { ValidatedOptions } from '@patternfly/react-core'; import { isEqual } from 'lodash'; @@ -44,11 +44,16 @@ export function validateLocation(location: string, hasSshKeys: boolean): Validat return ValidatedOptions.error; } -export const supportedProviders: api.GitOauthProvider[] = ['github', 'gitlab', 'bitbucket']; +export const supportedProviders: api.GitProvider[] = [ + 'github', + 'gitlab', + 'bitbucket-server', + 'azure-devops', +]; -export function getSupportedGitService(location: string): api.GitOauthProvider { +export function getSupportedGitService(location: string): api.GitProvider { const url = new URL(location); - const provider = supportedProviders.find(p => url.host.includes(p)); + const provider = supportedProviders.find(p => url.host.includes(p.split('-')[0])); if (!provider) { throw new Error(`Provider not supported: ${url.host}`); } @@ -80,26 +85,73 @@ export function getBranchFromLocation(location: string): string | undefined { branch = pathname.slice(4).join('/'); } break; - case 'bitbucket': + case 'bitbucket-server': if (pathname[2] === 'src') { branch = pathname.slice(3).join('/'); } break; + case 'azure-devops': + branch = getBranchFromAzureDevOpsLocation(location); + break; } return branch; } +/** + * Returns git branch from the encoded Azure DevOps repo location. + */ +function getBranchFromAzureDevOpsLocation(location: string): string | undefined { + const url = new URL(location); + + const _location = decodeURIComponent(`${url.origin}${url.pathname}`); + const _url = new URL(_location); + const _searchParams = new URLSearchParams(_url.search); + + const version = _searchParams.get('version') || ''; + if (!version || !version.startsWith('GB')) { + return undefined; + } + + return version.replace(/^GB/, ''); +} + +/** + * Returns updated location which includes Azure DevOps repo location with an encoded version as a param. + */ +function setBranchToAzureDevOpsLocation(location: string, branch: string | undefined): string { + const url = new URL(location); + const searchParams = new URLSearchParams(url.search); + const [pathname, search] = url.pathname.split('%3F'); + const _searchParams = new URLSearchParams(decodeURIComponent(search || '')); + if (!branch) { + _searchParams.delete('version'); + } else { + _searchParams.set('version', `GB${branch}`); + } + const encodedParams = + _searchParams.toString().length === 0 ? '' : encodeURIComponent(`?${_searchParams.toString()}`); + url.pathname = _searchParams.toString().length === 0 ? pathname : `${pathname}${encodedParams}`; + + return searchParams.toString().length === 0 + ? `${url.origin}${url.pathname}` + : `${url.origin}${url.pathname}?${searchParams.toString()}`; +} + export function setBranchToLocation(location: string, branch: string | undefined): string { const url = new URL(location); const pathname = url.pathname; const [user, project] = pathname.replace(/^\//, '').replace(/\/$/, '').split('/'); + const service = getSupportedGitService(location); if (!branch) { - url.pathname = `${user}/${project}`; + if (service === 'azure-devops') { + url.href = setBranchToAzureDevOpsLocation(location, branch); + } else { + url.pathname = `${user}/${project}`; + } } else { - const service = getSupportedGitService(location); switch (service) { case 'github': url.pathname = `${user}/${project}/tree/${branch}`; @@ -107,28 +159,62 @@ export function setBranchToLocation(location: string, branch: string | undefined case 'gitlab': url.pathname = `${user}/${project}/-/tree/${branch}`; break; - case 'bitbucket': + case 'bitbucket-server': url.pathname = `${user}/${project}/src/${branch}`; break; + case 'azure-devops': + url.href = setBranchToAzureDevOpsLocation(location, branch); + break; } } - return url.href; + return `${url.origin}${url.pathname}${decodeURIComponent(url.search)}`; } -export function getGitRepoOptionsFromLocation(location: string): { - location: string | undefined; - gitBranch: string | undefined; - remotes: GitRemote[] | undefined; - devfilePath: string | undefined; - hasSupportedGitService: boolean; +function getFactoryParamsFromLocation( + location: string, + ignoreBranch?: boolean, +): { + path: string; + searchParams: URLSearchParams; } { + if ( + !ignoreBranch && + isSupportedGitService(location) && + getSupportedGitService(location) === 'azure-devops' + ) { + const url = new URL(location); + const searchParams = new URLSearchParams(url.search); + const path = searchParams.get('path'); + const version = searchParams.get('version'); + const repoSearchParams = new URLSearchParams(); + if (path) { + searchParams.delete('path'); + if (path !== 'true') { + repoSearchParams.set('path', path); + } + } + if (version) { + searchParams.delete('version'); + if (version !== 'true') { + repoSearchParams.set('version', version); + } + } + if (repoSearchParams.toString().length > 0) { + const encodedParams = encodeURIComponent(`?${repoSearchParams.toString()}`); + location = `${url.origin}${url.pathname}${encodedParams}?${searchParams.toString()}`; + } + } + const factory = new FactoryLocationAdapter(location); + const { path } = factory; const factoryStr = factory.toString(); const factoryLoaderPath = buildFactoryLoaderPath(factoryStr, true); - const params = decodeURIComponent(factoryLoaderPath.split('?')[1] || ''); + const params = factoryLoaderPath.split('?')[1] || ''; const searchParams = new URLSearchParams(params); searchParams.delete('url'); + + // from override.devfileFilename to devfilePath const devfilePath = searchParams.get('override.devfileFilename') || undefined; if (devfilePath !== 'true' && devfilePath) { searchParams.set('devfilePath', devfilePath); @@ -136,46 +222,131 @@ export function getGitRepoOptionsFromLocation(location: string): { if (searchParams.has('override.devfileFilename')) { searchParams.delete('override.devfileFilename'); } + + return { path, searchParams }; +} + +export function getGitRepoOptionsFromLocation(location: string): { + location: string | undefined; + gitBranch: string | undefined; + remotes: GitRemote[] | undefined; + devfilePath: string | undefined; + hasSupportedGitService: boolean; +} { + const { path, searchParams } = getFactoryParamsFromLocation(location); + const devfilePath = searchParams.get('devfilePath') || undefined; let remotes: GitRemote[] | undefined; const _remotes = searchParams.get('remotes') || undefined; if (_remotes === 'true' || _remotes === '{}') { searchParams.delete('remotes'); remotes = undefined; } else { - remotes = getGitRemotes(_remotes); + try { + remotes = getGitRemotes(_remotes); + } catch (e) { + console.log(common.helpers.errors.getMessage(e)); + } } + location = + searchParams.toString().length === 0 ? `${path}` : `${path}?${searchParams.toString()}`; const hasSupportedGitService = isSupportedGitService(location); - const gitBranch = hasSupportedGitService ? getBranchFromLocation(location) : undefined; + let gitBranch = hasSupportedGitService ? getBranchFromLocation(location) : undefined; + if (hasSupportedGitService) { + try { + gitBranch = getBranchFromLocation(location); + } catch (e) { + console.log(`Unable to get branch from '${location}'.${common.helpers.errors.getMessage(e)}`); + } + } + return { location, gitBranch, remotes, devfilePath, hasSupportedGitService }; +} + +export function getAdvancedOptionsFromLocation(location: string): { + location: string | undefined; + containerImage: string | undefined; + temporaryStorage: boolean | undefined; + createNewIfExisting: boolean | undefined; + memoryLimit: number | undefined; + cpuLimit: number | undefined; +} { + const { path, searchParams } = getFactoryParamsFromLocation(location, true); + + let containerImage = searchParams.get('image') || undefined; + if (containerImage === '' || containerImage === 'true') { + searchParams.delete('image'); + containerImage = undefined; + } + + const _storageType = searchParams.get('storageType'); + let temporaryStorage: boolean | undefined = + _storageType !== null ? _storageType === 'ephemeral' : undefined; + if (_storageType === '' || _storageType === 'true') { + searchParams.delete('storageType'); + temporaryStorage = undefined; + } + + const _policies_create = searchParams.get('policies.create'); + let createNewIfExisting: boolean | undefined = + _policies_create !== null ? _policies_create === 'perclick' : undefined; + if (_policies_create === '' || _policies_create === 'true') { + searchParams.delete('policies.create'); + createNewIfExisting = undefined; + } + + let _memoryLimit = searchParams.get('memoryLimit') || undefined; + + if (_memoryLimit === '' || _memoryLimit === 'true') { + searchParams.delete('memoryLimit'); + _memoryLimit = undefined; + } + let memoryLimit = _memoryLimit ? getBytes(_memoryLimit) : undefined; + + if (memoryLimit && isNaN(memoryLimit)) { + searchParams.delete('memoryLimit'); + memoryLimit = undefined; + } + + let _cpuLimit = searchParams.get('cpuLimit') || undefined; + if (_cpuLimit === 'true') { + searchParams.delete('cpuLimit'); + _cpuLimit = undefined; + } + let cpuLimit = _cpuLimit ? parseInt(_cpuLimit) : undefined; + if (cpuLimit && isNaN(cpuLimit)) { + searchParams.delete('cpuLimit'); + cpuLimit = undefined; + } location = - searchParams.toString().length === 0 - ? `${factory.path}` - : `${factory.path}?${searchParams.toString()}`; + searchParams.toString().length === 0 ? `${path}` : `${path}?${searchParams.toString()}`; - return { location, gitBranch, remotes, devfilePath, hasSupportedGitService }; + return { + location, + containerImage, + temporaryStorage, + createNewIfExisting, + memoryLimit, + cpuLimit, + }; } -type GitRepoOptions = { +export interface IGitRepoOptions { location?: string; gitBranch?: string | undefined; remotes?: GitRemote[] | undefined; devfilePath?: string | undefined; -}; +} export function setGitRepoOptionsToLocation( - newOptions: GitRepoOptions, - currentOptions: GitRepoOptions, -): GitRepoOptions { - const state: GitRepoOptions = {}; + newOptions: IGitRepoOptions, + currentOptions: IGitRepoOptions, +): IGitRepoOptions { + const state: IGitRepoOptions = {}; let location = currentOptions.location; if (!location) { return newOptions; } - const factory = new FactoryLocationAdapter(location); - const factoryLoaderPath = buildFactoryLoaderPath(factory.toString(), true); - const params = decodeURIComponent(factoryLoaderPath.split('?')[1]); - const searchParams = new URLSearchParams(params); - searchParams.delete('url'); + const { path, searchParams } = getFactoryParamsFromLocation(location); if (!isEqual(newOptions.remotes, currentOptions.remotes)) { state.remotes = newOptions.remotes; @@ -208,21 +379,144 @@ export function setGitRepoOptionsToLocation( state.gitBranch = newOptions.gitBranch; } // update the location with the new gitBranch value + let searchParamsStr = decodeURIComponent(searchParams.toString()); + const hasSearchParams = searchParamsStr.length > 0; + if (hasSearchParams) { + searchParamsStr = decodeURIComponent(searchParamsStr); + } if (isSupportedGitService(location)) { location = setBranchToLocation( - searchParams.toString().length > 0 - ? `${factory.path}?${searchParams.toString()}` - : `${factory.path}`, + hasSearchParams ? `${path}?${searchParamsStr}` : `${path}`, newOptions.gitBranch, ); } else { - location = - searchParams.toString().length > 0 - ? `${factory.path}?${searchParams.toString()}` - : `${factory.path}`; + location = hasSearchParams ? `${path}?${searchParamsStr}` : `${path}`; + } + // update the location in the state + state.location = location; + + return state; +} + +export interface IAdvancedOptions { + location?: string; + containerImage?: string | undefined; + temporaryStorage?: boolean | undefined; + createNewIfExisting?: boolean | undefined; + memoryLimit?: number | undefined; + cpuLimit?: number | undefined; +} + +export function setAdvancedOptionsToLocation( + newOptions: IAdvancedOptions, + currentOptions: IAdvancedOptions, +): IAdvancedOptions { + const state: IAdvancedOptions = {}; + let location = currentOptions.location; + if (!location) { + return newOptions; + } + const { path, searchParams } = getFactoryParamsFromLocation(location, true); + + if (newOptions.containerImage !== currentOptions.containerImage) { + state.containerImage = newOptions.containerImage; + if (newOptions.containerImage) { + searchParams.set('image', newOptions.containerImage); + } else { + searchParams.delete('image'); + } } + + if (newOptions.temporaryStorage !== currentOptions.temporaryStorage) { + state.temporaryStorage = newOptions.temporaryStorage; + if (newOptions.temporaryStorage) { + searchParams.set('storageType', 'ephemeral'); + } else if (searchParams.get('storageType') === 'ephemeral') { + searchParams.delete('storageType'); + } + } + + if (newOptions.createNewIfExisting !== currentOptions.createNewIfExisting) { + state.createNewIfExisting = newOptions.createNewIfExisting; + if (searchParams.has('new')) { + searchParams.delete('new'); + } + if (newOptions.createNewIfExisting) { + searchParams.set('policies.create', 'perclick'); + } else { + searchParams.delete('policies.create'); + } + } + + if (newOptions.memoryLimit !== currentOptions.memoryLimit) { + state.memoryLimit = newOptions.memoryLimit; + if (newOptions.memoryLimit) { + const formattedMemoryLimit = formatBytes(newOptions.memoryLimit, 3, true); + if (formattedMemoryLimit) { + searchParams.set('memoryLimit', formattedMemoryLimit); + } else { + searchParams.delete('memoryLimit'); + } + } else { + searchParams.delete('memoryLimit'); + } + } + + if (newOptions.cpuLimit !== currentOptions.cpuLimit) { + state.cpuLimit = newOptions.cpuLimit; + if (newOptions.cpuLimit) { + searchParams.set('cpuLimit', newOptions.cpuLimit.toString()); + } else { + searchParams.delete('cpuLimit'); + } + } + + // update the location with the new gitBranch value + location = searchParams.toString().length > 0 ? `${path}?${searchParams.toString()}` : `${path}`; // update the location in the state state.location = location; return state; } + +const UNITS_OF_MEASUREMENT = ['', 'K', 'M', 'G', 'T', 'P']; + +export function formatBytes( + bytes: number | undefined, + decimals = 2, + binaryUnits = true, +): string | undefined { + if (!bytes) { + return undefined; + } + const k = binaryUnits ? 1024 : 1000; + const unitsOfMeasurement = UNITS_OF_MEASUREMENT.map((unit, index) => { + if (index > 0 && binaryUnits) { + unit += 'i'; + } + return unit; + }); + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + unitsOfMeasurement[i]; +} + +export function getBytes(value: string): number | undefined { + value = value.trim(); + if (value === '') { + return undefined; + } + const bytes = parseFloat(value); + if (isNaN(bytes)) { + return undefined; + } + const unitOfMeasurement = value.replace(bytes.toString(), '').trim().toLowerCase(); + if (!unitOfMeasurement) { + return bytes; + } + const k = unitOfMeasurement.match(/ib?$/) !== null ? 1024 : 1000; + const i = UNITS_OF_MEASUREMENT.map(unit => unit.toLowerCase()).indexOf(unitOfMeasurement[0]); + if (i === -1) { + return undefined; + } + return bytes * Math.pow(k, i); +} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/index.tsx b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/index.tsx index 95b9e32f52..54a6ca4ca9 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/index.tsx +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/ImportFromGit/index.tsx @@ -11,10 +11,6 @@ */ import { - Accordion, - AccordionContent, - AccordionItem, - AccordionToggle, Button, ButtonVariant, Flex, @@ -35,13 +31,8 @@ import { History } from 'history'; import React from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { GitRepoOptions } from '@/components/ImportFromGit/GitRepoOptions'; -import { - getGitRepoOptionsFromLocation, - setGitRepoOptionsToLocation, - validateLocation, -} from '@/components/ImportFromGit/helpers'; -import { GitRemote } from '@/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes'; +import { validateLocation } from '@/components/ImportFromGit/helpers'; +import RepoOptionsAccordion from '@/components/ImportFromGit/RepoOptionsAccordion'; import { FactoryLocationAdapter } from '@/services/factory-location-adapter'; import { EDITOR_ATTR, EDITOR_IMAGE_ATTR } from '@/services/helpers/factoryFlow/buildFactoryParams'; import { buildUserPreferencesLocation } from '@/services/helpers/location'; @@ -50,8 +41,6 @@ import { AppState } from '@/store'; import { selectSshKeys } from '@/store/SshKeys/selectors'; import * as WorkspacesStore from '@/store/Workspaces'; -type AccordionId = 'options'; - const FIELD_ID = 'git-repo-url'; export type Props = MappedProps & { @@ -63,13 +52,8 @@ export type State = { hasSshKeys: boolean; location: string; locationValidated: ValidatedOptions; - expandedId: AccordionId | undefined; - gitBranch: string | undefined; - remotes: GitRemote[] | undefined; remotesValidated: ValidatedOptions; - devfilePath: string | undefined; isFocused: boolean; - hasSupportedGitService: boolean; }; class ImportFromGit extends React.PureComponent { @@ -80,13 +64,8 @@ class ImportFromGit extends React.PureComponent { hasSshKeys: this.props.sshKeys.length > 0, locationValidated: ValidatedOptions.default, location: '', - expandedId: undefined, - gitBranch: undefined, - remotes: undefined, remotesValidated: ValidatedOptions.default, - devfilePath: undefined, isFocused: false, - hasSupportedGitService: false, }; } @@ -95,16 +74,14 @@ class ImportFromGit extends React.PureComponent { if (!isFocused && (location !== prevState.location || prevState.isFocused)) { const inputElement = document.getElementById(FIELD_ID) as HTMLInputElement; if (inputElement) { - inputElement.value = decodeURIComponent(location); + inputElement.value = location; } } } private handleCreate(): void { const { editorDefinition, editorImage } = this.props; - const location = decodeURIComponent(this.state.location); - - const factory = new FactoryLocationAdapter(location); + const factory = new FactoryLocationAdapter(this.state.location); // add the editor definition and editor image to the URL // if they are not already there @@ -122,15 +99,12 @@ class ImportFromGit extends React.PureComponent { } private handleChange(location: string): void { - if (this.state.location === location.trim()) { + location = location.trim(); + if (this.state.location === location) { return; } const validated = validateLocation(location, this.state.hasSshKeys); this.setState({ locationValidated: validated, location }); - if (validated !== ValidatedOptions.success) { - return; - } - this.setState(getGitRepoOptionsFromLocation(location) as State); } private getErrorMessage(location: string): string | React.ReactNode { @@ -157,7 +131,7 @@ class ImportFromGit extends React.PureComponent { } public buildForm(): React.JSX.Element { - const location = decodeURIComponent(this.state.location); + const { location } = this.state; const { locationValidated, remotesValidated } = this.state; const buttonDisabled = @@ -216,72 +190,9 @@ class ImportFromGit extends React.PureComponent { ); } - private handleToggle(id: AccordionId): void { - const { expandedId } = this.state; - this.setState({ - expandedId: expandedId === id ? undefined : id, - }); - } - - private handleGitRepoOptionsChange( - gitBranch: string | undefined, - remotes: GitRemote[] | undefined, - devfilePath: string | undefined, - isValid: boolean, - ): void { - const state = setGitRepoOptionsToLocation( - { gitBranch, remotes, devfilePath }, - { - location: this.state.location, - gitBranch: this.state.gitBranch, - remotes: this.state.remotes, - devfilePath: this.state.devfilePath, - }, - ) as State; - state.remotesValidated = isValid ? ValidatedOptions.success : ValidatedOptions.error; - this.setState(state); - } - - public buildGitRepoOptions(): React.JSX.Element { - const { expandedId, remotes, devfilePath, gitBranch, hasSupportedGitService } = this.state; - - return ( - - - { - this.handleToggle('options'); - }} - isExpanded={expandedId === 'options'} - id="accordion-item-options" - > - Git Repo Options - - - - - - - - this.handleGitRepoOptionsChange(gitBranch, remotes, devfilePath, isValid) - } - /> - - - - - - - ); - } - public render() { - const { locationValidated } = this.state; + const { history } = this.props; + const { locationValidated, location } = this.state; return ( @@ -292,7 +203,16 @@ class ImportFromGit extends React.PureComponent { {locationValidated === ValidatedOptions.success && ( - {this.buildGitRepoOptions()} + + { + const locationValidated = validateLocation(location, this.state.hasSshKeys); + this.setState({ location, remotesValidated, locationValidated }); + }} + /> + )} diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes.ts b/devspaces-dashboard/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes.ts index f1afcd47d4..b2b187b279 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes.ts +++ b/devspaces-dashboard/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes.ts @@ -72,7 +72,7 @@ function parseRemotes(remotes: string): string[] { try { return JSON.parse(sanitizeValue(remotes)); } catch (e) { - throw `Unable to parse remotes attribute. ${common.helpers.errors.getMessage(e)}`; + throw `Unable to parse remotes '${remotes}'. ${common.helpers.errors.getMessage(e)}`; } } diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/services/backend-client/__tests__/factoryApi.spec.ts b/devspaces-dashboard/packages/dashboard-frontend/src/services/backend-client/__tests__/factoryApi.spec.ts index 71ddc9ec4c..955289661c 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/services/backend-client/__tests__/factoryApi.spec.ts +++ b/devspaces-dashboard/packages/dashboard-frontend/src/services/backend-client/__tests__/factoryApi.spec.ts @@ -46,10 +46,13 @@ describe('Factory API', () => { mockPost.mockResolvedValueOnce({ data: expect.anything(), }); - await getFactoryResolver(location, {}); + await getFactoryResolver( + 'https://test.azure.com/_git/public-repo?version=GBtest%2Fbranch', + {}, + ); expect(mockPost).toHaveBeenCalledWith('/api/factory/resolver', { - url: 'https://github.com/eclipse-che/che-dashboard.git', + url: 'https://test.azure.com/_git/public-repo?version=GBtest/branch', }); }); diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/services/backend-client/factoryApi.ts b/devspaces-dashboard/packages/dashboard-frontend/src/services/backend-client/factoryApi.ts index 7ca29e35c8..48883a9678 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/services/backend-client/factoryApi.ts +++ b/devspaces-dashboard/packages/dashboard-frontend/src/services/backend-client/factoryApi.ts @@ -22,6 +22,11 @@ export async function getFactoryResolver( if (url.indexOf(' ') !== -1) { url = encodeURI(url); } + // In the case of the Azure repository, the search parameters are encoded twice and need to be decoded. + if (url.indexOf('?') !== -1) { + const [path, search] = url.split('?'); + url = `${path}?${decodeURIComponent(search)}`; + } const response = await axios.post( `${cheServerPrefix}/factory/resolver`, Object.assign({}, overrideParams, { url }), diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/services/helpers/factoryFlow/buildFactoryParams.ts b/devspaces-dashboard/packages/dashboard-frontend/src/services/helpers/factoryFlow/buildFactoryParams.ts index 365a584bbd..32a0cf063b 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/services/helpers/factoryFlow/buildFactoryParams.ts +++ b/devspaces-dashboard/packages/dashboard-frontend/src/services/helpers/factoryFlow/buildFactoryParams.ts @@ -20,6 +20,8 @@ export const POLICIES_CREATE_ATTR = 'policies.create'; export const STORAGE_TYPE_ATTR = 'storageType'; export const REMOTES_ATTR = 'remotes'; export const IMAGE_ATTR = 'image'; +export const CPU_LIMIT_ATTR = 'cpuLimit'; +export const MEMORY_LIMIT_ATTR = 'memoryLimit'; export const EDITOR_IMAGE_ATTR = 'editor-image'; export const USE_DEFAULT_DEVFILE = 'useDefaultDevfile'; export const DEBUG_WORKSPACE_START = 'debugWorkspaceStart'; @@ -33,6 +35,8 @@ export const PROPAGATE_FACTORY_ATTRS = [ STORAGE_TYPE_ATTR, REMOTES_ATTR, IMAGE_ATTR, + CPU_LIMIT_ATTR, + MEMORY_LIMIT_ATTR, EDITOR_IMAGE_ATTR, ]; export const OVERRIDE_ATTR_PREFIX = 'override.'; @@ -51,6 +55,8 @@ export type FactoryParams = { editorImage: string | undefined; remotes: string | undefined; image: string | undefined; + cpuLimit: string | undefined; + memoryLimit: string | undefined; useDefaultDevfile: boolean; debugWorkspaceStart: boolean; }; @@ -73,6 +79,8 @@ export function buildFactoryParams(searchParams: URLSearchParams): FactoryParams remotes: getRemotes(searchParams), useDevWorkspaceResources: getDevworkspaceResourcesUrl(searchParams) !== undefined, image: getImage(searchParams), + cpuLimit: getCpuLimit(searchParams), + memoryLimit: getMemoryLimit(searchParams), useDefaultDevfile: isSafeWorkspaceStart(searchParams) !== undefined, debugWorkspaceStart: isDebugWorkspaceStart(searchParams) !== undefined, }; @@ -157,6 +165,14 @@ function getImage(searchParams: URLSearchParams): string | undefined { return searchParams.get(IMAGE_ATTR) || undefined; } +function getMemoryLimit(searchParams: URLSearchParams): string | undefined { + return searchParams.get(MEMORY_LIMIT_ATTR) || undefined; +} + +function getCpuLimit(searchParams: URLSearchParams): string | undefined { + return searchParams.get(CPU_LIMIT_ATTR) || undefined; +} + function isSafeWorkspaceStart(searchParams: URLSearchParams): string | undefined { return searchParams.get(USE_DEFAULT_DEVFILE) === null ? undefined diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/helpers.normalizeDevfileV2.spec.ts b/devspaces-dashboard/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/helpers.normalizeDevfileV2.spec.ts index 8cce9282c2..67ac77bc09 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/helpers.normalizeDevfileV2.spec.ts +++ b/devspaces-dashboard/packages/dashboard-frontend/src/store/FactoryResolver/__tests__/helpers.normalizeDevfileV2.spec.ts @@ -204,6 +204,94 @@ describe('Normalize Devfile V2', () => { ); }); + it('should apply the custom memoryLimit from factory params', () => { + const devfile = { + schemaVersion: '2.2.2', + metadata: { + generateName: 'empty', + }, + components: [ + { + container: { + image: 'quay.io/devfile/custom-developer-image:custom', + }, + name: 'developer-image', + }, + ], + } as V230Devfile; + const factoryParams = { + memoryLimit: '4Gi', + }; + + const targetDevfile = normalizeDevfile( + { + devfile, + } as FactoryResolver, + 'http://dummy-registry/devfiles/empty.yaml', + defaultComponents, + 'che', + factoryParams, + ); + + expect(targetDevfile).toEqual( + expect.objectContaining({ + components: [ + { + container: { + image: 'quay.io/devfile/custom-developer-image:custom', + memoryLimit: '4Gi', + }, + name: 'developer-image', + }, + ], + }), + ); + }); + + it('should apply the custom cpuLimit from factory params', () => { + const devfile = { + schemaVersion: '2.2.2', + metadata: { + generateName: 'empty', + }, + components: [ + { + container: { + image: 'quay.io/devfile/custom-developer-image:custom', + }, + name: 'developer-image', + }, + ], + } as V230Devfile; + const factoryParams = { + cpuLimit: '2', + }; + + const targetDevfile = normalizeDevfile( + { + devfile, + } as FactoryResolver, + 'http://dummy-registry/devfiles/empty.yaml', + defaultComponents, + 'che', + factoryParams, + ); + + expect(targetDevfile).toEqual( + expect.objectContaining({ + components: [ + { + container: { + image: 'quay.io/devfile/custom-developer-image:custom', + cpuLimit: '2', + }, + name: 'developer-image', + }, + ], + }), + ); + }); + it('should apply the custom image from factory params', () => { const devfile = { schemaVersion: '2.2.2', diff --git a/devspaces-dashboard/packages/dashboard-frontend/src/store/FactoryResolver/helpers.ts b/devspaces-dashboard/packages/dashboard-frontend/src/store/FactoryResolver/helpers.ts index 7913ebae20..03f228f8df 100644 --- a/devspaces-dashboard/packages/dashboard-frontend/src/store/FactoryResolver/helpers.ts +++ b/devspaces-dashboard/packages/dashboard-frontend/src/store/FactoryResolver/helpers.ts @@ -203,11 +203,20 @@ export function normalizeDevfile( } if (devfile.components && devfile.components.length > 0) { - // apply the custom image from factory params - if (factoryParams.image && devfile.components[0].container?.image) { - devfile.components[0].container.image = factoryParams.image; + if (devfile.components[0].container) { + // apply the custom image from factory params + if (factoryParams.image && devfile.components[0].container.image) { + devfile.components[0].container.image = factoryParams.image; + } + // apply the custom memoryLimit from factory params + if (factoryParams.memoryLimit) { + devfile.components[0].container.memoryLimit = factoryParams.memoryLimit; + } + // apply the custom cpuLimit from factory params + if (factoryParams.cpuLimit) { + devfile.components[0].container.cpuLimit = factoryParams.cpuLimit; + } } - // temporary solution for fix che-server serialization bug with empty volume devfile.components.forEach(component => { if (Object.keys(component).length === 1 && component.name) {