.
+ .icon {
+ width: 16px;
+ height: 16px;
+ display: inline-flex;
+ }
+ }
+
+ &__message {
+ margin: 0;
+ font-size: $x-small;
+ font-weight: $regular;
+ line-height: $line-height;
+ color: $ui-fleet-black-75;
+ word-break: break-word;
+ }
+
+ &__actions {
+ display: flex;
+ align-items: center;
+ gap: $pad-small;
+ flex-shrink: 0;
+ }
+
+ &__action-button {
+ appearance: none;
+ background: transparent;
+ border: 0;
+ margin: 0;
+ padding: $pad-small;
+ width: $toast-notification-action-size;
+ height: $toast-notification-action-size;
+ cursor: pointer;
+ color: $ui-fleet-black-75;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: $border-radius;
+ transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
+
+ &:hover,
+ &:focus-visible {
+ background-color: $ui-fleet-black-5;
+ color: $core-fleet-black;
+ outline: none;
+ }
+ }
+
+ &__chevron {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 200ms ease-in-out;
+
+ &--open {
+ transform: rotate(180deg);
+ }
+ }
+
+ &__panel {
+ display: flex;
+ flex-direction: column;
+ gap: $pad-small;
+ width: 100%;
+ border-top: 1px solid $ui-fleet-black-5;
+ padding-top: $pad-medium;
+ }
+
+ &__panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: $pad-small;
+ }
+
+ &__panel-label {
+ font-size: $x-small;
+ font-weight: $bold;
+ color: $core-fleet-black;
+ }
+
+ &__copy-button {
+ appearance: none;
+ background: transparent;
+ border: 0;
+ padding: $pad-xsmall;
+ margin: 0;
+ cursor: pointer;
+ color: $ui-fleet-black-75;
+ display: inline-flex;
+ align-items: center;
+ gap: $pad-xsmall;
+ border-radius: $border-radius;
+ transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
+
+ &:hover,
+ &:focus-visible {
+ background-color: $ui-fleet-black-5;
+ color: $core-fleet-black;
+ outline: none;
+ }
+ }
+
+ &__copy-confirmation {
+ font-size: $xx-small;
+ color: $ui-fleet-black-75;
+ }
+
+ // Light JSON block — overrides the global dark `pre` to match the ace-fleet
+ // editor theme.
+ &__json-block {
+ margin: 0;
+ max-height: 200px;
+ overflow: auto;
+ box-sizing: border-box;
+ background-color: $ui-off-white;
+ color: $core-fleet-black;
+ border: 1px solid $ui-fleet-black-10;
+ border-radius: $border-radius;
+ padding: $pad-smedium;
+ font-size: $xx-small;
+
+ // Token colours darkened from the ace-fleet theme for light-bg contrast.
+ .key {
+ color: $ui-fleet-black-75;
+ }
+
+ .string {
+ color: #2e8b57;
+ }
+
+ .number {
+ color: #b5651d;
+ }
+
+ .boolean {
+ color: #4271ae;
+ }
+
+ .null {
+ color: #6b4290;
+ }
+ }
+}
diff --git a/frontend/components/ToastNotification/index.ts b/frontend/components/ToastNotification/index.ts
new file mode 100644
index 0000000000..1c5fdacda8
--- /dev/null
+++ b/frontend/components/ToastNotification/index.ts
@@ -0,0 +1,12 @@
+export {
+ default,
+ default as ToastNotification,
+ notify,
+} from "./ToastNotification";
+export type {
+ IToastNotificationProps,
+ INotify,
+ INotifyOptions,
+ INotifyBatchItem,
+ ToastId,
+} from "./ToastNotification";
diff --git a/frontend/components/top_nav/UserMenu/UserMenu.tsx b/frontend/components/top_nav/UserMenu/UserMenu.tsx
index 60be9bac30..245a6d65f9 100644
--- a/frontend/components/top_nav/UserMenu/UserMenu.tsx
+++ b/frontend/components/top_nav/UserMenu/UserMenu.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useState } from "react";
+import React, { useEffect, useState } from "react";
import Select, {
components,
DropdownIndicatorProps,
@@ -6,7 +6,6 @@ import Select, {
OptionProps,
StylesConfig,
} from "react-select-5";
-import { NotificationContext } from "context/notification";
import { IUser } from "interfaces/user";
import { ITeam, ITeamSummary } from "interfaces/team";
@@ -91,8 +90,6 @@ const UserMenu = ({
// Work around preventing react-select-5 from auto focusing first option unless using keyboard
const [isKeyboardFocus, setIsKeyboardFocus] = useState(false);
- const { renderFlash } = useContext(NotificationContext);
-
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Tab") {
@@ -161,18 +158,9 @@ const UserMenu = ({
if (currentTeamIsAdmin) {
onUserMenuItemClick(PATHS.FLEET_DETAILS_USERS(currentTeam.id));
} else {
- // Sort and pick the first team the user is admin of to display.
+ // Not an admin of the current team — redirect to the first team the
+ // user is an admin of.
const targetTeam = getSortedTeamOptions(userAdminTeams)[0];
- if (currentTeam) {
- const msg = (
- <>
- You're not authorized to view this page for{" "}
-
{currentTeam.name}. Now viewing
{targetTeam.label}
- .
- >
- );
- renderFlash("warning-filled", msg);
- }
onUserMenuItemClick(PATHS.FLEET_DETAILS_USERS(targetTeam.value));
}
};
diff --git a/frontend/context/notification.tsx b/frontend/context/notification.tsx
deleted file mode 100644
index 4ff9b913db..0000000000
--- a/frontend/context/notification.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import React, {
- createContext,
- useReducer,
- ReactNode,
- useCallback,
- useMemo,
-} from "react";
-import { INotification } from "interfaces/notification";
-import { noop } from "lodash";
-
-type Props = {
- children: ReactNode;
-};
-
-type FlashOptions = {
- /** `persistOnPageChange` is used to keep the flash message showing after a
- * router change if set to `true`.
- *
- * @default undefined
- * */
- persistOnPageChange?: boolean;
-};
-
-type MultiFlashOptions = FlashOptions & {
- notifications?: INotification[];
-};
-
-type InitialStateType = {
- notification: INotification | INotification[] | null;
- renderFlash: (
- alertType: "success" | "error" | "warning-filled" | null,
- message: JSX.Element | string | null,
- options?: FlashOptions
- ) => void;
- renderMultiFlash: (options?: MultiFlashOptions) => void;
- hideFlash: (id?: string) => void;
-};
-
-export type INotificationContext = InitialStateType;
-
-const initialState = {
- notification: null,
- renderFlash: noop,
- renderMultiFlash: noop,
- hideFlash: noop,
-};
-
-const actionTypes = {
- RENDER_FLASH: "RENDER_FLASH",
- RENDER_MULTIFLASH: "RENDER_MULTIFLASH",
- HIDE_FLASH: "HIDE_FLASH",
-} as const;
-
-type State = {
- notification: INotification | INotification[] | null;
-};
-
-type Action =
- | {
- type: typeof actionTypes.RENDER_MULTIFLASH;
- notifications: INotification[];
- }
- | {
- type: typeof actionTypes.RENDER_FLASH;
- alertType: "success" | "error" | "warning-filled" | null;
- message: JSX.Element | string | null;
- options?: FlashOptions;
- }
- | {
- type: typeof actionTypes.HIDE_FLASH;
- id?: string;
- };
-
-const reducer = (state: State, action: Action) => {
- switch (action.type) {
- case actionTypes.RENDER_FLASH:
- return {
- ...state,
- notification: {
- alertType: action.alertType,
- isVisible: true,
- message: action.message,
- persistOnPageChange: action.options?.persistOnPageChange ?? false,
- },
- };
- case actionTypes.RENDER_MULTIFLASH: {
- const multiNotifications = action.notifications;
-
- return {
- ...state,
- notification: multiNotifications,
- };
- }
- case actionTypes.HIDE_FLASH:
- if (Array.isArray(state.notification)) {
- return {
- ...state,
- notification: state.notification.filter(
- (n: INotification) => n.id !== action.id
- ),
- };
- }
- return initialState;
- default:
- return state;
- }
-};
-
-export const NotificationContext = createContext
(
- initialState
-);
-
-const NotificationProvider = ({ children }: Props) => {
- const [state, dispatch] = useReducer(reducer, initialState);
- const renderFlash = useCallback(
- (
- alertType: "success" | "error" | "warning-filled" | null,
- message: JSX.Element | string | null,
- options?: {
- persistOnPageChange?: boolean;
- }
- ) => {
- // wrapping the dispatch in a timeout ensures it is evaluated on the next event loop,
- // preventing bugs related to the FlashMessage's self-hiding behavior on URL changes.
- // react router v3 router.push is asynchronous
- setTimeout(() => {
- dispatch({
- type: actionTypes.RENDER_FLASH,
- alertType,
- message,
- options,
- });
- });
- },
- []
- );
-
- const renderMultiFlash = useCallback((options?: MultiFlashOptions) => {
- setTimeout(() => {
- if (options?.notifications) {
- dispatch({
- type: actionTypes.RENDER_MULTIFLASH,
- notifications: options.notifications,
- });
- }
- });
- }, []);
-
- const hideFlash = useCallback((id?: string) => {
- dispatch({ type: actionTypes.HIDE_FLASH, id });
- }, []);
-
- const value = useMemo(
- () => ({
- notification: state.notification,
- renderFlash,
- renderMultiFlash,
- hideFlash,
- }),
- [state.notification, renderFlash, renderMultiFlash, hideFlash]
- );
-
- return (
-
- {children}
-
- );
-};
-
-export default NotificationProvider;
diff --git a/frontend/interfaces/notification.ts b/frontend/interfaces/notification.ts
deleted file mode 100644
index 59d1bc6de9..0000000000
--- a/frontend/interfaces/notification.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import PropTypes from "prop-types";
-
-export default PropTypes.shape({
- alertType: PropTypes.string,
- isVisible: PropTypes.bool,
- message: PropTypes.string,
- persistOnPageChange: PropTypes.bool,
-});
-
-export type IAlertType = "success" | "error" | "warning-filled";
-
-export interface INotification {
- alertType: IAlertType | null;
- isVisible: boolean;
- message: JSX.Element | string | null;
- persistOnPageChange?: boolean;
- id?: string;
-}
diff --git a/frontend/layouts/CoreLayout/CoreLayout.tsx b/frontend/layouts/CoreLayout/CoreLayout.tsx
index 0dbb668cb8..f4dc3f9000 100644
--- a/frontend/layouts/CoreLayout/CoreLayout.tsx
+++ b/frontend/layouts/CoreLayout/CoreLayout.tsx
@@ -4,14 +4,9 @@ import { InjectedRouter } from "react-router";
import UnsupportedScreenSize from "layouts/UnsupportedScreenSize";
import { AppContext } from "context/app";
-import { NotificationContext } from "context/notification";
-import { TableContext } from "context/table";
-import { INotification } from "interfaces/notification";
import classNames from "classnames";
import paths from "router/paths";
-import useDeepEffect from "hooks/useDeepEffect";
-import FlashMessage from "components/FlashMessage";
import SiteTopNav from "components/top_nav/SiteTopNav";
import CommandPalette from "components/CommandPalette";
import { QueryParams } from "utilities/url";
@@ -31,25 +26,6 @@ interface ICoreLayoutProps {
const CoreLayout = ({ children, router, location }: ICoreLayoutProps) => {
const { config, currentUser } = useContext(AppContext);
- const { notification, hideFlash } = useContext(NotificationContext);
- const { setResetSelectedRows } = useContext(TableContext);
-
- // on success of an action, the table will reset its checkboxes.
- // setTimeout is to help with race conditions as table reloads
- // in some instances (i.e. Manage Hosts)
- useDeepEffect(() => {
- if (
- notification &&
- (notification as INotification).alertType === "success"
- ) {
- setTimeout(() => {
- setResetSelectedRows(true);
- setTimeout(() => {
- setResetSelectedRows(false);
- }, 300);
- }, 0);
- }
- }, [notification]);
const onLogoutUser = async () => {
const { LOGOUT } = paths;
@@ -60,8 +36,6 @@ const CoreLayout = ({ children, router, location }: ICoreLayoutProps) => {
router.push(path);
};
- const fullWidthFlash = !currentUser;
-
if (!currentUser || !config) {
return null;
}
@@ -85,16 +59,7 @@ const CoreLayout = ({ children, router, location }: ICoreLayoutProps) => {
onUserMenuItemClick={onUserMenuItemClick}
/>
-
-
-
- {children}
-
+ {children}
);
};
diff --git a/frontend/layouts/GatedLayout/GatedLayout.tsx b/frontend/layouts/GatedLayout/GatedLayout.tsx
index 9dbf25b2c5..99a81b24e3 100644
--- a/frontend/layouts/GatedLayout/GatedLayout.tsx
+++ b/frontend/layouts/GatedLayout/GatedLayout.tsx
@@ -1,24 +1,11 @@
-import React, { useContext } from "react";
-import { NotificationContext } from "context/notification";
-import FlashMessage from "components/FlashMessage";
+import React from "react";
interface IGatedLayoutProps {
children: React.ReactNode;
}
const GatedLayout = ({ children }: IGatedLayoutProps): JSX.Element => {
- const { notification, hideFlash } = useContext(NotificationContext);
-
- return (
-