diff --git a/console-extensions.ts b/console-extensions.ts index ade00f353..1a5971986 100644 --- a/console-extensions.ts +++ b/console-extensions.ts @@ -1,7 +1,10 @@ import type { EncodedExtension } from '@openshift/dynamic-plugin-sdk'; import type { + ActionProvider, HrefNavItem, NavSection, + ResourceListPage, + ResourceNSNavItem, RoutePage, Separator, } from '@openshift-console/dynamic-plugin-sdk'; @@ -38,16 +41,46 @@ const extensions: EncodedExtension[] = [ } as EncodedExtension, { - type: 'console.navigation/href', + type: 'console.navigation/resource-ns', properties: { id: 'providers', - insertAfter: 'importSeparator', - perspective: 'admin', section: 'virtualization', name: '%plugin__forklift-console-plugin~Providers for VM Import%', - href: '/mtv/providers', + model: { + group: 'forklift.konveyor.io', + kind: 'Provider', + version: 'v1beta1', + }, + dataAttributes: { + 'data-quickstart-id': 'qs-nav-providers', + 'data-test-id': 'providers-nav-item', + }, }, - } as EncodedExtension, + } as EncodedExtension, + + { + type: 'console.page/resource/list', + properties: { + component: { + $codeRef: 'ProvidersPage', + }, + model: { + group: 'forklift.konveyor.io', + kind: 'Provider', + version: 'v1beta1', + }, + }, + } as EncodedExtension, + + { + type: 'console.action/provider', + properties: { + contextId: 'mergedProvider', + provider: { + $codeRef: 'useMergedProviders', + }, + }, + } as EncodedExtension, { type: 'console.navigation/href', diff --git a/locales/en/plugin__forklift-console-plugin.json b/locales/en/plugin__forklift-console-plugin.json index e0815e3ad..bb7c2b670 100644 --- a/locales/en/plugin__forklift-console-plugin.json +++ b/locales/en/plugin__forklift-console-plugin.json @@ -1,23 +1,53 @@ { + "{{type}} provider {{name}} will no longer be selectable as a migration source.": "{{type}} provider {{name}} will no longer be selectable as a migration source.", + "{{type}} provider {{name}} will no longer be selectable as a migration target.": "{{type}} provider {{name}} will no longer be selectable as a migration target.", + "Actions": "Actions", + "Add Provider": "Add Provider", "Cancel": "Cancel", + "Cannot remove provider": "Cannot remove provider", "Clear all filters": "Clear all filters", + "Clusters": "Clusters", + "Delete": "Delete", + "Delete Provider": "Delete Provider", + "Edit Provider": "Edit Provider", + "Endpoint": "Endpoint", + "False": "False", + "Filter by endpoint": "Filter by endpoint", "Filter by name": "Filter by name", "Filter by namespace": "Filter by namespace", + "Hosts": "Hosts", + "KubeVirt": "KubeVirt", "Loading": "Loading", "Manage Columns": "Manage Columns", "Mappings for VM Import": "Mappings for VM Import", "Name": "Name", "Namespace": "Namespace", + "Networks": "Networks", + "No information": "No information", "No results found": "No results found", "No results match the filter criteria. Clear all filters and try again.": "No results match the filter criteria. Clear all filters and try again.", + "oVirt": "oVirt", + "Permanently delete provider?": "Permanently delete provider?", "Plans for VM Import": "Plans for VM Import", + "Providers": "Providers", "Providers for VM Import": "Providers for VM Import", + "Ready": "Ready", "Reorder": "Reorder", "Restore default colums": "Restore default colums", "Save": "Save", + "Select a default migration network for the provider. This network will be used for migrating data to all namespaces to which it is attached.": "Select a default migration network for the provider. This network will be used for migrating data to all namespaces to which it is attached.", "Select Filter": "Select Filter", + "Select migration network": "Select migration network", "Selected columns will be displayed in the table.": "Selected columns will be displayed in the table.", + "Storage": "Storage", "Table column management": "Table column management", + "The host provider cannot be edited": "The host provider cannot be edited", + "This provider cannot be edited because it has running migrations": "This provider cannot be edited because it has running migrations", + "True": "True", + "Type": "Type", "Unable to retrieve data": "Unable to retrieve data", - "Virtualization": "Virtualization" + "Unknown": "Unknown", + "Virtualization": "Virtualization", + "VMs": "VMs", + "VMware": "VMware" } diff --git a/pkg/web/src/app/Providers/components/ProvidersTable/ProviderActionsDropdown.tsx b/pkg/web/src/app/Providers/components/ProvidersTable/ProviderActionsDropdown.tsx index 5fc79c1d9..e16d128bc 100644 --- a/pkg/web/src/app/Providers/components/ProvidersTable/ProviderActionsDropdown.tsx +++ b/pkg/web/src/app/Providers/components/ProvidersTable/ProviderActionsDropdown.tsx @@ -1,7 +1,12 @@ import * as React from 'react'; import { Dropdown, KebabToggle, DropdownItem, DropdownPosition } from '@patternfly/react-core'; import { useDeleteProviderMutation } from '@app/queries'; -import { ICorrelatedProvider, InventoryProvider } from '@app/queries/types'; +import { + ICorrelatedProvider, + INameNamespaceRef, + InventoryProvider, + IPlan, +} from '@app/queries/types'; import { PATH_PREFIX, ProviderType, PROVIDER_TYPE_NAMES } from '@app/common/constants'; import { ConfirmModal } from '@app/common/components/ConfirmModal'; import { EditProviderContext } from '@app/Providers/ProvidersPage'; @@ -11,6 +16,21 @@ import { isSameResource } from '@app/queries/helpers'; import { useHistory } from 'react-router-dom'; import { useClusterProvidersQuery } from '@app/queries'; +export const hasRunningMigration = ({ + plans = [], + providerMetadata, +}: { + plans: IPlan[]; + providerMetadata: INameNamespaceRef; +}) => + !!plans + .filter((plan) => hasCondition(plan.status?.conditions || [], 'Executing')) + .find((runningPlan) => { + const { source, destination } = runningPlan.spec.provider; + return ( + isSameResource(providerMetadata, source) || isSameResource(providerMetadata, destination) + ); + }); interface IProviderActionsDropdownProps { provider: ICorrelatedProvider; providerType: ProviderType; @@ -37,15 +57,15 @@ export const ProviderActionsDropdown: React.FunctionComponent hasCondition(plan.status?.conditions || [], 'Executing')) - .find((runningPlan) => { - const { source, destination } = runningPlan.spec.provider; - return ( - isSameResource(provider.metadata, source) || isSameResource(provider.metadata, destination) - ); - }); - const isEditDeleteDisabled = !provider.spec.url || hasRunningMigration; + const isEditDeleteDisabled = + !provider.spec.url || hasRunningMigration({ plans, providerMetadata: provider.metadata }); + + const disabledEditTooltip = !provider.spec.url + ? 'The host provider cannot be edited' + : 'This provider cannot be edited because it has running migrations'; + const disabledDeleteTooltip = !provider.spec.url + ? 'The host provider cannot be removed' + : 'This provider cannot be removed because it has running migrations'; return ( <> @@ -58,13 +78,7 @@ export const ProviderActionsDropdown: React.FunctionComponent string; +} +const StatusCell = ({ value, entity: { conditions }, t }: CellProps) => { + const existingConditions = Object.values(conditions).filter(Boolean); + const toState = (value) => { + switch (value) { + case 'True': + return 'Ok'; + case 'False': + return 'Error'; + default: + return 'Unknown'; + } + }; + const label = CONDITIONS?.[value]?.(t); + return ( + + {existingConditions.length > 0 + ? existingConditions.map(({ message, status }) => { + return ; + }) + : t('No information')} + + } + > + + + ); +}; + +const TextCell = ({ value }: { value: string }) => <>{value ?? ''}; + +const TextWithIcon = ({ value, Icon }: { value: string; Icon: JSXElementConstructor }) => ( + <> + {value && ( + <> + + + )} + +); + +const ProviderLink = ({ value, entity }: CellProps) => ( + +); + +const HostCell = ({ value, entity: { ready, name, type } }: CellProps) => ( + <> + {ready === 'True' && value && type === 'vsphere' ? ( + + + + ) : ( + + )} + +); + +const cellCreator = { + [NAME]: ProviderLink, + [READY]: StatusCell, + [URL]: TextCell, + [TYPE]: ({ value, t }: CellProps) => , + [NAMESPACE]: ({ value }: CellProps) => , + [ACTIONS]: ProviderActions, + [NETWORK_COUNT]: ({ value }: CellProps) => , + [STORAGE_COUNT]: ({ value }: CellProps) => , + [HOST_COUNT]: HostCell, +}; + +const ProviderRow = ({ columns, entity }: RowProps) => { + const { t } = useTranslation(); + return ( + + {columns.map(({ id, toLabel }) => ( + + {cellCreator?.[id]?.({ + value: entity[id], + entity, + t, + }) ?? } + + ))} + + ); +}; + +export default ProviderRow; diff --git a/src/Providers/ProvidersPage.tsx b/src/Providers/ProvidersPage.tsx new file mode 100644 index 000000000..e4625b6f2 --- /dev/null +++ b/src/Providers/ProvidersPage.tsx @@ -0,0 +1,178 @@ +import React from 'react'; +import { fromI18nEnum } from '_/components/Filter/helpers'; +import withQueryClient from '_/extensions/QueryClientHoc'; +import { StandardPage } from 'src/components/StandardPage'; +import { Field } from 'src/components/types'; +import { CONDITIONS, PROVIDERS, useTranslation } from 'src/internal/i18n'; +import { ResourceConsolePageProps } from 'src/internal/k8s'; +import { + ACTIONS, + CLUSTER_COUNT, + HOST_COUNT, + NAME, + NAMESPACE, + NETWORK_COUNT, + READY, + STORAGE_COUNT, + TYPE, + URL, + VM_COUNT, +} from 'src/utils/constants'; + +import { AddEditProviderModal } from '@app/Providers/components/AddEditProviderModal'; +import { EditProviderContext } from '@app/Providers/ProvidersPage'; +import { useModal } from '@openshift-console/dynamic-plugin-sdk'; +import { Button } from '@patternfly/react-core'; + +import { MergedProvider, useProvidersWithInventory } from './data'; +import ProviderRow from './ProviderRow'; + +const fieldsMetadata: Field[] = [ + { + id: NAME, + toLabel: (t) => t('Name'), + isVisible: true, + isIdentity: true, // Name is sufficient ID when Namespace is pre-selected + filter: { + type: 'freetext', + toPlaceholderLabel: (t) => t('Filter by name'), + }, + sortable: true, + }, + { + id: NAMESPACE, + toLabel: (t) => t('Namespace'), + isVisible: true, + isIdentity: true, + filter: { + type: 'freetext', + toPlaceholderLabel: (t) => t('Filter by namespace'), + }, + sortable: true, + }, + { + id: READY, + toLabel: (t) => t('Ready'), + isVisible: true, + filter: { + type: 'enum', + primary: true, + toPlaceholderLabel: (t) => t('Ready'), + values: fromI18nEnum(CONDITIONS), + }, + sortable: true, + }, + { + id: URL, + toLabel: (t) => t('Endpoint'), + isVisible: true, + filter: { + type: 'freetext', + toPlaceholderLabel: (t) => t('Filter by endpoint'), + }, + sortable: true, + }, + { + id: TYPE, + toLabel: (t) => t('Type'), + isVisible: true, + filter: { + type: 'enum', + primary: true, + toPlaceholderLabel: (t) => t('Type'), + values: fromI18nEnum(PROVIDERS), + }, + sortable: true, + }, + { + id: VM_COUNT, + toLabel: (t) => t('VMs'), + isVisible: true, + sortable: true, + }, + { + id: NETWORK_COUNT, + toLabel: (t) => t('Networks'), + isVisible: true, + sortable: true, + }, + { + id: CLUSTER_COUNT, + toLabel: (t) => t('Clusters'), + isVisible: false, + sortable: true, + }, + { + id: HOST_COUNT, + toLabel: (t) => t('Hosts'), + isVisible: true, + sortable: true, + }, + { + id: STORAGE_COUNT, + toLabel: (t) => t('Storage'), + isVisible: false, + sortable: true, + }, + { + id: ACTIONS, + toLabel: () => '', + isVisible: true, + sortable: false, + }, +]; + +export const ProvidersPage = ({ namespace, kind }: ResourceConsolePageProps) => { + const { t } = useTranslation(); + + const dataSource = useProvidersWithInventory({ + kind, + namespace, + }); + + // data hook triggers frequent re-renders although data remains the same: + // both the content content and object reference + return ; +}; + +const Page = ({ + dataSource, + namespace, + title, +}: { + dataSource: [MergedProvider[], boolean, boolean]; + namespace: string; + title: string; +}) => ( + + addButton={} + dataSource={dataSource} + RowMapper={ProviderRow} + fieldsMetadata={fieldsMetadata} + namespace={namespace} + title={title} + /> +); + +const PageMemo = React.memo(Page); + +const AddProviderButton = () => { + const { t } = useTranslation(); + const launchModal = useModal(); + + return ( + + ); +}; + +const AddProviderModal = ({ closeModal }: { closeModal: () => void }) => { + return ( + undefined, plans: [] }}> + + + ); +}; + +export default ProvidersPage; diff --git a/src/Providers/data.ts b/src/Providers/data.ts new file mode 100644 index 000000000..d00f44e23 --- /dev/null +++ b/src/Providers/data.ts @@ -0,0 +1,152 @@ +import { useMemo } from 'react'; +import { ProviderResource } from 'src/internal/k8s'; +import { + API_VERSION, + CONNECTED, + DEFAULT_TRANSFER_NETWORK, + DEFAULT_TRANSFER_NETWORK_ANNOTATION, + INVENTORY, + KIND, + NAME, + NAMESPACE, + READY, + SECRET_NAME, + TYPE, + UID, + URL, + VALIDATED, +} from 'src/utils/constants'; +import { useProviders } from 'src/utils/fetch'; + +import { useInventoryProvidersQuery } from '@app/queries'; +import { + IOpenShiftProvider, + IProvidersByType, + IRHVProvider, + IVMwareProvider, +} from '@app/queries/types'; + +const conditionState = (state: string) => + state === 'True' || state === 'False' ? state : 'Unknown'; + +interface Condition { + status: string; + message: string; +} +interface SupportedConditions { + Ready?: Condition; + Validated?: Condition; + ConnectionTested?: Condition; + InventoryCreated?: Condition; +} + +interface FlattenedProvider { + [KIND]: string; + [NAME]: string; + [NAMESPACE]: string; + [URL]: string; + [TYPE]: string; + [UID]: string; + [SECRET_NAME]: string; + [API_VERSION]: string; + [DEFAULT_TRANSFER_NETWORK]: string; +} + +interface FlattenedConditions { + [READY]: string; + conditions: { + [READY]: Condition; + [CONNECTED]: Condition; + [VALIDATED]: Condition; + [INVENTORY]: Condition; + }; +} + +interface MergedInventory { + clusterCount: number; + hostCount: number; + vmCount: number; + networkCount: number; + storageCount: number; +} + +export type MergedProvider = FlattenedProvider & MergedInventory & FlattenedConditions; + +const mergeData = (resources: ProviderResource[], inventory: IProvidersByType) => + resources + .map( + ({ + metadata: { name, namespace, uid, annotations } = {}, + status: { conditions = [] } = {}, + spec: { url, type, secret: { name: secretName } = {} } = {}, + kind, + apiVersion, + }): [ + FlattenedProvider, + IVMwareProvider & IRHVProvider & IOpenShiftProvider, + SupportedConditions, + ] => [ + { + name, + namespace, + url, + type, + uid, + kind, + secretName, + apiVersion, + defaultTransferNetwork: annotations?.[DEFAULT_TRANSFER_NETWORK_ANNOTATION], + }, + inventory?.[type].find(({ uid: otherUid }) => otherUid === uid) ?? {}, + Object.fromEntries( + conditions.map(({ type, status, message }) => [ + type, + { status: conditionState(status), message }, + ]), + ), + ], + ) + .map( + ([ + provider, + { clusterCount, hostCount, vmCount, networkCount, datastoreCount, storageDomainCount }, + { Ready, Validated, ConnectionTested, InventoryCreated }, + ]): MergedProvider => ({ + ...provider, + clusterCount, + hostCount, + vmCount, + networkCount, + storageCount: storageDomainCount ?? datastoreCount, + ready: Ready?.status ?? 'Unknown', + conditions: { + ready: Ready, + inventory: InventoryCreated, + validated: Validated, + connected: ConnectionTested, + }, + }), + ); + +export const useProvidersWithInventory = ({ + kind, + namespace, + name = undefined, +}): [MergedProvider[], boolean, boolean] => { + const [resources, loaded, error] = useProviders({ kind, namespace, name }); + const { data, isSuccess, isError } = useInventoryProvidersQuery(); + const providersWithInventory = useMemo( + () => (resources && data ? mergeData(resources, data) : []), + [resources, data], + ); + + const totalSuccess = loaded && isSuccess; + const totalError = error || isError; + // extra memo to keep the tuple reference stable + // the tuple is used as data source and passed as prop + // which triggres unnecessary re-renders + return useMemo( + () => [providersWithInventory, totalSuccess, totalError], + [providersWithInventory, totalSuccess, totalError], + ); +}; diff --git a/src/Providers/providerActions.tsx b/src/Providers/providerActions.tsx new file mode 100644 index 000000000..49161fd38 --- /dev/null +++ b/src/Providers/providerActions.tsx @@ -0,0 +1,212 @@ +import React, { useMemo, useState } from 'react'; +import { createActions } from 'src/extensions/actions'; +import withQueryClient from 'src/extensions/QueryClientHoc'; +import { useTranslation } from 'src/internal/i18n'; + +import { ConfirmModal } from '@app/common/components/ConfirmModal'; +import { SelectOpenShiftNetworkModal } from '@app/common/components/SelectOpenShiftNetworkModal'; +import { ProviderType } from '@app/common/constants'; +import { AddEditProviderModal } from '@app/Providers/components/AddEditProviderModal'; +import { hasRunningMigration } from '@app/Providers/components/ProvidersTable/ProviderActionsDropdown'; +import { EditProviderContext } from '@app/Providers/ProvidersPage'; +import { + useDeleteProviderMutation, + useOCPMigrationNetworkMutation, + usePlansQuery, +} from '@app/queries'; +import { IOpenShiftProvider, IPlan, IProviderObject } from '@app/queries/types'; +import { ActionServiceProvider, useModal } from '@openshift-console/dynamic-plugin-sdk'; + +import { MergedProvider } from './data'; + +export const useMergedProviderActions = (entity: MergedProvider) => { + const { t } = useTranslation(); + const launchModal = useModal(); + const plansQuery = usePlansQuery(); + const editingDisabled = + !entity.url || + hasRunningMigration({ + plans: plansQuery?.data?.items, + providerMetadata: { + name: entity.name, + namespace: entity.namespace, + }, + }); + const disabledTooltip = !entity.url + ? t('The host provider cannot be edited') + : t('This provider cannot be edited because it has running migrations'); + + const actions = useMemo( + () => + [ + { + id: 'edit', + cta: () => + launchModal(withQueryClient(EditModal), { + entity, + plans: plansQuery?.data?.items, + }), + label: t('Edit Provider'), + disabled: editingDisabled, + disabledTooltip: editingDisabled ? disabledTooltip : '', + }, + { + id: 'delete', + cta: () => launchModal(withQueryClient(DeleteModal), { entity }), + label: t('Delete Provider'), + disabled: editingDisabled, + disabledTooltip: editingDisabled ? disabledTooltip : '', + }, + entity.type === 'openshift' && { + id: 'selectNetwork', + cta: () => launchModal(withQueryClient(SelectNetworkForOpenshift), { entity }), + label: t('Select migration network'), + }, + ].filter(Boolean), + [t, editingDisabled, disabledTooltip], + ); + + return [actions, true, undefined]; +}; + +const EditModal = ({ + entity, + closeModal, + plans = [], +}: { + closeModal: () => void; + entity: MergedProvider; + plans: IPlan[]; +}) => { + return ( + undefined, plans }}> + + + ); +}; + +const SelectNetworkForOpenshift = ({ + entity, + closeModal, +}: { + closeModal: () => void; + entity: MergedProvider; +}) => { + const { t } = useTranslation(); + const migrationNetworkMutation = useOCPMigrationNetworkMutation(closeModal); + const inventory = toIOpenShiftProvider(entity, toIProviderObject(entity)); + return ( + { + migrationNetworkMutation.reset(); + closeModal(); + }} + onSubmit={(network) => + migrationNetworkMutation.mutate({ + provider: inventory, + network, + }) + } + mutationResult={migrationNetworkMutation} + /> + ); +}; + +const DeleteModal = ({ + entity, + closeModal, +}: { + closeModal: () => void; + entity: MergedProvider; +}) => { + const { t } = useTranslation(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(true); + + const toggleDeleteModal = () => setIsDeleteModalOpen(!isDeleteModalOpen); + const deleteProviderMutation = useDeleteProviderMutation( + entity.type as ProviderType, + toggleDeleteModal, + ); + const isTarget = (type: ProviderType) => type !== 'openshift'; + + return ( + { + toggleDeleteModal(); + closeModal(); + }} + mutateFn={() => deleteProviderMutation.mutate(toIProviderObject(entity))} + mutateResult={deleteProviderMutation} + title={t('Permanently delete provider?')} + body={ + isTarget(entity.type as ProviderType) + ? t('{{type}} provider {{name}} will no longer be selectable as a migration target.', { + type: entity.type, + name: entity.name, + }) + : t('{{type}} provider {{name}} will no longer be selectable as a migration source.', { + type: entity.type, + name: entity.name, + }) + } + confirmButtonText={t('Delete')} + errorText={t('Cannot remove provider')} + cancelButtonText={t('Cancel')} + /> + ); +}; +export interface ProviderActionsProps { + entity: MergedProvider; + variant?: 'kebab' | 'dropdown'; +} + +export const ProviderActions = ({ entity, variant = 'kebab' }: ProviderActionsProps) => { + const ActionsComponent = useMemo(() => createActions(variant), [variant]); + return ( + + {ActionsComponent} + + ); +}; + +const toIProviderObject = ({ + name, + namespace, + type, + url, + secretName, + kind, + apiVersion, +}: MergedProvider): IProviderObject => ({ + metadata: { + name, + namespace, + }, + spec: { type: type as ProviderType, url, secret: { name: secretName, namespace } }, + kind, + apiVersion, +}); + +const toIOpenShiftProvider = ( + { name, namespace, networkCount, selfLink = 'foo', type, uid, vmCount }, + object, +): IOpenShiftProvider => ({ + object, + name, + namespace, + networkCount, + selfLink, + type, + uid, + vmCount, +}); diff --git a/src/components/Filter/helpers.ts b/src/components/Filter/helpers.ts index 5a15b3b21..44f7ac1d5 100644 --- a/src/components/Filter/helpers.ts +++ b/src/components/Filter/helpers.ts @@ -7,3 +7,6 @@ export const toFieldFilter = ({ toLabel: toFieldLabel, filter: filterDef, }: Field): FieldFilter => ({ fieldId, toFieldLabel, filterDef }); + +export const fromI18nEnum = (i18nEnum: { [k: string]: (t: (k: string) => string) => string }) => + Object.entries(i18nEnum).map(([type, toLabel]) => ({ id: type, toLabel })); diff --git a/src/components/types.ts b/src/components/types.ts index bc14e90cf..406100e03 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -16,3 +16,6 @@ export interface Field { // eslint-disable-next-line @typescript-eslint/no-explicit-any comparator?: (a: any, b: any, locale: string) => number; } + +export const K8sConditionStatusValues = ['True', 'False', 'Unknown'] as const; +export type K8sConditionStatus = typeof K8sConditionStatusValues[number]; diff --git a/src/extensions/ProvidersWrapper.tsx b/src/extensions/ProvidersWrapper.tsx index 4e0a8c5d2..4efe44393 100644 --- a/src/extensions/ProvidersWrapper.tsx +++ b/src/extensions/ProvidersWrapper.tsx @@ -1,4 +1,4 @@ -import { ProvidersPage } from '@app/Providers/ProvidersPage'; +import ProvidersPage from 'src/Providers/ProvidersPage'; import withQueryClient from './QueryClientHoc'; diff --git a/src/extensions/UseMergedProviders.ts b/src/extensions/UseMergedProviders.ts new file mode 100644 index 000000000..a98c2cfe8 --- /dev/null +++ b/src/extensions/UseMergedProviders.ts @@ -0,0 +1,3 @@ +import { useMergedProviderActions } from 'src/Providers/providerActions'; + +export default useMergedProviderActions; diff --git a/src/extensions/actions.tsx b/src/extensions/actions.tsx new file mode 100644 index 000000000..61f888eee --- /dev/null +++ b/src/extensions/actions.tsx @@ -0,0 +1,39 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'src/internal/i18n'; + +import { ActionService } from '@openshift-console/dynamic-plugin-sdk'; +import { Dropdown, DropdownItem, DropdownToggle, KebabToggle } from '@patternfly/react-core'; + +export const createActions = (variant: 'kebab' | 'dropdown') => + function GenericActions({ actions }: ActionService) { + const { t } = useTranslation(); + const [isActionMenuOpen, setIsActionMenuOpen] = useState(false); + const isPlain = variant === 'kebab'; + const toggle = + variant === 'kebab' ? ( + + ) : ( + {t('Actions')} + ); + return ( + <> + setIsActionMenuOpen(!isActionMenuOpen)} + toggle={toggle} + isOpen={isActionMenuOpen} + isPlain={isPlain} + dropdownItems={actions.map(({ id, label, cta, disabled, disabledTooltip }) => ( + undefined} + isAriaDisabled={disabled} + tooltip={disabledTooltip} + > + {label} + + ))} + /> + + ); + }; diff --git a/src/internal/i18n/enums.ts b/src/internal/i18n/enums.ts new file mode 100644 index 000000000..722730342 --- /dev/null +++ b/src/internal/i18n/enums.ts @@ -0,0 +1,15 @@ +import { K8sConditionStatus } from '_/components/types'; + +import { ProviderType } from '@app/common/constants'; + +export const PROVIDERS: Record string) => string> = { + vsphere: (t) => t('VMware'), + ovirt: (t) => t('oVirt'), + openshift: (t) => t('KubeVirt'), +}; + +export const CONDITIONS: Record string) => string> = { + True: (t) => t('True'), + False: (t) => t('False'), + Unknown: (t) => t('Unknown'), +}; diff --git a/src/internal/i18n/index.ts b/src/internal/i18n/index.ts index e82230f1b..d92880912 100644 --- a/src/internal/i18n/index.ts +++ b/src/internal/i18n/index.ts @@ -1 +1,2 @@ +export * from './enums'; export * from './i18n'; diff --git a/src/internal/k8s/types.ts b/src/internal/k8s/types.ts index 89ccece69..63a7d1937 100644 --- a/src/internal/k8s/types.ts +++ b/src/internal/k8s/types.ts @@ -30,3 +30,8 @@ export type ProviderResource = { conditions?: Condition[]; }; } & K8sResourceCommon; + +export type ResourceConsolePageProps = { + kind: string; + namespace: string; +}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 9125e9645..9d8c2c445 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,3 +1,20 @@ +export const ACTIONS = 'actions'; export const NAME = 'name'; -export const UID = 'uid'; +export const READY = 'ready'; +export const CONNECTED = 'connected'; +export const INVENTORY = 'inventory'; +export const VALIDATED = 'validated'; +export const TYPE = 'type'; +export const URL = 'url'; export const NAMESPACE = 'namespace'; +export const KIND = 'kind'; +export const UID = 'uid'; +export const CLUSTER_COUNT = 'clusterCount'; +export const HOST_COUNT = 'hostCount'; +export const VM_COUNT = 'vmCount'; +export const NETWORK_COUNT = 'networkCount'; +export const STORAGE_COUNT = 'storageCount'; +export const SECRET_NAME = 'secretName'; +export const API_VERSION = 'apiVersion'; +export const DEFAULT_TRANSFER_NETWORK = 'defaultTransferNetwork'; +export const DEFAULT_TRANSFER_NETWORK_ANNOTATION = 'forklift.konveyor.io/defaultTransferNetwork'; diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 000000000..61872c6ff --- /dev/null +++ b/src/utils/fetch.ts @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; +import { ProviderResource } from 'src/internal/k8s'; + +import { MOCK_CLUSTER_PROVIDERS } from '@app/queries/mocks/providers.mock'; +import { useK8sWatchResource, WatchK8sResult } from '@openshift-console/dynamic-plugin-sdk'; + +const isMock = process.env.DATA_SOURCE === 'mock'; + +function useMockK8sWatchResource(resource, mockData: T[] = []): WatchK8sResult { + return [mockData, true, false]; +} + +function useRealK8sWatchResource({ kind, namespace, name }): WatchK8sResult { + return useK8sWatchResource({ + kind, + isList: true, + namespaced: true, + namespace, + name, + }); +} + +export const useMockableK8sWatchResource = isMock + ? useMockK8sWatchResource + : useRealK8sWatchResource; + +export const useProviders = ({ kind, namespace, name }) => { + const mock: ProviderResource[] = useMemo( + () => + MOCK_CLUSTER_PROVIDERS?.filter( + (provider) => !name || provider?.metadata?.name === name, + ) as ProviderResource[], + [name], + ); + return useMockableK8sWatchResource({ kind, namespace, name }, mock); +};