Skip to content

Commit

Permalink
feat: message banner (#2726)
Browse files Browse the repository at this point in the history
Related to:
https://linear.app/unleash/issue/2-511/exploration-build-data-for-the-possibility-of-showing-an-alert-to-the
Namely:
https://unleash-internal.slack.com/archives/C046LV85N3C/p1671443897386729

The idea is to have a general message banner that can be controlled
through a feature flag in Unleash to display announcements, warnings,
informations, etc.

Currently using mock feature flags, but the idea is to bind this to a
feature flag we can manage in our Unleash instance, and use its payload
to provide information to end users whenever we want.

Co-authored-by: Gastón Fournier <gaston@getunleash.ai>
  • Loading branch information
nunogois and Gastón Fournier committed Dec 22, 2022
1 parent e533b44 commit aaa96f7
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 1 deletion.
139 changes: 139 additions & 0 deletions 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:
'<strong>Heads up!</strong> 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 (
<StyledBar variant={variant}>
<StyledIcon variant={variant}>
<BannerIcon icon={icon} variant={variant} />
</StyledIcon>
<StyledMessage dangerouslySetInnerHTML={{ __html: message }} />
<BannerButton
link={link}
linkText={linkText}
plausibleEvent={plausibleEvent}
/>
</StyledBar>
);
};

interface IBannerIconProps {
icon?: string;
variant?: BannerVariant;
}

const BannerIcon = ({ icon, variant }: IBannerIconProps) => {
if (icon === 'none') return null;
if (icon) return <Icon>{icon}</Icon>;
if (variant) return <WarningAmber />;
// 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 (
<StyledLink href={link} target="_blank" onClick={trackEvent}>
{linkText}
</StyledLink>
);
else
return (
<StyledLink
onClick={() => {
trackEvent();
navigate(link);
}}
>
{linkText}
</StyledLink>
);
};
3 changes: 2 additions & 1 deletion frontend/src/hooks/usePlausibleTracker.ts
Expand Up @@ -13,7 +13,8 @@ type CustomEvents =
| 'upgrade_plan_clicked'
| 'change_request'
| 'favorite'
| 'maintenance';
| 'maintenance'
| 'message_banner';

export const usePlausibleTracker = () => {
const plausible = useContext(PlausibleContext);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/index.tsx
Expand Up @@ -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(
<UIProviderContainer>
Expand All @@ -22,6 +23,7 @@ ReactDOM.render(
<AnnouncerProvider>
<FeedbackCESProvider>
<InstanceStatus>
<MessageBanner />
<ScrollTop />
<App />
</InstanceStatus>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/interfaces/uiConfig.ts
Expand Up @@ -46,6 +46,7 @@ export interface IFlags {
variantsPerEnvironment?: boolean;
networkView?: boolean;
maintenance?: boolean;
messageBanner?: boolean;
}

export interface IVersionInfo {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/__snapshots__/create-config.test.ts.snap
Expand Up @@ -76,6 +76,7 @@ exports[`should create default config 1`] = `
"embedProxyFrontend": true,
"maintenance": false,
"maintenanceMode": false,
"messageBanner": false,
"networkView": false,
"proxyReturnAllToggles": false,
"responseTimeWithAppName": false,
Expand All @@ -93,6 +94,7 @@ exports[`should create default config 1`] = `
"embedProxyFrontend": true,
"maintenance": false,
"maintenanceMode": false,
"messageBanner": false,
"networkView": false,
"proxyReturnAllToggles": false,
"responseTimeWithAppName": false,
Expand Down
4 changes: 4 additions & 0 deletions src/lib/types/experimental.ts
Expand Up @@ -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 = {
Expand Down

0 comments on commit aaa96f7

Please sign in to comment.