Fleet UI: Convert URLs in Policy resolution text to be clickable links (#13023)

This commit is contained in:
RachelElysia 2023-08-18 09:15:23 -04:00 committed by GitHub
parent 4e8696cb1d
commit ccdd1a02f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 72 additions and 11 deletions

View File

@ -0,0 +1 @@
- Policy resolutions that include URLs are clickable in the UI

View File

@ -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(<ClickableUrls text={TEXT_WITH_URLS} />);
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");
});
});

View File

@ -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 `<a href=${url} target="_blank" rel="noreferrer">
${match}
</a>`;
};
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 = (
<div
className={clickableUrlClasses}
dangerouslySetInnerHTML={{ __html: sanitizedResolutionContent }}
/>
);
return textWithLinks;
};
export default ClickableUrls;

View File

@ -0,0 +1 @@
export { default } from "./ClickableUrls";

View File

@ -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 && (
<div className={`${baseClass}__resolution`}>
<span className={`${baseClass}__resolve-header`}> Resolve:</span>
<p>{policy?.resolution}</p>
{policy?.resolution && <ClickableUrls text={policy?.resolution} />}
</div>
)}
<div className="modal-cta-wrap">

View File

@ -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"]
}