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,
+};