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 { 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">
|
||||
|
@ -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"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user