Skip to content

Commit

Permalink
Convert Providers screen to Console page
Browse files Browse the repository at this point in the history
Functional changes:
1. display all providers in one table
2. use only 'Ready' condition to describe the state of the provider

Components created:
1. primary filters component for displaying few (1-3) most important
   filters. The filters are grouped but displayed independently.
2. attribute-value filter implementation for grouping all other filters
   in a space efficient way
3. default filter types:
  a) free text filter
  - substring search based on multiple terms
  - search terms confirmed by 'Enter' key
  b) enum based filter
  - exact match based on checkboxes selected
4. generic table component providing sorting capabilities

Reference-Url: oVirt/ovirt-web-ui#1600
Reference-Url: oVirt/ovirt-web-ui#1592
Reference-Url: https://www.patternfly.org/v4/guidelines/filters#attribute-value-filter
  • Loading branch information
rszwajko committed Sep 22, 2022
1 parent 5024ae1 commit d8ef4af
Show file tree
Hide file tree
Showing 11 changed files with 874 additions and 2 deletions.
30 changes: 30 additions & 0 deletions console-extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,36 @@
]
}
},
{
"type": "console.navigation/resource-ns",
"properties": {
"id": "providers",
"section": "virtualization",
"name": "%plugin__forklift-console-plugin~Providers for VM Import%",
"model": {
"group": "forklift.konveyor.io",
"kind": "Provider",
"version": "v1beta1"
},
"dataAttributes": {
"data-quickstart-id": "qs-nav-providers",
"data-test-id": "providers-nav-item"
}
}
},
{
"type": "console.page/resource/list",
"properties": {
"component": {
"$codeRef": "ProvidersRes"
},
"model": {
"group": "forklift.konveyor.io",
"kind": "Provider",
"version": "v1beta1"
}
}
},
{
"type": "console.navigation/href",
"properties": {
Expand Down
19 changes: 19 additions & 0 deletions locales/en/plugin__forklift-console-plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
{
"AddProvider": "Add Provider",
"Any": "Any",
"FilterByName": "Filter by name",
"FilterByStatus": "Filter by status",
"FilterByType": "Filter by type",
"FilterByUrl": "Filter by endpoint",
"Mappings for VM Import": "Mappings for VM Import",
"Name": "Name",
"No": "No",
"Openshift": "Openshift",
"Ovirt": "oVirt",
"Plans for VM Import": "Plans for VM Import",
"Providers for VM Import": "Providers for VM Import",
"Virtualization": "Virtualization"
"Ready": "Ready",
"SelectFilter": "Select Filter",
"Status": "Status",
"Success": "Success",
"Type": "Type",
"Url": "Endpoint",
"Virtualization": "Virtualization",
"Vsphere": "vSphere",
"Yes": "Yes"
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,11 @@
"MappingsPage": "./extensions/MappingsWrapper",
"HostsPage": "./extensions/HostsPageWrapper",
"PlanWizard": "./extensions/PlanWizardWrapper",
"VMMigrationDetails": "./extensions/VMMigrationDetailsWrapper"
"VMMigrationDetails": "./extensions/VMMigrationDetailsWrapper",
"ProvidersRes": "./Providers/ProvidersPage"
},
"dependencies": {
"@console/pluginAPI": "*"
}
}
}
}
178 changes: 178 additions & 0 deletions src/Providers/ProvidersPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React, { useState } from 'react';
import { useTranslation } from 'src/internal/i18n';
import { ProviderResource } from 'src/internal/k8s';

import { MOCK_CLUSTER_PROVIDERS } from '@app/queries/mocks/providers.mock';
import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
import {
Button,
Level,
LevelItem,
PageSection,
Title,
Toolbar,
ToolbarContent,
ToolbarToggleGroup,
} from '@patternfly/react-core';
import { FilterIcon } from '@patternfly/react-icons/dist/esm/icons';

