Since we're moving away from IQE the `ouia-id` atributes should be no longer needed. This removes them from the entire code base.
315 lines
10 KiB
TypeScript
315 lines
10 KiB
TypeScript
import React from 'react';
|
|
|
|
import {
|
|
ActionGroup,
|
|
Button,
|
|
Checkbox,
|
|
FileUpload,
|
|
Form,
|
|
FormGroup,
|
|
FormHelperText,
|
|
HelperText,
|
|
HelperTextItem,
|
|
Modal,
|
|
ModalVariant,
|
|
Popover,
|
|
} from '@patternfly/react-core';
|
|
import { DropEvent } from '@patternfly/react-core/dist/esm/helpers';
|
|
import { HelpIcon } from '@patternfly/react-icons';
|
|
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { parse } from 'toml';
|
|
|
|
import { mapOnPremToHosted } from './helpers/onPremToHostedBlueprintMapper';
|
|
|
|
import {
|
|
ApiRepositoryRequest,
|
|
useBulkImportRepositoriesMutation,
|
|
} from '../../store/contentSourcesApi';
|
|
import { useAppDispatch } from '../../store/hooks';
|
|
import { BlueprintExportResponse } from '../../store/imageBuilderApi';
|
|
import { importCustomRepositories, wizardState } from '../../store/wizardSlice';
|
|
import { resolveRelPath } from '../../Utilities/path';
|
|
import { mapExportRequestToState } from '../CreateImageWizard/utilities/requestMapper';
|
|
|
|
interface ImportBlueprintModalProps {
|
|
setShowImportModal: React.Dispatch<React.SetStateAction<boolean>>;
|
|
isOpen: boolean;
|
|
}
|
|
|
|
export const ImportBlueprintModal: React.FunctionComponent<
|
|
ImportBlueprintModalProps
|
|
> = ({ setShowImportModal, isOpen }: ImportBlueprintModalProps) => {
|
|
const onImportClose = () => {
|
|
setShowImportModal(false);
|
|
setFilename('');
|
|
setFileContent('');
|
|
setIsOnPrem(false);
|
|
setIsRejected(false);
|
|
setIsInvalidFormat(false);
|
|
};
|
|
const [fileContent, setFileContent] = React.useState('');
|
|
const [importedBlueprint, setImportedBlueprint] =
|
|
React.useState<wizardState>();
|
|
const [isInvalidFormat, setIsInvalidFormat] = React.useState(false);
|
|
const [filename, setFilename] = React.useState('');
|
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
const [isRejected, setIsRejected] = React.useState(false);
|
|
const [isOnPrem, setIsOnPrem] = React.useState(false);
|
|
const [isCheckedImportRepos, setIsCheckedImportRepos] = React.useState(true);
|
|
const dispatch = useAppDispatch();
|
|
const [importRepositories] = useBulkImportRepositoriesMutation();
|
|
|
|
const handleFileInputChange = (
|
|
_event: React.ChangeEvent<HTMLInputElement> | React.DragEvent<HTMLElement>,
|
|
file: File
|
|
) => {
|
|
setFileContent('');
|
|
setFilename(file.name);
|
|
};
|
|
|
|
async function handleRepositoryImport(
|
|
blueprintExportedResponse: BlueprintExportResponse
|
|
) {
|
|
if (isCheckedImportRepos && blueprintExportedResponse.content_sources) {
|
|
const customRepositories: ApiRepositoryRequest[] =
|
|
blueprintExportedResponse.content_sources.map(
|
|
(item) => item as ApiRepositoryRequest
|
|
);
|
|
|
|
try {
|
|
const result = await importRepositories({
|
|
body: customRepositories,
|
|
}).unwrap();
|
|
dispatch(
|
|
importCustomRepositories(
|
|
blueprintExportedResponse.customizations.custom_repositories || []
|
|
)
|
|
);
|
|
|
|
if (Array.isArray(result)) {
|
|
const importedRepositoryNames: string[] = [];
|
|
result.forEach((repository) => {
|
|
if (repository.warnings?.length === 0 && repository.url) {
|
|
importedRepositoryNames.push(repository.url);
|
|
return;
|
|
}
|
|
dispatch(
|
|
addNotification({
|
|
variant: 'warning',
|
|
title: 'Failed to import custom repositories',
|
|
description: JSON.stringify(repository.warnings),
|
|
})
|
|
);
|
|
});
|
|
if (importedRepositoryNames.length !== 0) {
|
|
dispatch(
|
|
addNotification({
|
|
variant: 'info',
|
|
title: 'Successfully imported custom repositories',
|
|
description: importedRepositoryNames.join(', '),
|
|
})
|
|
);
|
|
}
|
|
}
|
|
} catch {
|
|
dispatch(
|
|
addNotification({
|
|
variant: 'danger',
|
|
title: 'Custom repositories import failed',
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
if (filename && fileContent) {
|
|
try {
|
|
const isToml = filename.endsWith('.toml');
|
|
const isJson = filename.endsWith('.json');
|
|
if (isToml) {
|
|
const tomlBlueprint = parse(fileContent);
|
|
const blueprintFromFile = mapOnPremToHosted(tomlBlueprint);
|
|
const importBlueprintState = mapExportRequestToState(
|
|
blueprintFromFile,
|
|
[]
|
|
);
|
|
setIsOnPrem(true);
|
|
setImportedBlueprint(importBlueprintState);
|
|
} else if (isJson) {
|
|
const blueprintFromFile = JSON.parse(fileContent);
|
|
try {
|
|
const blueprintExportedResponse: BlueprintExportResponse = {
|
|
name: blueprintFromFile.name,
|
|
description: blueprintFromFile.description,
|
|
distribution: blueprintFromFile.distribution,
|
|
customizations: blueprintFromFile.customizations,
|
|
metadata: blueprintFromFile.metadata,
|
|
content_sources: blueprintFromFile.content_sources,
|
|
};
|
|
const importBlueprintState = mapExportRequestToState(
|
|
blueprintExportedResponse,
|
|
blueprintFromFile.image_requests || []
|
|
);
|
|
|
|
if (blueprintExportedResponse.content_sources) {
|
|
handleRepositoryImport(blueprintExportedResponse);
|
|
}
|
|
setIsOnPrem(false);
|
|
setImportedBlueprint(importBlueprintState);
|
|
} catch {
|
|
const blueprintFromFileMapped =
|
|
mapOnPremToHosted(blueprintFromFile);
|
|
const importBlueprintState = mapExportRequestToState(
|
|
blueprintFromFileMapped,
|
|
[]
|
|
);
|
|
setIsOnPrem(true);
|
|
setImportedBlueprint(importBlueprintState);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
setIsInvalidFormat(true);
|
|
dispatch(
|
|
addNotification({
|
|
variant: 'warning',
|
|
title: 'File is not a valid blueprint',
|
|
description: error?.data?.error?.message,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
}, [filename, fileContent]);
|
|
|
|
const handleClear = () => {
|
|
setFilename('');
|
|
setFileContent('');
|
|
setIsOnPrem(false);
|
|
setIsRejected(false);
|
|
setIsInvalidFormat(false);
|
|
};
|
|
const handleDataChange = (_: DropEvent, value: string) => {
|
|
setFileContent(value);
|
|
};
|
|
const handleFileRejected = () => {
|
|
setIsRejected(true);
|
|
setIsOnPrem(false);
|
|
setFileContent('');
|
|
setFilename('');
|
|
};
|
|
const handleFileReadStarted = () => {
|
|
setIsLoading(true);
|
|
};
|
|
const handleFileReadFinished = () => {
|
|
setIsLoading(false);
|
|
};
|
|
const navigate = useNavigate();
|
|
|
|
const variantSwitch = () => {
|
|
switch (true) {
|
|
case isRejected || isInvalidFormat:
|
|
return 'error';
|
|
case isOnPrem:
|
|
return 'warning';
|
|
default:
|
|
return 'default';
|
|
}
|
|
};
|
|
return (
|
|
<Modal
|
|
variant={ModalVariant.medium}
|
|
isOpen={isOpen}
|
|
title={
|
|
<>
|
|
Import pipeline
|
|
<Popover
|
|
bodyContent={
|
|
<div>
|
|
You can import the blueprints you created by using the Red Hat
|
|
image builder into Insights images to create customized images.
|
|
</div>
|
|
}
|
|
>
|
|
<Button
|
|
variant="plain"
|
|
aria-label="About import"
|
|
className="pf-v5-u-pl-sm"
|
|
isInline
|
|
>
|
|
<HelpIcon />
|
|
</Button>
|
|
</Popover>
|
|
</>
|
|
}
|
|
onClose={onImportClose}
|
|
>
|
|
<Form>
|
|
<FormGroup fieldId="checkbox-import-custom-repositories">
|
|
<Checkbox
|
|
label="Import missing custom repositories after file upload."
|
|
isChecked={isCheckedImportRepos}
|
|
onChange={() => setIsCheckedImportRepos((prev) => !prev)}
|
|
aria-label="Import Custom Repositories checkbox"
|
|
id="checkbox-import-custom-repositories"
|
|
name="Import Repositories"
|
|
data-testid="checkbox-import-custom-repositories"
|
|
/>
|
|
</FormGroup>
|
|
<FormGroup fieldId="import-blueprint-file-upload">
|
|
<FileUpload
|
|
id="import-blueprint-file-upload"
|
|
type="text"
|
|
value={fileContent}
|
|
filename={filename}
|
|
filenamePlaceholder="Drag and drop a file or upload one"
|
|
onFileInputChange={handleFileInputChange}
|
|
onDataChange={handleDataChange}
|
|
onReadStarted={handleFileReadStarted}
|
|
onReadFinished={handleFileReadFinished}
|
|
onClearClick={handleClear}
|
|
isLoading={isLoading}
|
|
isReadOnly={true}
|
|
browseButtonText="Upload"
|
|
dropzoneProps={{
|
|
accept: { 'text/json': ['.json'], 'text/plain': ['.toml'] },
|
|
maxSize: 512000,
|
|
onDropRejected: handleFileRejected,
|
|
}}
|
|
validated={isRejected || isInvalidFormat ? 'error' : 'default'}
|
|
/>
|
|
<FormHelperText>
|
|
<HelperText>
|
|
<HelperTextItem variant={variantSwitch()}>
|
|
{isRejected
|
|
? 'Must be a valid Blueprint JSON/TOML file no larger than 512 KB'
|
|
: isInvalidFormat
|
|
? 'Not compatible with the blueprints format.'
|
|
: isOnPrem
|
|
? 'Importing on-premises blueprints is currently in beta. Results may vary.'
|
|
: 'Upload your blueprint file. Supported formats: JSON, TOML.'}
|
|
</HelperTextItem>
|
|
</HelperText>
|
|
</FormHelperText>
|
|
</FormGroup>
|
|
<ActionGroup>
|
|
<Button
|
|
type="button"
|
|
isDisabled={isRejected || isInvalidFormat || !fileContent}
|
|
onClick={() =>
|
|
navigate(resolveRelPath(`imagewizard/import`), {
|
|
state: { blueprint: importedBlueprint },
|
|
})
|
|
}
|
|
data-testid="import-blueprint-finish"
|
|
>
|
|
Review and finish
|
|
</Button>
|
|
<Button variant="link" type="button" onClick={onImportClose}>
|
|
Cancel
|
|
</Button>
|
|
</ActionGroup>
|
|
</Form>
|
|
</Modal>
|
|
);
|
|
};
|