diff --git a/components/SystemAlerts/SystemAlert.tsx b/components/SystemAlerts/SystemAlert.tsx
new file mode 100644
index 00000000..4db4a376
--- /dev/null
+++ b/components/SystemAlerts/SystemAlert.tsx
@@ -0,0 +1,121 @@
+/*
+ *
+ * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved
+ *
+ * This program and the accompanying materials are made available under the terms of
+ * the GNU Affero General Public License v3.0. You should have received a copy of the
+ * GNU Affero General Public License along with this program.
+ * If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+ * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+import { css } from '@emotion/react';
+
+import { default as theme } from '@/components/theme';
+import { Error as ErrorIcon, Info, Warning } from '@/components/theme/icons';
+import Dismiss from '@/components/theme/icons/dismiss';
+
+export type AlertLevel = 'info' | 'warning' | 'critical';
+
+export type AlertDef = {
+ level: AlertLevel;
+ title: string;
+ message?: string;
+ dismissible: boolean;
+ id: string;
+};
+
+type Props = {
+ alert: AlertDef;
+ onClose: () => void;
+};
+
+const alertContainerStyle = (backgroundColor: string, outline: string) => css`
+ padding: 12px;
+ display: flex;
+ justify-content: space-between;
+ background-color: ${backgroundColor};
+ border-bottom: 1px solid ${outline};
+`;
+
+const contentWrapperStyle = css`
+ display: flex;
+`;
+
+const iconContainerStyle = css`
+ margin: auto 15px auto auto;
+`;
+
+const titleStyle = (textColor: string, hasMessage: boolean) => css`
+ color: ${textColor};
+ margin-top: ${hasMessage ? '0px' : '6px'};
+ ${theme.typography.heading};
+`;
+
+const messageStyle = (textColor: string) => css`
+ color: ${textColor};
+ margin-bottom: 8px;
+ ${theme.typography.regular};
+`;
+
+const dismissButtonStyle = css`
+ cursor: pointer;
+`;
+
+const AlertVariants = {
+ critical: {
+ backgroundColor: theme.colors.error_2,
+ icon: ,
+ textColor: theme.colors.black,
+ outline: theme.colors.error_dark,
+ },
+ warning: {
+ backgroundColor: theme.colors.warning_light,
+ icon: ,
+ textColor: theme.colors.black,
+ outline: theme.colors.warning_dark,
+ },
+ info: {
+ backgroundColor: theme.colors.secondary_1,
+ icon: ,
+ textColor: theme.colors.black,
+ outline: theme.colors.secondary_dark,
+ },
+};
+
+/**
+ * Renders a single system alert with appropriate styling based on alert level
+ * @param alert - Alert definition containing level, title, message, dismissible flag, and id
+ * @param onClose - Callback function triggered when the alert is dismissed
+ * @returns JSX element representing the styled alert
+ */
+export const SystemAlert = ({ alert, onClose }: Props) => {
+ const { backgroundColor, icon, textColor, outline } = AlertVariants[alert.level];
+
+ return (
+
+
+
{icon}
+
+
{alert.title}
+ {alert.message && }
+
+
+ {alert.dismissible && (
+
+
+
+ )}
+
+ );
+};
diff --git a/components/SystemAlerts/SystemAlerts.tsx b/components/SystemAlerts/SystemAlerts.tsx
new file mode 100644
index 00000000..1f516e53
--- /dev/null
+++ b/components/SystemAlerts/SystemAlerts.tsx
@@ -0,0 +1,91 @@
+/*
+ *
+ * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved
+ *
+ * This program and the accompanying materials are made available under the terms of
+ * the GNU Affero General Public License v3.0. You should have received a copy of the
+ * GNU Affero General Public License along with this program.
+ * If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+ * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+import { useEffect, useState } from 'react';
+
+import { AlertDef, AlertLevel, SystemAlert } from '@/components/SystemAlerts/SystemAlert';
+import { getConfig } from '@/global/config';
+
+export const isAlertLevel = (level: any): level is AlertLevel => {
+ return level === 'info' || level === 'warning' || level === 'critical';
+};
+
+export const isAlertDef = (obj: any): obj is AlertDef => {
+ return obj.id && obj.title && obj.dismissible !== undefined && isAlertLevel(obj.level);
+};
+
+export const isAlertDefs = (obj: any): obj is AlertDef[] => {
+ return Array.isArray(obj) && obj.every(isAlertDef);
+};
+
+const LOCAL_STORAGE_KEY = 'SYSTEM_ALERTS_DISMISSED_IDS';
+
+type Props = {
+ alerts?: AlertDef[];
+};
+
+/**
+ * Manages and displays a collection of system alerts with dismissal functionality
+ * @param alerts - Optional array of alert definitions to display (falls back to config if not provided)
+ * @returns JSX element containing rendered system alerts
+ */
+export const SystemAlerts = ({ alerts }: Props) => {
+ const [displayAlerts, setDisplayAlerts] = useState([]);
+ const [dismissedIds, setDismissedIds] = useState([]);
+
+ const getParsedSystemAlerts = () => {
+ try {
+ const { NEXT_PUBLIC_SYSTEM_ALERTS } = getConfig();
+ const parsed = JSON.parse(NEXT_PUBLIC_SYSTEM_ALERTS);
+ if (!isAlertDefs(parsed)) {
+ throw new Error('System Alert types are invalid!');
+ }
+ return parsed;
+ } catch (e) {
+ console.error('Failed to parse systems alerts! Using empty array!', e);
+ return [];
+ }
+ };
+
+ const systemAlerts = alerts ?? getParsedSystemAlerts();
+ const systemAlertIds = systemAlerts.map((a) => a.id);
+
+ const handleClose = (id: string) => {
+ const updated = dismissedIds.concat(id).filter((id) => systemAlertIds.includes(id));
+ setDisplayAlerts(systemAlerts.filter((a) => !updated.includes(a.id)));
+ setDismissedIds(updated);
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(updated));
+ };
+
+ useEffect(() => {
+ const stored = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '[]');
+ setDismissedIds(stored);
+ setDisplayAlerts(systemAlerts.filter((a) => !stored.includes(a.id)));
+ }, []);
+
+ return (
+ <>
+ {displayAlerts.map((alert) => (
+ handleClose(alert.id)} />
+ ))}
+ >
+ );
+};
diff --git a/components/theme/colors.ts b/components/theme/colors.ts
index b68ade48..b7dbff31 100644
--- a/components/theme/colors.ts
+++ b/components/theme/colors.ts
@@ -75,6 +75,7 @@ const error = {
};
const warning = {
+ warning_light: '#ffff758c',
warning: '#f2d021',
warning_dark: '#e6c104',
};
diff --git a/components/theme/icons/index.tsx b/components/theme/icons/index.tsx
index fa8c5795..ba42b15a 100644
--- a/components/theme/icons/index.tsx
+++ b/components/theme/icons/index.tsx
@@ -36,6 +36,7 @@ import Checkmark from './checkmark';
import Spinner from './spinner';
import Error from './error';
import Warning from './warning';
+import Info from './info';
export {
GoogleLogo,
@@ -55,4 +56,5 @@ export {
Spinner,
Error,
Warning,
+ Info,
};
diff --git a/components/theme/icons/info.tsx b/components/theme/icons/info.tsx
new file mode 100644
index 00000000..266cd5e1
--- /dev/null
+++ b/components/theme/icons/info.tsx
@@ -0,0 +1,52 @@
+/*
+ *
+ * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved
+ *
+ * This program and the accompanying materials are made available under the terms of
+ * the GNU Affero General Public License v3.0. You should have received a copy of the
+ * GNU Affero General Public License along with this program.
+ * If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+ * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+import { ReactElement } from 'react';
+import { css } from '@emotion/react';
+
+import { IconProps } from './types';
+
+const Info = ({ fill, size = 30, style }: IconProps): ReactElement => {
+ return (
+
+ );
+};
+
+export default Info;
diff --git a/components/theme/icons/warning.tsx b/components/theme/icons/warning.tsx
index bec6ee1d..5115b7ae 100644
--- a/components/theme/icons/warning.tsx
+++ b/components/theme/icons/warning.tsx
@@ -23,7 +23,7 @@ import { css } from '@emotion/react';
import { IconProps } from './types';
import theme from '../';
-const Warning = ({ height, width, style, fill = theme.colors.error_dark }: IconProps) => {
+const Warning = ({ height, width, style, fill = theme.colors.warning_dark }: IconProps) => {
return (