Skip to content

Commit

Permalink
Merge pull request #791 from rszwajko/selectVm
Browse files Browse the repository at this point in the history
Extract selection logic to withIdBasedSelection() HoC
  • Loading branch information
yaacov authored Dec 5, 2023
2 parents ab39a45 + 72f21c3 commit 7336772
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 82 deletions.
108 changes: 108 additions & 0 deletions packages/forklift-console-plugin/src/components/page/withSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useState } from 'react';

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

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

export function withRowSelection<T>({ RowMapper, isSelected, toggleSelectFor }) {
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])}
/>
);
Enhanced.displayName = `${RowMapper.displayName || 'RowMapper'}WithSelection`;
return Enhanced;
}

export function withHeaderSelection<T>({ HeaderMapper, isSelected, canSelect, toggleSelectFor }) {
const Enhanced = ({ dataOnScreen, ...other }: TableViewHeaderProps<T>) => {
const selectableItems = dataOnScreen.filter(canSelect);
const allSelected = selectableItems.every((it) => isSelected(it));
return (
<>
<Th
select={{
onSelect: () => toggleSelectFor(selectableItems),
isSelected: allSelected,
isHeaderSelectDisabled: !selectableItems?.length, // Disable if no selectable items
}}
/>
<HeaderMapper {...{ ...other, dataOnScreen }} />
</>
);
};
Enhanced.displayName = `${HeaderMapper.displayName || 'HeaderMapper'}WithSelection`;
return Enhanced;
}

