diff --git a/ui/apps/platform/cypress/integration/accessControl/accessControlAccessScopes.test.js b/ui/apps/platform/cypress/integration/accessControl/accessControlAccessScopes.test.js index 4c477220b2040..9b6010df6ad64 100644 --- a/ui/apps/platform/cypress/integration/accessControl/accessControlAccessScopes.test.js +++ b/ui/apps/platform/cypress/integration/accessControl/accessControlAccessScopes.test.js @@ -39,7 +39,7 @@ describe('Access Control Access scopes', () => { cy.get('th:contains("Origin")'); cy.get('th:contains("Description")'); cy.get('th:contains("Roles")'); - cy.get('th[aria-label="Row actions"]'); + cy.get(`th:has('span.pf-v5-screen-reader:contains("Row actions")')`); }); it('list has default names', () => { diff --git a/ui/apps/platform/cypress/integration/accessControl/accessControlAuthProviders.test.js b/ui/apps/platform/cypress/integration/accessControl/accessControlAuthProviders.test.js index 7c2282a779e75..a6356a7e12aeb 100644 --- a/ui/apps/platform/cypress/integration/accessControl/accessControlAuthProviders.test.js +++ b/ui/apps/platform/cypress/integration/accessControl/accessControlAuthProviders.test.js @@ -58,7 +58,7 @@ describe('Access Control Auth providers', () => { cy.get('th:contains("Type")'); cy.get('th:contains("Minimum access role")'); cy.get('th:contains("Assigned rules")'); - cy.get('th[aria-label="Row actions"]'); + cy.get(`th:has('span.pf-v5-screen-reader:contains("Row actions")')`); }); it('add Auth0', () => { diff --git a/ui/apps/platform/cypress/integration/accessControl/accessControlPermissionSets.test.js b/ui/apps/platform/cypress/integration/accessControl/accessControlPermissionSets.test.js index fbab7b190c6ad..4047025f8c244 100644 --- a/ui/apps/platform/cypress/integration/accessControl/accessControlPermissionSets.test.js +++ b/ui/apps/platform/cypress/integration/accessControl/accessControlPermissionSets.test.js @@ -39,7 +39,7 @@ describe('Access Control Permission sets', () => { cy.get('th:contains("Origin")'); cy.get('th:contains("Description")'); cy.get('th:contains("Roles")'); - cy.get('th[aria-label="Row actions"]'); + cy.get(`th:has('span.pf-v5-screen-reader:contains("Row actions")')`); }); it('list has default names', () => { diff --git a/ui/apps/platform/cypress/integration/accessControl/accessControlRoles.test.js b/ui/apps/platform/cypress/integration/accessControl/accessControlRoles.test.js index 52fd637d91553..c936e0edeafc3 100644 --- a/ui/apps/platform/cypress/integration/accessControl/accessControlRoles.test.js +++ b/ui/apps/platform/cypress/integration/accessControl/accessControlRoles.test.js @@ -41,7 +41,7 @@ describe('Access Control Roles', () => { cy.get('th:contains("Description")'); cy.get('th:contains("Permission set")'); cy.get('th:contains("Access scope")'); - cy.get('th[aria-label="Row actions"]'); + cy.get(`th:has('span.pf-v5-screen-reader:contains("Row actions")')`); }); it('list has default names', () => { diff --git a/ui/apps/platform/eslint-plugins/accessibilityPlugin.js b/ui/apps/platform/eslint-plugins/accessibilityPlugin.js deleted file mode 100644 index 37a83d6bcb9a5..0000000000000 --- a/ui/apps/platform/eslint-plugins/accessibilityPlugin.js +++ /dev/null @@ -1,43 +0,0 @@ -const accessibilityPlugin = { - meta: { - name: 'accessibilityPlugin', - version: '0.0.1', - }, - rules: { - 'require-Alert-component': { - // Require alternative markup to prevent axe DevTools issue: - // Heading levels should only increase by one - meta: { - type: 'problem', - docs: { - description: 'Require that Alert element has component="p" prop', - }, - fixable: 'code', - schema: [], - }, - create(context) { - return { - JSXOpeningElement(node) { - if (node.name?.name === 'Alert') { - if ( - !node.attributes.some((nodeAttribute) => { - return ( - nodeAttribute.name?.name === 'component' && - nodeAttribute.value?.value === 'p' - ); - }) - ) { - context.report({ - node, - message: 'Alert element requires component="p" prop', - }); - } - } - }, - }; - }, - }, - }, -}; - -module.exports = accessibilityPlugin; diff --git a/ui/apps/platform/eslint-plugins/pluginAccessibility.js b/ui/apps/platform/eslint-plugins/pluginAccessibility.js new file mode 100644 index 0000000000000..3e8f8040f2646 --- /dev/null +++ b/ui/apps/platform/eslint-plugins/pluginAccessibility.js @@ -0,0 +1,172 @@ +/* globals module */ + +const rules = { + // ESLint naming convention for positive rules: + // If your rule is enforcing the inclusion of something, use a short name without a special prefix. + + 'Alert-component-prop': { + // Require alternative markup to prevent axe DevTools issue: + // Heading levels should only increase by one + // https://dequeuniversity.com/rules/axe/4.9/heading-order + meta: { + type: 'problem', + docs: { + description: 'Require that Alert element has component="p" prop', + }, + schema: [], + }, + create(context) { + return { + JSXOpeningElement(node) { + if (node.name?.name === 'Alert') { + if ( + !node.attributes.some((nodeAttribute) => { + return ( + nodeAttribute.name?.name === 'component' && + nodeAttribute.value?.value === 'p' + ); + }) + ) { + context.report({ + node, + message: 'Alert element requires component="p" prop', + }); + } + } + }, + }; + }, + }, + 'Th-screenReaderText-prop': { + // Require prop to prevent axe DevTools issue: + // Table header text should not be empty + // https://dequeuniversity.com/rules/axe/4.9/empty-table-header + + // Until upgrade to PatternFly 5.3 which has screenReaderText prop, + // temporary solution is to render child: + // {screenReaderText} + meta: { + type: 'problem', + docs: { + description: + 'Require that empty Th element has either expand, select, or screenReaderText prop', + }, + schema: [], + }, + create(context) { + return { + JSXElement(node) { + if (node.openingElement?.name?.name === 'Th') { + if (node.children?.length === 0) { + if ( + !node.openingElement?.attributes?.some((nodeAttribute) => { + return ['expand', 'select', 'screenReaderText'].includes( + nodeAttribute.name?.name + ); + }) + ) { + context.report({ + node, + message: + 'Require that empty Th element has either expand, select, or screenReaderText prop', + }); + } + } + } + }, + }; + }, + }, + + // ESLint naming convention for negative rules. + // If your rule only disallows something, prefix it with no. + // However, we can write forbid instead of disallow as the verb in description and message. + + 'no-Td-in-Thead': { + // Forbid work-around to prevent axe DevTools issue: + // Table header text should not be empty + // https://dequeuniversity.com/rules/axe/4.9/empty-table-header + meta: { + type: 'problem', + docs: { + description: 'Forbid Td as alternative to Th in Thead element', + }, + schema: [], + }, + create(context) { + return { + JSXElement(node) { + if (node.openingElement?.name?.name === 'Td') { + const ancestors = context.sourceCode.getAncestors(node); + if ( + ancestors.length >= 2 && + ancestors[ancestors.length - 1].openingElement?.name?.name === 'Tr' && + ancestors[ancestors.length - 2].openingElement?.name?.name === 'Thead' + ) { + context.report({ + node, + message: 'Forbid Td as alternative to Th in Thead element', + }); + } + } + }, + }; + }, + }, + 'no-Th-aria-label-prop': { + // Forbid work-around to prevent axe DevTools issue: + // Table header text should not be empty + // https://dequeuniversity.com/rules/axe/4.9/empty-table-header + + // Until upgrade to PatternFly 5.3 which has screenReaderText prop, + // temporary solution is to render child: + // {screenReaderText} + meta: { + type: 'problem', + docs: { + description: + 'Forbid aria-label as alternative to screenReaderText prop in Th element', + }, + schema: [], + }, + create(context) { + return { + JSXOpeningElement(node) { + if (node.name?.name === 'Th') { + if ( + node.attributes.some((nodeAttribute) => { + return nodeAttribute.name?.name === 'aria-label'; + }) + ) { + context.report({ + node, + message: + 'Forbid aria-label as alternative to screenReaderText prop in Th element', + }); + } + } + }, + }; + }, + }, +}; + +const pluginKey = 'accessibility'; // key of pluginAccessibility in eslint.config.js file + +const pluginAccessibility = { + meta: { + name: 'pluginAccessibility', + version: '0.0.1', + }, + rules, + // ...pluginAccessibility.configs.recommended.rules means all rules in eslint.config.js file. + configs: { + recommended: { + rules: Object.fromEntries( + Object.keys(rules).map((ruleKey) => [`${pluginKey}/${ruleKey}`, 'error']) + ), + }, + }, +}; + +module.exports = pluginAccessibility; diff --git a/ui/apps/platform/eslint.config.js b/ui/apps/platform/eslint.config.js index 2a5c746242a3c..040a8f5bf7ea5 100644 --- a/ui/apps/platform/eslint.config.js +++ b/ui/apps/platform/eslint.config.js @@ -4,7 +4,7 @@ const path = require('node:path'); const parserTypeScriptESLint = require('@typescript-eslint/parser'); -const pluginAccessibility = require('eslint-plugin-jsx-a11y'); +const pluginAccessibilityJSX = require('eslint-plugin-jsx-a11y'); const pluginCypress = require('eslint-plugin-cypress'); const pluginESLint = require('@eslint/js'); // eslint-disable-line import/no-extraneous-dependencies const pluginESLintComments = require('eslint-plugin-eslint-comments'); @@ -19,7 +19,7 @@ const pluginTypeScriptESLint = require('@typescript-eslint/eslint-plugin'); const { browser: browserGlobals, jest: jestGlobals, node: nodeGlobals } = require('globals'); -const accessibilityPlugin = require('./eslint-plugins/accessibilityPlugin'); +const pluginAccessibility = require('./eslint-plugins/pluginAccessibility'); const parserAndOptions = { parser: parserTypeScriptESLint, @@ -38,7 +38,6 @@ module.exports = [ 'coverage/**', 'react-app-rewired/**', 'scripts/**', - 'eslint-plugins/**', 'src/setupProxy.js', 'src/setupTests.js', 'cypress.d.ts', @@ -532,14 +531,14 @@ module.exports = [ // Key of plugin is namespace of its rules. plugins: { + accessibility: pluginAccessibility, import: pluginImport, - 'jsx-a11y': pluginAccessibility, + 'jsx-a11y': pluginAccessibilityJSX, react: pluginReact, 'react-hooks': pluginReactHooks, - accessibilityPlugin, }, rules: { - 'accessibilityPlugin/require-Alert-component': 'error', + ...pluginAccessibility.configs.recommended.rules, 'no-restricted-imports': [ 'error', @@ -701,7 +700,7 @@ module.exports = [ }, }, { - files: ['*.js', 'eslint-plugins/*.js', 'tailwind-plugins/*.js'], // non-product files + files: ['*.js', 'tailwind-plugins/*.js'], // non-product files languageOptions: { ...parserAndOptions, diff --git a/ui/apps/platform/src/Components/ExpandRowTh.tsx b/ui/apps/platform/src/Components/ExpandRowTh.tsx index bba63a8feb3da..be9b71c796b11 100644 --- a/ui/apps/platform/src/Components/ExpandRowTh.tsx +++ b/ui/apps/platform/src/Components/ExpandRowTh.tsx @@ -9,12 +9,13 @@ export default function ExpandRowTh(props: ThProps) { return ( + > + Row expansion + ); } diff --git a/ui/apps/platform/src/Containers/AccessControl/AccessScopes/AccessScopesList.tsx b/ui/apps/platform/src/Containers/AccessControl/AccessScopes/AccessScopesList.tsx index a480bbe938454..5db08618d2fde 100644 --- a/ui/apps/platform/src/Containers/AccessControl/AccessScopes/AccessScopesList.tsx +++ b/ui/apps/platform/src/Containers/AccessControl/AccessScopes/AccessScopesList.tsx @@ -81,7 +81,9 @@ function AccessScopesList({ Origin Description Roles - + + Row actions + diff --git a/ui/apps/platform/src/Containers/AccessControl/AccessScopes/LabelSelectorCard.tsx b/ui/apps/platform/src/Containers/AccessControl/AccessScopes/LabelSelectorCard.tsx index b85d166fc139c..76f6d6a13ffab 100644 --- a/ui/apps/platform/src/Containers/AccessControl/AccessScopes/LabelSelectorCard.tsx +++ b/ui/apps/platform/src/Containers/AccessControl/AccessScopes/LabelSelectorCard.tsx @@ -15,7 +15,7 @@ import { Tooltip, } from '@patternfly/react-core'; import { OutlinedQuestionCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; -import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { Table, Tbody, Th, Thead, Tr } from '@patternfly/react-table'; import { LabelSelectorRequirement, LabelSelectorsKey } from 'services/AccessScopesService'; @@ -203,7 +203,9 @@ function LabelSelectorCard({ Key - + + Operator + Values {isLabelSelectorActive && Action} diff --git a/ui/apps/platform/src/Containers/AccessControl/AuthProviders/AuthProvidersList.tsx b/ui/apps/platform/src/Containers/AccessControl/AuthProviders/AuthProvidersList.tsx index 688b6b774fa07..1ad26130ebc72 100644 --- a/ui/apps/platform/src/Containers/AccessControl/AuthProviders/AuthProvidersList.tsx +++ b/ui/apps/platform/src/Containers/AccessControl/AuthProviders/AuthProvidersList.tsx @@ -65,7 +65,9 @@ function AuthProvidersList({ entityId, authProviders }: AuthProvidersListProps): Type Minimum access role Assigned rules - + + Row actions + diff --git a/ui/apps/platform/src/Containers/AccessControl/PermissionSets/PermissionSetsList.tsx b/ui/apps/platform/src/Containers/AccessControl/PermissionSets/PermissionSetsList.tsx index 052c32cb0c450..5e025ffcc7a06 100644 --- a/ui/apps/platform/src/Containers/AccessControl/PermissionSets/PermissionSetsList.tsx +++ b/ui/apps/platform/src/Containers/AccessControl/PermissionSets/PermissionSetsList.tsx @@ -81,7 +81,9 @@ function PermissionSetsList({ Origin Description Roles - + + Row actions + diff --git a/ui/apps/platform/src/Containers/AccessControl/Roles/AccessScopesTable.tsx b/ui/apps/platform/src/Containers/AccessControl/Roles/AccessScopesTable.tsx index c036af58e3c7f..edfb11399749f 100644 --- a/ui/apps/platform/src/Containers/AccessControl/Roles/AccessScopesTable.tsx +++ b/ui/apps/platform/src/Containers/AccessControl/Roles/AccessScopesTable.tsx @@ -22,7 +22,9 @@ function AccessScopesTable({ - diff --git a/ui/apps/platform/src/Containers/AccessControl/Roles/PermissionSetsTable.tsx b/ui/apps/platform/src/Containers/AccessControl/Roles/PermissionSetsTable.tsx index 3dc9ed2ee4157..4564e4d339f5d 100644 --- a/ui/apps/platform/src/Containers/AccessControl/Roles/PermissionSetsTable.tsx +++ b/ui/apps/platform/src/Containers/AccessControl/Roles/PermissionSetsTable.tsx @@ -22,7 +22,9 @@ function PermissionSetsTable({
+ + Row selection + Name Description
- diff --git a/ui/apps/platform/src/Containers/AccessControl/Roles/RolesList.tsx b/ui/apps/platform/src/Containers/AccessControl/Roles/RolesList.tsx index b9b07bb1af56f..c97f47d91094b 100644 --- a/ui/apps/platform/src/Containers/AccessControl/Roles/RolesList.tsx +++ b/ui/apps/platform/src/Containers/AccessControl/Roles/RolesList.tsx @@ -115,7 +115,9 @@ function RolesList({ - diff --git a/ui/apps/platform/src/Containers/Audit/ListeningEndpoints/ListeningEndpointsTable.tsx b/ui/apps/platform/src/Containers/Audit/ListeningEndpoints/ListeningEndpointsTable.tsx index 87abcd870aba7..ad92d548499b2 100644 --- a/ui/apps/platform/src/Containers/Audit/ListeningEndpoints/ListeningEndpointsTable.tsx +++ b/ui/apps/platform/src/Containers/Audit/ListeningEndpoints/ListeningEndpointsTable.tsx @@ -78,9 +78,7 @@ function ListeningEndpointsTable({ }, }} width={10} - > - {/* Header for expanded column */} - + /> diff --git a/ui/apps/platform/src/Containers/Clusters/DelegateScanning/Components/DelegatedRegistriesTable.tsx b/ui/apps/platform/src/Containers/Clusters/DelegateScanning/Components/DelegatedRegistriesTable.tsx index 1126471836c89..fc5f2c4ef69da 100644 --- a/ui/apps/platform/src/Containers/Clusters/DelegateScanning/Components/DelegatedRegistriesTable.tsx +++ b/ui/apps/platform/src/Containers/Clusters/DelegateScanning/Components/DelegatedRegistriesTable.tsx @@ -228,7 +228,9 @@ function DelegatedRegistriesTable({ {/* */} - - + )} - diff --git a/ui/apps/platform/src/Containers/ComplianceEnhanced/Schedules/Wizard/ProfileSelection.tsx b/ui/apps/platform/src/Containers/ComplianceEnhanced/Schedules/Wizard/ProfileSelection.tsx index 1c90066a6b928..a17613fe6d2b6 100644 --- a/ui/apps/platform/src/Containers/ComplianceEnhanced/Schedules/Wizard/ProfileSelection.tsx +++ b/ui/apps/platform/src/Containers/ComplianceEnhanced/Schedules/Wizard/ProfileSelection.tsx @@ -185,8 +185,12 @@ function ProfileSelection({
+ + Row selection + Name Description
Description Permission set Access scope + + Row actions +
Deployment OrderSource registry Destination cluster (CLI/API only) + + Row deletion +
Description + {hasWriteAccess && ( + + Row actions +
Check + Controls - + Compliance status
- + diff --git a/ui/apps/platform/src/Containers/Integrations/IntegrationsListPage/IntegrationsTable.tsx b/ui/apps/platform/src/Containers/Integrations/IntegrationsListPage/IntegrationsTable.tsx index 5d5ee6802a428..61fd06da0b5ed 100644 --- a/ui/apps/platform/src/Containers/Integrations/IntegrationsListPage/IntegrationsTable.tsx +++ b/ui/apps/platform/src/Containers/Integrations/IntegrationsListPage/IntegrationsTable.tsx @@ -157,7 +157,9 @@ function IntegrationsTable({ ); })} - diff --git a/ui/apps/platform/src/Containers/NetworkGraph/common/FlowsTable.tsx b/ui/apps/platform/src/Containers/NetworkGraph/common/FlowsTable.tsx index 49372a7f25bd4..24b6a548a0896 100644 --- a/ui/apps/platform/src/Containers/NetworkGraph/common/FlowsTable.tsx +++ b/ui/apps/platform/src/Containers/NetworkGraph/common/FlowsTable.tsx @@ -184,7 +184,9 @@ function FlowsTable({
- + + Row selection + + Row expansion + Profile Rule set Applicability + + Row actions +
- {isEditable && ( - + )} {flows.map((row, rowIndex) => { diff --git a/ui/apps/platform/src/Containers/Policies/Table/PoliciesTable.tsx b/ui/apps/platform/src/Containers/Policies/Table/PoliciesTable.tsx index 883ef9c4fd59b..599848799cb47 100644 --- a/ui/apps/platform/src/Containers/Policies/Table/PoliciesTable.tsx +++ b/ui/apps/platform/src/Containers/Policies/Table/PoliciesTable.tsx @@ -301,7 +301,9 @@ function PoliciesTable({
+ + Row expansion + {columnNames.entity} {columnNames.direction} {columnNames.portAndProtocol} + {isEditable && ( + + Row actions +
- + {policies.map((policy) => { diff --git a/ui/apps/platform/src/Containers/Policies/Wizard/Step3/TableModal.tsx b/ui/apps/platform/src/Containers/Policies/Wizard/Step3/TableModal.tsx index 0c2b6b32b80ca..eca5c74f6c0d9 100644 --- a/ui/apps/platform/src/Containers/Policies/Wizard/Step3/TableModal.tsx +++ b/ui/apps/platform/src/Containers/Policies/Wizard/Step3/TableModal.tsx @@ -116,7 +116,6 @@ function TableModal({ ); })} - diff --git a/ui/apps/platform/src/Containers/SystemHealth/ClustersHealth/ClustersHealthTable.tsx b/ui/apps/platform/src/Containers/SystemHealth/ClustersHealth/ClustersHealthTable.tsx index 3230be240b5f6..d34c0da165eae 100644 --- a/ui/apps/platform/src/Containers/SystemHealth/ClustersHealth/ClustersHealthTable.tsx +++ b/ui/apps/platform/src/Containers/SystemHealth/ClustersHealth/ClustersHealthTable.tsx @@ -19,7 +19,9 @@ export function TheadClustersHealth({ return ( - diff --git a/ui/apps/platform/src/Containers/VulnMgmt/RiskAcceptance/AffectedComponents/AffectedComponentsModal.tsx b/ui/apps/platform/src/Containers/VulnMgmt/RiskAcceptance/AffectedComponents/AffectedComponentsModal.tsx index 41b40624c507c..c0dbb6236aa04 100644 --- a/ui/apps/platform/src/Containers/VulnMgmt/RiskAcceptance/AffectedComponents/AffectedComponentsModal.tsx +++ b/ui/apps/platform/src/Containers/VulnMgmt/RiskAcceptance/AffectedComponents/AffectedComponentsModal.tsx @@ -78,7 +78,9 @@ function AffectedComponentsModal({
{/* Header for expanded column */} + Row expansion + ); })} - + + Row actions +
+ + Clusters + {dataLabelHealthy || 'Healthy'}
- diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/ExceptionManagement/components/RequestCVEsTable.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/ExceptionManagement/components/RequestCVEsTable.tsx index 73fb221eae146..a10e4ce633c44 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/ExceptionManagement/components/RequestCVEsTable.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/ExceptionManagement/components/RequestCVEsTable.tsx @@ -93,7 +93,9 @@ function RequestCVEsTable({
+ + Row expansion + Component Version Fixed in
- diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/NodeCves/NodeCve/AffectedNodesTable.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/NodeCves/NodeCve/AffectedNodesTable.tsx index f74c3f22b73ed..02fb6b6ed0773 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/NodeCves/NodeCve/AffectedNodesTable.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/NodeCves/NodeCve/AffectedNodesTable.tsx @@ -111,7 +111,9 @@ function AffectedNodesTable({ > - diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/NodeCves/Overview/CVEsTable.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/NodeCves/Overview/CVEsTable.tsx index 599a18a025d6d..517044bd4a551 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/NodeCves/Overview/CVEsTable.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/NodeCves/Overview/CVEsTable.tsx @@ -141,7 +141,11 @@ function CVEsTable({ {isFiltered && } - {canSelectRows && + )} } - {canSelectRows && + )} - + - + {reportSnapshots.length === 0 && ( diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/CVEsTable.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/CVEsTable.tsx index f3dd3f8f607a5..e5eb8bd03f769 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/CVEsTable.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/CVEsTable.tsx @@ -184,7 +184,11 @@ function CVEsTable({ Request details )} - {createTableActions && + )} )} - {createTableActions && + )} - {hasWriteAccessForWatchedImage && + )} -
+ + Row expansion + CVE Images by severity CVSS
+ + Row expansion + Node CVE severity CVE statusFirst discovered} + {canSelectRows && ( + + CVE actions +
} + {canSelectRows && ( + + CVE actions +
{/* Header for expanded column */} + Row expansion + Completed Status Requestor{/* Header for table actions column */} + Row actions +
} + {createTableActions && ( + + CVE actions +
} + {createTableActions && ( + + CVE actions +
Age Scan time} + {hasWriteAccessForWatchedImage && ( + + Image action menu +
Image + + Remove watched image +