Skip to content

Commit

Permalink
feat: add floating notifications for applicable events
Browse files Browse the repository at this point in the history
- setup floating notification provider
- applied floating notifications to all relevant instance actions
- applied floating notifications to all relevant cluster actions
- applied floating notifications to all relevant profile actions
- applied floating notifications to all relevant network actions
- applied floating notifications to all relevant storage actions
- applied floating notifications to all relevant image actions
- applied floating notifications to all relevant operations actions
- applied floating notifications to all relevant project actions
- applied floating notifications to all relevant settings actions
- removed NotificationRowLegacy component
- removed NotificationRow from NetworkMap, not needed
- added filtering by severity for toast notifications

Signed-off-by: Mason Hu <[email protected]>
  • Loading branch information
mas-who committed Feb 8, 2024
1 parent 1f25cce commit f723ce2
Show file tree
Hide file tree
Showing 117 changed files with 1,732 additions and 545 deletions.
38 changes: 22 additions & 16 deletions src/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import React, { FC } from "react";
import Navigation from "components/Navigation";
import {
NotificationProvider,
QueuedNotification,
} from "@canonical/react-components";
import { QueuedNotification } from "@canonical/react-components";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import Panels from "components/Panels";
import { AuthProvider } from "context/auth";
Expand All @@ -15,6 +12,9 @@ import App from "./App";
import ErrorBoundary from "components/ErrorBoundary";
import ErrorPage from "components/ErrorPage";
import { useLocation } from "react-router-dom";
import CombinedNotificationProvider from "context/CombinedNotificationProvider";
import StatusBar from "components/StatusBar";
import OperationsProvider from "context/operationsProvider";

const queryClient = new QueryClient();

Expand All @@ -23,26 +23,32 @@ const Root: FC = () => {

return (
<ErrorBoundary fallback={ErrorPage}>
<NotificationProvider state={location.state} pathname={location.pathname}>
<CombinedNotificationProvider
state={location.state}
pathname={location.pathname}
>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<ProjectProvider>
<InstanceLoadingProvider>
<EventQueueProvider>
<div className="l-application" role="presentation">
<Navigation />
<ErrorBoundary fallback={ErrorPage}>
<App />
<Panels />
<Events />
</ErrorBoundary>
</div>
</EventQueueProvider>
<OperationsProvider>
<EventQueueProvider>
<div className="l-application" role="presentation">
<Navigation />
<ErrorBoundary fallback={ErrorPage}>
<App />
<Panels />
<Events />
<StatusBar />
</ErrorBoundary>
</div>
</EventQueueProvider>
</OperationsProvider>
</InstanceLoadingProvider>
</ProjectProvider>
</AuthProvider>
</QueryClientProvider>
</NotificationProvider>
</CombinedNotificationProvider>
</ErrorBoundary>
);
};
Expand Down
65 changes: 65 additions & 0 deletions src/components/Animate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, {
FC,
PropsWithChildren,
useLayoutEffect,
useRef,
useState,
} from "react";
import { usePrefersReducedMotion } from "util/usePreferReducedMotion";

interface Props {
show: boolean;
from: Keyframe;
to: Keyframe;
exitAnimation?: Keyframe[];
options?: KeyframeAnimationOptions;
className?: string;
}

const Animate: FC<PropsWithChildren<Props>> = ({
show,
children,
from,
to,
exitAnimation,
options = { duration: 500, fill: "forwards" },
className,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const preferReducedMotion = usePrefersReducedMotion();

// This state is used so that we trigger a extra render cycle
// to animate the child component when it is being unmounted
const [removeState, setRemove] = useState(!show);

useLayoutEffect(() => {
const element = containerRef.current;
if (show) {
setRemove(false);
if (!element || preferReducedMotion) return;
element.animate([from, to], options);
} else {
if (!element) return;
if (preferReducedMotion) {
setRemove(true);
return;
}
const animation = element.animate(exitAnimation || [to, from], options);
animation.onfinish = () => {
setRemove(true);
// This is important, else the next render cycle due to setRemove will cause flickering effect
element.style.display = "none";
};
}
}, [show, removeState]);

return (
!removeState && (
<div ref={containerRef} className={className}>
{children}
</div>
)
);
};

export default Animate;
2 changes: 0 additions & 2 deletions src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useAuth } from "context/auth";
import classnames from "classnames";
import Logo from "./Logo";
import ProjectSelector from "pages/projects/ProjectSelector";
import Version from "components/Version";
import { isWidthBelow, logout } from "util/helpers";
import { useProject } from "context/project";
import { useMenuCollapsed } from "context/menuCollapsed";
Expand Down Expand Up @@ -314,7 +313,6 @@ const Navigation: FC = () => {
Report a bug
</a>
</li>
<Version />
</ul>
</div>
</div>
Expand Down
63 changes: 0 additions & 63 deletions src/components/NotificationRowLegacy.tsx

This file was deleted.

28 changes: 28 additions & 0 deletions src/components/OperationStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Icon } from "@canonical/react-components";
import { useOperations } from "context/operationsProvider";
import React, { useState } from "react";
import { isWidthBelow } from "util/helpers";
import useEventListener from "@use-it/event-listener";
import { Link } from "react-router-dom";
import { pluralize } from "util/instanceBulkActions";

