V2Wizard: Add <TargetEnvironment> to Image Output step
DDF has been dropped from `<TargetEnvironment>`, state is managed with RTK.
This commit is contained in:
parent
937219ba51
commit
a3ef0cc2f7
3 changed files with 189 additions and 236 deletions
|
|
@ -1,149 +1,64 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import {
|
||||
Alert,
|
||||
Bullseye,
|
||||
Checkbox,
|
||||
FormGroup,
|
||||
Popover,
|
||||
Radio,
|
||||
Spinner,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
Tile,
|
||||
} from '@patternfly/react-core';
|
||||
import { HelpIcon } from '@patternfly/react-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useField } from 'react-final-form';
|
||||
|
||||
import { useGetArchitecturesQuery } from '../../../store/imageBuilderApi';
|
||||
import { provisioningApi } from '../../../store/provisioningApi';
|
||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
||||
import { useAppSelector, useAppDispatch } from '../../../../store/hooks';
|
||||
import {
|
||||
ImageTypes,
|
||||
useGetArchitecturesQuery,
|
||||
} from '../../../../store/imageBuilderApi';
|
||||
import { provisioningApi } from '../../../../store/provisioningApi';
|
||||
import {
|
||||
addImageType,
|
||||
removeImageType,
|
||||
selectArchitecture,
|
||||
selectDistribution,
|
||||
selectImageTypes,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
||||
|
||||
const useGetAllowedTargets = ({ architecture, release }) => {
|
||||
const { data, isFetching, isSuccess, isError } = useGetArchitecturesQuery({
|
||||
distribution: release,
|
||||
const TargetEnvironment = () => {
|
||||
const arch = useAppSelector((state) => selectArchitecture(state));
|
||||
const environments = useAppSelector((state) => selectImageTypes(state));
|
||||
const distribution = useAppSelector((state) => selectDistribution(state));
|
||||
const { data } = useGetArchitecturesQuery({
|
||||
distribution: distribution,
|
||||
});
|
||||
// TODO: Handle isFetching state (add skeletons)
|
||||
// TODO: Handle isError state (very unlikely...)
|
||||
|
||||
let image_types = [];
|
||||
if (isSuccess && data) {
|
||||
data.forEach((elem) => {
|
||||
if (elem.arch === architecture) {
|
||||
image_types = elem.image_types;
|
||||
}
|
||||
});
|
||||
}
|
||||
const [hasVSphere, setHasVSphere] = useState(false);
|
||||
|
||||
return {
|
||||
data: image_types,
|
||||
isFetching: isFetching,
|
||||
isSuccess: isSuccess,
|
||||
isError: isError,
|
||||
};
|
||||
};
|
||||
|
||||
const TargetEnvironment = ({ label, isRequired, ...props }) => {
|
||||
const { getState, change } = useFormApi();
|
||||
const { input } = useFieldApi({ label, isRequired, ...props });
|
||||
const [environment, setEnvironment] = useState({
|
||||
aws: false,
|
||||
azure: false,
|
||||
gcp: false,
|
||||
oci: false,
|
||||
'vsphere-ova': false,
|
||||
vsphere: false,
|
||||
'guest-image': false,
|
||||
'image-installer': false,
|
||||
wsl: false,
|
||||
});
|
||||
const dispatch = useAppDispatch();
|
||||
const prefetchSources = provisioningApi.usePrefetch('getSourceList');
|
||||
const { isBeta } = useGetEnvironment();
|
||||
const release = getState()?.values?.release;
|
||||
|
||||
useEffect(() => {
|
||||
if (getState()?.values?.[input.name]) {
|
||||
setEnvironment(getState().values[input.name]);
|
||||
}
|
||||
}, [getState, input.name]);
|
||||
const supportedEnvironments = data?.find(
|
||||
(elem) => elem.arch === arch
|
||||
)?.image_types;
|
||||
|
||||
const handleSetEnvironment = (env, checked) =>
|
||||
setEnvironment((prevEnv) => {
|
||||
const newEnv = {
|
||||
...prevEnv,
|
||||
[env]: checked,
|
||||
};
|
||||
change(input.name, newEnv);
|
||||
return newEnv;
|
||||
});
|
||||
|
||||
const handleKeyDown = (e, env, checked) => {
|
||||
if (e.key === ' ') {
|
||||
handleSetEnvironment(env, checked);
|
||||
const handleToggleEnvironment = (environment: ImageTypes) => {
|
||||
if (environments.includes(environment)) {
|
||||
dispatch(removeImageType(environment));
|
||||
} else {
|
||||
dispatch(addImageType(environment));
|
||||
}
|
||||
};
|
||||
|
||||
// Load all the allowed targets from the backend
|
||||
const architecture = useField('arch').input.value;
|
||||
|
||||
const {
|
||||
data: allowedTargets,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
} = useGetAllowedTargets({
|
||||
architecture: architecture,
|
||||
release: release,
|
||||
});
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<Bullseye>
|
||||
<Spinner size="lg" />
|
||||
</Bullseye>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !isSuccess) {
|
||||
return (
|
||||
<Alert
|
||||
variant={'danger'}
|
||||
isPlain
|
||||
isInline
|
||||
title={'Allowed targets unavailable'}
|
||||
>
|
||||
Allowed targets cannot be reached, try again later.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
// If the user already made a choice for some targets but then changes their
|
||||
// architecture or distribution, only keep the target choices that are still
|
||||
// compatible.
|
||||
const allTargets = [
|
||||
'aws',
|
||||
'gcp',
|
||||
'azure',
|
||||
'vsphere',
|
||||
'vsphere-ova',
|
||||
'guest-image',
|
||||
'image-installer',
|
||||
'wsl',
|
||||
];
|
||||
allTargets.forEach((target) => {
|
||||
if (environment[target] && !allowedTargets.includes(target)) {
|
||||
handleSetEnvironment(target, false);
|
||||
}
|
||||
});
|
||||
|
||||
// each item the user can select is depending on what's compatible with the
|
||||
// architecture and the distribution they previously selected
|
||||
return (
|
||||
<FormGroup
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
isRequired={true}
|
||||
label="Select target environments"
|
||||
data-testid="target-select"
|
||||
>
|
||||
<FormGroup
|
||||
|
|
@ -151,7 +66,7 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
|
|||
data-testid="target-public"
|
||||
>
|
||||
<div className="tiles">
|
||||
{allowedTargets.includes('aws') && (
|
||||
{supportedEnvironments?.includes('aws') && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-aws"
|
||||
|
|
@ -163,15 +78,16 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
|
|||
alt="Amazon Web Services logo"
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('aws', !environment.aws)}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'aws', !environment.aws)}
|
||||
onClick={() => {
|
||||
handleToggleEnvironment('aws');
|
||||
}}
|
||||
onMouseEnter={() => prefetchSources({ provider: 'aws' })}
|
||||
isSelected={environment.aws}
|
||||
isSelected={environments.includes('aws')}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
{allowedTargets.includes('gcp') && (
|
||||
{supportedEnvironments?.includes('gcp') && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-google"
|
||||
|
|
@ -185,15 +101,16 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
|
|||
alt="Google Cloud Platform logo"
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('gcp', !environment.gcp)}
|
||||
isSelected={environment.gcp}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'gcp', !environment.gcp)}
|
||||
onClick={() => {
|
||||
handleToggleEnvironment('gcp');
|
||||
}}
|
||||
isSelected={environments.includes('gcp')}
|
||||
onMouseEnter={() => prefetchSources({ provider: 'gcp' })}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
{allowedTargets.includes('azure') && (
|
||||
{supportedEnvironments?.includes('azure') && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-azure"
|
||||
|
|
@ -207,15 +124,16 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
|
|||
alt="Microsoft Azure logo"
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('azure', !environment.azure)}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'azure', !environment.azure)}
|
||||
onClick={() => {
|
||||
handleToggleEnvironment('azure');
|
||||
}}
|
||||
onMouseEnter={() => prefetchSources({ provider: 'azure' })}
|
||||
isSelected={environment.azure}
|
||||
isSelected={environments.includes('azure')}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
{allowedTargets.includes('oci') && (
|
||||
{supportedEnvironments?.includes('oci') && isBeta() && (
|
||||
<Tile
|
||||
className="tile pf-u-mr-sm"
|
||||
data-testid="upload-oci"
|
||||
|
|
@ -227,59 +145,101 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
|
|||
alt="Oracle Cloud Infrastructure logo"
|
||||
/>
|
||||
}
|
||||
onClick={() => handleSetEnvironment('oci', !environment.oci)}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'oci', !environment.oci)}
|
||||
isSelected={environment.oci}
|
||||
onClick={() => {
|
||||
handleToggleEnvironment('oci');
|
||||
}}
|
||||
isSelected={environments.includes('oci')}
|
||||
isStacked
|
||||
isDisplayLarge
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FormGroup>
|
||||
{allowedTargets.includes('vsphere') && (
|
||||
<FormGroup
|
||||
label={<Text component={TextVariants.small}>Private cloud</Text>}
|
||||
className="pf-u-mt-sm"
|
||||
data-testid="target-private"
|
||||
>
|
||||
<Checkbox
|
||||
label="VMware vSphere"
|
||||
isChecked={environment.vsphere || environment['vsphere-ova']}
|
||||
onChange={(_event, checked) => {
|
||||
handleSetEnvironment('vsphere-ova', checked);
|
||||
handleSetEnvironment('vsphere', false);
|
||||
}}
|
||||
aria-label="VMware checkbox"
|
||||
id="checkbox-vmware"
|
||||
name="VMware"
|
||||
data-testid="checkbox-vmware"
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{allowedTargets.includes('vsphere') && (
|
||||
<FormGroup
|
||||
className="pf-u-mt-sm pf-u-mb-sm pf-u-ml-xl"
|
||||
data-testid="target-private-vsphere-radio"
|
||||
>
|
||||
{allowedTargets.includes('vsphere-ova') && (
|
||||
{supportedEnvironments?.includes('vsphere') && (
|
||||
<>
|
||||
<FormGroup
|
||||
label={<Text component={TextVariants.small}>Private cloud</Text>}
|
||||
className="pf-u-mt-sm"
|
||||
data-testid="target-private"
|
||||
>
|
||||
<Checkbox
|
||||
label="VMWare vSphere"
|
||||
isChecked={
|
||||
environments.includes('vsphere') ||
|
||||
environments.includes('vsphere-ova')
|
||||
}
|
||||
onChange={() => {
|
||||
setHasVSphere(!hasVSphere);
|
||||
handleToggleEnvironment('vsphere-ova');
|
||||
}}
|
||||
aria-label="VMWare checkbox"
|
||||
id="checkbox-vmware"
|
||||
name="VMWare"
|
||||
data-testid="checkbox-vmware"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
className="pf-u-mt-sm pf-u-mb-sm pf-u-ml-xl"
|
||||
data-testid="target-private-vsphere-radio"
|
||||
>
|
||||
{supportedEnvironments?.includes('vsphere-ova') && (
|
||||
<Radio
|
||||
name="vsphere-radio"
|
||||
aria-label="VMWare vSphere radio button OVA"
|
||||
id="vsphere-radio-ova"
|
||||
label={
|
||||
<>
|
||||
Open virtualization format (.ova)
|
||||
<Popover
|
||||
maxWidth="30rem"
|
||||
position="right"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
An OVA file is a virtual appliance used by
|
||||
virtualization platforms such as VMWare vSphere. It
|
||||
is a package that contains files used to describe a
|
||||
virtual machine, which includes a VMDK image, OVF
|
||||
descriptor file and a manifest file.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<HelpIcon className="pf-u-ml-sm" />
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
onChange={() => {
|
||||
handleToggleEnvironment('vsphere-ova');
|
||||
handleToggleEnvironment('vsphere');
|
||||
}}
|
||||
isChecked={environments.includes('vsphere-ova')}
|
||||
isDisabled={
|
||||
!(
|
||||
environments.includes('vsphere') ||
|
||||
environments.includes('vsphere-ova')
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Radio
|
||||
className="pf-u-mt-sm"
|
||||
name="vsphere-radio"
|
||||
aria-label="VMware vSphere radio button OVA"
|
||||
id="vsphere-radio-ova"
|
||||
aria-label="VMWare vSphere radio button VMDK"
|
||||
id="vsphere-radio-vmdk"
|
||||
label={
|
||||
<>
|
||||
Open virtualization format (.ova)
|
||||
Virtual disk (.vmdk)
|
||||
<Popover
|
||||
maxWidth="30rem"
|
||||
position="right"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
An OVA file is a virtual appliance used by
|
||||
virtualization platforms such as VMware vSphere. It is
|
||||
a package that contains files used to describe a
|
||||
virtual machine, which includes a VMDK image, OVF
|
||||
descriptor file and a manifest file.
|
||||
A VMDK file is a virtual disk that stores the contents
|
||||
of a virtual machine. This disk has to be imported
|
||||
into vSphere using govc import.vmdk, use the OVA
|
||||
version when using the vSphere UI.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
|
|
@ -288,84 +248,58 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
|
|||
</Popover>
|
||||
</>
|
||||
}
|
||||
onChange={(_event, checked) => {
|
||||
handleSetEnvironment('vsphere-ova', checked);
|
||||
handleSetEnvironment('vsphere', !checked);
|
||||
onChange={() => {
|
||||
handleToggleEnvironment('vsphere-ova');
|
||||
handleToggleEnvironment('vsphere');
|
||||
}}
|
||||
isChecked={environment['vsphere-ova']}
|
||||
isDisabled={!(environment.vsphere || environment['vsphere-ova'])}
|
||||
isChecked={environments.includes('vsphere')}
|
||||
isDisabled={
|
||||
!(
|
||||
environments.includes('vsphere') ||
|
||||
environments.includes('vsphere-ova')
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Radio
|
||||
className="pf-u-mt-sm"
|
||||
name="vsphere-radio"
|
||||
aria-label="VMware vSphere radio button VMDK"
|
||||
id="vsphere-radio-vmdk"
|
||||
label={
|
||||
<>
|
||||
Virtual disk (.vmdk)
|
||||
<Popover
|
||||
maxWidth="30rem"
|
||||
position="right"
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<Text>
|
||||
A VMDK file is a virtual disk that stores the contents
|
||||
of a virtual machine. This disk has to be imported into
|
||||
vSphere using govc import.vmdk, use the OVA version when
|
||||
using the vSphere UI.
|
||||
</Text>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<HelpIcon className="pf-u-ml-sm" />
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
onChange={(_event, checked) => {
|
||||
handleSetEnvironment('vsphere-ova', !checked);
|
||||
handleSetEnvironment('vsphere', checked);
|
||||
}}
|
||||
isChecked={environment.vsphere}
|
||||
isDisabled={!(environment.vsphere || environment['vsphere-ova'])}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
<FormGroup
|
||||
label={<Text component={TextVariants.small}>Other</Text>}
|
||||
data-testid="target-other"
|
||||
>
|
||||
{allowedTargets.includes('guest-image') && (
|
||||
{supportedEnvironments?.includes('guest-image') && (
|
||||
<Checkbox
|
||||
label="Virtualization - Guest image (.qcow2)"
|
||||
isChecked={environment['guest-image']}
|
||||
onChange={(_event, checked) =>
|
||||
handleSetEnvironment('guest-image', checked)
|
||||
}
|
||||
isChecked={environments.includes('guest-image')}
|
||||
onChange={() => {
|
||||
handleToggleEnvironment('guest-image');
|
||||
}}
|
||||
aria-label="Virtualization guest image checkbox"
|
||||
id="checkbox-guest-image"
|
||||
name="Virtualization guest image"
|
||||
data-testid="checkbox-guest-image"
|
||||
/>
|
||||
)}
|
||||
{allowedTargets.includes('image-installer') && (
|
||||
{supportedEnvironments?.includes('image-installer') && (
|
||||
<Checkbox
|
||||
label="Bare metal - Installer (.iso)"
|
||||
isChecked={environment['image-installer']}
|
||||
onChange={(_event, checked) =>
|
||||
handleSetEnvironment('image-installer', checked)
|
||||
}
|
||||
isChecked={environments.includes('image-installer')}
|
||||
onChange={() => {
|
||||
handleToggleEnvironment('image-installer');
|
||||
}}
|
||||
aria-label="Bare metal installer checkbox"
|
||||
id="checkbox-image-installer"
|
||||
name="Bare metal installer"
|
||||
data-testid="checkbox-image-installer"
|
||||
/>
|
||||
)}
|
||||
{allowedTargets.includes('wsl') && isBeta() && (
|
||||
{supportedEnvironments?.includes('wsl') && isBeta() && (
|
||||
<Checkbox
|
||||
label="WSL - Windows Subsystem for Linux (.tar.gz)"
|
||||
isChecked={environment['wsl']}
|
||||
onChange={(_event, checked) => handleSetEnvironment('wsl', checked)}
|
||||
isChecked={environments.includes('wsl')}
|
||||
onChange={() => {
|
||||
handleToggleEnvironment('wsl');
|
||||
}}
|
||||
aria-label="windows subsystem for linux checkbox"
|
||||
id="checkbox-wsl"
|
||||
name="WSL"
|
||||
|
|
@ -377,14 +311,4 @@ const TargetEnvironment = ({ label, isRequired, ...props }) => {
|
|||
);
|
||||
};
|
||||
|
||||
TargetEnvironment.propTypes = {
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
||||
TargetEnvironment.defaultProps = {
|
||||
label: '',
|
||||
isRequired: false,
|
||||
};
|
||||
|
||||
export default TargetEnvironment;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Text, Form, Title } from '@patternfly/react-core';
|
|||
import ArchSelect from './ArchSelect';
|
||||
import ReleaseLifecycle from './ReleaseLifecycle';
|
||||
import ReleaseSelect from './ReleaseSelect';
|
||||
import TargetEnvironment from './TargetEnvironment';
|
||||
|
||||
import DocumentationButton from '../../../sharedComponents/DocumentationButton';
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ const ImageOutputStep = () => {
|
|||
<ReleaseSelect />
|
||||
<ReleaseLifecycle />
|
||||
<ArchSelect />
|
||||
<TargetEnvironment />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { Distributions, ImageRequest } from './imageBuilderApi';
|
||||
import { Distributions, ImageRequest, ImageTypes } from './imageBuilderApi';
|
||||
|
||||
import { RHEL_9, X86_64 } from '../constants';
|
||||
|
||||
|
|
@ -9,11 +9,13 @@ import { RootState } from '.';
|
|||
type wizardState = {
|
||||
architecture: ImageRequest['architecture'];
|
||||
distribution: Distributions;
|
||||
imageTypes: ImageTypes[];
|
||||
};
|
||||
|
||||
const initialState: wizardState = {
|
||||
architecture: X86_64,
|
||||
distribution: RHEL_9,
|
||||
imageTypes: [],
|
||||
};
|
||||
|
||||
export const selectArchitecture = (state: RootState) => {
|
||||
|
|
@ -24,6 +26,10 @@ export const selectDistribution = (state: RootState) => {
|
|||
return state.wizard.distribution;
|
||||
};
|
||||
|
||||
export const selectImageTypes = (state: RootState) => {
|
||||
return state.wizard.imageTypes;
|
||||
};
|
||||
|
||||
export const wizardSlice = createSlice({
|
||||
name: 'wizard',
|
||||
initialState,
|
||||
|
|
@ -38,9 +44,30 @@ export const wizardSlice = createSlice({
|
|||
changeDistribution: (state, action: PayloadAction<Distributions>) => {
|
||||
state.distribution = action.payload;
|
||||
},
|
||||
addImageType: (state, action: PayloadAction<ImageTypes>) => {
|
||||
// Remove (if present) before adding to avoid duplicates
|
||||
state.imageTypes = state.imageTypes.filter(
|
||||
(imageType) => imageType !== action.payload
|
||||
);
|
||||
state.imageTypes.push(action.payload);
|
||||
},
|
||||
removeImageType: (state, action: PayloadAction<ImageTypes>) => {
|
||||
state.imageTypes = state.imageTypes.filter(
|
||||
(imageType) => imageType !== action.payload
|
||||
);
|
||||
},
|
||||
changeImageTypes: (state, action: PayloadAction<ImageTypes[]>) => {
|
||||
state.imageTypes = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { initializeWizard, changeArchitecture, changeDistribution } =
|
||||
wizardSlice.actions;
|
||||
export const {
|
||||
initializeWizard,
|
||||
changeArchitecture,
|
||||
changeDistribution,
|
||||
addImageType,
|
||||
removeImageType,
|
||||
changeImageTypes,
|
||||
} = wizardSlice.actions;
|
||||
export default wizardSlice.reducer;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue