Skip to content

Commit

Permalink
Render dependency versions as links
Browse files Browse the repository at this point in the history
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.

Signed-off-by: Scott J Dickerson <[email protected]>
  • Loading branch information
sjd78 committed Nov 14, 2023
1 parent fcd2948 commit 9d9245e
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 5 deletions.
4 changes: 3 additions & 1 deletion client/src/app/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,10 +519,12 @@ export interface AnalysisAppDependency {
businessService: string;
dependency: {
id: number;
provider: string;
name: string;
version: string;
provider: string;
rule: string; // TODO: rename to 'sha' with https://github.com/konveyor/tackle2-hub/issues/557
indirect: boolean;
labels: Ref[];
//TODO: Glean from labels somehow
// management?: string;
};
Expand Down
26 changes: 26 additions & 0 deletions client/src/app/components/ExternalLink.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<Flex spaceItems={{ default: "spaceItemsSm" }}>
<FlexItem>
<Text component="a" href={href} target="_blank">
{children}
</Text>
</FlexItem>
<FlexItem>
<Icon size="sm" status="info">
<ExternalLinkAltIcon />
</Icon>
</FlexItem>
</Flex>
);

export default ExternalLink;
45 changes: 41 additions & 4 deletions client/src/app/pages/dependencies/dependency-apps-table.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core";
import {
Text,
TextContent,
Toolbar,
ToolbarContent,
ToolbarItem,
} from "@patternfly/react-core";
import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table";
import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing";
import { useSelectionState } from "@migtools/lib-ui";
import { AnalysisDependency } from "@app/api/models";
import { AnalysisAppDependency, AnalysisDependency } from "@app/api/models";
import {
useTableControlState,
useTableControlProps,
Expand All @@ -16,11 +22,13 @@ 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 { extractFirstSha } from "@app/utils/utils";

export interface IDependencyAppsTableProps {
dependency: AnalysisDependency;
Expand Down Expand Up @@ -180,7 +188,7 @@ export const DependencyAppsTable: React.FC<IDependencyAppsTableProps> = ({
<Tbody>
{currentPageAppDependencies?.map((appDependency, rowIndex) => (
<Tr
key={appDependency.name}
key={appDependency.id}
{...getTrProps({ item: appDependency })}
>
<TableRowContentWithControls
Expand All @@ -196,7 +204,7 @@ export const DependencyAppsTable: React.FC<IDependencyAppsTableProps> = ({
modifier="nowrap"
{...getTdProps({ columnKey: "version" })}
>
{appDependency.dependency.version}
<DependencyVersionColumn appDependency={appDependency} />
</Td>
{/* <Td
width={20}
Expand Down Expand Up @@ -229,3 +237,32 @@ export const DependencyAppsTable: React.FC<IDependencyAppsTableProps> = ({
</>
);
};

const DependencyVersionColumn = ({
appDependency: {
dependency: {
provider,
name,
version,
rule: sha, // TODO: rename to 'sha' with https://github.com/konveyor/tackle2-hub/issues/557
},
},
}: {
appDependency: AnalysisAppDependency;
}) => {
const isJavaDependency = name && version && sha && provider === "";

const mavenCentralLink = isJavaDependency
? `https://search.maven.org/search?q=1:${extractFirstSha(sha)}`
: undefined;

return (
<TextContent>
{mavenCentralLink ? (
<ExternalLink href={mavenCentralLink}>{version}</ExternalLink>
) : (
<Text>{version}</Text>
)}
</TextContent>
);
};
45 changes: 45 additions & 0 deletions client/src/app/utils/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
gitUrlRegex,
standardURLRegex,
formatPath,
extractFirstSha,
} from "./utils";
import { Paths } from "@app/Paths";

Expand Down Expand Up @@ -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;
Expand All @@ -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");
});
});
18 changes: 18 additions & 0 deletions client/src/app/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,21 @@ 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);
console.log("match:", match);
return match && match[0] ? match[0] : undefined;
};

0 comments on commit 9d9245e

Please sign in to comment.