const OperationStatus = () => {
const [isSmallScreen, setIsSmallScreen] = useState(false);
const { runningOperations } = useOperations();

useEventListener("resize", () => setIsSmallScreen(isWidthBelow(620)));

let operationsStatus = `${runningOperations.length} ${pluralize("operation", runningOperations.length)} in progress...`;
if (isSmallScreen) {
operationsStatus = `${runningOperations.length} Ops`;
}

return runningOperations.length ? (
<div className="operation-status" role="alert">
<Icon name="status-in-progress-small" className="status-icon" />
<Link to="ui/operations">{operationsStatus}</Link>
</div>
) : null;
};

export default OperationStatus;
4 changes: 3 additions & 1 deletion src/components/ScrollableContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ const ScrollableContainer: FC<Props> = ({
}
const above = childContainer.getBoundingClientRect().top + 1;
const below = getAbsoluteHeightBelow(belowId);
const parentsBottomSpacing = getParentsBottomSpacing(childContainer);
const parentsBottomSpacing =
getParentsBottomSpacing(childContainer) +
getAbsoluteHeightBelow("status-bar");
const offset = Math.ceil(above + below + parentsBottomSpacing);
const style = `height: calc(100vh - ${offset}px); min-height: calc(100vh - ${offset}px)`;
childContainer.setAttribute("style", style);
Expand Down
9 changes: 6 additions & 3 deletions src/components/ScrollableTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { getAbsoluteHeightBelow, getParentsBottomSpacing } from "util/helpers";
interface Props {
children: ReactNode;
dependencies: DependencyList;
belowId?: string;
belowIds?: string[];
tableId: string;
}

const ScrollableTable: FC<Props> = ({
dependencies,
children,
belowId = "",
belowIds = [],
tableId,
}) => {
const updateTBodyHeight = () => {
Expand All @@ -22,7 +22,10 @@ const ScrollableTable: FC<Props> = ({
}
const tBody = table.children[1];
const above = tBody.getBoundingClientRect().top + 1;
const below = getAbsoluteHeightBelow(belowId);
const below = belowIds.reduce(
(acc, belowId) => acc + getAbsoluteHeightBelow(belowId),
0,
);
const parentsBottomSpacing = getParentsBottomSpacing(table);
const offset = Math.ceil(above + below + parentsBottomSpacing);
const style = `height: calc(100vh - ${offset}px); min-height: calc(100vh - ${offset}px)`;
Expand Down
67 changes: 67 additions & 0 deletions src/components/StatusBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { FC } from "react";
import classnames from "classnames";
import Version from "./Version";
import OperationStatus from "./OperationStatus";
import { useToastNotification } from "context/toastNotificationProvider";
import { ICONS, Icon } from "@canonical/react-components";
import { iconLookup, severityOrder } from "util/notifications";
import useEventListener from "@use-it/event-listener";

interface Props {
className?: string;
}

const StatusBar: FC<Props> = ({ className }) => {
const { toggleListView, notifications, countBySeverity, isListView } =
useToastNotification();

useEventListener("keydown", (e: KeyboardEvent) => {
// Close notifications list if Escape pressed
if (e.code === "Escape" && isListView) {
toggleListView();
}
});

const notificationIcons = severityOrder.map((severity) => {
if (countBySeverity[severity]) {
return (
<Icon
key={severity}
name={iconLookup[severity]}
aria-label={`${severity} notification exists`}
/>
);
}
});

const hasNotifications = !!notifications.length;
return (
<>
<aside
className={classnames("l-status status-bar", className)}
id="status-bar"
>
<Version />
<div className="status-right-container">
<OperationStatus />
{hasNotifications && (
<button
className={classnames(
"u-no-margin u-no-padding u-no-border expand-button",
{ "button-active": isListView },
)}
onClick={toggleListView}
aria-label="Expand notifications list"
>
{notificationIcons}
<span className="total-count">{notifications.length}</span>
<Icon name={isListView ? ICONS.chevronDown : ICONS.chevronUp} />
</button>
)}
</div>
</aside>
</>
);
};

export default StatusBar;
Loading

0 comments on commit f723ce2

Please sign in to comment.