mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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