export interface IdBasedSelectionProps<T> {
/**
* @returns string that can be used as an unique identifier
*/
toId: (item: T) => string;

/**
* @returns true if items can be selected, false otherwise
*/
canSelect: (item: T) => boolean;

/**
* global toolbar actions
*/
actions: React.FC<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
*/
export function withIdBasedSelection<T>({ toId, canSelect, actions }: IdBasedSelectionProps<T>) {
const Enhanced = (props: StandardPageProps<T>) => {
const [selectedIds, setSelectedIds]: [string[], (selected: string[]) => void] = useState([]);
const isSelected = (item: T) => selectedIds.includes(toId(item));
const toggleSelectFor = (items: T[]) => {
const ids = items.map(toId);
const allSelected = ids.every((id) => selectedIds.includes(id));
setSelectedIds([
...selectedIds.filter((it) => !ids.includes(it)),
...(allSelected ? [] : ids),
]);
};
return (
<StandardPage
{...props}
RowMapper={withRowSelection({
RowMapper: props.RowMapper,
isSelected,
toggleSelectFor,
})}
HeaderMapper={withHeaderSelection({
HeaderMapper: props.HeaderMapper ?? DefaultHeader,
canSelect,
isSelected,
toggleSelectFor,
})}
GlobalActionToolbarItems={actions.map((Action) => {
const ActionWithSelection = (props) => <Action {...{ ...props, selectedIds }} />;
ActionWithSelection.displayName = `${Action.displayName || 'Action'}WithSelection`;
return ActionWithSelection;
})}
/>
);
};
Enhanced.displayName = 'StandardPageWithSelection';
return Enhanced;
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import React, { useCallback, useState } from 'react';
import React, { useState } from 'react';
import StandardPage from 'src/components/page/StandardPage';
import { 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';

import { loadUserSettings, ResourceFieldFactory } from '@kubev2v/common';
import {
DefaultHeader,
loadUserSettings,
ResourceFieldFactory,
RowProps,
TableViewHeaderProps,
} from '@kubev2v/common';
import { HostModelGroupVersionKind, V1beta1Host, VSphereHost } from '@kubev2v/types';
HostModelGroupVersionKind,
V1beta1Host,
V1beta1Provider,
VSphereHost,
} from '@kubev2v/types';
import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
import { Button } from '@patternfly/react-core';
import { Th } from '@patternfly/react-table';
import { Button, ToolbarItem } from '@patternfly/react-core';

import { VSphereNetworkModal } from './modals/VSphereNetworkModal';
import { InventoryHostPair, matchHostsToInventory } from './utils/helpers';
Expand Down Expand Up @@ -63,10 +62,8 @@ export const hostsFieldsMetadataFactory: ResourceFieldFactory = (t) => [

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

const [userSettings] = useState(() => loadUserSettings({ pageId: 'ProviderHosts' }));
const [selected, setSelected]: [string[], (selected: string[]) => void] = useState([]);

const { provider, permissions } = obj;
const { namespace } = provider?.metadata || {};
Expand All @@ -89,87 +86,49 @@ export const VSphereHostsList: React.FC<ProviderHostsProps> = ({ obj }) => {

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

const RowMapper = React.useCallback(
(props: RowProps<InventoryHostPair>) => {
const isSelected = selected.includes(props.resourceData.inventory.id);

const handleToggle = () => {
const newSelected = selected.filter((id) => id !== props.resourceData.inventory.id);
setSelected(isSelected ? newSelected : [...newSelected, props.resourceData.inventory.id]);
};

return (
<VSphereHostsRow
{...props}
isSelected={isSelected}
toggleSelect={permissions.canPatch ? handleToggle : undefined}
/>
);
},
[selected, permissions],
);

const HeaderMapper = React.useCallback(
({ dataOnScreen, ...other }: TableViewHeaderProps<InventoryHostPair>) => {
const selectableItems = dataOnScreen.filter((d) => d?.inventory?.networkAdapters?.length > 0);
const selectableIDs = selectableItems.map((d) => d?.inventory?.id);
const allSelected = selectableIDs.every((id) => selected.includes(id));
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 handleSelect = () => {
const selectableNotSelected = selectableIDs.filter((id) => !selected.includes(id));
setSelected(
allSelected
? selected.filter((id) => !selectableIDs.includes(id)) // Unselect all
: [...selected, ...selectableNotSelected], // Select all
);
};

return (
<>
{permissions.canPatch && (
<Th
select={{
onSelect: handleSelect,
isSelected: allSelected,
isHeaderSelectDisabled: selectableItems.length === 0, // Disable if no selectable items
}}
/>
)}
<DefaultHeader {...{ ...other, dataOnScreen }} />
</>
);
},
[selected, permissions],
return (
<Page
data-testid="hosts-list"
dataSource={[hostsData || [], !loading, error]}
RowMapper={VSphereHostsRow}
fieldsMetadata={hostsFieldsMetadataFactory(t)}
namespace={namespace}
title={t('Hosts')}
userSettings={userSettings}
/>
);
};

const AddButton = useCallback(
() => (
const SelectNetworkBtn: React.FC<{
selectedIds: string[];
provider: V1beta1Provider;
hostsData: InventoryHostPair[];
}> = ({ selectedIds, provider, hostsData }) => {
const { t } = useForkliftTranslation();
const { showModal } = useModal();
return (
<ToolbarItem>
<Button
variant="secondary"
onClick={() =>
showModal(
<VSphereNetworkModal provider={provider} data={hostsData} selected={selected} />,
<VSphereNetworkModal provider={provider} data={hostsData} selected={selectedIds} />,
)
}
isDisabled={selected.length === 0}
isDisabled={!selectedIds?.length}
>
{t('Select migration network')}
</Button>
),
[selected],
);

return (
<StandardPage<InventoryHostPair>
data-testid="hosts-list"
addButton={permissions.canPatch && <AddButton />}
dataSource={[hostsData || [], !loading, error]}
RowMapper={RowMapper}
HeaderMapper={HeaderMapper}
fieldsMetadata={hostsFieldsMetadataFactory(t)}
namespace={namespace}
title={t('Hosts')}
userSettings={userSettings}
/>
</ToolbarItem>
);
};

0 comments on commit 7336772

Please sign in to comment.