Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
"**/node_modules": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"js/ts.tsdk.path": "node_modules/typescript/lib",
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import Fallback from '@/components/Fallback';
import WorkspacesList from '@/pages/WorkspacesList';
import { RootState } from '@/store';
import { fetchBackupConfig, fetchWorkspaceBackupStatus } from '@/store/Backups/actions';
import { selectAllBackupsByWorkspace, selectBackupConfig } from '@/store/Backups/selectors';
import {
selectAllBackupsByWorkspace,
selectBackupConfig,
selectBackupSchedule,
} from '@/store/Backups/selectors';
import { selectBranding } from '@/store/Branding/selectors';
import { selectCmEditors } from '@/store/Plugins/devWorkspacePlugins/selectors';
import { workspacesActionCreators } from '@/store/Workspaces';
Expand Down Expand Up @@ -56,7 +60,7 @@ export class WorkspacesListContainer extends React.PureComponent<Props> {
}

private fetchBackupStatuses() {
const { allWorkspaces, backupConfig, backupsByWorkspace } = this.props;
const { allWorkspaces, backupConfig, backupsByWorkspace: backupsByWorkspace } = this.props;
if (!backupConfig?.registry) {
return;
}
Expand All @@ -73,10 +77,11 @@ export class WorkspacesListContainer extends React.PureComponent<Props> {

render() {
const {
backupConfig,
branding,
allWorkspaces,
backupConfig,
backupsByWorkspace,
backupSchedule,
branding,
editors,
isLoading,
location,
Expand All @@ -90,12 +95,13 @@ export class WorkspacesListContainer extends React.PureComponent<Props> {
return (
<WorkspacesList
backupConfig={backupConfig}
backupsByWorkspace={backupsByWorkspace}
backupSchedule={backupSchedule}
branding={branding}
editors={editors}
location={location}
navigate={navigate}
workspaces={allWorkspaces}
backupsByWorkspace={backupsByWorkspace}
/>
);
}
Expand All @@ -110,10 +116,11 @@ function ContainerWrapper(props: MappedProps) {

const mapStateToProps = (state: RootState) => {
return {
backupConfig: selectBackupConfig(state),
branding: selectBranding(state),
allWorkspaces: selectAllWorkspaces(state),
backupConfig: selectBackupConfig(state),
backupsByWorkspace: selectAllBackupsByWorkspace(state),
backupSchedule: selectBackupSchedule(state),
branding: selectBranding(state),
editors: selectCmEditors(state),
isLoading: selectIsLoading(state),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@
* Red Hat, Inc. - initial API and implementation
*/

// Generated by Claude Opus 4.6
// Generated by Claude Sonnet 4.6

import { BackupItem } from '@eclipse-che/common';
import { Form, ValidatedOptions } from '@patternfly/react-core';
import React, { useEffect, useState } from 'react';
import { Form } from '@patternfly/react-core';
import React, { useCallback, useEffect, useState } from 'react';

import { BackupSelectorField } from '@/pages/RestoreFromBackup/DefaultRegistryForm/BackupSelectorField';
import { ImagePreviewField } from '@/pages/RestoreFromBackup/DefaultRegistryForm/ImagePreviewField';
import { useRestoreFormValidation } from '@/pages/RestoreFromBackup/useRestoreFormValidation';
import { WorkspaceNameField } from '@/pages/RestoreFromBackup/WorkspaceNameField';

export type DefaultRegistryRestoreData = {
Expand All @@ -45,57 +44,41 @@ export const DefaultRegistryRestoreForm: React.FC<Props> = ({
}) => {
const [selectedBackup, setSelectedBackup] = useState<BackupItem | undefined>(undefined);

const {
workspaceName,
workspaceNameValidated,
workspaceNameError,
workspaceNameWarning,
setWorkspaceName,
handleWorkspaceNameChange,
} = useRestoreFormValidation(existingWorkspaceNames, existingBackupNames);
const [workspaceName, setWorkspaceName] = useState('');
const [isWorkspaceNameValid, setIsWorkspaceNameValid] = useState(false);

// Determine validity and notify parent
useEffect(() => {
const isValid =
workspaceNameValidated === ValidatedOptions.success ||
workspaceNameValidated === ValidatedOptions.warning;
const imageUrl = selectedBackup ? selectedBackup.imageUrl : '';

if (isValid) {
onValidationChange(true, { workspaceName, imageUrl });
} else {
onValidationChange(false, null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedBackup, workspaceName, workspaceNameValidated]);

// Pre-select backup and populate workspace name from initialImageUrl
// Pre-select backup from initialImageUrl
useEffect(() => {
if (!initialImageUrl || selectedBackup) {
return;
}
const match = backups.find(b => b.imageUrl === initialImageUrl);
if (match) {
setSelectedBackup(match);
setWorkspaceName(match.workspaceName);
}
}, [backups, initialImageUrl, selectedBackup, setWorkspaceName]);
}, [backups, initialImageUrl, selectedBackup]);

// Notify parent whenever form validity changes
useEffect(() => {
const imageUrl = selectedBackup ? selectedBackup.imageUrl : '';
if (isWorkspaceNameValid) {
onValidationChange(true, { workspaceName, imageUrl });
} else {
onValidationChange(false, null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedBackup, workspaceName, isWorkspaceNameValid]);

const handleBackupChange = (backup: BackupItem | undefined) => {
setSelectedBackup(backup);
if (backup) {
setWorkspaceName(backup.workspaceName);
}
};

// Re-validate when existingWorkspaceNames changes
// (e.g. a workspace is created or deleted while the form is open)
useEffect(() => {
if (workspaceName) {
setWorkspaceName(workspaceName);
}
}, [existingWorkspaceNames, workspaceName, setWorkspaceName]);
const handleWorkspaceNameChange = useCallback((name: string, isValid: boolean) => {
setWorkspaceName(name);
setIsWorkspaceNameValid(isValid);
}, []);

const suggestedWorkspaceName = selectedBackup?.workspaceName ?? '';
const imageUrl = selectedBackup ? selectedBackup.imageUrl : '';

return (
Expand All @@ -109,11 +92,10 @@ export const DefaultRegistryRestoreForm: React.FC<Props> = ({
<WorkspaceNameField
fieldId="restore-workspace-name"
helperText="Enter the name of the workspace to restore."
value={workspaceName}
validated={workspaceNameValidated}
error={workspaceNameError}
warning={workspaceNameWarning}
onChange={value => handleWorkspaceNameChange(value)}
value={suggestedWorkspaceName}
existingWorkspaceNames={existingWorkspaceNames}
existingBackupNames={existingBackupNames}
onChange={handleWorkspaceNameChange}
actionButton={actionButton}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ describe('ExternalRegistryRestoreForm', () => {

expect(mockOnValidationChange).toHaveBeenCalledWith(false, null);
});

test('should NOT reset workspace name when backup image URL changes after manual edit', async () => {
renderComponent();

const nameInput = screen.getByLabelText('Workspace name');
await userEvent.type(nameInput, 'my-custom-name');

const urlInput = screen.getByLabelText('Backup image URL');
await userEvent.type(urlInput, 'registry.example.com/ns/workspace:latest');

expect(nameInput).toHaveValue('my-custom-name');
});
});

type ComponentOptions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,10 @@

import { BACKUP_IMAGE_URL_PATTERN } from '@eclipse-che/common';
import { Form, ValidatedOptions } from '@patternfly/react-core';
import React, { useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';

import { ImageUrlField } from '@/pages/RestoreFromBackup/ExternalRegistryForm/ImageUrlField';
import {
sanitizeImageUrl,
sanitizeWorkspaceName,
validateWorkspaceName,
ValidationState,
} from '@/pages/RestoreFromBackup/helpers';
import { useRestoreFormValidation } from '@/pages/RestoreFromBackup/useRestoreFormValidation';
import { sanitizeImageUrl, ValidationState } from '@/pages/RestoreFromBackup/helpers';
import { WorkspaceNameField } from '@/pages/RestoreFromBackup/WorkspaceNameField';

export type ExternalRegistryRestoreData = {
Expand Down Expand Up @@ -82,58 +76,29 @@ export const ExternalRegistryRestoreForm: React.FC<Props> = ({
const initialUrl = initialImageUrl || '';
const [imageUrl, setImageUrl] = useState(initialUrl);

const initialWorkspaceName = detectWorkspaceName(initialUrl);
const {
workspaceName,
workspaceNameValidated,
workspaceNameError,
workspaceNameWarning,
setWorkspaceName,
handleWorkspaceNameChange,
} = useRestoreFormValidation(existingWorkspaceNames, existingBackupNames);

// Initialize workspace name from initialImageUrl on first render
React.useEffect(() => {
if (initialWorkspaceName) {
setWorkspaceName(initialWorkspaceName);
}
const { validationState } = computeImageUrlValidation(initialUrl);
const wsIsValid = initialWorkspaceName.length > 0;
const isValid = validationState === 'valid' && wsIsValid;
onValidationChange(
isValid,
isValid ? { workspaceName: initialWorkspaceName, imageUrl: initialUrl } : null,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [workspaceName, setWorkspaceName] = useState('');
const [isWorkspaceNameValid, setIsWorkspaceNameValid] = useState(false);

// Compute URL validation state during render (derived from imageUrl)
const { validationState, imageUrlValidated, imageUrlError } = computeImageUrlValidation(imageUrl);

const detectedWorkspaceName = validationState === 'valid' ? detectWorkspaceName(imageUrl) : '';

// Notify parent whenever form validity changes
useEffect(() => {
const isValid = validationState === 'valid' && isWorkspaceNameValid;
onValidationChange(isValid, isValid ? { workspaceName, imageUrl } : null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [imageUrl, workspaceName, isWorkspaceNameValid, validationState]);

const handleImageUrlChange = (value: string) => {
const sanitized = sanitizeImageUrl(value);
setImageUrl(sanitized);

const { validationState: newValidationState } = computeImageUrlValidation(sanitized);
const newWorkspaceName = newValidationState === 'valid' ? detectWorkspaceName(sanitized) : '';
setWorkspaceName(newWorkspaceName);

const isValid = newValidationState === 'valid' && newWorkspaceName.length > 0;
onValidationChange(
isValid,
isValid ? { workspaceName: newWorkspaceName, imageUrl: sanitized } : null,
);
};

const wrappedHandleWorkspaceNameChange = (value: string) => {
handleWorkspaceNameChange(value);
const sanitized = sanitizeWorkspaceName(value);
const { validated } = validateWorkspaceName(sanitized);
const wsIsValid =
validated === ValidatedOptions.success || validated === ValidatedOptions.warning;
const isValid = validationState === 'valid' && wsIsValid;
onValidationChange(isValid, isValid ? { workspaceName: sanitized, imageUrl } : null);
};
const handleWorkspaceNameChange = useCallback((name: string, isValid: boolean) => {
setWorkspaceName(name);
setIsWorkspaceNameValid(isValid);
}, []);

return (
<Form isHorizontal={true} onSubmit={e => e.preventDefault()}>
Expand All @@ -147,11 +112,10 @@ export const ExternalRegistryRestoreForm: React.FC<Props> = ({
<WorkspaceNameField
fieldId="restore-workspace-name-cross"
helperText="Name for the restored workspace. Auto-detected from the backup image."
value={workspaceName}
validated={workspaceNameValidated}
error={workspaceNameError}
warning={workspaceNameWarning}
onChange={wrappedHandleWorkspaceNameChange}
value={detectedWorkspaceName}
existingWorkspaceNames={existingWorkspaceNames}
existingBackupNames={existingBackupNames}
onChange={handleWorkspaceNameChange}
actionButton={actionButton}
/>
</Form>
Expand Down

This file was deleted.

Loading
Loading