From 2500fbcf75ad24a4002e7ea10f6980e11077f30b Mon Sep 17 00:00:00 2001 From: Scott Dickerson Date: Wed, 15 Nov 2023 16:46:36 -0500 Subject: [PATCH] :bug: Render dependency versions as links (#1540) Resolves: #1340 On the Dependencies page, detail drawer, applications table: render the version text as a link to the maven central repository. The link uses the dependency's sha as the key in the maven central search. Add component `ExternalLink` to standardize rendering links outside of the app opening in a new tab. Note: The resolution to #1338 will require a change to the application table's query filter and response object. This will be done in PR #1536. See https://github.com/konveyor/tackle2-hub/issues/557 for the response object change. Screenshot: ![image](https://github.com/konveyor/tackle2-ui/assets/3985964/50f0c144-b4fe-4ba2-a625-3c8e6ae3be6c) --------- Signed-off-by: Scott J Dickerson Co-authored-by: Ian Bolton --- client/src/app/components/ExternalLink.tsx | 26 +++++++++++ .../dependencies/dependency-apps-table.tsx | 31 ++++++++++++- client/src/app/utils/utils.test.ts | 45 +++++++++++++++++++ client/src/app/utils/utils.ts | 17 +++++++ 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 client/src/app/components/ExternalLink.tsx diff --git a/client/src/app/components/ExternalLink.tsx b/client/src/app/components/ExternalLink.tsx new file mode 100644 index 0000000000..3566361648 --- /dev/null +++ b/client/src/app/components/ExternalLink.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import { Flex, FlexItem, Icon, Text } from "@patternfly/react-core"; +import ExternalLinkAltIcon from "@patternfly/react-icons/dist/esm/icons/external-link-alt-icon"; + +/** + * Render a link open an external href in another tab with appropriate styling. + */ +export const ExternalLink: React.FC<{ + href: string; + children: React.ReactNode; +}> = ({ href, children }) => ( + + + + {children} + + + + + + + + +); + +export default ExternalLink; diff --git a/client/src/app/pages/dependencies/dependency-apps-table.tsx b/client/src/app/pages/dependencies/dependency-apps-table.tsx index 3dda528072..9427f4caa4 100644 --- a/client/src/app/pages/dependencies/dependency-apps-table.tsx +++ b/client/src/app/pages/dependencies/dependency-apps-table.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import { + Text, TextContent, Toolbar, ToolbarContent, @@ -21,12 +22,14 @@ import { TableHeaderContentWithControls, TableRowContentWithControls, } from "@app/components/TableControls"; +import { ExternalLink } from "@app/components/ExternalLink"; import { SimplePagination } from "@app/components/SimplePagination"; import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; import { useFetchAppDependencies } from "@app/queries/dependencies"; import { useFetchBusinessServices } from "@app/queries/businessservices"; import { useFetchTagsWithTagItems } from "@app/queries/tags"; import { getParsedLabel } from "@app/utils/rules-utils"; +import { extractFirstSha } from "@app/utils/utils"; export interface IDependencyAppsTableProps { dependency: AnalysisDependency; @@ -184,7 +187,7 @@ export const DependencyAppsTable: React.FC = ({ {currentPageAppDependencies?.map((appDependency, rowIndex) => ( = ({ modifier="nowrap" {...getTdProps({ columnKey: "version" })} > - {appDependency.dependency.version} + {isJavaDependency ? "Managed" : "Embedded"}; }; + +const DependencyVersionColumn = ({ + appDependency: { + dependency: { provider, name, version, sha }, + }, +}: { + appDependency: AnalysisAppDependency; +}) => { + const isJavaDependency = name && version && sha && provider === "java"; + + const mavenCentralLink = isJavaDependency + ? `https://search.maven.org/search?q=1:${extractFirstSha(sha)}` + : undefined; + + return ( + + {mavenCentralLink ? ( + {version} + ) : ( + {version} + )} + + ); +}; diff --git a/client/src/app/utils/utils.test.ts b/client/src/app/utils/utils.test.ts index 92efa040b9..016eab1841 100644 --- a/client/src/app/utils/utils.test.ts +++ b/client/src/app/utils/utils.test.ts @@ -7,6 +7,7 @@ import { gitUrlRegex, standardURLRegex, formatPath, + extractFirstSha, } from "./utils"; import { Paths } from "@app/Paths"; @@ -157,6 +158,7 @@ describe("utils", () => { expect(standardURLRegex.test(url)).toBe(true); }); }); + describe("formatPath function", () => { it("should replace path parameters with values", () => { const path = Paths.applicationsImportsDetails; @@ -174,3 +176,46 @@ describe("formatPath function", () => { expect(result).toBe("/applications/assessment/:assessmentId"); }); }); + +describe("SHA extraction", () => { + it("empty string is undefined", () => { + const first = extractFirstSha(""); + expect(first).toBeUndefined(); + }); + + it("no SHA is undefined", () => { + const first = extractFirstSha( + "The quick brown fox jumps over the lazy dog." + ); + expect(first).toBeUndefined(); + }); + + it("a SHA is found", () => { + const tests = [ + "83cd2cd674a217ade95a4bb83a8a14f351f48bd0", + " 83cd2cd674a217ade95a4bb83a8a14f351f48bd0 ", + "83cd2cd674a217ade95a4bb83a8a14f351f48bd0 The quick brown fox jumps over the lazy dog.", + "The quick brown fox jumps over the lazy dog. 83cd2cd674a217ade95a4bb83a8a14f351f48bd0", + "The quick brown fox 83cd2cd674a217ade95a4bb83a8a14f351f48bd0 jumps over the lazy dog.", + ]; + + for (const test of tests) { + const first = extractFirstSha(test); + expect(first).toBe("83cd2cd674a217ade95a4bb83a8a14f351f48bd0"); + } + }); + + it("multiple SHAs are in the string, only the first is returned", () => { + const first = extractFirstSha( + "83cd2cd674a217ade95a4bb83a8a14f351f48bd0 9c04cd6372077e9b11f70ca111c9807dc7137e4b" + ); + expect(first).toBe("83cd2cd674a217ade95a4bb83a8a14f351f48bd0"); + }); + + it("multiple SHAs are in the string, only the first is returned even if it is shorter", () => { + const first = extractFirstSha( + "9c04cd6372077e9b11f70ca111c9807dc7137e4b 83cd2cd674a217ade95a4bb83a8a14f351f48bd0 b47cc0f104b62d4c7c30bcd68fd8e67613e287dc4ad8c310ef10cbadea9c4380" + ); + expect(first).toBe("9c04cd6372077e9b11f70ca111c9807dc7137e4b"); + }); +}); diff --git a/client/src/app/utils/utils.ts b/client/src/app/utils/utils.ts index ce1b98e0d1..29c3a8840a 100644 --- a/client/src/app/utils/utils.ts +++ b/client/src/app/utils/utils.ts @@ -151,3 +151,20 @@ export const formatPath = (path: Paths, data: any) => { return url; }; + +/** + * Regular expression to match a SHA hash in a string. Different versions of the SHA + * hash have different lengths. Check in descending length order so the longest SHA + * string can be captured. + */ +const SHA_REGEX = + /([a-f0-9]{128}|[a-f0-9]{96}|[a-f0-9]{64}|[a-f0-9]{56}|[a-f0-9]{40})/g; + +/** + * In any given string, find the first thing that looks like a sha hash and return it. + * If nothing looks like a sha hash, return undefined. + */ +export const extractFirstSha = (str: string): string | undefined => { + const match = str.match(SHA_REGEX); + return match && match[0] ? match[0] : undefined; +};