diff --git a/src/Utilities/constants.js b/src/Utilities/constants.js index 637ef1eaa..cb71bd7a9 100644 --- a/src/Utilities/constants.js +++ b/src/Utilities/constants.js @@ -23,8 +23,11 @@ export const INVENTORY_TOTAL_FETCH_URL_SERVER = '/api/inventory/v1/hosts'; export const INVENTORY_TOTAL_FETCH_EDGE_PARAMS = '?filter[system_profile][host_type]=edge&page=1&per_page=1'; export const INVENTORY_TOTAL_FETCH_CONVENTIONAL_PARAMS = '?page=1&per_page=1'; -export const INVENTORY_TOTAL_FETCH_BIFROST_PARAMS = - '?filter[system_profile][bootc_status][booted][image_digest][is]=not_nil&per_page=1'; +export const INVENTORY_FETCH_BOOTC_PARAMS = + '?filter[system_profile][bootc_status][booted][image_digest][is]'; +export const INVENTORY_FETCH_BOOTC = `${INVENTORY_FETCH_BOOTC_PARAMS}=not_nil`; +export const INVENTORY_FETCH_NON_BOOTC = `${INVENTORY_FETCH_BOOTC_PARAMS}=nil`; +export const INVENTORY_TOTAL_FETCH_BOOTC_PARAMS = `${INVENTORY_FETCH_BOOTC}&per_page=1`; export function subtractDate(days) { const date = new Date(); date.setDate(date.getDate() - days); diff --git a/src/Utilities/edge.js b/src/Utilities/edge.js index 965ef9d21..269b5d019 100644 --- a/src/Utilities/edge.js +++ b/src/Utilities/edge.js @@ -4,7 +4,7 @@ import { useGetImageData } from '../api'; import { INVENTORY_TOTAL_FETCH_EDGE_PARAMS, INVENTORY_TOTAL_FETCH_URL_SERVER, - INVENTORY_TOTAL_FETCH_BIFROST_PARAMS, + INVENTORY_TOTAL_FETCH_BOOTC_PARAMS, } from './constants'; const manageEdgeInventoryUrlName = 'manage-edge-inventory'; @@ -59,7 +59,7 @@ const inventoryHasEdgeSystems = async () => { const inventoryHasBootcImages = async () => { const result = await axios.get( - `${INVENTORY_TOTAL_FETCH_URL_SERVER}${INVENTORY_TOTAL_FETCH_BIFROST_PARAMS}` + `${INVENTORY_TOTAL_FETCH_URL_SERVER}${INVENTORY_TOTAL_FETCH_BOOTC_PARAMS}` ); return result?.data?.total > 0; }; diff --git a/src/routes/InventoryComponents/BifrostPage.js b/src/routes/InventoryComponents/BifrostPage.js new file mode 100644 index 000000000..34f331670 --- /dev/null +++ b/src/routes/InventoryComponents/BifrostPage.js @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import { + INVENTORY_FETCH_BOOTC, + INVENTORY_FETCH_NON_BOOTC, + INVENTORY_TOTAL_FETCH_URL_SERVER, +} from '../../Utilities/constants'; +import BifrostTable from './BifrostTable'; + +const BifrostPage = () => { + const [bootcImages, setBootcImages] = useState(); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + const fetchBootcImages = async () => { + setLoaded(false); + const result = await axios.get( + `${INVENTORY_TOTAL_FETCH_URL_SERVER}${INVENTORY_FETCH_BOOTC}&fields[system_profile]=bootc_status` + ); + + const packageBasedSystems = await axios.get( + `${INVENTORY_TOTAL_FETCH_URL_SERVER}${INVENTORY_FETCH_NON_BOOTC}&per_page=1` + ); + + const booted = result.data.results.map( + (system) => system.system_profile.bootc_status.booted + ); + + const target = {}; + + booted.forEach((bootedImage) => { + const { image, image_digest } = bootedImage; + if (!target[image]) { + target[image] = { + image, + systemCount: 1, + hashes: {}, + hashCommitCount: 0, + }; + } else { + target[image].systemCount += 1; + } + + if (!target[image].hashes[image_digest]) { + target[image].hashes[image_digest] = { + image_digest, + hashSystemCount: 1, + }; + target[image].hashCommitCount += 1; + } else { + target[image].hashes[image_digest].hashSystemCount += 1; + } + }); + + const updated = [ + ...Object.values(target).map((val) => ({ + ...val, + hashes: Object.values(val.hashes), + })), + { + image: 'Package based systems', + systemCount: packageBasedSystems.data.total, + hashCommitCount: '-', + }, + ]; + + setLoaded(true); + setBootcImages(updated); + }; + + fetchBootcImages(); + }, []); + + return ; +}; + +export default BifrostPage; diff --git a/src/routes/InventoryComponents/BifrostTable.js b/src/routes/InventoryComponents/BifrostTable.js index 0052e00cf..07c28ae07 100644 --- a/src/routes/InventoryComponents/BifrostTable.js +++ b/src/routes/InventoryComponents/BifrostTable.js @@ -1,7 +1,110 @@ -import React from 'react'; +import React, { useState } from 'react'; +import propTypes from 'prop-types'; +import { + Table, + Thead, + Tr, + Th, + Tbody, + Td, + ExpandableRowContent, +} from '@patternfly/react-table'; +import { SkeletonTable } from '@redhat-cloud-services/frontend-components/SkeletonTable'; +import { imageTableColumns } from './BifrostTableColumns'; +import NestedHashTable from './NestedHashTable'; +import BifrostTableRows from './BifrostTableRows'; -const BifrostTable = () => { - return
Bifrost table will be here
; +const BifrostTable = ({ bootcImages, loaded }) => { + const [expandedImageNames, setExpandedImageNames] = useState([]); + + const setImageExpanded = (image, isExpanding = true) => + setExpandedImageNames((prevExpanded) => { + const otherExpandedImageNames = prevExpanded.filter( + (r) => r !== image.image + ); + return isExpanding + ? [...otherExpandedImageNames, image.image] + : otherExpandedImageNames; + }); + + const isImageExpanded = (image) => expandedImageNames.includes(image.image); + + return ( + <> + {loaded ? ( + + + + + ))} + + + + {bootcImages?.map((image, rowIndex) => ( + <> + + {image.hashes ? ( + + {image.hashes ? ( + + + + ) : null} + + ))} + +
+ {imageTableColumns.map((col) => ( + + {col.title} +
+ setImageExpanded(image, !isImageExpanded(image)), + expandId: 'composable-nested-table-expandable-example', + }} + /> + ) : ( + + )} + {imageTableColumns.map((col) => ( + + ))} +
+ + + +
+ ) : ( + title)} + rows={15} + variant={'compact'} + /> + )} + + ); +}; + +BifrostTable.propTypes = { + bootcImages: propTypes.array, + loaded: propTypes.bool, }; export default BifrostTable; diff --git a/src/routes/InventoryComponents/BifrostTableColumns.js b/src/routes/InventoryComponents/BifrostTableColumns.js new file mode 100644 index 000000000..0f6fd5693 --- /dev/null +++ b/src/routes/InventoryComponents/BifrostTableColumns.js @@ -0,0 +1,39 @@ +const ImageColumn = { + title: 'Image name', + colSpan: 8, + ref: 'image', +}; + +const HashCommitsColumn = { + title: 'Hash commits', + colSpan: 2, + ref: 'hashCommitCount', +}; + +const SystemsColumn = { + title: 'Systems', + colSpan: 2, + ref: 'systemCount', + classname: 'ins-c-inventory__bootc-systems-count-cell', +}; + +const HashCommitColumn = { + title: 'Hash commit', + colSpan: 2, + ref: 'image_digest', +}; + +const HashSystemColumn = { + title: '', + colSpan: 10, + ref: 'hashSystemCount', + classname: 'ins-c-inventory__bootc-systems-count-cell', +}; + +export const imageTableColumns = [ + ImageColumn, + HashCommitsColumn, + SystemsColumn, +]; + +export const hashTableColumns = [HashCommitColumn, HashSystemColumn]; diff --git a/src/routes/InventoryComponents/BifrostTableRows.js b/src/routes/InventoryComponents/BifrostTableRows.js new file mode 100644 index 000000000..f031a061d --- /dev/null +++ b/src/routes/InventoryComponents/BifrostTableRows.js @@ -0,0 +1,22 @@ +import React from 'react'; +import propTypes from 'prop-types'; +import { Td } from '@patternfly/react-table'; + +const BifrostTableRows = ({ column, data }) => { + return ( + + {data[column.ref]} + + ); +}; + +BifrostTableRows.propTypes = { + column: propTypes.object, + data: propTypes.object, +}; + +export default BifrostTableRows; diff --git a/src/routes/InventoryComponents/NestedHashTable.js b/src/routes/InventoryComponents/NestedHashTable.js new file mode 100644 index 000000000..d2bef75bd --- /dev/null +++ b/src/routes/InventoryComponents/NestedHashTable.js @@ -0,0 +1,42 @@ +import React from 'react'; +import propTypes from 'prop-types'; +import { Table, Thead, Tr, Th, Tbody } from '@patternfly/react-table'; +import { hashTableColumns } from './BifrostTableColumns'; +import BifrostTableRows from './BifrostTableRows'; + +const NestedHashTable = ({ hashes }) => { + return ( + + + + {hashTableColumns.map((col) => { + return ( + + ); + })} + + + + {hashes.map((hash) => ( + + {hashTableColumns.map((col) => ( + + ))} + + ))} + +
+ {col.title} +
+ ); +}; + +NestedHashTable.propTypes = { + hashes: propTypes.array, +}; + +export default NestedHashTable; diff --git a/src/routes/InventoryPage.js b/src/routes/InventoryPage.js index dff9be042..32fc71ba0 100644 --- a/src/routes/InventoryPage.js +++ b/src/routes/InventoryPage.js @@ -3,7 +3,7 @@ import './inventory.scss'; import Main from '@redhat-cloud-services/frontend-components/Main'; import HybridInventory from './InventoryComponents/HybridInventory'; import InventoryPageHeader from './InventoryComponents/InventoryPageHeader'; -import BifrostTable from './InventoryComponents/BifrostTable'; +import BifrostPage from './InventoryComponents/BifrostPage'; export const pageContents = { hybridInventory: { @@ -12,7 +12,7 @@ export const pageContents = { }, bifrost: { key: 'bifrost', - component: BifrostTable, + component: BifrostPage, }, }; diff --git a/src/routes/InventoryPage.test.js b/src/routes/InventoryPage.test.js index 3fd9df499..df18ecc35 100644 --- a/src/routes/InventoryPage.test.js +++ b/src/routes/InventoryPage.test.js @@ -9,8 +9,8 @@ import InventoryPage from './InventoryPage'; jest.mock('./InventoryComponents/HybridInventory', () => () => (
)); -jest.mock('./InventoryComponents/BifrostTable', () => () => ( -
+jest.mock('./InventoryComponents/BifrostPage', () => () => ( +
)); jest.mock('../Utilities/useFeatureFlag', () => () => true); const defaultContextValues = { @@ -39,6 +39,6 @@ describe('Inventory', () => { await userEvent.click(bifrostToggle); - expect(screen.getByTestId('BifrostTable')).toBeInTheDocument(); + expect(screen.getByTestId('BifrostPage')).toBeInTheDocument(); }); }); diff --git a/src/routes/inventory.scss b/src/routes/inventory.scss index 2340e5509..976a81328 100644 --- a/src/routes/inventory.scss +++ b/src/routes/inventory.scss @@ -35,3 +35,8 @@ overflow: hidden; text-overflow: ellipsis; } + +.ins-c-inventory__bootc-systems-count-cell { + text-align: right; + padding-right: 64px; +}