Skip to content

Commit

Permalink
Add action column and error handling
Browse files Browse the repository at this point in the history
Changes:
1. per row actions - a kebab button with list of actions
2. implement Delete Provider action
3. use 'kind' value from the entity
4. handle border cases via specilized  EmptyState:
  a) no items
  b) no items due to filtering
  c) loading data
  d) error retrieving data
  • Loading branch information
rszwajko committed Sep 27, 2022
1 parent 3ef4031 commit 7ded997
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 48 deletions.
15 changes: 14 additions & 1 deletion locales/en/plugin__forklift-console-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,48 @@
"AddProvider": "Add Provider",
"Any": "Any",
"Cancel": "Cancel",
"CannotDeleteProvider": "Cannot remove provider",
"ClearAllFilters": "Clear all filters",
"Clusters": "Clusters",
"Delete": "Delete",
"DeleteProvider": "Delete Provider",
"EditProvider": "Edit Provider",
"False": "False",
"FilterByName": "Filter by name",
"FilterByNamespace": "Filter by namespace",
"FilterByStatus": "Filter by status",
"FilterByType": "Filter by type",
"FilterByUrl": "Filter by endpoint",
"Hosts": "Hosts",
"Loading": "Loading...",
"ManageColumns": "Manage columns",
"Mappings for VM Import": "Mappings for VM Import",
"Name": "Name",
"Namespace": "Namespace",
"Networks": "Newtworks",
"Networks": "Networks",
"NoResultsFound": "No results found",
"NoResultsMatchFilter": "No results match the filter criteria. Clear all filters and try again.",
"Openshift": "Openshift",
"Ovirt": "oVirt",
"PermanentlyDeleteProvider": "Permanently delete provider?",
"Plans for VM Import": "Plans for VM Import",
"ProviderNoLongerSelectableAsSource": "{{type}} provider {{name}} will no longer be selectable as a migration source.",
"ProviderNoLongerSelectableAsTarget": "{{type}} provider {{name}} will no longer be selectable as a migration target.",
"Providers": "Providers",
"Providers for VM Import": "Providers for VM Import",
"Ready": "Ready",
"Reorder": "Reorder",
"RestoreDefaultColums": "Restore default colums",
"Save": "Save",
"SelectFilter": "Select Filter",
"SelectMigrationNetwork": "Select migration network",
"Status": "Status",
"Storage": "Storage",
"Success": "Success",
"TableColumnManagement": "Table column management",
"Type": "Type",
"True": "True",
"UnableToRetrieve": "Unable to retrieve data",
"Unknown": "Unknown",
"Url": "Endpoint",
"Virtualization": "Virtualization",
Expand Down
108 changes: 87 additions & 21 deletions src/Providers/ProviderRow.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import React from 'react';
import React, { useState } from 'react';
import { RowProps } from 'src/components/TableView';
import { useTranslation } from 'src/internal/i18n';
import { NAME, NAMESPACE, READY, TYPE, URL } from 'src/utils/constants';

import { ConfirmModal } from '@app/common/components/ConfirmModal';
import { ProviderType } from '@app/common/constants';
import { useDeleteProviderMutation } from '@app/queries';
import { StatusIcon } from '@migtools/lib-ui';
import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk';
import { Button, Popover } from '@patternfly/react-core';
import {
Button,
Dropdown,
DropdownItem,
KebabToggle,
Popover,
} from '@patternfly/react-core';
import { Td, Tr } from '@patternfly/react-table';

import { MergedProvider } from './data';