import AttributeValueFilter from './components/AttributeValueFilter';
import EnumFilter from './components/EnumFilter';
import FreetextFilter from './components/FreetextFilter';
import PrimaryFilters from './components/PrimaryFilters';
import ProviderRow from './components/ProviderRow';
import { createMetaMatcher, Field } from './components/shared';
import { NAME, READY, TYPE, URL } from './components/shared';
import TableView from './components/TableView';

const isMock = process.env.DATA_SOURCE === 'mock';

const useProviders = ({ kind, namespace }) => {
const [providers, loaded, error] = isMock
? [MOCK_CLUSTER_PROVIDERS, true, false]
: useK8sWatchResource<ProviderResource[]>({
kind,
isList: true,
namespaced: true,
namespace,
});

// const inventoryProvidersQuery = useInventoryProvidersQuery();
// providers.map(p => enhanceWithInventory(inventoryProvidersQuery))

// const allErrorTitles = [
// 'Cannot load providers from cluster API',
// 'Cannot load providers from inventory API',
// ];

return [providers, loaded, error];
};

const fields: Field[] = [
{
id: NAME,
tKey: 'plugin__forklift-console-plugin~Name',
filter: {
type: 'freetext',
placeholderKey: 'plugin__forklift-console-plugin~FilterByName',
},
sortable: true,
toValue: (provider) => provider?.metadata?.name ?? '',
},
{
id: READY,
tKey: 'plugin__forklift-console-plugin~Ready',
filter: {
type: 'enum',
primary: true,
placeholderKey: 'plugin__forklift-console-plugin~Ready',
values: [
{ id: 'Yes', tKey: 'plugin__forklift-console-plugin~Yes' },
{ id: 'No', tKey: 'plugin__forklift-console-plugin~No' },
],
},
sortable: true,
toValue: (provider) =>
provider?.status?.conditions?.find(({ type }) => type === 'Ready')
?.status === 'True'
? 'Yes'
: 'No',
},
{
id: URL,
tKey: 'plugin__forklift-console-plugin~Url',
filter: {
type: 'freetext',
placeholderKey: 'plugin__forklift-console-plugin~FilterByUrl',
},
sortable: true,
toValue: (provider) => provider?.spec?.url ?? '',
},
{
id: TYPE,
tKey: 'plugin__forklift-console-plugin~Type',
filter: {
type: 'enum',
primary: true,
placeholderKey: 'plugin__forklift-console-plugin~Type',
values: [
{ id: 'vsphere', tKey: 'plugin__forklift-console-plugin~Vsphere' },
{ id: 'ovirt', tKey: 'plugin__forklift-console-plugin~Ovirt' },
{ id: 'openshift', tKey: 'plugin__forklift-console-plugin~Openshift' },
],
},
sortable: true,
toValue: (provider) => provider?.spec?.type ?? '',
},
];

export const ProvidersPage = ({ namespace, kind }: ProvidersPageProps) => {
const { t } = useTranslation();
const [providers, loaded, error] = useProviders({ kind, namespace });
const [selectedFilters, setSelectedFilters] = useState({});

console.error('Providers', providers, fields, selectedFilters);

return (
<>
<PageSection variant="light">
<Level>
<LevelItem>
<Title headingLevel="h1">
{t('plugin__forklift-console-plugin~Providers')}
</Title>
</LevelItem>
<LevelItem>
<Button variant="primary" onClick={() => ''}>
{t('plugin__forklift-console-plugin~AddProvider')}
</Button>
</LevelItem>
</Level>
</PageSection>
<PageSection>
<Toolbar clearAllFilters={() => setSelectedFilters({})}>
<ToolbarContent>
<ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint="xl">
<PrimaryFilters
filterTypes={fields.filter((field) => field.filter.primary)}
onFilterUpdate={setSelectedFilters}
selectedFilters={selectedFilters}
supportedFilters={{ enum: EnumFilter }}
/>
<AttributeValueFilter
filterTypes={fields.filter((field) => !field.filter.primary)}
onFilterUpdate={setSelectedFilters}
selectedFilters={selectedFilters}
supportedFilters={{ freetext: FreetextFilter }}
/>
</ToolbarToggleGroup>
</ToolbarContent>
</Toolbar>

{loaded && error && <Errors />}
{!loaded && <Loading />}
{loaded && !error && (
<TableView<ProviderResource>
resources={providers.filter(
createMetaMatcher(selectedFilters, fields),
)}
fields={fields}
aria-label={t('plugin__forklift-console-plugin~Providers')}
Row={ProviderRow}
/>
)}
</PageSection>
</>
);
};

