Skip to content

Commit

Permalink
Add CellMapper to StandardPage
Browse files Browse the repository at this point in the history
Switch to CellMapper in VM List pages and VSPhere Hosts List.

Cell mapper allows enhancing the row by adding cells before or after the
main content. Main use case is adding technical columns i.e. for
selection or with actions.

Signed-off-by: Radoslaw Szwajkowski <[email protected]>
  • Loading branch information
rszwajko committed Jan 2, 2024
1 parent 31e5d8a commit f3154da
Show file tree
Hide file tree
Showing 17 changed files with 124 additions and 107 deletions.
1 change: 1 addition & 0 deletions packages/common/src/components/TableView/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export * from './ManageColumnsToolbarItem';
export * from './sort';
export * from './TableView';
export * from './types';
export * from './withTr';
// @endindex
15 changes: 15 additions & 0 deletions packages/common/src/components/TableView/withTr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';

import { Tr } from '@patternfly/react-table';

import { RowProps } from './types';

export function withTr<T>(Component: React.FC<RowProps<T>>) {
const Enhanced = (props: RowProps<T>) => (
<Tr>
<Component {...props} />
</Tr>
);
Enhanced.displayName = `${Component.displayName || 'Component'}WithTr`;
return Enhanced;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,31 @@ import { useForkliftTranslation } from 'src/utils/i18n';
import {
AttributeValueFilter,
createMetaMatcher,
DEFAULT_PER_PAGE,
DefaultHeader,
DefaultRow,
defaultSupportedFilters,
defaultValueMatchers,
ErrorState,
FilterGroup,
FilterRenderer,
GlobalActionToolbarProps,
toFieldFilter,
useUrlFilters,
ValueMatcher,
} from '@kubev2v/common';
import {
DEFAULT_PER_PAGE,
ErrorState,
Loading,
NoResultsFound,
NoResultsMatchFilter,
ResourceField,
RowProps,
TableView,
TableViewHeaderProps,
toFieldFilter,
useFields,
usePagination,
UserSettings,
useSort,
useUrlFilters,
ValueMatcher,
withTr,
} from '@kubev2v/common';
import { DefaultHeader, RowProps, TableView, TableViewHeaderProps, useSort } from '@kubev2v/common';
import { ResourceField } from '@kubev2v/common';
import { DefaultRow } from '@kubev2v/common';
import {
Level,
LevelItem,
Expand Down Expand Up @@ -98,6 +101,12 @@ export interface StandardPageProps<T> {
*/
RowMapper?: FC<RowProps<T>>;

/**
* (optional) Maps entity to a list of cells (without wrapping them in <Tr>).
* If present, it is used instead of RowMapper.
*/
CellMapper?: FC<RowProps<T>>;

/**
* (optional) Maps field list to table header.
* Defaults to all visible fields.
Expand Down Expand Up @@ -166,6 +175,7 @@ export function StandardPage<T>({
namespace,
dataSource: [flatData, loaded, error],
RowMapper = DefaultRow<T>,
CellMapper,
title,
addButton,
fieldsMetadata,
Expand Down Expand Up @@ -298,7 +308,7 @@ export function StandardPage<T>({
entities={showPagination ? pageData : filteredData}
visibleColumns={fields.filter(({ isVisible, isHidden }) => isVisible && !isHidden)}
aria-label={title}
Row={RowMapper}
Row={CellMapper ? withTr(CellMapper) : RowMapper}
Header={HeaderMapper}
activeSort={activeSort}
setActiveSort={setActiveSort}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import React, { useState } from 'react';
import React, { FC, useState } from 'react';

import {
DefaultHeader,
GlobalActionToolbarProps,
RowProps,
TableViewHeaderProps,
} from '@kubev2v/common';
import { Th } from '@patternfly/react-table';
import { Td, Th } from '@patternfly/react-table';

import StandardPage, { StandardPageProps } from './StandardPage';

export function withRowSelection<T>({ RowMapper, isSelected, toggleSelectFor }) {
export function withRowSelection<T>({ CellMapper, isSelected, toggleSelectFor, canSelect }) {
const Enhanced = (props: RowProps<T>) => (
<RowMapper
{...props}
isSelected={isSelected(props.resourceData)}
// the check box will be always visible
// with current interface disabling/hiding needs to be implemented at the row mapper level
toggleSelect={() => toggleSelectFor([props.resourceData])}
/>
<>
<Td
select={{
rowIndex: props.resourceIndex,
onSelect: () => toggleSelectFor([props.resourceData]),
isSelected: isSelected(props.resourceData),
disable: !canSelect(props.resourceData),
}}
/>
<CellMapper {...props} />
</>
);
Enhanced.displayName = `${RowMapper.displayName || 'RowMapper'}WithSelection`;
Enhanced.displayName = `${CellMapper.displayName || 'CellMapper'}WithSelection`;
return Enhanced;
}

Expand Down Expand Up @@ -55,21 +59,19 @@ export interface IdBasedSelectionProps<T> {
* @returns true if items can be selected, false otherwise
*/
canSelect: (item: T) => boolean;

/**
* global toolbar actions
*/
actions: React.FC<GlobalActionToolbarProps<T> & { selectedIds: string[] }>[];
}

export type GlobalActionWithSelection<T> = GlobalActionToolbarProps<T> & {
selectedIds: string[];
};

/**
* Adds ID based multi selection to StandardPage component.
* Contract:
* 1. provided row mapper renders check boxes when needed
* 2. IDs provided with toId() function are unique and constant in time
* 3. check box status at row level does not depend from other rows and can be calculated from the item via canSelect() function
* 1. IDs provided with toId() function are unique and constant in time
* 2. check box status at row level does not depend from other rows and can be calculated from the item via canSelect() function
*/
export function withIdBasedSelection<T>({ toId, canSelect, actions }: IdBasedSelectionProps<T>) {
export function withIdBasedSelection<T>({ toId, canSelect }: IdBasedSelectionProps<T>) {
const Enhanced = (props: StandardPageProps<T>) => {
const [selectedIds, setSelectedIds]: [string[], (selected: string[]) => void] = useState([]);
const isSelected = (item: T) => selectedIds.includes(toId(item));
Expand All @@ -84,8 +86,9 @@ export function withIdBasedSelection<T>({ toId, canSelect, actions }: IdBasedSel
return (
<StandardPage
{...props}
RowMapper={withRowSelection({
RowMapper: props.RowMapper,
CellMapper={withRowSelection({
CellMapper: props.CellMapper,
canSelect,
isSelected,
toggleSelectFor,
})}
Expand All @@ -95,11 +98,13 @@ export function withIdBasedSelection<T>({ toId, canSelect, actions }: IdBasedSel
isSelected,
toggleSelectFor,
})}
GlobalActionToolbarItems={actions.map((Action) => {
const ActionWithSelection = (props) => <Action {...{ ...props, selectedIds }} />;
ActionWithSelection.displayName = `${Action.displayName || 'Action'}WithSelection`;
return ActionWithSelection;
})}
GlobalActionToolbarItems={props.GlobalActionToolbarItems?.map(
(Action: FC<GlobalActionWithSelection<T>>) => {
const ActionWithSelection = (props) => <Action {...{ ...props, selectedIds }} />;
ActionWithSelection.displayName = `${Action.displayName || 'Action'}WithSelection`;
return ActionWithSelection;
},
)}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React, { FC, useState } from 'react';
import StandardPage from 'src/components/page/StandardPage';
import { withIdBasedSelection } from 'src/components/page/withSelection';
import { GlobalActionWithSelection, withIdBasedSelection } from 'src/components/page/withSelection';
import { useProviderInventory } from 'src/modules/Providers/hooks';
import { useModal } from 'src/modules/Providers/modals';
import { useForkliftTranslation } from 'src/utils/i18n';
Expand All @@ -18,7 +18,7 @@ import { Button, ToolbarItem } from '@patternfly/react-core';
import { VSphereNetworkModal } from './modals/VSphereNetworkModal';
import { InventoryHostPair, matchHostsToInventory } from './utils/helpers';
import { ProviderHostsProps } from './ProviderHosts';
import { VSphereHostsRow } from './VSphereHostsRow';
import { VSphereHostsCells } from './VSphereHostsRow';

export const hostsFieldsMetadataFactory: ResourceFieldFactory = (t) => [
{
Expand Down Expand Up @@ -60,7 +60,12 @@ export const hostsFieldsMetadataFactory: ResourceFieldFactory = (t) => [
},
];

export const VSphereHostsList: React.FC<ProviderHostsProps> = ({ obj }) => {
const PageWithSelection = withIdBasedSelection<InventoryHostPair>({
toId: (item: InventoryHostPair) => item.inventory.id,
canSelect: (item: InventoryHostPair) => item?.inventory?.networkAdapters?.length > 0,
});

export const VSphereHostsList: FC<ProviderHostsProps> = ({ obj }) => {
const { t } = useForkliftTranslation();

const [userSettings] = useState(() => loadUserSettings({ pageId: 'ProviderHosts' }));
Expand All @@ -86,30 +91,26 @@ export const VSphereHostsList: React.FC<ProviderHostsProps> = ({ obj }) => {

const hostsData = matchHostsToInventory(hostsInventory, hosts, provider);

const Page = permissions?.canPatch
? withIdBasedSelection<InventoryHostPair>({
toId: (item: InventoryHostPair) => item.inventory.id,
canSelect: (item: InventoryHostPair) => item?.inventory?.networkAdapters?.length > 0,
actions: [
({ selectedIds }) => <SelectNetworkBtn {...{ hostsData, provider, selectedIds }} />,
],
})
: StandardPage<InventoryHostPair>;
const Page = permissions?.canPatch ? PageWithSelection : StandardPage<InventoryHostPair>;
const actions: FC<GlobalActionWithSelection<InventoryHostPair>>[] = permissions?.canPatch
? [({ selectedIds }) => <SelectNetworkBtn {...{ hostsData, provider, selectedIds }} />]
: [];

return (
<Page
data-testid="hosts-list"
dataSource={[hostsData || [], !loading, error]}
RowMapper={VSphereHostsRow}
CellMapper={VSphereHostsCells}
fieldsMetadata={hostsFieldsMetadataFactory(t)}
namespace={namespace}
title={t('Hosts')}
userSettings={userSettings}
GlobalActionToolbarItems={actions}
/>
);
};

const SelectNetworkBtn: React.FC<{
const SelectNetworkBtn: FC<{
selectedIds: string[];
provider: V1beta1Provider;
hostsData: InventoryHostPair[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import { ResourceField, RowProps } from '@kubev2v/common';
import { Td, Tr } from '@patternfly/react-table';
import { Td } from '@patternfly/react-table';

import { NameCellRenderer } from './components/NameCellRenderer';
import { InventoryHostPair } from './utils/helpers';
Expand All @@ -12,31 +12,16 @@ import {
NetworkCellRenderer,
} from './components';

export const VSphereHostsRow: React.FC<RowProps<InventoryHostPair>> = ({
export const VSphereHostsCells: React.FC<RowProps<InventoryHostPair>> = ({
resourceFields,
resourceData,
isSelected,
toggleSelect,
resourceIndex: rowIndex,
}) => {
return (
<Tr>
{!!toggleSelect && (
<Td
select={{
rowIndex,
onSelect: toggleSelect,
isSelected,
disable:
resourceData?.inventory?.networkAdapters === undefined ||
resourceData?.inventory?.networkAdapters?.length === 0,
}}
/>
)}
<>
{resourceFields?.map(({ resourceFieldId }) =>
renderTd({ resourceData, resourceFieldId, resourceFields }),
)}
</Tr>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EnumToTuple, ResourceFieldFactory } from '@kubev2v/common';

import { concernFilter } from './utils/concernFilter';
import { ProviderVirtualMachinesList, VmData } from './components';
import { OVirtVirtualMachinesRow } from './OVirtVirtualMachinesRow';
import { OVirtVirtualMachinesCells } from './OVirtVirtualMachinesRow';
import { ProviderVirtualMachinesProps } from './ProviderVirtualMachines';
import { getVmPowerState } from './utils';

Expand Down Expand Up @@ -97,7 +97,7 @@ export const OVirtVirtualMachinesList: React.FC<ProviderVirtualMachinesProps> =
obj={obj}
loaded={loaded}
loadError={loadError}
rowMapper={OVirtVirtualMachinesRow}
cellMapper={OVirtVirtualMachinesCells}
fieldsMetadataFactory={oVirtVmFieldsMetadataFactory}
pageId="OVirtVirtualMachinesList"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TableCell } from 'src/modules/Providers/utils';

import { ResourceField, RowProps } from '@kubev2v/common';
import { OVirtVM } from '@kubev2v/types';
import { Td, Tr } from '@patternfly/react-table';
import { Td } from '@patternfly/react-table';

import { PowerStateCellRenderer } from './components/PowerStateCellRenderer';
import { VMCellProps, VMConcernsCellRenderer, VMNameCellRenderer } from './components';
Expand Down Expand Up @@ -41,15 +41,15 @@ const cellRenderers: Record<string, React.FC<VMCellProps>> = {
description: ({ data }) => <TableCell>{(data?.vm as OVirtVM)?.description}</TableCell>,
};

export const OVirtVirtualMachinesRow: React.FC<RowProps<VmData>> = ({
export const OVirtVirtualMachinesCells: React.FC<RowProps<VmData>> = ({
resourceFields,
resourceData,
}) => {
return (
<Tr>
<>
{resourceFields?.map(({ resourceFieldId }) =>
renderTd({ resourceData, resourceFieldId, resourceFields }),
)}
</Tr>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EnumToTuple, ResourceFieldFactory } from '@kubev2v/common';

import { toVmFeatureEnum } from './utils/helpers/toVmFeatureEnum';
import { ProviderVirtualMachinesList, VmData } from './components';
import { OpenShiftVirtualMachinesRow } from './OpenShiftVirtualMachinesRow';
import { OpenShiftVirtualMachinesCells } from './OpenShiftVirtualMachinesRow';
import { ProviderVirtualMachinesProps } from './ProviderVirtualMachines';
import { getOpenShiftFeatureMap, getVmPowerState } from './utils';

Expand Down Expand Up @@ -70,7 +70,7 @@ export const OpenShiftVirtualMachinesList: React.FC<ProviderVirtualMachinesProps
obj={obj}
loaded={loaded}
loadError={loadError}
rowMapper={OpenShiftVirtualMachinesRow}
cellMapper={OpenShiftVirtualMachinesCells}
fieldsMetadataFactory={openShiftVmFieldsMetadataFactory}
pageId="OpenShiftVirtualMachinesList"
/>
Expand Down
Loading

0 comments on commit f3154da

Please sign in to comment.