diff --git a/frontend/components/BackLink/BackLink.tsx b/frontend/components/BackLink/BackLink.tsx
index 4a9ba2617d..84f68acdee 100644
--- a/frontend/components/BackLink/BackLink.tsx
+++ b/frontend/components/BackLink/BackLink.tsx
@@ -1,7 +1,6 @@
import React from "react";
import { browserHistory, Link } from "react-router";
import Icon from "components/Icon";
-import Button from "components/buttons/Button";
import classnames from "classnames";
interface IBackLinkProps {
diff --git a/frontend/components/CustomLink/CustomLink.tsx b/frontend/components/CustomLink/CustomLink.tsx
new file mode 100644
index 0000000000..1c897a888a
--- /dev/null
+++ b/frontend/components/CustomLink/CustomLink.tsx
@@ -0,0 +1,65 @@
+import React from "react";
+
+import Icon from "components/Icon";
+import classnames from "classnames";
+
+interface ICustomLinkProps {
+ url: string;
+ text: string;
+ className?: string;
+ newTab?: boolean;
+ /** Icon wraps on new line with last word */
+ multiline?: boolean;
+}
+
+const baseClass = "custom-link";
+
+const CustomLink = ({
+ url,
+ text,
+ className,
+ newTab = false,
+ multiline = false,
+}: ICustomLinkProps): JSX.Element => {
+ const customLinkClass = classnames(baseClass, className);
+
+ const target = newTab ? "_blank" : "";
+
+ const multilineText = text.substring(0, text.lastIndexOf(" ") + 1);
+ const lastWord = text.substring(text.lastIndexOf(" ") + 1, text.length);
+
+ const content = multiline ? (
+ <>
+ {multilineText}
+
+ {lastWord}
+ {newTab && (
+
+ )}
+
+ >
+ ) : (
+ <>
+ {text}
+ {newTab && (
+
+ )}
+ >
+ );
+
+ return (
+
+ {content}
+
+ );
+};
+
+export default CustomLink;
diff --git a/frontend/components/CustomLink/_styles.scss b/frontend/components/CustomLink/_styles.scss
new file mode 100644
index 0000000000..1685ee2d5c
--- /dev/null
+++ b/frontend/components/CustomLink/_styles.scss
@@ -0,0 +1,10 @@
+.custom-link {
+ &__no-wrap {
+ white-space: nowrap;
+ }
+
+ &__external-icon {
+ display: inline;
+ margin-left: $pad-xsmall;
+ }
+}
diff --git a/frontend/components/CustomLink/index.ts b/frontend/components/CustomLink/index.ts
new file mode 100644
index 0000000000..8be6b555c6
--- /dev/null
+++ b/frontend/components/CustomLink/index.ts
@@ -0,0 +1 @@
+export { default } from "./CustomLink";
diff --git a/frontend/components/DataError/DataError.tsx b/frontend/components/DataError/DataError.tsx
index 3789d27b25..498d1ae025 100644
--- a/frontend/components/DataError/DataError.tsx
+++ b/frontend/components/DataError/DataError.tsx
@@ -1,6 +1,6 @@
import React from "react";
-import ExternalLinkIcon from "../../../assets/images/icon-external-link-12x12@2x.png";
+import CustomLink from "components/CustomLink";
import ErrorIcon from "../../../assets/images/icon-error-16x16@2x.png";
const baseClass = "data-error";
@@ -21,14 +21,11 @@ const DataError = ({ card }: IDataErrorProps): JSX.Element => {
Refresh the page or log in again.
If this keeps happening, please
-
- file an issue
-
-
+
diff --git a/frontend/components/FleetMarkdown/FleetMarkdown.tsx b/frontend/components/FleetMarkdown/FleetMarkdown.tsx
index 4d50bc83a6..92d81dfd44 100644
--- a/frontend/components/FleetMarkdown/FleetMarkdown.tsx
+++ b/frontend/components/FleetMarkdown/FleetMarkdown.tsx
@@ -6,24 +6,7 @@ import { IAceEditor } from "react-ace/lib/types";
import { noop } from "lodash";
import FleetAce from "components/FleetAce";
-
-import ExternalLinkIcon from "../../../assets/images/icon-external-link-12x12@2x.png";
-
-interface ICustomLinkProps {
- text: React.ReactNode;
- href: string;
- newTab?: boolean;
-}
-
-const CustomLink = ({ text, href, newTab = false }: ICustomLinkProps) => {
- const target = newTab ? "__blank" : "";
- return (
-
- {text}
-
-
- );
-};
+import CustomLink from "components/CustomLink";
interface IFleetMarkdownProps {
markdown: string;
@@ -46,7 +29,9 @@ const FleetMarkdown = ({ markdown, className }: IFleetMarkdownProps) => {
remarkPlugins={[remarkGfm]}
components={{
a: ({ href = "", children }) => {
- return ;
+ return (
+
+ );
},
// Overrides code display to use FleetAce with Readonly overrides.
diff --git a/frontend/components/Icon/Icon.tsx b/frontend/components/Icon/Icon.tsx
index 14bee6621d..3abc23e42a 100644
--- a/frontend/components/Icon/Icon.tsx
+++ b/frontend/components/Icon/Icon.tsx
@@ -7,22 +7,28 @@ interface IIconProps {
color?: "coreVibrantBlue" | "coreFleetBlack";
direction?: "up" | "down" | "left" | "right";
className?: string;
+ size?: "small" | "medium";
}
const baseClass = "icon";
-const Icon = ({ name, color, direction, className }: IIconProps) => {
+const Icon = ({ name, color, direction, className, size }: IIconProps) => {
const classNames = classnames(baseClass, className);
// createPassedProps creates a props object that we pass to the specific icon
// for values that are not null or undefined
const props = useMemo(() => {
const createPassedProps = () => {
- return Object.assign({}, { color, direction });
+ return Object.assign(
+ {},
+ color === undefined ? undefined : { color },
+ direction === undefined ? undefined : { direction },
+ size === undefined ? undefined : { size }
+ );
};
return createPassedProps();
- }, [color, direction]);
+ }, [color, direction, size]);
const IconComponent = ICON_MAP[name];
diff --git a/frontend/components/Sandbox/SandboxDemoMessage/SandboxDemoMessage.tsx b/frontend/components/Sandbox/SandboxDemoMessage/SandboxDemoMessage.tsx
index 43a202d46b..a6e26d79da 100644
--- a/frontend/components/Sandbox/SandboxDemoMessage/SandboxDemoMessage.tsx
+++ b/frontend/components/Sandbox/SandboxDemoMessage/SandboxDemoMessage.tsx
@@ -1,7 +1,7 @@
import classnames from "classnames";
import React from "react";
-import ExternalLinkIcon from "../../../../assets/images/icon-external-link-12x12@2x.png";
+import CustomLink from "components/CustomLink";
interface ISandboxDemoMessageProps {
/** message to display in the sandbox error */
@@ -24,15 +24,12 @@ const SandboxDemoMessage = ({
);
diff --git a/frontend/components/icons/ExternalLink.tsx b/frontend/components/icons/ExternalLink.tsx
new file mode 100644
index 0000000000..915d8339dc
--- /dev/null
+++ b/frontend/components/icons/ExternalLink.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+
+interface IExternalLinkProps {
+ size: "small" | "medium";
+}
+
+const SIZE_MAP = {
+ small: "12",
+ medium: "16",
+};
+
+const ExternalLink = ({ size = "small" }: IExternalLinkProps) => {
+ return (
+
+ );
+};
+
+export default ExternalLink;
diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts
index d509802a1c..3d5b22ae7b 100644
--- a/frontend/components/icons/index.ts
+++ b/frontend/components/icons/index.ts
@@ -8,6 +8,7 @@ import LowDiskSpaceHosts from "./LowDiskSpaceHosts";
import ApplePurple from "./ApplePurple";
import LinuxGreen from "./LinuxGreen";
import WindowsBlue from "./WindowsBlue";
+import ExternalLink from "./ExternalLink";
// a mapping of the usable names of icons to the icon source.
export const ICON_MAP = {
@@ -24,6 +25,7 @@ export const ICON_MAP = {
"linux-green": LinuxGreen,
"missing-hosts": MissingHosts,
"low-disk-space-hosts": LowDiskSpaceHosts,
+ "external-link": ExternalLink,
};
export type IconNames = keyof typeof ICON_MAP;
diff --git a/frontend/components/side_panels/PackInfoSidePanel/PackInfoSidePanel.jsx b/frontend/components/side_panels/PackInfoSidePanel/PackInfoSidePanel.jsx
index bb9be7448c..ba18bb56ce 100644
--- a/frontend/components/side_panels/PackInfoSidePanel/PackInfoSidePanel.jsx
+++ b/frontend/components/side_panels/PackInfoSidePanel/PackInfoSidePanel.jsx
@@ -1,8 +1,9 @@
import React from "react";
+import CustomLink from "components/CustomLink";
+
import DifferentialIcon from "../../../../assets/images/icon-plus-minus-black-16x16@2x.png";
import SnapshotIcon from "../../../../assets/images/icon-snapshot-black-16x14@2x.png";
-import ExternalLinkIcon from "../../../../assets/images/icon-external-link-12x12@2x.png";
const baseClass = "pack-info-side-panel";
@@ -55,18 +56,11 @@ const PackInfoSidePanel = () => {
Learn more about log aggregation in the{" "}
-
- documentation
-
-
+
);
diff --git a/frontend/layouts/CoreLayout/CoreLayout.tsx b/frontend/layouts/CoreLayout/CoreLayout.tsx
index 6dde829933..6c76dfa64a 100644
--- a/frontend/layouts/CoreLayout/CoreLayout.tsx
+++ b/frontend/layouts/CoreLayout/CoreLayout.tsx
@@ -8,9 +8,9 @@ import paths from "router/paths";
import useDeepEffect from "hooks/useDeepEffect";
import FlashMessage from "components/FlashMessage";
import SiteTopNav from "components/top_nav/SiteTopNav";
+import CustomLink from "components/CustomLink";
import { INotification } from "interfaces/notification";
import { licenseExpirationWarning } from "utilities/helpers";
-import ExternalLinkIcon from "../../../assets/images/icon-external-link-12x12@2x.png";
import smallScreenImage from "../../../assets/images/small-screen-160x80@2x.png";
@@ -23,14 +23,12 @@ const expirationMessage = (
<>
Your license for Fleet Premium is about to expire. If you’d like to renew or
have questions about downgrading,{" "}
-
- please head to the Fleet documentation
-
-
+
>
);
diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx
index 34db3ed474..57c7fbc65f 100644
--- a/frontend/pages/DashboardPage/DashboardPage.tsx
+++ b/frontend/pages/DashboardPage/DashboardPage.tsx
@@ -33,6 +33,7 @@ import { ITableQueryData } from "components/TableContainer";
import TeamsDropdown from "components/TeamsDropdown";
import Spinner from "components/Spinner";
+import CustomLink from "components/CustomLink";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import MainContent from "components/MainContent";
@@ -49,7 +50,6 @@ import Mdm from "./cards/MDM";
import Munki from "./cards/Munki";
import OperatingSystems from "./cards/OperatingSystems";
import AddHostsModal from "../../components/AddHostsModal";
-import ExternalLinkIcon from "../../../assets/images/icon-external-link-12x12@2x.png";
const baseClass = "dashboard-page";
@@ -476,14 +476,11 @@ const DashboardPage = (): JSX.Element => {
description: (
Munki is a tool for managing software on macOS devices.{" "}
-
- Learn about Munki
-
-
+
),
children: (
@@ -503,14 +500,11 @@ const DashboardPage = (): JSX.Element => {
description: (
MDM is used to manage configuration on macOS devices.{" "}
-
- Learn about MDM
-
-
+
),
children: (
diff --git a/frontend/pages/DashboardPage/cards/OperatingSystems/OperatingSystems.tsx b/frontend/pages/DashboardPage/cards/OperatingSystems/OperatingSystems.tsx
index 629d2b0de7..d25bda2301 100644
--- a/frontend/pages/DashboardPage/cards/OperatingSystems/OperatingSystems.tsx
+++ b/frontend/pages/DashboardPage/cards/OperatingSystems/OperatingSystems.tsx
@@ -18,8 +18,7 @@ import TableContainer from "components/TableContainer";
import Spinner from "components/Spinner";
import TableDataError from "components/DataError";
import LastUpdatedText from "components/LastUpdatedText";
-
-import ExternalLinkIcon from "../../../../../assets/images/icon-external-link-12x12@2x.png";
+import CustomLink from "components/CustomLink";
import generateTableHeaders from "./OperatingSystemsTableConfig";
@@ -89,17 +88,12 @@ const OperatingSystems = ({
{OS_VENDOR_BY_PLATFORM[selectedPlatform]} releases updates and fixes for
supported operating systems.{" "}
-
- See supported operating{" "}
-
- systems
-
-
-
+
) : null;
diff --git a/frontend/pages/ForgotPasswordPage/ForgotPasswordPage.tsx b/frontend/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
index 7a83460468..94e4ba2d91 100644
--- a/frontend/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
+++ b/frontend/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
@@ -10,7 +10,7 @@ import ForgotPasswordForm from "components/forms/ForgotPasswordForm";
import StackedWhiteBoxes from "components/StackedWhiteBoxes";
import AuthenticationFormWrapper from "components/AuthenticationFormWrapper";
import Spinner from "components/Spinner";
-import ExternalLinkIcon from "../../../assets/images/icon-external-link-12x12@2x.png";
+import CustomLink from "components/CustomLink";
const ForgotPasswordPage = () => {
const [email, setEmail] = useState("");
@@ -54,15 +54,11 @@ const ForgotPasswordPage = () => {
You can find more information on resetting passwords at the{" "}
-
- Password reset FAQ
-
-
+
);
diff --git a/frontend/pages/UserSettingsPage/UserSettingsPage.tsx b/frontend/pages/UserSettingsPage/UserSettingsPage.tsx
index 3d31f7c8a2..ff54aeee06 100644
--- a/frontend/pages/UserSettingsPage/UserSettingsPage.tsx
+++ b/frontend/pages/UserSettingsPage/UserSettingsPage.tsx
@@ -25,9 +25,9 @@ import SandboxGate from "components/Sandbox/SandboxGate";
import SandboxDemoMessage from "components/Sandbox/SandboxDemoMessage";
import MainContent from "components/MainContent";
import SidePanelContent from "components/SidePanelContent";
+import CustomLink from "components/CustomLink";
import UserSidePanel from "./UserSidePanel";
-import ExternalLinkIcon from "../../../assets/images/icon-external-link-12x12@2x.png";
const baseClass = "user-settings";
@@ -188,14 +188,11 @@ const UserSettingsPage = ({
This token expires. If you want an API key for a
permanent integration, create an
-
- API-only user
-
-
+
instead.
@@ -205,14 +202,11 @@ const UserSettingsPage = ({
This token is intended for SSO users to authenticate in the fleetctl
CLI. It expires based on the{" "}
-
- session duration configuration
-
-
+
diff --git a/frontend/pages/admin/AppSettingsPage/cards/Statistics/Statistics.tsx b/frontend/pages/admin/AppSettingsPage/cards/Statistics/Statistics.tsx
index f72632386d..1e820e7468 100644
--- a/frontend/pages/admin/AppSettingsPage/cards/Statistics/Statistics.tsx
+++ b/frontend/pages/admin/AppSettingsPage/cards/Statistics/Statistics.tsx
@@ -5,7 +5,7 @@ import Button from "components/buttons/Button";
import Checkbox from "components/forms/fields/Checkbox";
import Modal from "components/Modal";
-import ExternalLinkIcon from "../../../../../../assets/images/icon-external-link-12x12@2x.png";
+import CustomLink from "components/CustomLink";
import {
IAppConfigFormProps,
IFormField,
@@ -95,14 +95,11 @@ const Statistics = ({
we can make better product decisions.
-
- Learn more about usage statistics
-
-
+
)}
-
- Learn more about agent{" "}
-
- options
-
-
-
+
{isFetchingTeamOptions ? (
diff --git a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx
index 59c98c72b0..991021200b 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx
@@ -13,11 +13,12 @@ import teamsAPI, {
import Button from "components/buttons/Button";
import TableContainer from "components/TableContainer";
import TableDataError from "components/DataError";
+import CustomLink from "components/CustomLink";
+
import CreateTeamModal from "./components/CreateTeamModal";
import DeleteTeamModal from "./components/DeleteTeamModal";
import EditTeamModal from "./components/EditTeamModal";
import { generateTableHeaders, generateDataSet } from "./TeamTableConfig";
-import ExternalLinkIcon from "../../../../assets/images/icon-external-link-12x12@2x.png";
const baseClass = "team-management";
const noTeamsClass = "no-teams";
@@ -203,14 +204,11 @@ const TeamManagementPage = (): JSX.Element => {
Want to learn more?
-
- Read about teams
-
-
+