diff --git a/changes/12243-policy-resolution-urls b/changes/12243-policy-resolution-urls new file mode 100644 index 000000000..c57c9930b --- /dev/null +++ b/changes/12243-policy-resolution-urls @@ -0,0 +1 @@ +- Policy resolutions that include URLs are clickable in the UI diff --git a/frontend/components/ClickableUrls/ClickableUrls.tests.tsx b/frontend/components/ClickableUrls/ClickableUrls.tests.tsx new file mode 100644 index 000000000..f8da29280 --- /dev/null +++ b/frontend/components/ClickableUrls/ClickableUrls.tests.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import ClickableUrls from "./ClickableUrls"; + +const TEXT_WITH_URLS = + "Contact your IT administrator to ensure your Mac is receiving a profile that disables advertisement tracking. https://privacyinternational.org/guide-step/4335/macos-opt-out-targeted-ads or https://support.apple.com/en-us/HT202074"; +const URL_1 = + "https://privacyinternational.org/guide-step/4335/macos-opt-out-targeted-ads"; +const URL_2 = "https://support.apple.com/en-us/HT202074"; + +describe("ClickableUrls - component", () => { + it("renders text and icon", () => { + render(); + + const link1 = screen.getByRole("link", { name: URL_1 }); + const link2 = screen.getByRole("link", { name: URL_2 }); + + expect(link1).toHaveAttribute("href", URL_1); + expect(link1).toHaveAttribute("target", "_blank"); + expect(link2).toHaveAttribute("href", URL_2); + expect(link2).toHaveAttribute("target", "_blank"); + }); +}); diff --git a/frontend/components/ClickableUrls/ClickableUrls.tsx b/frontend/components/ClickableUrls/ClickableUrls.tsx new file mode 100644 index 000000000..6a381c1cb --- /dev/null +++ b/frontend/components/ClickableUrls/ClickableUrls.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import * as DOMPurify from "dompurify"; +import classnames from "classnames"; + +interface IClickableUrls { + text: string; + className?: string; +} + +const baseClass = "clickable-urls"; + +const urlReplacer = (match: string) => { + const url = match.startsWith("http") ? match : `https://${match}`; + return ` + ${match} + `; +}; + +const ClickableUrls = ({ text, className }: IClickableUrls): JSX.Element => { + const clickableUrlClasses = classnames(baseClass, className); + + // Regex to find case insensitive URLs and replace with link + const replacedLinks = text.replaceAll( + /(https?)?(:\/\/)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g, + urlReplacer + ); + const sanitizedResolutionContent = DOMPurify.sanitize(replacedLinks, { + ADD_ATTR: ["target"], // Allows opening in a new tab + }); + + const textWithLinks = ( +
+ ); + + return textWithLinks; +}; +export default ClickableUrls; diff --git a/frontend/components/ClickableUrls/index.ts b/frontend/components/ClickableUrls/index.ts new file mode 100644 index 000000000..d78d32339 --- /dev/null +++ b/frontend/components/ClickableUrls/index.ts @@ -0,0 +1 @@ +export { default } from "./ClickableUrls"; diff --git a/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyDetailsModal/PolicyDetailsModal.tsx b/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyDetailsModal/PolicyDetailsModal.tsx index b230751c2..495648695 100644 --- a/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyDetailsModal/PolicyDetailsModal.tsx +++ b/frontend/pages/hosts/details/cards/Policies/HostPoliciesTable/PolicyDetailsModal/PolicyDetailsModal.tsx @@ -3,6 +3,7 @@ import Button from "components/buttons/Button"; import Modal from "components/Modal"; import { IHostPolicy } from "interfaces/policy"; +import ClickableUrls from "components/ClickableUrls/ClickableUrls"; interface IPolicyDetailsProps { onCancel: () => void; @@ -26,7 +27,7 @@ const PolicyDetailsModal = ({ {policy?.resolution && (
Resolve: -

{policy?.resolution}

+ {policy?.resolution && }
)}
diff --git a/tsconfig.json b/tsconfig.json index a10655ca9..14ad4b5d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,15 +6,10 @@ "sourceMap": true, "jsx": "react", "allowSyntheticDefaultImports": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "lib": ["ES2021.String"] }, - "include": [ - "./frontend/**/*" - ], - "exclude": [ - "node_modules" - ], - "typeRoots": [ - "./node_modules/@types", "./typings" - ] + "include": ["./frontend/**/*"], + "exclude": ["node_modules"], + "typeRoots": ["./node_modules/@types", "./typings"] }