interface CellProps {
value: string;
entity: MergedProvider;
kind: string;
}
const StatusCell = ({ value, entity: { conditions } }: CellProps) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -48,8 +56,8 @@ const StatusCell = ({ value, entity: { conditions } }: CellProps) => {

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

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

const cellCreator = {
Expand All @@ -62,21 +70,79 @@ const cellCreator = {
),
};

const ProviderRow = (kind: string) =>
function ProviderRow({ columns, entity }: RowProps<MergedProvider>) {
return (
<Tr>
{columns.map(({ id }) => (
<Td key={id} dataLabel="foo">
{cellCreator?.[id]?.({
kind,
value: entity[id],
entity,
}) ?? <TextCell value={String(entity[id] ?? '')} />}
</Td>
))}
</Tr>
);
};
const ProviderRow = ({ columns, entity }: RowProps<MergedProvider>) => {
const { t } = useTranslation();
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
console.warn('Modal open?', isDeleteModalOpen, entity);
const toggleDeleteModal = () => setIsDeleteModalOpen(!isDeleteModalOpen);
const deleteProviderMutation = useDeleteProviderMutation(
entity.type as ProviderType,
toggleDeleteModal,
);
const editProvider = () => '';
const selectNetwork = () => '';
const isTarget = (type: ProviderType) => type !== 'openshift';
return (
<Tr>
{columns.map(({ id }) => (
<Td key={id} dataLabel="foo">
{cellCreator?.[id]?.({
value: entity[id],
entity,
}) ?? <TextCell value={String(entity[id] ?? '')} />}
</Td>
))}
<Td modifier="fitContent">
<Dropdown
position="right"
onSelect={() => setIsActionMenuOpen(!isActionMenuOpen)}
toggle={<KebabToggle onToggle={setIsActionMenuOpen} />}
isOpen={isActionMenuOpen}
isPlain
dropdownItems={[
<DropdownItem key="edit" onClick={editProvider}>
{t('EditProvider')}
</DropdownItem>,
<DropdownItem key="delete" onClick={toggleDeleteModal}>
{t('DeleteProvider')}
</DropdownItem>,
<DropdownItem key="selectNetwork" onClick={selectNetwork}>
{t('SelectMigrationNetwork')}
</DropdownItem>,
]}
/>
<ConfirmModal
confirmationVariant="danger"
position="top"
isOpen={isDeleteModalOpen}
toggleOpen={toggleDeleteModal}
mutateFn={() =>
deleteProviderMutation.mutate({
metadata: {
name: entity.name,
namespace: entity.namespace,
},
spec: { type: entity.type as ProviderType },
kind: '',
apiVersion: '',
})
}
mutateResult={deleteProviderMutation}
title={t('PermanentlyDeleteProvider')}
body={t(
isTarget(entity.type as ProviderType)
? 'ProviderNoLongerSelectableAsTarget'
: 'ProviderNoLongerSelectableAsSource',
{ type: entity.type, name: entity.name },
)}
confirmButtonText={t('Delete')}
errorText={t('CannotDeleteProvider')}
cancelButtonText={t('Cancel')}
/>
</Td>
</Tr>
);
};

export default ProviderRow;
108 changes: 91 additions & 17 deletions src/Providers/ProvidersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,24 @@ import {
VM_COUNT,
} from 'src/utils/constants';

import { RedExclamationCircleIcon } from '@openshift-console/dynamic-plugin-sdk';
import {
Button,
EmptyState,
EmptyStateBody,
EmptyStateIcon,
EmptyStatePrimary,
Level,
LevelItem,
PageSection,
Spinner,
Title,
Toolbar,
ToolbarContent,
ToolbarToggleGroup,
} from '@patternfly/react-core';
import { FilterIcon } from '@patternfly/react-icons';
import { SearchIcon } from '@patternfly/react-icons';

import { MergedProvider, useProvidersWithInventory } from './data';
import ProviderRow from './ProviderRow';
Expand Down Expand Up @@ -155,10 +162,15 @@ export const ProvidersPage = ({ namespace, kind }: ProvidersPageProps) => {
namespace,
});
const [selectedFilters, setSelectedFilters] = useState({});
const clearAllFilters = () => setSelectedFilters({});
const [fields, setFields] = useFields(namespace, fieldsMetadata);

console.error('Providers', providers, fields, namespace, kind);

const filteredProviders = providers.filter(
createMetaMatcher(selectedFilters, fields),
);

