mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
Fleet UI: Convert URLs in Policy resolution text to be clickable links (#13023)
This commit is contained in:
parent
4e8696cb1d
commit
ccdd1a02f4
1
changes/12243-policy-resolution-urls
Normal file
1
changes/12243-policy-resolution-urls
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Policy resolutions that include URLs are clickable in the UI
|
23
frontend/components/ClickableUrls/ClickableUrls.tests.tsx
Normal file
23
frontend/components/ClickableUrls/ClickableUrls.tests.tsx
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
40
frontend/components/ClickableUrls/ClickableUrls.tsx
Normal file
40
frontend/components/ClickableUrls/ClickableUrls.tsx
Normal 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;
|
1
frontend/components/ClickableUrls/index.ts
Normal file
1
frontend/components/ClickableUrls/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./ClickableUrls";
|
@ -3,6 +3,7 @@ import Button from "components/buttons/Button";
|
|||||||
import Modal from "components/Modal";
|
import Modal from "components/Modal";
|
||||||
|
|
||||||
import { IHostPolicy } from "interfaces/policy";
|
import { IHostPolicy } from "interfaces/policy";
|
||||||
|
import ClickableUrls from "components/ClickableUrls/ClickableUrls";
|
||||||
|
|
||||||
interface IPolicyDetailsProps {
|
interface IPolicyDetailsProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@ -26,7 +27,7 @@ const PolicyDetailsModal = ({
|
|||||||
{policy?.resolution && (
|
{policy?.resolution && (
|
||||||
<div className={`${baseClass}__resolution`}>
|
<div className={`${baseClass}__resolution`}>
|
||||||
<span className={`${baseClass}__resolve-header`}> Resolve:</span>
|
<span className={`${baseClass}__resolve-header`}> Resolve:</span>
|
||||||
<p>{policy?.resolution}</p>
|
{policy?.resolution && <ClickableUrls text={policy?.resolution} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="modal-cta-wrap">
|
<div className="modal-cta-wrap">
|
||||||
|
@ -6,15 +6,10 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
|
"lib": ["ES2021.String"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["./frontend/**/*"],
|
||||||
"./frontend/**/*"
|
"exclude": ["node_modules"],
|
||||||
],
|
"typeRoots": ["./node_modules/@types", "./typings"]
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"typeRoots": [
|
|
||||||
"./node_modules/@types", "./typings"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user