From fff6566bcafc9b617d776d944d04250309aeb505 Mon Sep 17 00:00:00 2001 From: Tamar Weisskopf Date: Tue, 15 Sep 2026 01:51:55 +0300 Subject: [PATCH 01/10] feat(TC-5554): Add version display to Exploit Intelligence UI Implement basic version information display in the UI About modal. This provides users, QE, and Support with visibility into which release is deployed. Changes: - Add /api/v1/version REST endpoint (VersionResource.java) - Reads EXPLOIT_IQ_VERSION environment variable - Returns JSON with version info - Falls back to "unknown" for local dev - Add About modal component (AboutModal.tsx) - PatternFly AboutModal with Red Hat branding - Displays product description and version - Supports future component details from TC-5943 - Add version data hook (useVersion.ts) - Fetches version from API with caching - Handles loading and error states - Update PageHeader component - Add help icon (?) in toolbar - Trigger About modal on click - Update UserAvatarDropdown - Add "About" menu item before "Logout" - Alternative way to open About modal The implementation is designed to be extended by TC-5943 which will add component-specific versions, build metadata, and deployment info. Co-Authored-By: Claude Sonnet 4.5 --- .../exploitiq/rest/VersionResource.java | 84 ++++++++++++++ src/main/webui/src/components/AboutModal.tsx | 109 ++++++++++++++++++ src/main/webui/src/components/PageHeader.tsx | 70 +++++++---- .../src/components/UserAvatarDropdown.tsx | 24 +++- src/main/webui/src/hooks/useVersion.ts | 72 ++++++++++++ 5 files changed, 331 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java create mode 100644 src/main/webui/src/components/AboutModal.tsx create mode 100644 src/main/webui/src/hooks/useVersion.ts diff --git a/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java b/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java new file mode 100644 index 00000000..83834401 --- /dev/null +++ b/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, Red Hat Inc. & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.ecosystemappeng.exploitiq.rest; + +import jakarta.annotation.security.PermitAll; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.util.HashMap; +import java.util.Map; + +/** + * REST endpoint for version information (TC-5554) + * Returns application version for display in UI About modal + * + * TC-5943 will enhance this endpoint with component details, build metadata, etc. + */ +@Path("/version") +@Tag(name = "Version", description = "Application version information") +public class VersionResource { + + /** + * Get application version information + * + * Returns basic version info for TC-5554. Future enhancement (TC-5943) will add: + * - Component-specific versions (webapp, engine, cache) + * - Build metadata (commit hash, build date) + * - Deployment environment information + * + * @return JSON response with version information + */ + @GET + @PermitAll + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Get application version", description = "Returns version information for the Exploit Intelligence application") + public Response getVersion() { + Map versionInfo = new HashMap<>(); + + // Read version from environment variable (injected by Kubernetes ConfigMap) + // Falls back to "unknown" for local development without env vars + String version = getEnvOrDefault("EXPLOIT_IQ_VERSION", "unknown"); + versionInfo.put("version", version); + + // TC-5943 will extend this response with: + // - components.webapp.version and .image + // - components.engine.version and .image + // - components.cache.version and .image + // - buildInfo (commit hash, build date) + // - operator version + // - environment/cluster info + + return Response.ok(versionInfo).build(); + } + + /** + * Helper method to read environment variable with fallback + * + * @param key Environment variable name + * @param defaultValue Value to return if env var is not set + * @return Environment variable value or default + */ + private String getEnvOrDefault(String key, String defaultValue) { + String value = System.getenv(key); + return value != null ? value : defaultValue; + } +} diff --git a/src/main/webui/src/components/AboutModal.tsx b/src/main/webui/src/components/AboutModal.tsx new file mode 100644 index 00000000..f649bf9a --- /dev/null +++ b/src/main/webui/src/components/AboutModal.tsx @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, Red Hat Inc. & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * About modal component displaying application version and product information + * Implements design from Figma (TC-5554) + */ + +import React from 'react'; +import { AboutModal as PFAboutModal } from '@patternfly/react-core'; +import { useVersion, VersionInfo } from '../hooks/useVersion'; + +interface AboutModalProps { + isOpen: boolean; + onClose: () => void; +} + +const AboutModal: React.FC = ({ isOpen, onClose }) => { + const { versionInfo, loading } = useVersion(); + + // Check if components exist with proper type checking + const hasComponents = versionInfo && 'components' in versionInfo; + const components = hasComponents ? (versionInfo as VersionInfo & { components: { webapp?: { version: string }, engine?: { version: string }, cache?: { version: string } } }).components : undefined; + + // Product description matching Figma design + const productDescription = ( +
+

+ Exploit Intelligence is an AI-powered exploitability analysis engine that performs + deep, function-level analysis on application code paths to determine if detected + vulnerabilities (CVEs) are reachable and executable in your environment. +

+

+ For more information refer to{' '} + + Red Hat Exploit Intelligence documentation + +

+
+ ); + + // Version information section + const versionSection = ( +
+