return (
<>
<PageSection variant="light">
Expand All @@ -174,7 +186,10 @@ export const ProvidersPage = ({ namespace, kind }: ProvidersPageProps) => {
</Level>
</PageSection>
<PageSection>
<Toolbar clearAllFilters={() => setSelectedFilters({})}>
<Toolbar
clearAllFilters={clearAllFilters}
clearFiltersButtonText={t('ClearAllFilters')}
>
<ToolbarContent>
<ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint="xl">
<PrimaryFilters
Expand All @@ -200,28 +215,87 @@ export const ProvidersPage = ({ namespace, kind }: ProvidersPageProps) => {
</ToolbarToggleGroup>
</ToolbarContent>
</Toolbar>

{loaded && error && <Errors />}
{!loaded && <Loading />}
{loaded && !error && (
<TableView<MergedProvider>
entities={providers.filter(
createMetaMatcher(selectedFilters, fields),
)}
allColumns={fields}
visibleColumns={fields.filter(({ isVisible }) => isVisible)}
aria-label={t('Providers')}
Row={ProviderRow(kind)}
/>
)}
<TableView<MergedProvider>
entities={filteredProviders}
allColumns={fields}
visibleColumns={fields.filter(({ isVisible }) => isVisible)}
aria-label={t('Providers')}
Row={ProviderRow}
>
{[
!loaded && <Loading />,
loaded && error && <ErrorState />,
loaded && !error && providers.length == 0 && <NoResultsFound />,
loaded &&
!error &&
filteredProviders.length === 0 &&
providers.length > 0 && (
<NoResultsMatchFilter clearAllFilters={clearAllFilters} />
),
].filter(Boolean)}
</TableView>
</PageSection>
</>
);
};

const Errors = () => <> Erorrs!</>;
const ErrorState = () => {
const { t } = useTranslation();
return (
<EmptyState>
<EmptyStateIcon icon={RedExclamationCircleIcon} />
<Title headingLevel="h4" size="lg">
{t('UnableToRetrieve')}
</Title>
</EmptyState>
);
};

const Loading = () => <> Loading!</>;
const Loading = () => {
const { t } = useTranslation();
return (
<EmptyState>
<EmptyStateIcon variant="container" component={Spinner} />
<Title size="lg" headingLevel="h4">
{t('Loading')}
</Title>
</EmptyState>
);
};

const NoResultsFound = () => {
const { t } = useTranslation();
return (
<EmptyState>
<EmptyStateIcon icon={SearchIcon} />
<Title size="lg" headingLevel="h4">
{t('NoResultsFound')}
</Title>
</EmptyState>
);
};

const NoResultsMatchFilter = ({
clearAllFilters,
}: {
clearAllFilters: () => void;
}) => {
const { t } = useTranslation();
return (
<EmptyState>
<EmptyStateIcon icon={SearchIcon} />
<Title size="lg" headingLevel="h4">
{t('NoResultsFound')}
</Title>
<EmptyStateBody>{t('NoResultsMatchFilter')}</EmptyStateBody>
<EmptyStatePrimary>
<Button variant="link" onClick={clearAllFilters}>
{t('ClearAllFilters')}
</Button>
</EmptyStatePrimary>
</EmptyState>
);
};

type ProvidersPageProps = {
kind: string;
Expand Down
4 changes: 4 additions & 0 deletions src/Providers/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ProviderResource } from 'src/internal/k8s';
import {
CONNECTED,
INVENTORY,
KIND,
NAME,
NAMESPACE,
READY,
Expand Down Expand Up @@ -36,6 +37,7 @@ interface SupportedConditions {
}

interface FlattenedProvider {
[KIND]: string;
[NAME]: string;
[NAMESPACE]: string;
[URL]: string;
Expand Down Expand Up @@ -75,6 +77,7 @@ const mergeData = (
metadata: { name, namespace, uid } = {},
status: { conditions = [] } = {},
spec: { url, type } = {},
kind,
}): [
FlattenedProvider,
IVMwareProvider & IRHVProvider & IOpenShiftProvider,
Expand All @@ -86,6 +89,7 @@ const mergeData = (
url,
type,
uid,
kind,
},
inventory?.[type].find(({ uid: otherUid }) => otherUid === uid) ?? {},
Object.fromEntries(
Expand Down
Loading

0 comments on commit 7ded997

Please sign in to comment.