diff --git a/libs/locales/lib/en/translation.json b/libs/locales/lib/en/translation.json index c2b9eabf7c..be0efd34b1 100644 --- a/libs/locales/lib/en/translation.json +++ b/libs/locales/lib/en/translation.json @@ -50,6 +50,7 @@ "ai:Add host using Baseboard Management Controller (BMC)": "Add host using Baseboard Management Controller (BMC)", "ai:Add hosts": "Add hosts", "ai:Add hosts by uploading YAML (BMC)": "Add hosts by uploading YAML (BMC)", + "ai:Add hosts from Cisco Intersight": "Add hosts from Cisco Intersight", "ai:Add hosts with {{cpuArchitecture}} architecture to an <6>infrastructure environment": "Add hosts with {{cpuArchitecture}} architecture to an <6>infrastructure environment", "ai:Add label": "Add label", "ai:Add more": "Add more", diff --git a/libs/ui-lib-tests/cypress/views/bareMetalDiscovery.ts b/libs/ui-lib-tests/cypress/views/bareMetalDiscovery.ts index 936460e47b..abf30f8d5f 100644 --- a/libs/ui-lib-tests/cypress/views/bareMetalDiscovery.ts +++ b/libs/ui-lib-tests/cypress/views/bareMetalDiscovery.ts @@ -37,7 +37,7 @@ export const bareMetalDiscoveryPage = { numWorkers: number = Cypress.env('NUM_WORKERS'), timeout = Cypress.env('HOST_REGISTRATION_TIMEOUT'), ) => { - cy.get('table.hosts-table > tbody', { timeout: timeout }).should(($els) => { + cy.get('table.hosts-table > tbody > tr:not([hidden])', { timeout: timeout }).should(($els) => { expect($els.length).to.be.eq(numMasters + numWorkers); if (numMasters + numWorkers === 1) { expect($els[0].textContent).not.to.contain('Waiting for host'); diff --git a/libs/ui-lib-tests/cypress/views/hostsTableSection.ts b/libs/ui-lib-tests/cypress/views/hostsTableSection.ts index 445facfc70..37c1341c53 100644 --- a/libs/ui-lib-tests/cypress/views/hostsTableSection.ts +++ b/libs/ui-lib-tests/cypress/views/hostsTableSection.ts @@ -24,7 +24,7 @@ export const hostsTableSection = { }); }, validateHostCpuCores: () => { - cy.get('td[data-label="CPU Cores"]') + cy.get('td[data-testid="host-cpu-cores"]') .should('have.length', numMasters() + numWorkers()) .each((hostCpuCores, idx) => { const isMaster = idx <= numMasters() - 1; @@ -36,7 +36,7 @@ export const hostsTableSection = { }); }, validateHostMemory: () => { - cy.get('td[data-label="Memory"]') + cy.get('td[data-testid="host-memory"]') .should('have.length', numMasters() + numWorkers()) .each((hostMemory, idx) => { const isMaster = idx <= numMasters() - 1; @@ -48,7 +48,7 @@ export const hostsTableSection = { }); }, validateHostDiskSize: (masterDiskTotalSize: number, workerDiskTotalSize: number) => { - cy.get('td[data-label="Total storage"]') + cy.get('td[data-testid="host-disks"]') .should('have.length', numMasters() + numWorkers()) .each((hostDisk, idx) => { const isMaster = idx <= numMasters() - 1; @@ -60,16 +60,15 @@ export const hostsTableSection = { }); }, waitForHardwareStatus: (status: string) => { - // Start at index 2 here because of selector - for (let i = 2; i <= numMasters() + numWorkers() + 1; i++) { - cy.hostDetailSelector(i, 'Status', Cypress.env('HOST_READY_TIMEOUT')).should( - 'contain.text', - status, - ); - } + cy.get('table.hosts-table > tbody > tr:not([hidden])').each((row) => + cy + .wrap(row) + .find('td[data-testid="host-hw-status"]', { timeout: Cypress.env('HOST_READY_TIMEOUT') }) + .should('contain.text', status), + ); }, getHostDisksExpander: (hostIndex: number) => { - return cy.get(`#expandable-toggle${hostIndex * 2}`); + return cy.get(`#expand-toggle${hostIndex}`); }, getHostDetailsTitle: (hostIndex: number) => { return cy.get(`h3[data-testid="disks-section"]`).then((hostTables) => { @@ -89,7 +88,7 @@ export const hostsTableSection = { }); }, validateGroupingByDiskHolders: (disks: ValidateDiskHoldersParams, message?: string) => { - cy.get('td[data-testid="disk-name"]').then(($diskNames) => { + cy.get('.pf-m-expanded td[data-testid="disk-name"]').then(($diskNames) => { disks.forEach((disk, index) => { cy.wrap($diskNames).eq(index).should('contain.text', disk.name); if (disk.indented || disk.warning) { diff --git a/libs/ui-lib-tests/cypress/views/modals/EventsModal.ts b/libs/ui-lib-tests/cypress/views/modals/EventsModal.ts index aa7a04a98d..f9d188159f 100644 --- a/libs/ui-lib-tests/cypress/views/modals/EventsModal.ts +++ b/libs/ui-lib-tests/cypress/views/modals/EventsModal.ts @@ -30,7 +30,7 @@ export class EventsModal { } get contents() { - return this.body.find('.pf-v5-c-table tr'); + return this.body.find('.pf-v5-c-table__tbody tr'); } get hostFilter() { diff --git a/libs/ui-lib-tests/cypress/views/networkingPage.ts b/libs/ui-lib-tests/cypress/views/networkingPage.ts index 861a452b9d..0bd808d345 100644 --- a/libs/ui-lib-tests/cypress/views/networkingPage.ts +++ b/libs/ui-lib-tests/cypress/views/networkingPage.ts @@ -61,25 +61,15 @@ export const networkingPage = { .should('be.visible') .should('contain.text', Cypress.env('devPreviewSupportLevel')); }, - waitForNetworkStatusToNotContain: ( - text, - numMasters: number = Cypress.env('NUM_MASTERS'), - numWorkers: number = Cypress.env('NUM_WORKERS'), - timeout = Cypress.env('HOST_READY_TIMEOUT'), - ) => { - for (let i = 2; i <= numMasters + numWorkers + 1; i++) { - cy.hostDetailSelector(i, 'Status', timeout).should('not.contain', text); - } - }, - waitForNetworkStatus: ( - status, - numMasters: number = Cypress.env('NUM_MASTERS'), - numWorkers: number = Cypress.env('NUM_WORKERS'), - timeout = Cypress.env('HOST_READY_TIMEOUT'), - ) => { - for (let i = 2; i <= numMasters + numWorkers + 1; i++) { - cy.hostDetailSelector(i, 'Status', timeout).should('contain.text', status); - } + waitForNetworkStatusToNotContain: (text, timeout = Cypress.env('HOST_READY_TIMEOUT')) => { + cy.get('table.hosts-table > tbody > tr:not([hidden])').each((row) => + cy.wrap(row).find('td[data-testid="nic-status"]', { timeout }).should('not.contain', text), + ); + }, + waitForNetworkStatus: (status, timeout = Cypress.env('HOST_READY_TIMEOUT')) => { + cy.get('table.hosts-table > tbody > tr:not([hidden])').each((row) => + cy.wrap(row).find('td[data-testid="nic-status"]', { timeout }).should('contain.text', status), + ); }, waitForHostNetworkStatusInsufficient: ( idx, diff --git a/libs/ui-lib-tests/cypress/views/storagePage.ts b/libs/ui-lib-tests/cypress/views/storagePage.ts index b4b50847ab..5f23db7958 100644 --- a/libs/ui-lib-tests/cypress/views/storagePage.ts +++ b/libs/ui-lib-tests/cypress/views/storagePage.ts @@ -3,7 +3,7 @@ export const storagePage = { numMasters: number = Cypress.env('NUM_MASTERS'), numWorkers: number = Cypress.env('NUM_WORKERS'), ) => { - cy.get('td[data-label="ODF Usage"]') + cy.get('td[data-testid="use-odf"]') .should('have.length', numMasters + numWorkers) .each((hostRole, idx) => { const isMaster = idx <= numMasters - 1; @@ -18,7 +18,7 @@ export const storagePage = { numMasters: number = Cypress.env('NUM_MASTERS'), numWorkers: number = Cypress.env('NUM_WORKERS'), ) => { - cy.get('td[data-label="Number of disks"]') + cy.get('td[data-testid="disk-number"]') .should('have.length', numMasters + numWorkers) .each((hostDisk) => { expect(hostDisk).to.contain('3'); @@ -28,7 +28,7 @@ export const storagePage = { return cy.get(`input[id="select-formatted-${hostId}-${indexSelect}"]`); }, validateSkipFormattingDisks: (hostId: string, numDisks: number) => { - cy.get("td[data-label='Format?']").should('have.length', numDisks); + cy.get("tr.pf-m-expanded td[data-testid='disk-formatted']").should('have.length', numDisks); //Checking if checkboxes are checked/unchecked storagePage.getSkipFormattingCheckbox(hostId, 0).should('not.be.checked'); storagePage.getSkipFormattingCheckbox(hostId, 1).should('be.checked'); diff --git a/libs/ui-lib/lib/cim/components/Agent/tableUtils.tsx b/libs/ui-lib/lib/cim/components/Agent/tableUtils.tsx index 0c1025cc88..2237e6279f 100644 --- a/libs/ui-lib/lib/cim/components/Agent/tableUtils.tsx +++ b/libs/ui-lib/lib/cim/components/Agent/tableUtils.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { sortable, expandable, breakWord } from '@patternfly/react-table'; import { Link } from 'react-router-dom-v5-compat'; import { @@ -48,10 +47,9 @@ export const agentHostnameColumn = ( title: t('ai:Hostname'), props: { id: 'col-header-hostname', // ACM jest tests require id over testId + modifier: 'breakWord', }, - transforms: [sortable], - cellFormatters: [expandable], - cellTransforms: [breakWord], + sort: true, }, cell: (host) => { const inventory = getInventory(host); @@ -97,7 +95,7 @@ export const discoveryTypeColumn = ( props: { id: 'col-header-discovery-type', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const agent = agents.find((a) => a.metadata?.uid === host.id); @@ -151,7 +149,7 @@ export const agentStatusColumn = ({ props: { id: 'col-header-infraenvstatus', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const agent = agents.find((a) => a.metadata?.uid === host.id); @@ -199,7 +197,7 @@ export const clusterColumn = ( props: { id: 'col-header-cluster', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const agent = agents.find((a) => a.metadata?.uid === host.id); @@ -239,7 +237,7 @@ export const infraEnvColumn = (agents: AgentK8sResource[], t: TFunction): TableR props: { id: 'col-header-infraenv', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const agent = agents.find((a) => a.metadata?.uid === host.id) as AgentK8sResource; diff --git a/libs/ui-lib/lib/cim/components/Hypershift/DetailsPage/NodePoolsTable.tsx b/libs/ui-lib/lib/cim/components/Hypershift/DetailsPage/NodePoolsTable.tsx index a78a6bbcf3..154d3b4651 100644 --- a/libs/ui-lib/lib/cim/components/Hypershift/DetailsPage/NodePoolsTable.tsx +++ b/libs/ui-lib/lib/cim/components/Hypershift/DetailsPage/NodePoolsTable.tsx @@ -2,17 +2,7 @@ import * as React from 'react'; import { Link } from 'react-router-dom-v5-compat'; import { Button, Label, Popover, Stack, StackItem } from '@patternfly/react-core'; import { PlusCircleIcon } from '@patternfly/react-icons/dist/js/icons/plus-circle-icon'; -import { - breakWord, - expandable, - sortable, - Table, - Tbody, - Td, - Th, - Thead, - Tr, -} from '@patternfly/react-table'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; import classnames from 'classnames'; import { TableRow } from '../../../../common/components/hosts/AITable'; @@ -61,10 +51,9 @@ export const nodePoolNameColumn = (): TableRow => { title: 'Nodepool', props: { id: 'col-header-hostname', // ACM jest tests require id over testId + modifier: 'breakWord', }, - transforms: [sortable], - cellFormatters: [expandable], - cellTransforms: [breakWord], + sort: true, }, cell: ({ nodePool }) => { return { diff --git a/libs/ui-lib/lib/cim/components/modals/MassApproveAgentModal.tsx b/libs/ui-lib/lib/cim/components/modals/MassApproveAgentModal.tsx index f5bdbe23c8..44740b005c 100644 --- a/libs/ui-lib/lib/cim/components/modals/MassApproveAgentModal.tsx +++ b/libs/ui-lib/lib/cim/components/modals/MassApproveAgentModal.tsx @@ -9,7 +9,6 @@ import { Stack, StackItem, } from '@patternfly/react-core'; -import { sortable } from '@patternfly/react-table'; import { getAIHosts } from '../helpers'; import { AgentK8sResource } from '../../types'; import HostsTable from '../../../common/components/hosts/HostsTable'; @@ -38,7 +37,7 @@ const hostnameColumn = (agents: AgentK8sResource[], t: TFunction): TableRow { const agent = agents.find((a) => a.metadata?.uid === host.id); @@ -63,7 +62,7 @@ const statusColumn = ( props: { id: 'col-header-status', // ACM jest tests require id over testId }, - transforms: [sortable], + sort: true, }, cell: (host) => { const agent = agents.find((a) => a.metadata?.uid === host.id); diff --git a/libs/ui-lib/lib/cim/components/modals/MassDeleteAgentModal.tsx b/libs/ui-lib/lib/cim/components/modals/MassDeleteAgentModal.tsx index 42daf6455a..d63ae5fb82 100644 --- a/libs/ui-lib/lib/cim/components/modals/MassDeleteAgentModal.tsx +++ b/libs/ui-lib/lib/cim/components/modals/MassDeleteAgentModal.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { Button, Flex, FlexItem, Popover } from '@patternfly/react-core'; import { InfoCircleIcon } from '@patternfly/react-icons/dist/js/icons/info-circle-icon'; import { Link } from 'react-router-dom-v5-compat'; -import { sortable } from '@patternfly/react-table'; import { global_palette_blue_300 as blueInfoColor } from '@patternfly/react-tokens/dist/js/global_palette_blue_300'; import { getHostname, @@ -35,7 +34,7 @@ const hostnameColumn = (agents: AgentK8sResource[], t: TFunction): TableRow { const inventory = getInventory(host); @@ -68,7 +67,7 @@ const statusColumn = ( props: { id: 'col-header-status', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const agent = agents.find((a) => a.metadata?.uid === host.id); diff --git a/libs/ui-lib/lib/common/components/clusterConfiguration/DownloadIso.tsx b/libs/ui-lib/lib/common/components/clusterConfiguration/DownloadIso.tsx index cbaa232af4..ad65381cf7 100644 --- a/libs/ui-lib/lib/common/components/clusterConfiguration/DownloadIso.tsx +++ b/libs/ui-lib/lib/common/components/clusterConfiguration/DownloadIso.tsx @@ -15,6 +15,8 @@ import { DetailItem, DetailList } from '../ui'; import DiscoveryInstructions from './DiscoveryInstructions'; import { StaticIPInfo } from './DiscoveryImageConfigForm'; import { useTranslation } from '../../hooks/use-translation-wrapper'; +import { ExternalLinkAltIcon } from '@patternfly/react-icons/dist/js/icons/external-link-alt-icon'; +import { getCiscoIntersightLink } from '../../config'; export type DownloadISOProps = { hasDHCP?: boolean; @@ -58,6 +60,17 @@ const DownloadIso = ({ )} + + + { const rows = React.useMemo(() => { @@ -30,31 +30,41 @@ const ReviewHostsInventory = ({ hosts = [] }: { hosts?: Host[] }) => { return [ { - cells: ['Hosts', summary.count], + cells: [{ title: 'Hosts' }, { title: summary.count }], + rowId: 'hosts', }, { - cells: ['Total cores', summary.cores], + cells: [{ title: 'Total cores' }, { title: summary.cores }], + rowId: 'total-cores', }, { - cells: ['Total memory', fileSize(summary.memory, 2, 'iec')], + cells: [{ title: 'Total memory' }, { title: fileSize(summary.memory, 2, 'iec') }], + rowId: 'total-memory', }, { - cells: ['Total storage', fileSize(summary.fs, 2, 'si')], + cells: [{ title: 'Total storage' }, { title: fileSize(summary.fs, 2, 'si') }], + rowId: 'total-storage', }, ]; }, [hosts]); return ( - + + {rows.map((row, i) => ( + + {row.cells.map((cell, j) => ( + + ))} + + ))} +
{cell.title}
); }; diff --git a/libs/ui-lib/lib/common/components/hosts/AITable.tsx b/libs/ui-lib/lib/common/components/hosts/AITable.tsx index d40f2748fd..fc8fc9cd10 100644 --- a/libs/ui-lib/lib/common/components/hosts/AITable.tsx +++ b/libs/ui-lib/lib/common/components/hosts/AITable.tsx @@ -5,104 +5,28 @@ import { SortByDirection, ISortBy, OnSort, - RowWrapperProps, - RowWrapper, - ICell, IAction, ISeparator, - InnerScrollContainer, + TableProps, } from '@patternfly/react-table'; -import { Table, TableHeader, TableBody, TableProps } from '@patternfly/react-table/deprecated'; -import { ExtraParamsType } from '@patternfly/react-table/dist/js/components/Table/base/types'; import xor from 'lodash-es/xor.js'; -import classnames from 'classnames'; -import { getColSpanRow, rowSorter } from '../ui'; +import { getColSpanRow, HumanizedSortable, rowSorter } from '../ui'; import { WithTestID } from '../../types'; +import { AITableMemo, TableMemoColType, TableMemoProps } from './AITableMemo'; import { usePagination } from './usePagination'; import './HostsTable.css'; -const rowKey = ({ rowData }: ExtraParamsType) => rowData?.key as string; - -type TableMemoProps = { - rows: TableProps['rows']; - cells: TableProps['cells']; - onCollapse: TableProps['onCollapse']; - className: TableProps['className']; - sortBy: TableProps['sortBy']; - onSort: TableProps['onSort']; - rowWrapper: TableProps['rowWrapper']; - variant?: TableProps['variant']; - // eslint-disable-next-line - actionResolver?: ActionsResolver; -}; -const TableMemo: React.FC = React.memo( - ({ - rows, - cells, - onCollapse, - className, - sortBy, - onSort, - rowWrapper, - testId, - variant, - actionResolver, - }) => { - const tableActionResolver = React.useCallback( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (rowData) => actionResolver?.(rowData.obj) as (IAction | ISeparator)[], - [actionResolver], - ); - // new prop for @patternfly/react-table 4.67.7 which is used in ACM, but not in OCM - const newProps = { - canCollapseAll: false, - }; - return ( - - document.body} - {...newProps} - > - - -
-
- ); - }, -); -TableMemo.displayName = 'tableMemo'; -const getMainIndex = (hasOnSelect: boolean, hasExpandComponent: boolean) => { - if (hasOnSelect && hasExpandComponent) { - return 2; - } - if (hasOnSelect || hasExpandComponent) { - return 1; - } - return 0; -}; type OpenRows = { [id: string]: boolean; }; -const HostsTableRowWrapper = (props: RowWrapperProps) => ( - -); + export type TableRow = { - header: ICell | string; + header: TableMemoColType; // eslint-disable-next-line cell?: (obj: R) => { title: React.ReactNode; props: any; sortableValue?: string | number }; }; + export type ActionsResolver = (obj: R) => (IAction | ISeparator)[]; export type ExpandComponentProps = { obj: R; @@ -120,6 +44,7 @@ type SelectCheckboxProps = { onSelect: (isChecked: boolean) => void; id: string; }; + const SelectCheckbox: React.FC = ({ onSelect, id }) => { const { selectedIDs } = React.useContext(SelectionProvider); const isChecked = selectedIDs?.includes(id); @@ -156,6 +81,7 @@ export type AITableProps = ReturnType & { canSelectAll?: boolean; variant?: TableProps['variant']; }; + // eslint-disable-next-line const AITable = ({ data, @@ -179,6 +105,7 @@ const AITable = ({ variant, }: WithTestID & AITableProps) => { const itemIDs = React.useMemo(() => data.map(getDataId), [data, getDataId]); + React.useEffect(() => { if (selectedIDs && setSelectedIDs) { const idsToRemove: string[] = []; @@ -193,9 +120,10 @@ const AITable = ({ } } }, [data, setSelectedIDs, selectedIDs, getDataId]); + const [openRows, setOpenRows] = React.useState({}); const [sortBy, setSortBy] = React.useState({ - index: getMainIndex(!!onSelect, !!ExpandComponent), + index: onSelect ? 1 : 0, direction: SortByDirection.asc, }); @@ -210,14 +138,14 @@ const AITable = ({ [setSelectedIDs, getDataId], ); - const [contentWithAdditions, columns] = React.useMemo<[TableRow[], (string | ICell)[]]>(() => { + const [contentWithAdditions, columns] = React.useMemo(() => { let newContent = content; if (onSelect) { newContent = [ { header: { title: canSelectAll ? : '', - cellFormatters: [], + sort: false, }, cell: (obj) => { const id = getDataId(obj); @@ -231,71 +159,51 @@ const AITable = ({ ...content, ]; } - const columns = newContent.map((c) => c.header); + const columns = newContent.map( + (c) => + ({ + ...c.header, + sort: c.header.sort ?? true, + } as TableMemoColType), + ); return [newContent, columns]; - }, [content, onSelect, getDataId, onSelectAll, canSelectAll]); + }, [canSelectAll, content, getDataId, onSelect, onSelectAll]); const hostRows = React.useMemo(() => { - let rows = (data || []) + const rows = (data || []) .map((obj) => { const id = getDataId(obj); const cells = contentWithAdditions.filter((c) => !!c.cell).map((c) => c.cell?.(obj)); const isOpen = !!openRows[id]; return { - // visible row isOpen, cells, key: `${id}-master`, - obj, id, + actions: actionResolver ? actionResolver(obj) : undefined, + nestedComponent: ExpandComponent ? : undefined, }; }) - .sort( - rowSorter(sortBy, (row, index = 1) => - // eslint-disable-next-line - ExpandComponent ? row.cells?.[index - 1] : (row.cells?.[index] as any), - ), - ) .slice((page - 1) * perPage, page * perPage); - if (ExpandComponent) { - rows = rows.reduce((allRows, row: IRow, index) => { - allRows.push(row); - if (ExpandComponent) { - allRows.push({ - // expandable detail - // parent will be set after sorting - cells: [ - { - // do not render unnecessarily to improve performance - title: row.isOpen ? : undefined, - props: { colSpan: columns.length }, - }, - ], - key: `${(row.id as string) || ''}-detail`, - parent: index * 2, - }); - } - return allRows; - }, [] as IRow[]); - } return rows; }, [ - contentWithAdditions, - ExpandComponent, - getDataId, data, - openRows, - sortBy, page, perPage, - columns.length, + ExpandComponent, + getDataId, + contentWithAdditions, + openRows, + actionResolver, ]); + const rows = React.useMemo(() => { if (hostRows.length) { return hostRows; } return getColSpanRow(children, columns.length); }, [hostRows, columns, children]); + const onCollapse = React.useCallback( (_event, rowKey: number) => { const id = hostRows[rowKey].id as string; @@ -305,6 +213,7 @@ const AITable = ({ }, [hostRows, openRows], ); + const onSort: OnSort = React.useCallback((_event, index, direction) => { setOpenRows({}); // collapse all setSortBy({ @@ -312,6 +221,13 @@ const AITable = ({ direction, }); }, []); + + const sortedRows = React.useMemo(() => { + return rows.sort( + rowSorter(sortBy, (row: IRow, index = 0) => row.cells?.[index] as string | HumanizedSortable), + ); + }, [rows, sortBy]); + return ( <> ({ allIDs: itemIDs, }} > - diff --git a/libs/ui-lib/lib/common/components/hosts/AITableMemo.tsx b/libs/ui-lib/lib/common/components/hosts/AITableMemo.tsx new file mode 100644 index 0000000000..75d719b509 --- /dev/null +++ b/libs/ui-lib/lib/common/components/hosts/AITableMemo.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { + ActionsColumn, + ExpandableRowContent, + IAction, + InnerScrollContainer, + ISeparator, + ISortBy, + OnCollapse, + OnSort, + Table, + TableProps, + Tbody, + Td, + TdProps, + Th, + Thead, + ThProps, + Tr, +} from '@patternfly/react-table'; +import classnames from 'classnames'; + +import { WithTestID } from '../../types'; +import './HostsTable.css'; + +export type ActionsResolver = (obj: R) => (IAction | ISeparator)[]; + +type TableMemoRowType = { + cells: { title: string | React.ReactNode; props?: Omit }[]; + actions: IAction[]; + nestedComponent?: React.ReactNode; + isOpen?: boolean; +}; + +export type TableMemoColType = { + title: string | React.ReactNode; + props?: Omit; + sort?: boolean; +}; + +export type TableMemoProps = { + rows: TableMemoRowType[]; + cols: TableMemoColType[]; + sortBy: ISortBy; + onSort: OnSort; + onCollapse?: OnCollapse; + className: TableProps['className']; + variant?: TableProps['variant']; +} & WithTestID; + +export const AITableMemo = React.memo( + ({ rows, cols, onCollapse, className, variant, onSort, sortBy, testId }: TableMemoProps) => { + const getSortParams = (columnIndex: number): ThProps['sort'] => ({ + sortBy, + onSort, + columnIndex, + }); + + return ( + + + + + {onCollapse && + ))} + + + + {rows.map((row, i) => ( + + + {row.nestedComponent && ( + + ))} + + {row.actions && ( + + )} + + + {row.nestedComponent && ( + + + + )} + + ))} + +
} + {cols.map((col, i) => ( + + {col.title} +
+ )} + + {row.cells.map((cell, j) => ( + + {cell.title} + + document.body, position: 'end' }} + /> +
+ {row.nestedComponent} +
+
+ ); + }, +); + +AITableMemo.displayName = 'tableMemo'; diff --git a/libs/ui-lib/lib/common/components/hosts/HostRowDetail.tsx b/libs/ui-lib/lib/common/components/hosts/HostRowDetail.tsx index 3de7baff9c..0ca7d23851 100644 --- a/libs/ui-lib/lib/common/components/hosts/HostRowDetail.tsx +++ b/libs/ui-lib/lib/common/components/hosts/HostRowDetail.tsx @@ -1,8 +1,6 @@ import React from 'react'; import { Grid, GridItem } from '@patternfly/react-core'; -import { TableVariant, RowWrapperProps, RowWrapper, IRow } from '@patternfly/react-table'; -import { Table, TableHeader, TableBody } from '@patternfly/react-table/deprecated'; -import { ExtraParamsType } from '@patternfly/react-table/dist/js/components/Table/base/types'; +import { TableVariant, Tbody, Thead, Table, Th, Tr, Td } from '@patternfly/react-table'; import { DetailItem, DetailList, DetailListProps } from '../ui'; import type { Disk, Host, Interface } from '@openshift-assisted/types/assisted-installer-service'; import type { ValidationsInfo } from '../../types/hosts'; @@ -56,16 +54,9 @@ const nicsColumns = (t: TFunction) => [ // { title: 'Product' }, ]; -// eslint-disable-next-line -const nicsRowKey = ({ rowData }: ExtraParamsType) => rowData?.name?.title; - -const NICsTableRowWrapper = (props: RowWrapperProps) => ( - -); - const NicsTable: React.FC = ({ interfaces, testId }) => { const { t } = useTranslation(); - const rows: IRow[] = interfaces + const rows = interfaces .sort((nicA, nicB) => nicA.name?.localeCompare(nicB.name || '') || 0) .map((nic) => ({ cells: [ @@ -85,20 +76,33 @@ const NicsTable: React.FC = ({ interfaces, testId } }, ], key: nic.name, - })); + })) as { cells: { title: string | React.ReactNode; props?: object }[]; key: string }[]; return ( - - + + + {nicsColumns(t).map((col, i) => ( + + ))} + + + + {rows.map((row, i) => ( + + {row.cells?.map((cell, j) => ( + + ))} + + ))} +
{col.title}
+ {cell.title} +
); }; diff --git a/libs/ui-lib/lib/common/components/hosts/HostValidationGroups.tsx b/libs/ui-lib/lib/common/components/hosts/HostValidationGroups.tsx index 21af5f3814..7943e3e5fb 100644 --- a/libs/ui-lib/lib/common/components/hosts/HostValidationGroups.tsx +++ b/libs/ui-lib/lib/common/components/hosts/HostValidationGroups.tsx @@ -64,7 +64,8 @@ const ValidationsAlert = ({
    {validations.map((v) => (
  • - {hostValidationLabels(t)[v.id] || v.id}: {toSentence(v.message)}{' '} + {hostValidationLabels(t)[v.id] || v.id}:  + {toSentence(v.message.replace(/\\n/, ' '))}{' '} {v.status === 'failure' && hostValidationFailureHints(t)[v.id]}
  • ))} diff --git a/libs/ui-lib/lib/common/components/hosts/tableUtils.tsx b/libs/ui-lib/lib/common/components/hosts/tableUtils.tsx index 39e0b7a402..9dc8179a92 100644 --- a/libs/ui-lib/lib/common/components/hosts/tableUtils.tsx +++ b/libs/ui-lib/lib/common/components/hosts/tableUtils.tsx @@ -1,4 +1,3 @@ -import { breakWord, expandable, sortable } from '@patternfly/react-table'; import * as React from 'react'; import { Address4, Address6 } from 'ip-address'; import type { @@ -66,10 +65,9 @@ export const hostnameColumn = ( title: t('ai:Hostname'), props: { id: 'col-header-hostname', // ACM jest tests require id over testId + modifier: 'breakWord', }, - transforms: [sortable], - cellFormatters: [expandable], - cellTransforms: [breakWord], + sort: true, }, cell: (host) => { const inventory = getInventory(host); @@ -105,7 +103,7 @@ export const roleColumn = ( props: { id: 'col-header-role', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const editRole = onEditRole @@ -137,7 +135,7 @@ export const statusColumn = ( props: { id: 'col-header-status', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const validationsInfo = stringToJSON(host.validationsInfo) || {}; @@ -174,7 +172,7 @@ export const discoveredAtColumn = (t: TFunction): TableRow => ({ props: { id: 'col-header-discoveredat', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const { createdAt } = host; @@ -193,7 +191,7 @@ export const cpuArchitectureColumn = (t: TFunction): TableRow => ({ props: { id: 'col-header-cpuarchitecture', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const inventory = getInventory(host); @@ -211,7 +209,7 @@ export const cpuCoresColumn = (t: TFunction): TableRow => ({ props: { id: 'col-header-cpucores', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const inventory = getInventory(host); @@ -238,7 +236,7 @@ export const memoryColumn = (t: TFunction): TableRow => ({ props: { id: 'col-header-memory', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const inventory = getInventory(host); @@ -263,7 +261,7 @@ export const disksColumn = (t: TFunction): TableRow => ({ props: { id: 'col-header-disk', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const inventory = getInventory(host); @@ -287,7 +285,10 @@ export const countColumn = (cluster: Cluster): TableRow => ({ }); export const activeNICColumn = (cluster: Cluster, t: TFunction): TableRow => ({ - header: { title: t('ai:Active NIC'), transforms: [sortable] }, + header: { + title: t('ai:Active NIC'), + sort: true, + }, cell: (host) => { const inventory = getInventory(host); const nics = inventory.interfaces || []; @@ -303,7 +304,10 @@ export const activeNICColumn = (cluster: Cluster, t: TFunction): TableRow }); export const ipv4Column = (cluster: Cluster): TableRow => ({ - header: { title: 'IPv4 address', transforms: [sortable] }, + header: { + title: 'IPv4 address', + sort: true, + }, cell: (host) => { const inventory = getInventory(host); const nics = inventory.interfaces || []; @@ -320,7 +324,10 @@ export const ipv4Column = (cluster: Cluster): TableRow => ({ }); export const ipv6Column = (cluster: Cluster): TableRow => ({ - header: { title: 'IPv6 address', transforms: [sortable] }, + header: { + title: 'IPv6 address', + sort: true, + }, cell: (host) => { const inventory = getInventory(host); const nics = inventory.interfaces || []; @@ -337,7 +344,10 @@ export const ipv6Column = (cluster: Cluster): TableRow => ({ }); export const macAddressColumn = (cluster: Cluster): TableRow => ({ - header: { title: 'MAC address', transforms: [sortable] }, + header: { + title: 'MAC address', + sort: true, + }, cell: (host) => { const inventory = getInventory(host); const nics = inventory.interfaces || []; diff --git a/libs/ui-lib/lib/common/components/storage/DisksTable.tsx b/libs/ui-lib/lib/common/components/storage/DisksTable.tsx index 9979981b33..706f1764b8 100644 --- a/libs/ui-lib/lib/common/components/storage/DisksTable.tsx +++ b/libs/ui-lib/lib/common/components/storage/DisksTable.tsx @@ -7,9 +7,7 @@ import { Alert, AlertVariant, } from '@patternfly/react-core'; -import { TableVariant, RowWrapperProps, RowWrapper, IRow } from '@patternfly/react-table'; -import { Table, TableHeader, TableBody } from '@patternfly/react-table/deprecated'; -import { ExtraParamsType } from '@patternfly/react-table/dist/js/components/Table/base/types'; +import { TableVariant, Thead, Tbody, Table, Th, Tr, Td } from '@patternfly/react-table'; import type { Disk, Host } from '@openshift-assisted/types/assisted-installer-service'; import type { WithTestID } from '../../types/index'; import DiskRole, { OnDiskRoleType } from '../hosts/DiskRole'; @@ -34,29 +32,25 @@ interface DisksTableProps extends WithTestID { updateDiskSkipFormatting?: DiskFormattingType; } -const diskColumns = (t: TFunction, showFormat: boolean) => [ - { title: t('ai:Name') }, - { title: t('ai:Role') }, - { title: t('ai:Limitations') }, - showFormat ? { title: t('ai:Format?') } : '', - { title: t('ai:Drive type') }, - { title: t('ai:Size') }, - { title: t('ai:Serial') }, - { title: t('ai:Model') }, - { - title: ( - <> - WWN - - ), - }, -]; - -const diskRowKey = ({ rowData }: ExtraParamsType) => rowData?.key as string; - -const DisksTableRowWrapper = (props: RowWrapperProps) => ( - -); +const diskColumns = (t: TFunction, showFormat: boolean) => + [ + { title: t('ai:Name') }, + { title: t('ai:Role') }, + { title: t('ai:Limitations') }, + showFormat ? { title: t('ai:Format?') } : '', + { title: t('ai:Drive type') }, + { title: t('ai:Size') }, + { title: t('ai:Serial') }, + { title: t('ai:Model') }, + { + title: ( + <> + WWN{' '} + + + ), + }, + ].filter(Boolean) as { title: string | React.ReactNode }[]; const SkipFormattingDisk = () => ( @@ -151,7 +145,7 @@ const DisksTable = ({ const isEditable = !!canEditDisks?.(host); const diskColumnTitles = diskColumns(t, isEditable); - const rows: IRow[] = disks + const rows = disks .filter((disk) => disk.driveType !== 'LVM') .sort((a, b) => (a.name && a.name.localeCompare(b.name as string)) || 0) .sort((a, b) => { @@ -205,20 +199,28 @@ const DisksTable = ({ { title: disk.wwn, props: { 'data-testid': 'disk-wwn' } }, ], key: disk.path, - })); + })) as { key: string; cells: { title: string | React.ReactNode; props: object }[] }[]; return ( - - - +
    + + + {diskColumnTitles.map((col, i) => ( + + ))} + + + + {rows.map((row, i) => ( + + {row.cells.map((cell, j) => ( + + ))} + + ))} +
    {col.title}
    + {cell.title} +
    ); }; diff --git a/libs/ui-lib/lib/common/components/storage/StorageUtils.tsx b/libs/ui-lib/lib/common/components/storage/StorageUtils.tsx index e69cfd4c7d..36f3607755 100644 --- a/libs/ui-lib/lib/common/components/storage/StorageUtils.tsx +++ b/libs/ui-lib/lib/common/components/storage/StorageUtils.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { sortable } from '@patternfly/react-table'; import { TFunction } from 'i18next'; import { getHostRole, getInventory, RoleCell, UiIcon } from '../../index'; import { TableRow } from '../hosts/AITable'; @@ -20,7 +19,7 @@ export const roleColumn = (t: TFunction, schedulableMasters: boolean): TableRow< props: { id: 'col-header-role', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const hostRole = getHostRole(host, t, schedulableMasters); @@ -39,7 +38,7 @@ export const numberOfDisksColumn: TableRow = { props: { id: 'col-header-num-disks', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const inventory = getInventory(host); @@ -70,7 +69,7 @@ export const odfUsageColumn = (excludeMasters: boolean): TableRow => { props: { id: 'col-header-odf', }, - transforms: [sortable], + sort: true, }, cell: (host) => { const isMaster = host.role === 'master' || host.suggestedRole === 'master'; diff --git a/libs/ui-lib/lib/common/components/ui/EventsList.tsx b/libs/ui-lib/lib/common/components/ui/EventsList.tsx index 30fb688632..538cf326aa 100644 --- a/libs/ui-lib/lib/common/components/ui/EventsList.tsx +++ b/libs/ui-lib/lib/common/components/ui/EventsList.tsx @@ -1,21 +1,16 @@ import React from 'react'; import { Button, ButtonVariant, Label } from '@patternfly/react-core'; -import { TableVariant, breakWord } from '@patternfly/react-table'; -import { Table, TableBody } from '@patternfly/react-table/deprecated'; +import { Table, TableText, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; import { InfoCircleIcon } from '@patternfly/react-icons/dist/js/icons/info-circle-icon'; import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/js/icons/exclamation-triangle-icon'; import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/js/icons/exclamation-circle-icon'; import { SearchIcon } from '@patternfly/react-icons/dist/js/icons/search-icon'; -import { ExtraParamsType } from '@patternfly/react-table/dist/js/components/Table/base/types'; import { Event, EventList } from '@openshift-assisted/types/assisted-installer-service'; import { EmptyState } from './uiState'; import { getHumanizedDateTime } from './utils'; -import { fitContent, noPadding } from './table'; import { useTranslation } from '../../hooks/use-translation-wrapper'; -const getEventRowKey = ({ rowData }: ExtraParamsType) => - // eslint-disable-next-line - rowData?.props?.event.eventTime + rowData?.props?.event.message; +const getEventRowKey = (event: Event) => event.eventTime + event.message; const getLabelColor = (severity: Event['severity']) => { switch (severity) { @@ -74,11 +69,15 @@ const EventsList = ({ events, resetFilters }: EventsListProps) => { const rows = events.map((event) => ({ cells: [ { - title: {getHumanizedDateTime(event.eventTime)}, + title: ( + + {getHumanizedDateTime(event.eventTime)} + + ), }, { title: ( - <> + {event.severity !== 'info' && ( <> ), }, ], @@ -95,17 +94,28 @@ const EventsList = ({ events, resetFilters }: EventsListProps) => { })); return ( - - +
    + + + + + + + + {rows.map((row, i) => ( + + {row.cells.map((cell, j) => ( + + ))} + + ))} +
    + + {t('ai:Time')} + + + {t('ai:Message')} +
    {cell.title}
    ); }; diff --git a/libs/ui-lib/lib/common/components/ui/table/index.ts b/libs/ui-lib/lib/common/components/ui/table/index.ts index 76cb4d26c8..04bca77e0d 100644 --- a/libs/ui-lib/lib/common/components/ui/table/index.ts +++ b/libs/ui-lib/lib/common/components/ui/table/index.ts @@ -1,2 +1 @@ export * from './utils'; -export * from './wrappable'; diff --git a/libs/ui-lib/lib/common/components/ui/table/utils.ts b/libs/ui-lib/lib/common/components/ui/table/utils.ts index 173ef15822..86bce2802e 100644 --- a/libs/ui-lib/lib/common/components/ui/table/utils.ts +++ b/libs/ui-lib/lib/common/components/ui/table/utils.ts @@ -7,17 +7,12 @@ import { getHumanizedDateTime } from '../utils'; export type HumanizedSortable = { title: string | React.ReactNode; sortableValue: number | string; + props?: IRow['props']; }; type GetCellType = (row: IRow, index: number | undefined) => string | HumanizedSortable; -export type GenericTableProps = { - rowId: string; - cells: IRow['cells'][]; -}; - -export const genericTableRowKey = ({ rowData }: { rowData: GenericTableProps }): string => - `row-key${rowData.rowId}`; +export const genericTableRowKey = (rowId: string): string => `row-key${rowId}`; /** * Generates rows array for item which spans across all table columns. diff --git a/libs/ui-lib/lib/common/components/ui/table/wrappable.css b/libs/ui-lib/lib/common/components/ui/table/wrappable.css deleted file mode 100644 index 9aa22f70f1..0000000000 --- a/libs/ui-lib/lib/common/components/ui/table/wrappable.css +++ /dev/null @@ -1,3 +0,0 @@ -.table-no-padding { - --pf-v5-c-table--m-compact--cell--first-last-child--PaddingLeft: 0 !important; -} diff --git a/libs/ui-lib/lib/common/components/ui/table/wrappable.ts b/libs/ui-lib/lib/common/components/ui/table/wrappable.ts deleted file mode 100644 index 4d93a7e991..0000000000 --- a/libs/ui-lib/lib/common/components/ui/table/wrappable.ts +++ /dev/null @@ -1,29 +0,0 @@ -// TODO(jtomasek): replace this module with transforms from @patternfly/react-table -// when available -import { ITransform } from '@patternfly/react-table'; - -import './wrappable.css'; - -export const breakWord: ITransform = () => ({ - className: 'pf-m-break-word', -}); - -export const fitContent: ITransform = () => ({ - className: 'pf-m-fit-content', -}); - -export const nowrap: ITransform = () => ({ - className: 'pf-m-nowrap', -}); - -export const truncate: ITransform = () => ({ - className: 'pf-m-truncate', -}); - -export const wrappable: ITransform = () => ({ - className: 'pf-m-wrap', -}); - -export const noPadding: ITransform = () => ({ - className: 'table-no-padding', -}); diff --git a/libs/ui-lib/lib/common/config/docs_links.ts b/libs/ui-lib/lib/common/config/docs_links.ts index f21fd0ec55..d5f177083c 100644 --- a/libs/ui-lib/lib/common/config/docs_links.ts +++ b/libs/ui-lib/lib/common/config/docs_links.ts @@ -100,3 +100,6 @@ export const FEEDBACK_FORM_LINK = export const CHANGE_ISO_PASSWORD_FILE_LINK = 'https://raw.githubusercontent.com/openshift/assisted-service/master/docs/change-iso-password.sh'; + +export const getCiscoIntersightLink = (downloadIsoUrl: string) => + `https://www.intersight.com/an/workflow/workflow-definitions/execute/AddServersFromISO?_workflow_Version=1&IsoUrl=${downloadIsoUrl}`; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationTableBase.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationTableBase.tsx index 31b8d2f84e..a30d7e5ce5 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationTableBase.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/networkConfiguration/NetworkConfigurationTableBase.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { sortable } from '@patternfly/react-table'; import { HostsTableActions, selectSchedulableMasters } from '../../../../common'; import NetworkingStatus from '../../hosts/NetworkingStatus'; import { useTranslation } from '../../../../common/hooks/use-translation-wrapper'; @@ -24,7 +23,10 @@ import { Cluster, Host } from '@openshift-assisted/types/assisted-installer-serv export const networkingStatusColumn = ( onEditHostname?: HostsTableActions['onEditHost'], ): TableRow => ({ - header: { title: 'Status', transforms: [sortable] }, + header: { + title: 'Status', + sort: true, + }, cell: (host) => { const editHostname = onEditHostname ? () => onEditHostname(host) : undefined; const validationsInfo = stringToJSON(host.validationsInfo) || {}; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewClusterDetailTable.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewClusterDetailTable.tsx index fcc2366ba7..3f936b23ab 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewClusterDetailTable.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewClusterDetailTable.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { TableVariant } from '@patternfly/react-table'; -import { Table, TableBody } from '@patternfly/react-table/deprecated'; +import { Table, TableVariant, Tbody, Td, Tr } from '@patternfly/react-table'; import { genericTableRowKey, getDefaultCpuArchitecture } from '../../../../common'; import { getDiskEncryptionEnabledOnStatus } from '../../clusterDetail/ClusterProperties'; import OpenShiftVersionDetail from '../../clusterDetail/OpenShiftVersionDetail'; @@ -64,6 +63,7 @@ export const ReviewClusterDetailTable = ({ cluster }: { cluster: Cluster }) => { ], }, ]; + const diskEncryptionTitle = getDiskEncryptionEnabledOnStatus(cluster.diskEncryption?.enableOn); if (diskEncryptionTitle) { rows.push({ @@ -82,14 +82,22 @@ export const ReviewClusterDetailTable = ({ cluster }: { cluster: Cluster }) => { return ( - + + {rows.map((row, i) => ( + + {row.cells.map((cell, j) => ( + + ))} + + ))} +
    + {cell.title} +
    ); }; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewCustomManifestsTable.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewCustomManifestsTable.tsx index 3435e14e25..a4f687ddd9 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewCustomManifestsTable.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewCustomManifestsTable.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { TableVariant } from '@patternfly/react-table'; -import { Table, TableBody } from '@patternfly/react-table/deprecated'; +import { Table, TableVariant, Tbody, Td, Tr } from '@patternfly/react-table'; import { Alert } from '@patternfly/react-core'; import { ListManifestsExtended } from '../manifestsConfiguration/data/dataTypes'; import { genericTableRowKey } from '../../../../common'; @@ -28,14 +27,22 @@ export const ReviewCustomManifestsTable = ({ manifests }: { manifests: ListManif return ( <> - + + {rows.map((row, i) => ( + + {row.cells.map((cell, j) => ( + + ))} + + ))} +
    + {cell.title} +
    { const rows = React.useMemo(() => { - const networkRows: TableProps['rows'] = [ + const networkRows = [ { rowId: 'network-management', cells: [ @@ -40,7 +42,7 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => { }, ], }, - ]; + ] as ReviewTableRowsType; !!cluster.machineNetworks?.length && networkRows.push({ @@ -56,7 +58,7 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => { )), props: { 'data-testid': 'machine-networks' }, }, - isDualStack(cluster) && { title: 'Primary' }, + isDualStack(cluster) ? { title: 'Primary' } : { title: '' }, ], }); @@ -94,7 +96,7 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => { { rowId: 'cluster-network-cidr', cells: [ - 'Cluster network CIDR', + { title: 'Cluster network CIDR' }, { title: cluster.clusterNetworks?.map((network) => ( @@ -105,12 +107,12 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => { props: { 'data-testid': 'cluster-network-cidr' }, }, isDualStack(cluster) && { title: 'Primary' }, - ], + ].filter(Boolean), }, { rowId: 'cluster-network-host-prefix', cells: [ - 'Cluster network host prefix', + { title: 'Cluster network host prefix' }, { title: cluster.clusterNetworks?.map((network) => ( @@ -121,12 +123,12 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => { props: { 'data-testid': 'cluster-network-prefix' }, }, isDualStack(cluster) && { title: 'Primary' }, - ], + ].filter(Boolean), }, { rowId: 'service-network-cidr', cells: [ - 'Service network CIDR', + { title: 'Service network CIDR' }, { title: cluster.serviceNetworks?.map((network) => ( @@ -137,32 +139,40 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => { props: { 'data-testid': 'service-network-cidr' }, }, isDualStack(cluster) && { title: 'Primary' }, - ], + ].filter(Boolean), }, { rowId: 'networking-type', cells: [ - 'Networking type', + { title: 'Networking type' }, { title: getNetworkType(cluster.networkType), props: { 'data-testid': 'networking-type', colSpan: 2 }, }, ], }, - ]; + ] as ReviewTableRowsType; }, [cluster]); return ( <> - + + {rows.map((row, i) => ( + + {row.cells.map((cell, j) => ( + + ))} + + ))} +
    + {cell.title} +

    @@ -171,14 +181,22 @@ export const ReviewNetworkingTable = ({ cluster }: { cluster: Cluster }) => { - + + {rowsAdvanced.map((row, i) => ( + + {row.cells.map((cell, j) => ( + + ))} + + ))} +
    + {cell.title} +
    ); diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewOperatorsTable.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewOperatorsTable.tsx index 576fedf46b..7d6a1c5b6b 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewOperatorsTable.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewOperatorsTable.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { TableVariant } from '@patternfly/react-table'; -import { Table, TableBody } from '@patternfly/react-table/deprecated'; +import { Table, TableVariant, Tbody, Td, Tr } from '@patternfly/react-table'; import { ExposedOperatorNames, hasEnabledOperators, @@ -22,7 +21,7 @@ export const ReviewOperatorsTable = ({ cluster }: { cluster: Cluster }) => { ).map((operator) => ({ rowId: `operator-${operator}`, cells: [ - operatorNames[operator], + { title: operatorNames[operator] }, { title: 'Enabled', props: { 'data-testid': `operator-${operator}` }, @@ -33,14 +32,22 @@ export const ReviewOperatorsTable = ({ cluster }: { cluster: Cluster }) => { return ( - + + {rows.map((row, i) => ( + + {row.cells.map((cell, j) => ( + + ))} + + ))} +
    + {cell.title} +
    ); }; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewPlatformTable.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewPlatformTable.tsx index 4b95c2ac51..6543cce4e5 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewPlatformTable.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/review/ReviewPlatformTable.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { TableVariant } from '@patternfly/react-table'; -import { Table, TableBody } from '@patternfly/react-table/deprecated'; +import { Table, TableVariant, Tbody, Td, Tr } from '@patternfly/react-table'; import { genericTableRowKey } from '../../../../common'; import { Cluster, PlatformType } from '@openshift-assisted/types/assisted-installer-service'; import { ExternalPlatformLabels } from '../platformIntegration/constants'; @@ -23,14 +22,22 @@ export const ReviewPlatformTable = ({ cluster }: { cluster: Cluster }) => { return ( - + + {rows.map((row, i) => ( + + {row.cells.map((cell, j) => ( + + ))} + + ))} +
    + {cell.title} +
    ); }; diff --git a/libs/ui-lib/lib/ocm/components/clusters/ClustersTable.tsx b/libs/ui-lib/lib/ocm/components/clusters/ClustersTable.tsx index f08c2a1a6b..3e0372d870 100644 --- a/libs/ui-lib/lib/ocm/components/clusters/ClustersTable.tsx +++ b/libs/ui-lib/lib/ocm/components/clusters/ClustersTable.tsx @@ -1,32 +1,34 @@ import React from 'react'; import { - RowWrapper, - RowWrapperProps, + Table, + Thead, + Tr, + Th, + Tbody, + Td, + BaseCellProps, + ActionsColumn, + ThProps, SortByDirection, - ISortBy, - OnSort, IRow, - IActionsResolver, - cellWidth, - breakWord, - sortable, + ISortBy, + TrProps, } from '@patternfly/react-table'; -import { Table, TableProps, TableHeader, TableBody } from '@patternfly/react-table/deprecated'; -import { ClusterTableRows } from '../../../common/types/clusters'; -import DeleteClusterModal from './DeleteClusterModal'; -import { clusterStatusLabels, rowSorter, HumanizedSortable } from '../../../common'; -import ClustersListToolbar, { ClusterFiltersType } from './ClustersListToolbar'; -import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; import { ClusterRowDataProps, getClusterTableStatusCell, } from '../../store/slices/clusters/selectors'; +import ClustersListToolbar, { ClusterFiltersType } from './ClustersListToolbar'; +import { + clusterStatusLabels, + ClusterTableRows, + HumanizedSortable, + rowSorter, +} from '../../../common'; +import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; +import DeleteClusterModal from './DeleteClusterModal'; type DeleteClusterID = Pick; - -const rowKey = ({ rowData }: { rowData?: { props?: ClusterRowDataProps } }): string | undefined => - rowData?.props?.id; - const STORAGE_KEY_CLUSTERS_FILTER = 'assisted-installer-cluster-list-filters'; interface ClustersTableProps { @@ -34,56 +36,35 @@ interface ClustersTableProps { deleteCluster: (id: string) => Promise; } -type TablePropsCellType = TableProps['cells'][0]; type StoredFilters = { filters: ClusterFiltersType; sortBy: ISortBy; searchString: string }; -const columnConfig: TablePropsCellType = { - transforms: [sortable], - cellTransforms: [], - formatters: [], - cellFormatters: [], - props: {}, -}; - -const columns: TablePropsCellType[] = [ - { - title: 'Name', - dataLabel: 'Name', - ...columnConfig, - transforms: columnConfig?.transforms?.concat(cellWidth(20)), - cellTransforms: columnConfig?.cellTransforms?.concat(breakWord), - }, - { - title: 'Base domain', - dataLabel: 'Base domain', - ...columnConfig, - transforms: columnConfig?.transforms?.concat(cellWidth(40)), - cellTransforms: columnConfig?.cellTransforms?.concat(breakWord), - }, - { title: 'Version', dataLabel: 'Version', ...columnConfig }, - { title: 'Status', dataLabel: 'Status', ...columnConfig }, - { title: 'Hosts', dataLabel: 'Hosts', ...columnConfig }, - { title: 'Created at', dataLabel: 'Created at', ...columnConfig }, +const columns = [ + { title: 'Name', cellWidth: 20 }, + { title: 'Base domain', cellWidth: 40 }, + { title: 'Version' }, + { title: 'Status' }, + { title: 'Hosts' }, + { title: 'Created at' }, ]; const getStatusCell = (row: IRow) => row.cells?.[3] as HumanizedSortable | undefined; -const ClusterRowWrapper = (_props: RowWrapperProps) => { - const name = (_props?.row?.props as ClusterRowDataProps | undefined)?.name || ''; - const props = { - ..._props, +const getRowProps = (props?: ClusterRowDataProps) => { + const name = props?.name || ''; + return { + ...props, id: `cluster-row-${name}`, 'data-testid': `cluster-row-${name}`, - }; - return ; + } as Omit; }; -const ClustersTable: React.FC = ({ rows, deleteCluster }) => { - const [deleteClusterID, setDeleteClusterID] = React.useState(); +const ClustersTable = ({ rows, deleteCluster }: ClustersTableProps) => { const [sortBy, setSortBy] = React.useState({ index: 0, // Name-column direction: SortByDirection.asc, }); + + const [deleteClusterID, setDeleteClusterID] = React.useState(); const [isDeleteInProgress, setDeleteInProgress] = React.useState(false); const [isDeleteModalOpen, setDeleteModalOpen] = React.useState(false); @@ -94,6 +75,14 @@ const ClustersTable: React.FC = ({ rows, deleteCluster }) => const { t } = useTranslation(); + const getSortParams = (columnIndex: number): ThProps['sort'] => ({ + sortBy, + onSort: (_event, index, direction) => { + setSortBy({ index, direction }); + }, + columnIndex, + }); + React.useEffect(() => { const marshalled = window.sessionStorage.getItem(STORAGE_KEY_CLUSTERS_FILTER); if (marshalled) { @@ -115,35 +104,6 @@ const ClustersTable: React.FC = ({ rows, deleteCluster }) => ); }, [filters, sortBy, searchString]); - const actionResolver: IActionsResolver = React.useCallback( - (rowData) => { - const props = rowData.props as ClusterRowDataProps; - return [ - { - title: 'Delete', - id: `button-delete-${props.name}`, - isDisabled: - getClusterTableStatusCell(rowData).sortableValue === clusterStatusLabels(t).installing, - onClick: (/*event: React.MouseEvent, rowIndex: number, rowData: IRowData*/) => { - setDeleteClusterID({ id: props.id, name: props.name }); - setDeleteModalOpen(true); - }, - }, - ]; - }, - [t], - ); - - const onSort: OnSort = React.useCallback( - (_event, index, direction) => { - setSortBy({ - index, - direction, - }); - }, - [setSortBy], - ); - const rowFilter = React.useCallback( (row: IRow) => { const props = row.props as ClusterRowDataProps; @@ -198,18 +158,56 @@ const ClustersTable: React.FC = ({ rows, deleteCluster }) => filters={filters} setFilters={setFilters} /> - - - +
    + + + {columns.map((col, i) => ( + + ))} + + + + {sortedRows.map((row, i) => ( + + {row.cells?.map((cell, j) => ( + + ))} + + + ))} +
    + {col.title} + +
    + {(cell as HumanizedSortable)?.title} + + { + setDeleteClusterID({ + id: (row.props as ClusterRowDataProps).id, + name: (row.props as ClusterRowDataProps).name, + }); + setDeleteModalOpen(true); + }, + }, + ]} + /> +
    { const validationsInfo = stringToJSON(host.validationsInfo) || {};