Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Providers screen to Console page #33

Merged
merged 1 commit into from
Nov 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const config: Config.InitialOptions = {
moduleNameMapper: {
'\\.(css|less|scss|svg)$': '<rootDir>/src/__mocks__/dummy.ts',
'@console/*': '<rootDir>/src/__mocks__/dummy.ts',
'@openshift-console/*': '<rootDir>/src/__mocks__/dummy.ts',
'react-i18next': '<rootDir>/src/__mocks__/react-i18next.ts',
...pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/',
Expand Down
32 changes: 31 additions & 1 deletion locales/en/plugin__forklift-console-plugin.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,52 @@
{
"{{type}} provider {{name}} will no longer be selectable as a migration source.": "",
"{{type}} provider {{name}} will no longer be selectable as a migration target.": "",
"Actions": "",
"Add Provider": "",
"Cancel": "",
"Cannot remove provider": "",
"Clear all filters": "",
"Clusters": "",
"Delete": "",
"Delete Provider": "",
"Edit Provider": "",
"Endpoint": "",
"False": "",
"Filter by endpoint": "",
"Filter by name": "",
"Filter by namespace": "",
"Hosts": "",
"KubeVirt": "",
"Loading": "",
"Manage Columns": "",
"Mappings for Import": "",
"Name": "",
"Namespace": "",
"Networks": "",
"No information": "",
"No results found": "",
"No results match the filter criteria. Clear all filters and try again.": "",
"oVirt": "",
"Permanently delete provider?": "",
"Plans for Import": "",
"Providers": "",
"Providers for Import": "",
"Ready": "",
"Reorder": "",
"Restore default colums": "",
"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 Filter": "",
"Select migration network": "",
"Selected columns will be displayed in the table.": "",
"Storage": "",
"Table column management": "",
"Unable to retrieve data": ""
"The host provider cannot be edited": "",
"This provider cannot be edited because it has running migrations": "",
"True": "",
"Type": "",
"Unable to retrieve data": "",
"Unknown": "",
"VMs": "",
"VMware": ""
}
6 changes: 2 additions & 4 deletions pkg/web/src/app/Providers/HostsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@patternfly/react-core';
import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing';
import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core';
import { Link, useRouteMatch } from 'react-router-dom';
import { useRouteMatch } from 'react-router-dom';
import { VMwareProviderHostsTable } from './components/VMwareProviderHostsTable';
import PlusCircleIcon from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon';
import { useHostsQuery, useInventoryProvidersQuery } from '@app/queries';
Expand Down Expand Up @@ -45,9 +45,7 @@ export const HostsPage: React.FunctionComponent = () => {
<LevelItem>
<Breadcrumb>
<BreadcrumbItem>Providers</BreadcrumbItem>
<BreadcrumbItem>
<Link to={`${PATH_PREFIX}/providers/vsphere`}>{PROVIDER_TYPE_NAMES.vsphere}</Link>
</BreadcrumbItem>
<BreadcrumbItem>{PROVIDER_TYPE_NAMES.vsphere}</BreadcrumbItem>
<BreadcrumbItem>{match?.params.providerName}</BreadcrumbItem>
<BreadcrumbItem isActive>Hosts</BreadcrumbItem>
</Breadcrumb>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,6 +16,22 @@ 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;
}): boolean =>
!!plans
.filter((plan) => hasCondition(plan.status?.conditions || [], 'Executing'))
.find((runningPlan) => {
const { source, destination } = runningPlan.spec.provider;
return (
isSameResource(providerMetadata, source) || isSameResource(providerMetadata, destination)
);
});
rszwajko marked this conversation as resolved.
Show resolved Hide resolved

interface IProviderActionsDropdownProps {
provider: ICorrelatedProvider<InventoryProvider>;
providerType: ProviderType;
Expand All @@ -37,15 +58,15 @@ export const ProviderActionsDropdown: React.FunctionComponent<IProviderActionsDr
}
});

