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`] = `
-
-`;
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}
+
onChange('new-container-image')}>Container Image Change
+
+ );
+ }
+}
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`] = `
+
+
+
+
+ Container Image
+
+
+
+
+
+
+
+
+
+`;
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()}
+
onChange(1)}>Cpu Limit Change
+
+ );
+ }
+}
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`] = `
+
+
+
+
+ CPU Limit (2 cores)
+
+
+
+
+
+
+
+
+
+`;
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'}
+
+
onChange(!createNewIfExisting)}>
+ Create New If Existing Change
+
+
+ );
+ }
+}
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`] = `
+
+
+
+
+ Temporary Storage
+
+
+
+
+
+
+`;
+
+exports[`TemporaryStorageField switched on snapshot 1`] = `
+
+
+
+
+ Temporary Storage
+
+
+
+
+
+
+`;
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()}
+
onChange(1073741824)}>Memory Limit Change
+
+ );
+ }
+}
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`] = `
+
+
+
+
+ Memory Limit
+
+
+
+
+
+
+
+
+
+`;
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'}
+
+
onChange(!isTemporary)}>Temporary Storage Change
+
+ );
+ }
+}
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`] = `
+
+
+
+
+ Temporary Storage
+
+
+
+
+
+
+`;
+
+exports[`TemporaryStorageField switched on snapshot 1`] = `
+
+
+
+
+ Temporary Storage
+
+
+
+
+
+
+`;
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}`}
+
+ onChange('newContainerImage', !temporaryStorage, !createNewIfExisting, 1073741824, 1)
+ }
+ >
+ Advanced Options Change
+
+
+ );
+ }
+}
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`] = `
+
+`;
+
+exports[`AdvancedOptions snapshot with default values 1`] = `
+
+`;
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 (
+
+ );
+ }
+}
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'}
+
onChange([{ name: 'test-updated', url: 'http://test' }], true)}>
+ Git Remotes Change
+
+
+ );
+ }
+}
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
+
+
+
+
+
+
+
+
+
+ Remote Name
+
+
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Remote URL
+
+
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Remove Remote
+
+
+
+
+
+
+
+
+
+ Remote Name
+
+
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Remote URL
+
+
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Remove Remote
+
+
+
+
+
+
+
+
+
+ Remote Name
+
+
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Remote URL
+
+
+
+ *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Remove Remote
+
+
+
+
+
+
+
+
+
+
+
+
+ Add 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.handleAdd()}
+ variant={ButtonVariant.link}
+ style={{ textDecoration: 'none' }}
+ isInline
+ icon={ }
+ >
+ Add Remote
+
+
+
+
);
}
}
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}
+
onChange('new-branch')}>Git Branch Change
+
+ );
+ }
+}
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}
+
onChange('new-devfile-path')}>Devfile Path Change
+
+ );
+ }
+}
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}`}
+
+ onChange(
+ 'newBranch',
+ [{ name: 'test-updated', url: 'http://test' }],
+ 'newDevfilePath',
+ true,
+ )
+ }
+ >
+ Git Repo Options Change
+
+
+ );
+ }
+}
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`] = `
+
+`;
+
+exports[`GitRepoOptions snapshot with default values 1`] = `
+
+`;
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`] = `
+
+
+
+
+ Git Repo Options
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Git Repo Options
+
+
+ undefined, undefined, undefined, false
+
+
+ Git Repo Options Change
+
+
+
+
+
+
+
+
+
+
+ Advanced Options
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Advanced Options
+
+
+ undefined, undefined, undefined, undefined, undefined
+
+
+ Advanced Options Change
+
+
+
+
+
+
+
+
+`;
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) {