Skip to content

Commit

Permalink
Merge pull request #33 from rszwajko/providers
Browse files Browse the repository at this point in the history
Convert Providers screen to Console page
  • Loading branch information
yaacov authored Nov 20, 2022
2 parents 7f79949 + 01c2968 commit 21d3f62
Show file tree
Hide file tree
Showing 21 changed files with 1,224 additions and 39 deletions.
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)
);
});

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

0 comments on commit 21d3f62

Please sign in to comment.