const Errors = () => <> Erorrs!</>;

const Loading = () => <> Loading!</>;

type ProvidersPageProps = {
kind: string;
namespace: string;
};

export default ProvidersPage;
113 changes: 113 additions & 0 deletions src/Providers/components/AttributeValueFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useState } from 'react';
import { useTranslation } from 'src/internal/i18n';

import {
Select,
SelectOption,
SelectOptionObject,
SelectVariant,
ToolbarGroup,
ToolbarItem,
} from '@patternfly/react-core';

import { FilterDef } from './shared';

interface IdOption extends SelectOptionObject {
id: string;
}

const toSelectOption = (id: string, label: string): IdOption => ({
id,
compareTo: (other: IdOption): boolean => id === other?.id,
toString: () => label,
});

const AttributeValueFilter = ({
selectedFilters,
onFilterUpdate,
filterTypes,
supportedFilters = {},
}: MetaFilterProps) => {
const { t } = useTranslation();
const [currentFilterType, setCurrentFilterType] = useState(filterTypes[0]);
const [expanded, setExpanded] = useState(false);

const selectOptionToFilter = (selectedId) =>
filterTypes.find(({ id }) => id === selectedId) ?? currentFilterType;

const onFilterTypeSelect = (event, value, isPlaceholder) => {
if (!isPlaceholder) {
setCurrentFilterType(selectOptionToFilter(value?.id));
setExpanded(!expanded);
}
};

return (
<ToolbarGroup variant="filter-group">
<ToolbarItem>
<Select
onSelect={onFilterTypeSelect}
onToggle={setExpanded}
isOpen={expanded}
variant={SelectVariant.single}
aria-label={t('SelectFilter')}
selections={toSelectOption(
currentFilterType.id,
t(currentFilterType.tKey),
)}
>
{filterTypes.map(({ id, tKey }) => (
<SelectOption key={id} value={toSelectOption(id, t(tKey))} />
))}
</Select>
</ToolbarItem>

{filterTypes.map(({ id, tKey: fieldKey, filter }) => {
const FieldFilter = supportedFilters[filter.type];
return (
FieldFilter && (
<FieldFilter
filterId={id}
onFilterUpdate={(values) =>
onFilterUpdate({
...selectedFilters,
[id]: values,
})
}
placeholderLabel={t(filter.placeholderKey)}
selectedFilters={selectedFilters[id] ?? []}
showFilter={currentFilterType?.id === id}
title={t(filter.tKey ?? fieldKey)}
supportedValues={filter.values}
/>
)
);
})}
</ToolbarGroup>
);
};

export interface FieldFilterProps {
filterId: string;
onFilterUpdate(values: string[]);
placeholderLabel: string;
selectedFilters: string[];
showFilter: boolean;
title: string;
supportedValues?: { id: string; tKey?: string }[];
}

export interface MetaFilterProps {
selectedFilters: { [id: string]: string[] };
filterTypes: {
id: string;
tKey: string;
filter: FilterDef;
}[];
onFilterUpdate(filters: { [id: string]: string[] }): void;
supportedFilters: {
[type: string]: (props: FieldFilterProps) => JSX.Element;
};
}

export default AttributeValueFilter;
Loading

0 comments on commit d8ef4af

Please sign in to comment.