Version

+

+ {loading ? 'Loading...' : versionInfo?.version || 'unknown'} +

+ + {/* Components section - only show if components data exists (TC-5943) */} + {components && ( + <> +

+ Components +

+
    + {components.engine && ( +
  • + EI engine {components.engine.version} +
  • + )} + {components.webapp && ( +
  • + EI webapp {components.webapp.version} +
  • + )} + {components.cache && ( +
  • + Nginx cache {components.cache.version} +
  • + )} +
+ + )} +
+ ); + + return ( + +
+

Exploit Intelligence

+
+ {productDescription} + {versionSection} +
+ ); +}; + +export default AboutModal; diff --git a/src/main/webui/src/components/PageHeader.tsx b/src/main/webui/src/components/PageHeader.tsx index feb1b300..01610117 100644 --- a/src/main/webui/src/components/PageHeader.tsx +++ b/src/main/webui/src/components/PageHeader.tsx @@ -15,7 +15,7 @@ * Based on reference implementation pattern */ -import React from 'react'; +import React, { useState } from 'react'; import { Masthead, MastheadMain, @@ -30,10 +30,13 @@ import { Flex, Stack, FlexItem, + Button, } from '@patternfly/react-core'; import { PageToggleButton } from '@patternfly/react-core'; import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; +import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; import UserAvatarDropdown from './UserAvatarDropdown'; +import AboutModal from './AboutModal'; /** * Brand component - displays Red Hat logo and product name @@ -72,10 +75,12 @@ interface PageHeaderProps { } const PageHeader: React.FC = ({ isSidebarOpen, onSidebarToggle }) => { + const [isAboutModalOpen, setIsAboutModalOpen] = useState(false); + const headerToolbar = ( - @@ -104,32 +109,49 @@ const PageHeader: React.FC = ({ isSidebarOpen, onSidebarToggle md: 'visible', }} > - + + + + setIsAboutModalOpen(true)} /> ); return ( - - - - - - - - - - - - {headerToolbar} - + <> + + + + + + + + + + + + {headerToolbar} + + setIsAboutModalOpen(false)} /> + ); }; diff --git a/src/main/webui/src/components/UserAvatarDropdown.tsx b/src/main/webui/src/components/UserAvatarDropdown.tsx index 029dfff3..719d5625 100644 --- a/src/main/webui/src/components/UserAvatarDropdown.tsx +++ b/src/main/webui/src/components/UserAvatarDropdown.tsx @@ -29,7 +29,11 @@ import { import { UserIcon } from "@patternfly/react-icons"; import { useAuth, logout } from "../hooks/useAuth"; -const UserAvatarDropdown: React.FC = () => { +interface UserAvatarDropdownProps { + onAboutClick?: () => void; +} + +const UserAvatarDropdown: React.FC = ({ onAboutClick }) => { const [isOpen, setIsOpen] = useState(false); const { userName, loading, error } = useAuth(); @@ -41,6 +45,13 @@ const UserAvatarDropdown: React.FC = () => { setIsOpen(false); }; + const handleAbout = () => { + setIsOpen(false); + if (onAboutClick) { + onAboutClick(); + } + }; + const handleLogout = async () => { setIsOpen(false); try { @@ -86,9 +97,14 @@ const UserAvatarDropdown: React.FC = () => { } const userDropdownItems = ( - - Logout - + <> + + About + + + Logout + + ); return ( diff --git a/src/main/webui/src/hooks/useVersion.ts b/src/main/webui/src/hooks/useVersion.ts new file mode 100644 index 00000000..0183232c --- /dev/null +++ b/src/main/webui/src/hooks/useVersion.ts @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, Red Hat Inc. & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Hook for fetching application version information + * Follows the pattern from useAuth hook + */ + +import { useState, useEffect } from "react"; + +/** + * Version information response from /api/v1/version + * + * TC-5554: Basic version only + * TC-5943 will add: components, buildInfo, operator version, etc. + */ +export interface VersionInfo { + version: string; + // Future fields from TC-5943: + // components?: { + // webapp?: { version: string; image?: string }; + // engine?: { version: string; image?: string }; + // cache?: { version: string; image?: string }; + // }; + // buildInfo?: string; + // operator?: string; +} + +/** + * Custom hook to fetch version information from the API + * Caches result to avoid repeated calls + * + * @returns Object with version data, loading state, and error state + */ +export const useVersion = () => { + const [versionInfo, setVersionInfo] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchVersion = async () => { + try { + const response = await fetch("/api/v1/version"); + if (!response.ok) { + throw new Error(`Failed to fetch version: ${response.statusText}`); + } + const data = await response.json(); + setVersionInfo(data); + } catch (err) { + console.error("Error fetching version:", err); + setError(err instanceof Error ? err : new Error("Unknown error")); + // Set fallback version info on error + setVersionInfo({ version: "unknown" }); + } finally { + setLoading(false); + } + }; + + fetchVersion(); + }, []); // Empty dependency array - fetch once on mount + + return { versionInfo, loading, error }; +}; From 48a0252212fee558e906821f989c036b5b991eec Mon Sep 17 00:00:00 2001 From: Tamar Weisskopf Date: Thu, 17 Sep 2026 10:12:41 +0300 Subject: [PATCH 02/10] feat(TC-5554): Refine About modal layout to match Figma specs - Use PF design tokens for spacing (spacer/2xl, font-size/2xl, etc.) - Replace PF DescriptionList with dl/dt/dd for correct single-column layout - Override AboutModal grid to remove empty brand row at top - Add CSS overrides for brand/header/close button grid areas - Add Vite proxy config for local development without Quarkus - Add mock-server.js for testing version API locally Co-Authored-By: Claude Opus 4.6 --- mock-server.js | 39 +++++ src/main/webui/src/components/AboutModal.css | 15 ++ src/main/webui/src/components/AboutModal.tsx | 149 +++++++++++-------- src/main/webui/vite.config.ts | 15 +- 4 files changed, 152 insertions(+), 66 deletions(-) create mode 100644 mock-server.js create mode 100644 src/main/webui/src/components/AboutModal.css diff --git a/mock-server.js b/mock-server.js new file mode 100644 index 00000000..98422169 --- /dev/null +++ b/mock-server.js @@ -0,0 +1,39 @@ +// Simple mock server for testing version API +const http = require('http'); + +const server = http.createServer((req, res) => { + // Enable CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + if (req.url === '/api/v1/version') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + version: '3.1.0', + components: { + engine: { version: 'v0.0.3-e5f6g7h' }, + webapp: { version: 'v0.0.3-9k8j7h6' }, + cache: { version: 'v0.0.3-4m3n2bl' } + } + })); + } else if (req.url === '/api/v1/user') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ name: 'Test User' })); + } else { + res.writeHead(404); + res.end('Not Found'); + } +}); + +const PORT = 8080; +server.listen(PORT, () => { + console.log(`Mock server running on http://localhost:${PORT}`); + console.log(`Version API: http://localhost:${PORT}/api/v1/version`); +}); diff --git a/src/main/webui/src/components/AboutModal.css b/src/main/webui/src/components/AboutModal.css new file mode 100644 index 00000000..718a0b55 --- /dev/null +++ b/src/main/webui/src/components/AboutModal.css @@ -0,0 +1,15 @@ +/* We render the logo inside custom content, so remove the default brand/header + grid rows and let content start from the top alongside the close button. */ +.ei-about-modal.pf-v6-c-about-modal-box { + grid-template-areas: "content close" !important; + grid-template-rows: auto !important; +} + +.ei-about-modal.pf-v6-c-about-modal-box .pf-v6-c-about-modal-box__brand, +.ei-about-modal.pf-v6-c-about-modal-box .pf-v6-c-about-modal-box__header { + display: none !important; +} + +.ei-about-modal.pf-v6-c-about-modal-box .pf-v6-c-about-modal-box__close { + padding-block-start: var(--pf-t--global--spacer--xl); +} diff --git a/src/main/webui/src/components/AboutModal.tsx b/src/main/webui/src/components/AboutModal.tsx index f649bf9a..050e408e 100644 --- a/src/main/webui/src/components/AboutModal.tsx +++ b/src/main/webui/src/components/AboutModal.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { AboutModal as PFAboutModal } from '@patternfly/react-core'; import { useVersion, VersionInfo } from '../hooks/useVersion'; +import './AboutModal.css'; interface AboutModalProps { isOpen: boolean; @@ -27,81 +28,99 @@ interface AboutModalProps { const AboutModal: React.FC = ({ isOpen, onClose }) => { const { versionInfo, loading } = useVersion(); - // Check if components exist with proper type checking const hasComponents = versionInfo && 'components' in versionInfo; const components = hasComponents ? (versionInfo as VersionInfo & { components: { webapp?: { version: string }, engine?: { version: string }, cache?: { version: string } } }).components : undefined; - // Product description matching Figma design - const productDescription = ( -
-

- Exploit Intelligence is an AI-powered exploitability analysis engine that performs - deep, function-level analysis on application code paths to determine if detected - vulnerabilities (CVEs) are reachable and executable in your environment. -

-

- For more information refer to{' '} - - Red Hat Exploit Intelligence documentation - -

-
- ); - - // Version information section - const versionSection = ( -
-

Version

-

- {loading ? 'Loading...' : versionInfo?.version || 'unknown'} -

- - {/* Components section - only show if components data exists (TC-5943) */} - {components && ( - <> -

- Components -

-
    - {components.engine && ( -
  • - EI engine {components.engine.version} -
  • - )} - {components.webapp && ( -
  • - EI webapp {components.webapp.version} -
  • - )} - {components.cache && ( -
  • - Nginx cache {components.cache.version} -
  • - )} -
- - )} -
- ); - return ( -
-

Exploit Intelligence

+ {/* Content container — vertical flow, gap global/spacer/2xl (24px) */} +
+ {/* Frame 119: logo + title — horizontal, 8px gap, 32px height */} +
+ Red Hat +

+ Red Hat Trusted Profile Analyzer +

+
+ + {/* Heading/XL (Hero) — size 3xl, weight Medium */} +

+ Exploit Intelligence +

+ + {/* Description — Body/Default/Regular */} +
+

+ Exploit Intelligence is an AI-powered exploitability analysis engine that performs + deep, function-level analysis on application code paths to determine if detected + vulnerabilities (CVEs) are reachable and executable in your environment. +

+

+ For more information refer to{' '} + + Red Hat Exploit Intelligence documentation + +

+
+ + {/* Description list — vertical gap md (16px), horizontal items gap sm (8px) */} +
+
+
Version
+
{loading ? 'Loading...' : versionInfo?.version || 'unknown'}
+
+ + {components && ( + <> +
+
Components
+
+ + {components.engine && ( +
+
EI engine
+
{components.engine.version}
+
+ )} + {components.webapp && ( +
+
EI webapp
+
{components.webapp.version}
+
+ )} + {components.cache && ( +
+
Nginx cache
+
{components.cache.version}
+
+ )} + + )} +
- {productDescription} - {versionSection} ); }; diff --git a/src/main/webui/vite.config.ts b/src/main/webui/vite.config.ts index c2eb0242..f6e60ebf 100644 --- a/src/main/webui/vite.config.ts +++ b/src/main/webui/vite.config.ts @@ -61,7 +61,20 @@ const getConfig = () => { } return config; } else { - return baseConfig; + // Non-standalone mode: add proxy for local development + return { + ...baseConfig, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + secure: false, + } + } + } + }; } }; From 1d241ebdf1f25eadce4285776d1193bdd8ec8c79 Mon Sep 17 00:00:00 2001 From: Tamar Weisskopf Date: Thu, 17 Sep 2026 11:11:34 +0300 Subject: [PATCH 03/10] fix(TC-5554): Address PR review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove misleading @PermitAll from VersionResource (endpoint requires auth via Quarkus HTTP policy; annotation was dead code) - Add module-level cache to useVersion hook so re-mounts skip re-fetch - Add components as optional field on VersionInfo interface, drop unsafe type cast in AboutModal - Fix aria-label="Help" → "About" on question-circle button Co-Authored-By: Claude Opus 4.6 --- .../exploitiq/rest/VersionResource.java | 2 -- src/main/webui/src/components/AboutModal.tsx | 3 +- src/main/webui/src/components/PageHeader.tsx | 2 +- src/main/webui/src/hooks/useVersion.ts | 33 +++++++++---------- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java b/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java index 83834401..bdcdc55d 100644 --- a/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java +++ b/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java @@ -14,7 +14,6 @@ package com.redhat.ecosystemappeng.exploitiq.rest; -import jakarta.annotation.security.PermitAll; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -48,7 +47,6 @@ public class VersionResource { * @return JSON response with version information */ @GET - @PermitAll @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Get application version", description = "Returns version information for the Exploit Intelligence application") public Response getVersion() { diff --git a/src/main/webui/src/components/AboutModal.tsx b/src/main/webui/src/components/AboutModal.tsx index 050e408e..5798ca6a 100644 --- a/src/main/webui/src/components/AboutModal.tsx +++ b/src/main/webui/src/components/AboutModal.tsx @@ -28,8 +28,7 @@ interface AboutModalProps { const AboutModal: React.FC = ({ isOpen, onClose }) => { const { versionInfo, loading } = useVersion(); - const hasComponents = versionInfo && 'components' in versionInfo; - const components = hasComponents ? (versionInfo as VersionInfo & { components: { webapp?: { version: string }, engine?: { version: string }, cache?: { version: string } } }).components : undefined; + const components = versionInfo?.components; return ( = ({ isSidebarOpen, onSidebarToggle >