Skip to content

Commit 4305cce

Browse files
committed
Add appliance management UI input for OVA providers
Add checkbox field to enable appliance management when creating OVA providers and in the provider details page. This setting controls whether users can upload OVA files directly to the provider. Changes: - Add checkbox in create provider form for OVA type - Add details item with edit capability in OVA provider details page - Add E2E test verification for appliance management setting Resolves: MTV-4481 Signed-off-by: Aviv Turgeman <[email protected]>
1 parent 53f9ef8 commit 4305cce

File tree

17 files changed

+250
-13
lines changed

17 files changed

+250
-13
lines changed

Claude.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,24 @@ This is an **OpenShift Console dynamic plugin** for [Forklift](https://github.co
3434
}
3535
```
3636

37-
2. **Use inline type imports**:
38-
```typescript
39-
// ✅ Good
40-
import { type FC, useState } from 'react';
41-
import { type UserProps } from './types';
42-
43-
// ❌ Bad
44-
import type { FC } from 'react';
45-
import { useState } from 'react';
46-
```
37+
2. **Type imports**:
38+
- If **all** imports from a module are types, use `import type`:
39+
```typescript
40+
// ✅ Good - all imports are types
41+
import type { FC, ReactNode } from 'react';
42+
import type { UserProps } from './types';
43+
```
44+
- If **some** imports are types and some are values, use inline `type` keyword:
45+
```typescript
46+
// ✅ Good - mixed types and values
47+
import { type FC, useState, type ReactNode } from 'react';
48+
```
49+
- Never separate type and value imports from the same module:
50+
```typescript
51+
// ❌ Bad - separate imports from same module
52+
import type { FC } from 'react';
53+
import { useState } from 'react';
54+
```
4755

4856
3. **Strict null checks are enabled** - always handle nullable values properly.
4957

locales/en/plugin__forklift-console-plugin.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
"Ansible playbook. If you specify a playbook, the image must be hook-runner.": "Ansible playbook. If you specify a playbook, the image must be hook-runner.",
153153
"Any underscore (_) is changed to a dash (-).": "Any underscore (_) is changed to a dash (-).",
154154
"API endpoint URL": "API endpoint URL",
155+
"Appliance management": "Appliance management",
155156
"Application credential ID": "Application credential ID",
156157
"Application credential ID:": "Application credential ID:",
157158
"Application credential name": "Application credential name",
@@ -363,6 +364,7 @@
363364
"Edit {{hookTypeLowercase}} migration hook": "Edit {{hookTypeLowercase}} migration hook",
364365
"Edit affinity rule": "Edit affinity rule",
365366
"Edit Ansible hook configuration for your migration plan. Hooks are applied to all virtual machines in the plan.": "Edit Ansible hook configuration for your migration plan. Hooks are applied to all virtual machines in the plan.",
367+
"Edit appliance management": "Edit appliance management",
366368
"Edit cutover": "Edit cutover",
367369
"Edit default transfer network": "Edit default transfer network",
368370
"Edit description": "Edit description",
@@ -392,6 +394,7 @@
392394
"Edit volume name template": "Edit volume name template",
393395
"Empty": "Empty",
394396
"Enable {{hookTypeLowercase}} migration hook": "Enable {{hookTypeLowercase}} migration hook",
397+
"Enable appliance management for local OVA file uploads": "Enable appliance management for local OVA file uploads",
395398
"Enabled": "Enabled",
396399
"Enables hardware-assisted copying by instructing the vSphere ESXi host to transfer data directly on the storage backend using technologies like XCOPY and VAAI.": "Enables hardware-assisted copying by instructing the vSphere ESXi host to transfer data directly on the storage backend using technologies like XCOPY and VAAI.",
397400
"Endpoint": "Endpoint",
@@ -1345,6 +1348,7 @@
13451348
"Weight must be a number between 1-100": "Weight must be a number between 1-100",
13461349
"Welcome": "Welcome",
13471350
"What type of provider do you want to create?": "What type of provider do you want to create?",
1351+
"When enabled, allows uploading OVA files directly to the provider instead of using NFS shared directory only.": "When enabled, allows uploading OVA files directly to the provider instead of using NFS shared directory only.",
13481352
"When the URL field is left empty, the local OpenShift cluster is used.": "When the URL field is left empty, the local OpenShift cluster is used.",
13491353
"When the VM will migrate during a warm migration. VMs included in the migration plan will be shut down when the cutover starts.": "When the VM will migrate during a warm migration. VMs included in the migration plan will be shut down when the cutover starts.",
13501354
"When you migrate from OpenStack, or when you run a cold migration from Red Hat Virtualization to the Red Hat OpenShift cluster that MTV is deployed on, the migration allocates persistent volumes without CDI. In these cases, you might need to adjust the file system overhead.": "When you migrate from OpenStack, or when you run a cold migration from Red Hat Virtualization to the Red Hat OpenShift cluster that MTV is deployed on, the migration allocates persistent volumes without CDI. In these cases, you might need to adjust the file system overhead.",

src/providers/create/ProviderTypeFields.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ServiceAccountTokenField from './fields/openshift/ServiceAccountTokenFiel
1515
import OpenStackAuthenticationTypeField from './fields/openstack/OpenStackAuthenticationTypeField';
1616
import OpenStackUrlField from './fields/openstack/OpenStackUrlField';
1717
import NfsDirectoryField from './fields/ova/NfsDirectoryField';
18+
import OvaApplianceManagementField from './fields/ova/OvaApplianceManagementField';
1819
import OvirtCredentialsFields from './fields/ovirt/OvirtCredentialsFields';
1920
import OvirtUrlField from './fields/ovirt/OvirtUrlField';
2021
import ProviderNameField from './fields/ProviderNameField';
@@ -48,7 +49,12 @@ const ProviderTypeFields: FC = () => {
4849

4950
{selectedProviderType && <ProviderNameField />}
5051

51-
{selectedProviderType === PROVIDER_TYPES.ova && <NfsDirectoryField />}
52+
{selectedProviderType === PROVIDER_TYPES.ova && (
53+
<>
54+
<NfsDirectoryField />
55+
<OvaApplianceManagementField />
56+
</>
57+
)}
5258

5359
{selectedProviderType === PROVIDER_TYPES.openshift && (
5460
<>

src/providers/create/fields/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ enum OpenstackProviderFormFieldId {
4040

4141
enum OvaProviderFormFieldId {
4242
NfsDirectory = 'nfsDirectory',
43+
OvaApplianceManagement = 'ovaApplianceManagement',
4344
}
4445

4546
enum OvirtProviderFormFieldId {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { FC } from 'react';
2+
import { useController } from 'react-hook-form';
3+
import {
4+
OVA_APPLIANCE_MANAGEMENT_DESCRIPTION,
5+
OVA_APPLIANCE_MANAGEMENT_LABEL,
6+
} from 'src/providers/utils/constants';
7+
8+
import { Checkbox, FormGroup } from '@patternfly/react-core';
9+
10+
import { useCreateProviderFormContext } from '../../hooks/useCreateProviderFormContext';
11+
import { ProviderFormFieldId } from '../constants';
12+
13+
const OvaApplianceManagementField: FC = () => {
14+
const { control } = useCreateProviderFormContext();
15+
16+
const {
17+
field: { onChange, value },
18+
} = useController({
19+
control,
20+
name: ProviderFormFieldId.OvaApplianceManagement,
21+
});
22+
23+
return (
24+
<FormGroup fieldId={ProviderFormFieldId.OvaApplianceManagement}>
25+
<Checkbox
26+
label={OVA_APPLIANCE_MANAGEMENT_LABEL}
27+
isChecked={value ?? false}
28+
onChange={(_event, checked) => {
29+
onChange(checked);
30+
}}
31+
id={ProviderFormFieldId.OvaApplianceManagement}
32+
name={ProviderFormFieldId.OvaApplianceManagement}
33+
data-testid="ova-appliance-management-checkbox"
34+
description={OVA_APPLIANCE_MANAGEMENT_DESCRIPTION}
35+
/>
36+
</FormGroup>
37+
);
38+
};
39+
40+
export default OvaApplianceManagementField;

src/providers/create/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type OpenshiftFields = {
2323

2424
type OvaFields = {
2525
[ProviderFormFieldId.NfsDirectory]?: string;
26+
[ProviderFormFieldId.OvaApplianceManagement]?: boolean;
2627
};
2728

2829
type HypervFields = {

src/providers/create/utils/buildOvaProviderResources.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PROVIDER_TYPES } from 'src/providers/utils/constants';
1+
import { PROVIDER_TYPES, TRUE_VALUE } from 'src/providers/utils/constants';
22

33
import type { IoK8sApiCoreV1Secret, V1beta1Provider } from '@forklift-ui/types';
44

@@ -17,6 +17,7 @@ export const buildOvaProviderResources = (formData: OvaFormData): ProviderResour
1717
const namespace = formData[ProviderFormFieldId.ProviderProject];
1818
const providerName = formData[ProviderFormFieldId.ProviderName];
1919
const nfsDirectory = formData[ProviderFormFieldId.NfsDirectory] ?? '';
20+
const applianceManagement = formData[ProviderFormFieldId.OvaApplianceManagement];
2021

2122
const provider = buildProviderObject({
2223
name: providerName,
@@ -25,6 +26,13 @@ export const buildOvaProviderResources = (formData: OvaFormData): ProviderResour
2526
url: nfsDirectory,
2627
});
2728

29+
if (applianceManagement && provider.spec) {
30+
provider.spec.settings = {
31+
...provider.spec.settings,
32+
applianceManagement: TRUE_VALUE,
33+
};
34+
}
35+
2836
const secret = buildSecretObject({
2937
namespace,
3038
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { FC } from 'react';
2+
import { DetailsItem } from 'src/components/DetailItems/DetailItem';
3+
import { OVA_APPLIANCE_MANAGEMENT_DESCRIPTION } from 'src/providers/utils/constants';
4+
5+
import { useModal } from '@openshift-console/dynamic-plugin-sdk';
6+
import { Label } from '@patternfly/react-core';
7+
import { getApplianceManagement } from '@utils/crds/common/selectors';
8+
import { useForkliftTranslation } from '@utils/i18n';
9+
10+
import type { ProviderDetailsItemProps } from './utils/types';
11+
import EditApplianceManagement, {
12+
type EditApplianceManagementProps,
13+
} from './EditApplianceManagement';
14+
15+
const ApplianceManagementDetailsItem: FC<ProviderDetailsItemProps> = ({
16+
canPatch,
17+
helpContent,
18+
resource: provider,
19+
}) => {
20+
const { t } = useForkliftTranslation();
21+
const launcher = useModal();
22+
23+
const applianceManagement = getApplianceManagement(provider);
24+
const isEnabled = applianceManagement === 'true';
25+
26+
return (
27+
<DetailsItem
28+
testId="appliance-management-detail-item"
29+
title={t('Appliance management')}
30+
content={
31+
isEnabled ? (
32+
<Label isCompact status="success">
33+
{t('Enabled')}
34+
</Label>
35+
) : (
36+
<Label isCompact status="info">
37+
{t('Disabled')}
38+
</Label>
39+
)
40+
}
41+
helpContent={helpContent ?? OVA_APPLIANCE_MANAGEMENT_DESCRIPTION}
42+
crumbs={['Provider', 'spec', 'settings', 'applianceManagement']}
43+
onEdit={() => {
44+
launcher<EditApplianceManagementProps>(EditApplianceManagement, { provider });
45+
}}
46+
canEdit={canPatch}
47+
/>
48+
);
49+
};
50+
51+
export default ApplianceManagementDetailsItem;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useState } from 'react';
2+
import {
3+
OVA_APPLIANCE_MANAGEMENT_DESCRIPTION,
4+
OVA_APPLIANCE_MANAGEMENT_LABEL,
5+
} from 'src/providers/utils/constants';
6+
import { useForkliftTranslation } from 'src/utils/i18n';
7+
8+
import ModalForm from '@components/ModalForm/ModalForm';
9+
import type { V1beta1Provider } from '@forklift-ui/types';
10+
import type { ModalComponent } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
11+
import { Checkbox, Form, FormGroup } from '@patternfly/react-core';
12+
import { getApplianceManagement } from '@utils/crds/common/selectors';
13+
14+
import onUpdateApplianceManagement from './onUpdateApplianceManagement';
15+
16+
export type EditApplianceManagementProps = {
17+
provider: V1beta1Provider;
18+
};
19+
20+
const EditApplianceManagement: ModalComponent<EditApplianceManagementProps> = ({
21+
closeModal,
22+
provider,
23+
}) => {
24+
const { t } = useForkliftTranslation();
25+
26+
const currentValue = getApplianceManagement(provider) === 'true';
27+
const [enabled, setEnabled] = useState(currentValue);
28+
29+
const onSubmit = async (): Promise<void> => {
30+
await onUpdateApplianceManagement(provider, enabled);
31+
};
32+
33+
return (
34+
<ModalForm closeModal={closeModal} title={t('Edit appliance management')} onConfirm={onSubmit}>
35+
<Form>
36+
<FormGroup fieldId="appliance-management">
37+
<Checkbox
38+
label={OVA_APPLIANCE_MANAGEMENT_LABEL}
39+
isChecked={enabled}
40+
onChange={(_event, checked) => {
41+
setEnabled(checked);
42+
}}
43+
id="appliance-management-checkbox"
44+
name="appliance-management"
45+
data-testid="edit-appliance-management-checkbox"
46+
description={OVA_APPLIANCE_MANAGEMENT_DESCRIPTION}
47+
/>
48+
</FormGroup>
49+
</Form>
50+
</ModalForm>
51+
);
52+
};
53+
54+
export default EditApplianceManagement;

src/providers/details/tabs/Details/components/DetailsSection/OVADetailsSection.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ import OwnerDetailsItem from '@components/DetailItems/OwnerDetailItem';
1010
import { DescriptionList } from '@patternfly/react-core';
1111

1212
import type { DetailsSectionProps } from './utils/types';
13+
import ApplianceManagementDetailsItem from './ApplianceManagementDetailsItem';
1314

1415
const OVADetailsSection: FC<DetailsSectionProps> = ({ data }) => {
1516
const { t } = useForkliftTranslation();
16-
const { provider } = data;
17+
const { permissions, provider } = data;
1718

1819
if (!provider) return null;
1920

21+
const canPatch = permissions?.canPatch ?? false;
22+
2023
return (
2124
<DescriptionList
2225
columnModifier={{
@@ -33,6 +36,7 @@ const OVADetailsSection: FC<DetailsSectionProps> = ({ data }) => {
3336
`URL of the NFS file share that serves the OVA., for example, 10.10.0.10:/ova`,
3437
)}
3538
/>
39+
<ApplianceManagementDetailsItem resource={provider} canPatch={canPatch} />
3640
<CreatedAtDetailsItem resource={provider} />
3741
<OwnerDetailsItem resource={provider} />
3842
</DescriptionList>

0 commit comments

Comments
 (0)