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' | ||||
|     | 'change_request' | ||||
|     | 'favorite' | ||||
|     | 'maintenance'; | ||||
|     | 'maintenance' | ||||
|     | 'message_banner'; | ||||
| 
 | ||||
| export const usePlausibleTracker = () => { | ||||
|     const plausible = useContext(PlausibleContext); | ||||
|  | ||||
| @ -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> | ||||
| @ -22,6 +23,7 @@ ReactDOM.render( | ||||
|                     <AnnouncerProvider> | ||||
|                         <FeedbackCESProvider> | ||||
|                             <InstanceStatus> | ||||
|                                 <MessageBanner /> | ||||
|                                 <ScrollTop /> | ||||
|                                 <App /> | ||||
|                             </InstanceStatus> | ||||
|  | ||||
| @ -46,6 +46,7 @@ export interface IFlags { | ||||
|     variantsPerEnvironment?: boolean; | ||||
|     networkView?: boolean; | ||||
|     maintenance?: boolean; | ||||
|     messageBanner?: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface IVersionInfo { | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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 = { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user