mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
feat: message banner (#2726)
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>
This commit is contained in:
parent
e533b44c5b
commit
aaa96f71cb
139
frontend/src/component/common/MessageBanner/MessageBanner.tsx
Normal file
139
frontend/src/component/common/MessageBanner/MessageBanner.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -13,7 +13,8 @@ type CustomEvents =
|
|||||||
| 'upgrade_plan_clicked'
|
| 'upgrade_plan_clicked'
|
||||||
| 'change_request'
|
| 'change_request'
|
||||||
| 'favorite'
|
| 'favorite'
|
||||||
| 'maintenance';
|
| 'maintenance'
|
||||||
|
| 'message_banner';
|
||||||
|
|
||||||
export const usePlausibleTracker = () => {
|
export const usePlausibleTracker = () => {
|
||||||
const plausible = useContext(PlausibleContext);
|
const plausible = useContext(PlausibleContext);
|
||||||
|
@ -13,6 +13,7 @@ import { FeedbackCESProvider } from 'component/feedback/FeedbackCESContext/Feedb
|
|||||||
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
|
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
|
||||||
import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
|
import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
|
||||||
import { UIProviderContainer } from 'component/providers/UIProvider/UIProviderContainer';
|
import { UIProviderContainer } from 'component/providers/UIProvider/UIProviderContainer';
|
||||||
|
import { MessageBanner } from 'component/common/MessageBanner/MessageBanner';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<UIProviderContainer>
|
<UIProviderContainer>
|
||||||
@ -22,6 +23,7 @@ ReactDOM.render(
|
|||||||
<AnnouncerProvider>
|
<AnnouncerProvider>
|
||||||
<FeedbackCESProvider>
|
<FeedbackCESProvider>
|
||||||
<InstanceStatus>
|
<InstanceStatus>
|
||||||
|
<MessageBanner />
|
||||||
<ScrollTop />
|
<ScrollTop />
|
||||||
<App />
|
<App />
|
||||||
</InstanceStatus>
|
</InstanceStatus>
|
||||||
|
@ -46,6 +46,7 @@ export interface IFlags {
|
|||||||
variantsPerEnvironment?: boolean;
|
variantsPerEnvironment?: boolean;
|
||||||
networkView?: boolean;
|
networkView?: boolean;
|
||||||
maintenance?: boolean;
|
maintenance?: boolean;
|
||||||
|
messageBanner?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -76,6 +76,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxyFrontend": true,
|
"embedProxyFrontend": true,
|
||||||
"maintenance": false,
|
"maintenance": false,
|
||||||
"maintenanceMode": false,
|
"maintenanceMode": false,
|
||||||
|
"messageBanner": false,
|
||||||
"networkView": false,
|
"networkView": false,
|
||||||
"proxyReturnAllToggles": false,
|
"proxyReturnAllToggles": false,
|
||||||
"responseTimeWithAppName": false,
|
"responseTimeWithAppName": false,
|
||||||
@ -93,6 +94,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxyFrontend": true,
|
"embedProxyFrontend": true,
|
||||||
"maintenance": false,
|
"maintenance": false,
|
||||||
"maintenanceMode": false,
|
"maintenanceMode": false,
|
||||||
|
"messageBanner": false,
|
||||||
"networkView": false,
|
"networkView": false,
|
||||||
"proxyReturnAllToggles": false,
|
"proxyReturnAllToggles": false,
|
||||||
"responseTimeWithAppName": false,
|
"responseTimeWithAppName": false,
|
||||||
|
@ -47,6 +47,10 @@ const flags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_MAINTENANCE_MODE,
|
process.env.UNLEASH_EXPERIMENTAL_MAINTENANCE_MODE,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
messageBanner: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_MESSAGE_BANNER,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
Loading…
Reference in New Issue
Block a user