diff --git a/package-lock.json b/package-lock.json index 8add3dc..07e74c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@sentry/webpack-plugin": "^3.5.0", "@unleash/proxy-client-react": "^5.0.0", "axios": "^1.10.0", + "bastilian-tabletools": "^2.11.0", "classnames": "^2.3.1", "graphql": "^15.10.1", "lodash": "^4.17.21", @@ -4429,6 +4430,16 @@ "tslib": "2" } }, + "node_modules/@jsonquerylang/jsonquery": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@jsonquerylang/jsonquery/-/jsonquery-5.0.4.tgz", + "integrity": "sha512-QdgVkapeGRxUqOOJuh2svDutejKaCizhupEmO4ZKSsaLolD7w5QhgrjmBNuS1wMCM5TyNKifK4i1wBDfNzO9xQ==", + "license": "ISC", + "peer": true, + "bin": { + "jsonquery": "bin/cli.js" + } + }, "node_modules/@keyv/serialize": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.0.tgz", @@ -6381,6 +6392,99 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tanstack/pacer": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@tanstack/pacer/-/pacer-0.14.0.tgz", + "integrity": "sha512-DCwgDvJoDmApnUIK5/SVeBVtlCn8iDa6hEj81SjxEsYML2Yirv7LCC8AQirHsFCJs9GuiJl6gvX7fDjDuoPduA==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "^0.7.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.86.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.86.0.tgz", + "integrity": "sha512-Y6ibQm6BXbw6w1p3a5LrPn8Ae64M0dx7hGmnhrm9P+XAkCCKXOwZN0J5Z1wK/0RdNHtR9o+sWHDXd4veNI60tQ==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-pacer": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-pacer/-/react-pacer-0.15.0.tgz", + "integrity": "sha512-6ycBIh5Hd0WW3/iv1vwiNDewQuzrkULhAM80/zlIMLLIaSWsfxtSj5eo7vO5dCtvwZCD8oWnF3V1eCmx658Z3w==", + "license": "MIT", + "dependencies": { + "@tanstack/pacer": "0.14.0", + "@tanstack/react-store": "^0.7.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.86.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.86.0.tgz", + "integrity": "sha512-jgS/v0oSJkGHucv9zxOS8rL7mjATh1XO3K4eqAV4WMpAly8okcBrGi1YxRZN5S4B59F54x9JFjWrK5vMAvJYqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tanstack/query-core": "5.86.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.4.tgz", + "integrity": "sha512-DyG1e5Qz/c1cNLt/NdFbCA7K1QGuFXQYT6EfUltYMJoQ4LzBOGnOl5IjuxepNcRtmIKkGpmdMzdFZEkevgU9bQ==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.7.4", + "use-sync-external-store": "^1.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/store": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.4.tgz", + "integrity": "sha512-F1XqZQici1Aq6WigEfcxJSml92nW+85Om8ElBMokPNg5glCYVOmPkZGIQeieYFxcPiKTfwo0MTOQpUyJtwncrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -8704,6 +8808,31 @@ "dev": true, "license": "MIT" }, + "node_modules/bastilian-tabletools": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bastilian-tabletools/-/bastilian-tabletools-2.11.0.tgz", + "integrity": "sha512-bZIIgD7gHiVfDRPQ9ifEiMExt/TSPnjtxZqW81VmsjoMAJS/utRFpT5ypQP4fooL7swcB3iyORbDdpe7rmV6UQ==", + "license": "Apache-2.0", + "dependencies": { + "@tanstack/react-pacer": "^0.15.0" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@jsonquerylang/jsonquery": "^5.0.4", + "@patternfly/patternfly": "^6.0.0", + "@patternfly/react-component-groups": "^6.0.0", + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-table": "^6.0.0", + "@redhat-cloud-services/frontend-components": ">= 6.1.0", + "@redhat-cloud-services/frontend-components-utilities": ">= 6.1.0", + "@tanstack/react-query": "^5.83.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "use-deep-compare": "^1.3.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -10933,7 +11062,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -24253,6 +24381,19 @@ "dev": true, "license": "MIT" }, + "node_modules/use-deep-compare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-deep-compare/-/use-deep-compare-1.3.0.tgz", + "integrity": "sha512-94iG+dEdEP/Sl3WWde+w9StIunlV8Dgj+vkt5wTwMoFQLaijiEZSXXy8KtcStpmEDtIptRJiNeD4ACTtVvnIKA==", + "license": "MIT", + "peer": true, + "dependencies": { + "dequal": "2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", diff --git a/package.json b/package.json index 8545737..7b0c1bd 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@sentry/webpack-plugin": "^3.5.0", "@unleash/proxy-client-react": "^5.0.0", "axios": "^1.10.0", + "bastilian-tabletools": "^2.11.0", "classnames": "^2.3.1", "graphql": "^15.10.1", "lodash": "^4.17.21", diff --git a/src/Routes/Signatures/Signatures.js b/src/Routes/Signatures/Signatures.js index 0c5237b..eb957d8 100644 --- a/src/Routes/Signatures/Signatures.js +++ b/src/Routes/Signatures/Signatures.js @@ -20,11 +20,13 @@ import { import messages from '../../Messages'; import { hasMalware } from '../../store/cache'; import { DocumentationLink } from '../../Components/Common'; -import SigTable from '../../Components/SigTable/SigTable'; + import StatusCard from '../../Components/StatusCard/StatusCard'; import ChartCard from '../../Components/ChartCard/ChartCard'; import { useFeatureFlag } from '../../Utilities/Hooks'; +import SignaturesTable from './components/SignaturesTable/SignaturesTable'; + const Signatures = () => { const isLightspeedEnabled = useFeatureFlag('platform.lightspeed-rebrand'); @@ -104,7 +106,7 @@ const Signatures = () => { - + diff --git a/src/Routes/Signatures/components/SignaturesTable/SignaturesTable.js b/src/Routes/Signatures/components/SignaturesTable/SignaturesTable.js new file mode 100644 index 0000000..9349dc4 --- /dev/null +++ b/src/Routes/Signatures/components/SignaturesTable/SignaturesTable.js @@ -0,0 +1,64 @@ +import React from 'react'; +import { useQuery } from '@apollo/client'; +import { + TableToolsTable, + TableStateProvider, + useSerialisedTableState, +} from 'bastilian-tabletools'; + +import { GET_SIGNATURE_TABLE } from '../../../../operations/queries'; + +import columns from './columns'; +import filters from './filters'; +import serialisers from './serialisers'; +import SignatureDescription from './components/SignatureDescription'; + +const SignaturesTable = () => { + const serialisedState = useSerialisedTableState(); + const { pagination, sort: orderBy } = serialisedState || {}; + const { + data: { rules: { totalCount: total } = {}, rulesList: items } = {}, + loading, + error, + } = useQuery(GET_SIGNATURE_TABLE, { + variables: { + orderBy, + ...(pagination || {}), + }, + skip: !serialisedState, + }); + + return ( + + ); +}; + +const SignaturesTableWithTableStateProvider = (props) => ( + + + +); + +export default SignaturesTableWithTableStateProvider; diff --git a/src/Routes/Signatures/components/SignaturesTable/columns.js b/src/Routes/Signatures/components/SignaturesTable/columns.js new file mode 100644 index 0000000..75492f3 --- /dev/null +++ b/src/Routes/Signatures/components/SignaturesTable/columns.js @@ -0,0 +1,30 @@ +import SignatureName from './components/cells/SignatureName'; +import LastStatus from './components/cells/LastStatus'; +import Systems from './components/cells/Systems'; +import LastMatched from './components/cells/LastMatched'; + +const signatureName = { + title: 'Signature name', + Component: SignatureName, + sortable: 'NAME', +}; + +const lastStatus = { + title: 'Last status', + Component: LastStatus, + sortable: 'LAST_STATUS', +}; + +const systems = { + title: 'Systems', + Component: Systems, + sortable: 'HOST_COUNT', +}; + +const lastMatched = { + title: 'Last matched', + Component: LastMatched, + sortable: 'LAST_MATCH_DATE_NULLS_LAST', +}; + +export default [signatureName, lastStatus, systems, lastMatched]; diff --git a/src/Routes/Signatures/components/SignaturesTable/components/SignatureDescription.js b/src/Routes/Signatures/components/SignaturesTable/components/SignatureDescription.js new file mode 100644 index 0000000..495b5d7 --- /dev/null +++ b/src/Routes/Signatures/components/SignaturesTable/components/SignatureDescription.js @@ -0,0 +1,28 @@ +import React from 'react'; +import propTypes from 'prop-types'; +import { + Grid, + GridItem, + Content, + ContentVariants, +} from '@patternfly/react-core'; + +const SignatureDesctiprion = ({ item: signature }) => ( +
+ + + + {' '} + Description + {signature.metadata.description} + + + +
+); + +SignatureDesctiprion.propTypes = { + item: propTypes.object, +}; + +export default SignatureDesctiprion; diff --git a/src/Routes/Signatures/components/SignaturesTable/components/cells/LastMatched.js b/src/Routes/Signatures/components/SignaturesTable/components/cells/LastMatched.js new file mode 100644 index 0000000..5df61cb --- /dev/null +++ b/src/Routes/Signatures/components/SignaturesTable/components/cells/LastMatched.js @@ -0,0 +1,37 @@ +import React from 'react'; +import propTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Tooltip } from '@patternfly/react-core'; +import { DateFormat } from '@redhat-cloud-services/frontend-components/DateFormat'; + +import messages from '../../../../../../Messages'; + +const LastMatched = ({ lastMatchDate, isDisabled }) => ( + <> + {isDisabled ? ( + + ) : lastMatchDate ? ( + } + > + + + + + ) : ( + }> + + + + + )} + +); + +LastMatched.propTypes = { + hostCount: propTypes.string, + lastMatchDate: propTypes.object, + isDisabled: propTypes.bool, +}; + +export default LastMatched; diff --git a/src/Routes/Signatures/components/SignaturesTable/components/cells/LastStatus.js b/src/Routes/Signatures/components/SignaturesTable/components/cells/LastStatus.js new file mode 100644 index 0000000..f7577f2 --- /dev/null +++ b/src/Routes/Signatures/components/SignaturesTable/components/cells/LastStatus.js @@ -0,0 +1,23 @@ +import React from 'react'; +import propTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import StatusLabel from '../../../../../../Components/StatusLabel/StatusLabel'; + +import messages from '../../../../../../Messages'; + +const LastStatus = ({ lastStatus, isDisabled }) => ( + <> + {isDisabled ? ( + + ) : ( + + )} + +); + +LastStatus.propTypes = { + lastStatus: propTypes.string, + isDisabled: propTypes.bool, +}; + +export default LastStatus; diff --git a/src/Routes/Signatures/components/SignaturesTable/components/cells/SignatureName.js b/src/Routes/Signatures/components/SignaturesTable/components/cells/SignatureName.js new file mode 100644 index 0000000..0eb42ba --- /dev/null +++ b/src/Routes/Signatures/components/SignaturesTable/components/cells/SignatureName.js @@ -0,0 +1,28 @@ +import React from 'react'; +import propTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Label } from '@patternfly/react-core'; +import InsightsLink from '@redhat-cloud-services/frontend-components/InsightsLink'; + +import messages from '../../../../../../Messages'; + +const SignatureName = ({ name, isDisabled }) => ( + <> + {name} + {isDisabled && ( + <> +
+ + + )} + +); + +SignatureName.propTypes = { + name: propTypes.string, + isDisabled: propTypes.bool, +}; + +export default SignatureName; diff --git a/src/Routes/Signatures/components/SignaturesTable/components/cells/Systems.js b/src/Routes/Signatures/components/SignaturesTable/components/cells/Systems.js new file mode 100644 index 0000000..f8884bf --- /dev/null +++ b/src/Routes/Signatures/components/SignaturesTable/components/cells/Systems.js @@ -0,0 +1,24 @@ +import React from 'react'; +import propTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import InsightsLink from '@redhat-cloud-services/frontend-components/InsightsLink'; + +import messages from '../../../../../../Messages'; + +const Systems = ({ name, hostCount, isDisabled }) => ( + <> + {isDisabled ? ( + + ) : ( + {hostCount} + )} + +); + +Systems.propTypes = { + name: propTypes.string, + hostCount: propTypes.string, + isDisabled: propTypes.bool, +}; + +export default Systems; diff --git a/src/Routes/Signatures/components/SignaturesTable/filters.js b/src/Routes/Signatures/components/SignaturesTable/filters.js new file mode 100644 index 0000000..407bcc9 --- /dev/null +++ b/src/Routes/Signatures/components/SignaturesTable/filters.js @@ -0,0 +1,29 @@ +const signaturesIncluded = { + type: 'checkbox', + label: 'Signatures included in malware analysis', + items: [ + { + label: 'Enabled signatures', + value: 'false', + }, + { + label: 'Disabled signatures', + value: 'true', + }, + ], +}; +const signature = { + label: 'Signature', + type: 'text', +}; + +const status = { + label: 'Status', + type: 'singleSelect', + items: [ + { label: 'Matched', value: 'matched' }, + { label: 'Not matched', value: 'notMatched' }, + ], +}; + +export default [signaturesIncluded, signature, status]; diff --git a/src/Routes/Signatures/components/SignaturesTable/helpers.js b/src/Routes/Signatures/components/SignaturesTable/helpers.js new file mode 100644 index 0000000..ac59667 --- /dev/null +++ b/src/Routes/Signatures/components/SignaturesTable/helpers.js @@ -0,0 +1,3 @@ +export const paginationSerialiser = () => { + return; +}; diff --git a/src/Routes/Signatures/components/SignaturesTable/serialisers.js b/src/Routes/Signatures/components/SignaturesTable/serialisers.js new file mode 100644 index 0000000..44581da --- /dev/null +++ b/src/Routes/Signatures/components/SignaturesTable/serialisers.js @@ -0,0 +1,67 @@ +export const paginationSerialiser = ({ perPage, page } = {}) => { + if (perPage && page) { + const offset = (page - 1) * perPage; + + return { offset, limit: perPage }; + } +}; + +export const sortSerialiser = ({ index, direction } = {}, columns) => { + const tableSort = + columns[index]?.sortable && + `${columns[index].sortable}_${direction}`.toUpperCase(); + + return [...(tableSort ? [tableSort] : []), 'NAME_ASC']; +}; + +const textFilterSerialiser = (filterConfigItem, value) => + `${filterConfigItem.filterAttribute} ~ "${value}"`; + +const checkboxFilterSerialiser = (filterConfigItem, values) => + `${filterConfigItem.filterAttribute} ^ (${values + .map((value) => `${value}`) + .join(' ')})`; + +const raidoFilterSerialiser = (filterConfigItem, values) => + `${filterConfigItem.filterAttribute} = "${values[0]}"`; + +const filterSerialisers = { + text: textFilterSerialiser, + checkbox: checkboxFilterSerialiser, + radio: raidoFilterSerialiser, +}; + +const findFilterSerialiser = (filterConfigItem) => { + if (filterConfigItem.filterSerialiser) { + return filterConfigItem.filterSerialiser; + } else { + return ( + filterConfigItem.filterAttribute && + filterSerialisers[filterConfigItem?.type] + ); + } +}; + +export const filtersSerialiser = (state, filters) => { + const queryParts = Object.entries(state || {}) + .reduce((filterQueryParts, [filterId, value]) => { + const filterConfigItem = filters.find((filter) => filter.id === filterId); + const filterSerialiser = findFilterSerialiser(filterConfigItem || {}); + + return [ + ...filterQueryParts, + ...(filterSerialiser + ? [filterSerialiser(filterConfigItem, value)] + : []), + ]; + }, []) + .filter((v) => !!v); + + return queryParts.length > 0 ? queryParts.join(' AND ') : undefined; +}; + +export default { + pagination: paginationSerialiser, + sort: sortSerialiser, + filters: filtersSerialiser, +};