const hasRunningMigration = !!plans
.filter((plan) => 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 (
<>
Expand All @@ -58,13 +79,7 @@ export const ProviderActionsDropdown: React.FunctionComponent<IProviderActionsDr
<ConditionalTooltip
key="edit"
isTooltipEnabled={isEditDeleteDisabled}
content={
!provider.spec.url
? 'The host provider cannot be edited'
: hasRunningMigration
? 'This provider cannot be edited because it has running migrations'
: ''
}
content={isEditDeleteDisabled ? disabledEditTooltip : ''}
>
<DropdownItem
aria-label="Edit"
Expand All @@ -80,13 +95,7 @@ export const ProviderActionsDropdown: React.FunctionComponent<IProviderActionsDr
<ConditionalTooltip
key="remove"
isTooltipEnabled={isEditDeleteDisabled}
content={
!provider.spec.url
? 'The host provider cannot be removed'
: hasRunningMigration
? 'This provider cannot be removed because it has running migrations'
: ''
}
content={isEditDeleteDisabled ? disabledDeleteTooltip : ''}
>
<DropdownItem
aria-label="Remove"
Expand Down
39 changes: 39 additions & 0 deletions src/components/ActionServiceDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useState } from 'react';
import { useTranslation } from 'src/utils/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' ? (
<KebabToggle onToggle={setIsActionMenuOpen} />
) : (
<DropdownToggle onToggle={setIsActionMenuOpen}>{t('Actions')}</DropdownToggle>
);
return (
<>
<Dropdown
position="right"
onSelect={() => setIsActionMenuOpen(!isActionMenuOpen)}
toggle={toggle}
isOpen={isActionMenuOpen}
isPlain={isPlain}
dropdownItems={actions.map(({ id, label, cta, disabled, disabledTooltip }) => (
<DropdownItem
key={id}
onClick={typeof cta === 'function' ? cta : () => undefined}
isAriaDisabled={disabled}
tooltip={disabledTooltip}
>
{label}
</DropdownItem>
))}
/>
</>
);
};
3 changes: 3 additions & 0 deletions src/components/Filter/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
3 changes: 3 additions & 0 deletions src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
115 changes: 115 additions & 0 deletions src/modules/Providers/ProviderRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { JSXElementConstructor } from 'react';
import { Link } from 'react-router-dom';
import { RowProps } from 'src/components/TableView';
import * as C from 'src/utils/constants';
import { CONDITIONS, PROVIDERS } from 'src/utils/enums';
import { useTranslation } from 'src/utils/i18n';

import { PATH_PREFIX } from '@app/common/constants';
import { StatusIcon } from '@migtools/lib-ui';
import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk';
import { Button, Popover } from '@patternfly/react-core';
import { DatabaseIcon, NetworkIcon, OutlinedHddIcon } from '@patternfly/react-icons';
import { Td, Tr } from '@patternfly/react-table';

import { MergedProvider } from './data';
import { ProviderActions } from './providerActions';

interface CellProps {
value: string;
entity: MergedProvider;
t?: (k: string) => 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) ?? t('Unknown');
return (
<Popover
hasAutoWidth
bodyContent={
<div>
{existingConditions.length > 0
? existingConditions.map(({ message, status }) => {
return <StatusIcon key={message} status={toState(status)} label={message} />;
})
: t('No information')}
</div>
}
>
<Button variant="link" isInline aria-label={label}>
<StatusIcon status={toState(value)} label={label} />
</Button>
</Popover>
);
};

const TextCell = ({ value }: { value: string }) => <>{value ?? ''}</>;

const TextWithIcon = ({ value, Icon }: { value: string; Icon: JSXElementConstructor<unknown> }) => (
<>
{value && (
<>
<Icon /> <TextCell value={value} />
</>
)}
</>
);

const ProviderLink = ({ value, entity }: CellProps) => (
<ResourceLink kind={entity.kind} name={value} namespace={entity?.namespace} />
);

const HostCell = ({ value, entity: { ready, name, type } }: CellProps) => (
<>
{ready === 'True' && value && type === 'vsphere' ? (
<Link to={`${PATH_PREFIX}/providers/vsphere/${name}`}>
<TextWithIcon Icon={OutlinedHddIcon} value={value} />
</Link>
) : (
<TextWithIcon Icon={OutlinedHddIcon} value={value} />
)}
</>
);

const cellCreator: Record<string, (props: CellProps) => JSX.Element> = {
[C.NAME]: ProviderLink,
[C.READY]: StatusCell,
[C.URL]: TextCell,
[C.TYPE]: ({ value, t }: CellProps) => <TextCell value={PROVIDERS?.[value]?.(t)} />,
[C.NAMESPACE]: ({ value }: CellProps) => <ResourceLink kind="Namespace" name={value} />,
[C.ACTIONS]: ProviderActions,
[C.NETWORK_COUNT]: ({ value }: CellProps) => <TextWithIcon Icon={NetworkIcon} value={value} />,
[C.STORAGE_COUNT]: ({ value }: CellProps) => <TextWithIcon Icon={DatabaseIcon} value={value} />,
[C.HOST_COUNT]: HostCell,
};

const ProviderRow = ({ columns, entity }: RowProps<MergedProvider>) => {
const { t } = useTranslation();
return (
<Tr>
{columns.map(({ id, toLabel }) => (
<Td key={id} dataLabel={toLabel(t)}>
{cellCreator?.[id]?.({
value: entity[id],
entity,
t,
}) ?? <TextCell value={String(entity[id] ?? '')} />}
</Td>
))}
</Tr>
);
};

export default ProviderRow;
Loading