diff --git a/frontend/src/component/common/MessageBanner/MessageBanner.tsx b/frontend/src/component/common/MessageBanner/MessageBanner.tsx
new file mode 100644
index 0000000000..13e48083d3
--- /dev/null
+++ b/frontend/src/component/common/MessageBanner/MessageBanner.tsx
@@ -0,0 +1,139 @@
+import { WarningAmber } from '@mui/icons-material';
+import { styled, Icon, Link } from '@mui/material';
+import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
+import { useNavigate } from 'react-router-dom';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
+
+const StyledBar = styled('aside', {
+ shouldForwardProp: prop => prop !== 'variant',
+})<{ variant?: BannerVariant }>(({ theme, variant = 'neutral' }) => ({
+ position: 'relative',
+ zIndex: 1,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: theme.spacing(1),
+ gap: theme.spacing(1),
+ borderBottom: '1px solid',
+ borderColor: theme.palette[variant].border,
+ background: theme.palette[variant].light,
+ color: theme.palette[variant].dark,
+}));
+
+const StyledIcon = styled('div', {
+ shouldForwardProp: prop => prop !== 'variant',
+})<{ variant?: BannerVariant }>(({ theme, variant = 'neutral' }) => ({
+ display: 'flex',
+ alignItems: 'center',
+ color: theme.palette[variant].main,
+}));
+
+const StyledMessage = styled('div')(({ theme }) => ({
+ fontSize: theme.fontSizes.smallBody,
+}));
+
+const StyledLink = styled(Link)(({ theme }) => ({
+ fontSize: theme.fontSizes.smallBody,
+}));
+
+type BannerVariant = 'warning' | 'info' | 'error' | 'success' | 'neutral';
+
+interface IMessageFlag {
+ enabled: boolean;
+ message: string;
+ variant?: BannerVariant;
+ icon?: string;
+ link?: string;
+ linkText?: string;
+ plausibleEvent?: string;
+}
+
+// TODO: Grab a real feature flag instead
+const mockFlag: IMessageFlag = {
+ enabled: true,
+ message:
+ 'Heads up! It seems like one of your client instances might be misbehaving.',
+ variant: 'warning',
+ link: '/admin/network',
+ linkText: 'View Network',
+ plausibleEvent: 'network_warning',
+};
+
+export const MessageBanner = () => {
+ const { uiConfig } = useUiConfig();
+
+ const { enabled, message, variant, icon, link, linkText, plausibleEvent } =
+ { ...mockFlag, enabled: uiConfig.flags.messageBanner };
+
+ if (!enabled) return null;
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+interface IBannerIconProps {
+ icon?: string;
+ variant?: BannerVariant;
+}
+
+const BannerIcon = ({ icon, variant }: IBannerIconProps) => {
+ if (icon === 'none') return null;
+ if (icon) return {icon};
+ if (variant) return ;
+ // TODO: Add defaults for other variants?
+ return null;
+};
+
+interface IBannerButtonProps {
+ link?: string;
+ linkText?: string;
+ plausibleEvent?: string;
+}
+
+const BannerButton = ({
+ link,
+ linkText = 'More info',
+ plausibleEvent,
+}: IBannerButtonProps) => {
+ if (!link) return null;
+
+ const navigate = useNavigate();
+ const tracker = usePlausibleTracker();
+ const external = link.startsWith('http');
+
+ const trackEvent = () => {
+ if (!plausibleEvent) return;
+ tracker.trackEvent('message_banner', {
+ props: { event: plausibleEvent },
+ });
+ };
+
+ if (external)
+ return (
+
+ {linkText}
+
+ );
+ else
+ return (
+ {
+ trackEvent();
+ navigate(link);
+ }}
+ >
+ {linkText}
+
+ );
+};
diff --git a/frontend/src/hooks/usePlausibleTracker.ts b/frontend/src/hooks/usePlausibleTracker.ts
index da2fd5c3da..295e6bfbc1 100644
--- a/frontend/src/hooks/usePlausibleTracker.ts
+++ b/frontend/src/hooks/usePlausibleTracker.ts
@@ -13,7 +13,8 @@ type CustomEvents =
| 'upgrade_plan_clicked'
| 'change_request'
| 'favorite'
- | 'maintenance';
+ | 'maintenance'
+ | 'message_banner';
export const usePlausibleTracker = () => {
const plausible = useContext(PlausibleContext);
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 5e28bea193..0b800d72b0 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -13,6 +13,7 @@ import { FeedbackCESProvider } from 'component/feedback/FeedbackCESContext/Feedb
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
import { UIProviderContainer } from 'component/providers/UIProvider/UIProviderContainer';
+import { MessageBanner } from 'component/common/MessageBanner/MessageBanner';
ReactDOM.render(
@@ -22,6 +23,7 @@ ReactDOM.render(
+
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index b7d6dbd0e3..ebc915cda1 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -46,6 +46,7 @@ export interface IFlags {
variantsPerEnvironment?: boolean;
networkView?: boolean;
maintenance?: boolean;
+ messageBanner?: boolean;
}
export interface IVersionInfo {
diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap
index 10007de7d8..a70140e103 100644
--- a/src/lib/__snapshots__/create-config.test.ts.snap
+++ b/src/lib/__snapshots__/create-config.test.ts.snap
@@ -76,6 +76,7 @@ exports[`should create default config 1`] = `
"embedProxyFrontend": true,
"maintenance": false,
"maintenanceMode": false,
+ "messageBanner": false,
"networkView": false,
"proxyReturnAllToggles": false,
"responseTimeWithAppName": false,
@@ -93,6 +94,7 @@ exports[`should create default config 1`] = `
"embedProxyFrontend": true,
"maintenance": false,
"maintenanceMode": false,
+ "messageBanner": false,
"networkView": false,
"proxyReturnAllToggles": false,
"responseTimeWithAppName": false,
diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts
index 483f15bfd1..6189feafc1 100644
--- a/src/lib/types/experimental.ts
+++ b/src/lib/types/experimental.ts
@@ -47,6 +47,10 @@ const flags = {
process.env.UNLEASH_EXPERIMENTAL_MAINTENANCE_MODE,
false,
),
+ messageBanner: parseEnvVarBoolean(
+ process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER,
+ false,
+ ),
};
export const defaultExperimentalOptions: IExperimentalOptions = {