addons/isl/src/TopLevelAlert.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ReactNode} from 'react';
b69ab319import type {Alert, AlertSeverity} from './types';
b69ab3110
b69ab3111import * as stylex from '@stylexjs/stylex';
b69ab3112import {Banner, BannerKind} from 'isl-components/Banner';
b69ab3113import {Button} from 'isl-components/Button';
b69ab3114import {Icon} from 'isl-components/Icon';
b69ab3115import {Subtle} from 'isl-components/Subtle';
b69ab3116import {atom, useAtom, useAtomValue} from 'jotai';
b69ab3117import {useEffect} from 'react';
b69ab3118import {colors, font, radius, spacing} from '../../components/theme/tokens.stylex';
b69ab3119import serverAPI from './ClientToServerAPI';
b69ab3120import {Link} from './Link';
b69ab3121import {tracker} from './analytics';
b69ab3122import {T} from './i18n';
b69ab3123import {localStorageBackedAtom, writeAtom} from './jotaiUtils';
b69ab3124import {applicationinfo} from './serverAPIState';
b69ab3125import {layout} from './stylexUtils';
b69ab3126
b69ab3127const dismissedAlerts = localStorageBackedAtom<{[key: string]: boolean}>(
b69ab3128 'isl.dismissed-alerts',
b69ab3129 {},
b69ab3130);
b69ab3131
b69ab3132const activeAlerts = atom<Array<Alert>>([]);
b69ab3133
b69ab3134const ALERT_FETCH_INTERVAL_MS = 5 * 60 * 1000;
b69ab3135
b69ab3136const alertsAlreadyLogged = new Set<string>();
b69ab3137
b69ab3138serverAPI.onMessageOfType('fetchedActiveAlerts', event => {
b69ab3139 writeAtom(activeAlerts, event.alerts);
b69ab3140});
b69ab3141serverAPI.onSetup(() => {
b69ab3142 const fetchAlerts = () =>
b69ab3143 serverAPI.postMessage({
b69ab3144 type: 'fetchActiveAlerts',
b69ab3145 });
b69ab3146 const interval = setInterval(fetchAlerts, ALERT_FETCH_INTERVAL_MS);
b69ab3147 fetchAlerts();
b69ab3148 return () => clearInterval(interval);
b69ab3149});
b69ab3150
b69ab3151export function TopLevelAlerts() {
b69ab3152 const [dismissed, setDismissed] = useAtom(dismissedAlerts);
b69ab3153 const alerts = useAtomValue(activeAlerts);
b69ab3154 const info = useAtomValue(applicationinfo);
b69ab3155 const version = info?.version;
b69ab3156
b69ab3157 useEffect(() => {
b69ab3158 for (const {key} of alerts) {
b69ab3159 if (!alertsAlreadyLogged.has(key)) {
b69ab3160 tracker.track('AlertShown', {extras: {key}});
b69ab3161 alertsAlreadyLogged.add(key);
b69ab3162 }
b69ab3163 }
b69ab3164 }, [alerts]);
b69ab3165
b69ab3166 return (
b69ab3167 <div>
b69ab3168 {alerts
b69ab3169 .filter(
b69ab3170 alert =>
b69ab3171 dismissed[alert.key] !== true &&
b69ab3172 (alert['isl-version-regex'] == null ||
b69ab3173 (version != null && new RegExp(alert['isl-version-regex']).test(version))),
b69ab3174 )
b69ab3175 .map((alert, i) => (
b69ab3176 <TopLevelAlert
b69ab3177 alert={alert}
b69ab3178 key={i}
b69ab3179 onDismiss={() => {
b69ab3180 setDismissed(old => ({...old, [alert.key]: true}));
b69ab3181 tracker.track('AlertDismissed', {extras: {key: alert.key}});
b69ab3182 }}
b69ab3183 />
b69ab3184 ))}
b69ab3185 </div>
b69ab3186 );
b69ab3187}
b69ab3188
b69ab3189const styles = stylex.create({
b69ab3190 alertContainer: {
b69ab3191 margin: spacing.pad,
b69ab3192 position: 'relative',
b69ab3193 },
b69ab3194 alert: {
b69ab3195 fontSize: font.bigger,
b69ab3196 padding: spacing.pad,
b69ab3197 gap: spacing.half,
b69ab3198 alignItems: 'flex-start',
b69ab3199 },
b69ab31100 alertContent: {
b69ab31101 verticalAlign: 'center',
b69ab31102 gap: spacing.half,
b69ab31103 fontWeight: 'bold',
b69ab31104 },
b69ab31105 sev: {
b69ab31106 color: 'white',
b69ab31107 paddingInline: spacing.half,
b69ab31108 paddingBlock: spacing.quarter,
b69ab31109 borderRadius: radius.small,
b69ab31110 fontSize: font.small,
b69ab31111 },
b69ab31112 dismissX: {
b69ab31113 position: 'absolute',
b69ab31114 right: spacing.double,
b69ab31115 top: spacing.pad,
b69ab31116 },
b69ab31117 'SEV 0': {backgroundColor: colors.purple},
b69ab31118 'SEV 1': {backgroundColor: colors.red},
b69ab31119 'SEV 2': {backgroundColor: colors.orange},
b69ab31120 'SEV 3': {backgroundColor: colors.blue},
b69ab31121 'SEV 4': {backgroundColor: colors.grey},
b69ab31122 UBN: {backgroundColor: colors.purple},
b69ab31123});
b69ab31124
b69ab31125function SevBadge({children, severity}: {children: ReactNode; severity: AlertSeverity}) {
b69ab31126 return <span {...stylex.props(styles.sev, styles[severity])}>{children}</span>;
b69ab31127}
b69ab31128
b69ab31129function TopLevelAlert({alert, onDismiss}: {alert: Alert; onDismiss: () => unknown}) {
b69ab31130 const {title, description, url, severity} = alert;
b69ab31131 return (
b69ab31132 <div {...stylex.props(styles.alertContainer)}>
b69ab31133 <Banner kind={BannerKind.default} icon={<Icon icon="flame" size="M" color="red" />}>
b69ab31134 <div {...stylex.props(layout.flexCol, styles.alert)}>
b69ab31135 <div {...stylex.props(styles.dismissX)}>
b69ab31136 <Button onClick={onDismiss} data-testid="dismiss-alert">
b69ab31137 <Icon icon="x" />
b69ab31138 </Button>
b69ab31139 </div>
b69ab31140 <b>
b69ab31141 <T>Ongoing Issue</T>
b69ab31142 </b>
b69ab31143 <span {...stylex.props(layout.flexRow, styles.alertContent)}>
b69ab31144 <SevBadge severity={severity}>{severity}</SevBadge> <Link href={url}>{title}</Link>
b69ab31145 <Icon icon="link-external" />
b69ab31146 </span>
b69ab31147 <Subtle>{description}</Subtle>
b69ab31148 </div>
b69ab31149 </Banner>
b69ab31150 </div>
b69ab31151 );
b69ab31152}