From ccdd1a02f4bc32c3e66d1713011d4bce80e62e82 Mon Sep 17 00:00:00 2001
From: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
Date: Fri, 18 Aug 2023 09:15:23 -0400
Subject: [PATCH] Fleet UI: Convert URLs in Policy resolution text to be
clickable links (#13023)
---
changes/12243-policy-resolution-urls | 1 +
.../ClickableUrls/ClickableUrls.tests.tsx | 23 +++++++++++
.../ClickableUrls/ClickableUrls.tsx | 40 +++++++++++++++++++
frontend/components/ClickableUrls/index.ts | 1 +
.../PolicyDetailsModal/PolicyDetailsModal.tsx | 3 +-
tsconfig.json | 15 +++----
6 files changed, 72 insertions(+), 11 deletions(-)
create mode 100644 changes/12243-policy-resolution-urls
create mode 100644 frontend/components/ClickableUrls/ClickableUrls.tests.tsx
create mode 100644 frontend/components/ClickableUrls/ClickableUrls.tsx
create mode 100644 frontend/components/ClickableUrls/index.ts
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"]
}