Skip to content

Commit 7840390

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 7840390

File tree

20 files changed

+266
-24
lines changed

20 files changed

+266
-24
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 & 1 deletion
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",
@@ -1228,7 +1231,6 @@
12281231
"Upload a VDDK archive to generate the image URL": "Upload a VDDK archive to generate the image URL",
12291232
"Upload an OVA file which will be added to provider's virtual machines. The uploaded file name extension should be .ova": "Upload an OVA file which will be added to provider's virtual machines. The uploaded file name extension should be .ova",
12301233
"Upload local OVA file": "Upload local OVA file",
1231-
"Upload local OVA files": "Upload local OVA files",
12321234
"Uploading...": "Uploading...",
12331235
"Uppercase letters are switched to lowercase letters.": "Uppercase letters are switched to lowercase letters.",
12341236
"URL": "URL",
@@ -1345,6 +1347,7 @@
13451347
"Weight must be a number between 1-100": "Weight must be a number between 1-100",
13461348
"Welcome": "Welcome",
13471349
"What type of provider do you want to create?": "What type of provider do you want to create?",
1350+
"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.",
13481351
"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.",
13491352
"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.",
13501353
"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/components/DetailItems/DetailItem.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ const DescriptionTitleWithHelp: FC<{
6666

6767
{!isEmpty(crumbs) && (
6868
<FlexItem>
69-
<Breadcrumb>
70-
{crumbs?.map((crumb) => (
71-
<BreadcrumbItem key={crumb}>{crumb}</BreadcrumbItem>
72-
))}
73-
</Breadcrumb>
69+
<Flex direction={{ default: 'row' }} flexWrap={{ default: 'nowrap' }}>
70+
<Breadcrumb>
71+
{crumbs?.map((crumb) => (
72+
<BreadcrumbItem key={crumb}>{crumb}</BreadcrumbItem>
73+
))}
74+
</Breadcrumb>
75+
</Flex>
7476
</FlexItem>
7577
)}
7678
</Flex>

src/components/OvaFileUploader/OvaFileUploader.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ const OvaFileUploader: FC<OvaFileUploaderProps> = ({ provider }) => {
2828
const [response, setResponse] = useState<UploadOvaResponse | null>(null);
2929
const [error, setError] = useState<string | null>(null);
3030
const [uploading, setUploading] = useState(false);
31-
const [validation, setValidation] = useState<OvaValidationVariant>(
32-
OvaValidationVariant.Indeterminate,
33-
);
31+
const [validation, setValidation] = useState<OvaValidationVariant>(OvaValidationVariant.Default);
3432

3533
const handleUpload = async () => {
3634
if (!file) return;
@@ -68,7 +66,7 @@ const OvaFileUploader: FC<OvaFileUploaderProps> = ({ provider }) => {
6866
onClearClick={() => {
6967
setFile(undefined);
7068
setFilename('');
71-
setValidation(OvaValidationVariant.Indeterminate);
69+
setValidation(OvaValidationVariant.Default);
7270
setResponse(null);
7371
setError(null);
7472
}}

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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 { isApplianceManagementEnabled } 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 isEnabled = isApplianceManagementEnabled(provider);
24+
25+
return (
26+
<DetailsItem
27+
testId="appliance-management-detail-item"
28+
title={t('Appliance management')}
29+
content={
30+
isEnabled ? (
31+
<Label isCompact status="success">
32+
{t('Enabled')}
33+
</Label>
34+
) : (
35+
<Label isCompact>{t('Disabled')}</Label>
36+
)
37+
}
38+
helpContent={helpContent ?? OVA_APPLIANCE_MANAGEMENT_DESCRIPTION}
39+
crumbs={['Provider', 'spec', 'settings', 'applianceManagement']}
40+
onEdit={() => {
41+
launcher<EditApplianceManagementProps>(EditApplianceManagement, { provider });
42+
}}
43+
canEdit={canPatch}
44+
/>
45+
);
46+
};
47+
48+
export default ApplianceManagementDetailsItem;

0 commit comments

Comments
 (0)