4.5 KB153 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {ReactNode} from 'react';
9import type {Alert, AlertSeverity} from './types';
10
11import * as stylex from '@stylexjs/stylex';
12import {Banner, BannerKind} from 'isl-components/Banner';
13import {Button} from 'isl-components/Button';
14import {Icon} from 'isl-components/Icon';
15import {Subtle} from 'isl-components/Subtle';
16import {atom, useAtom, useAtomValue} from 'jotai';
17import {useEffect} from 'react';
18import {colors, font, radius, spacing} from '../../components/theme/tokens.stylex';
19import serverAPI from './ClientToServerAPI';
20import {Link} from './Link';
21import {tracker} from './analytics';
22import {T} from './i18n';
23import {localStorageBackedAtom, writeAtom} from './jotaiUtils';
24import {applicationinfo} from './serverAPIState';
25import {layout} from './stylexUtils';
26
27const dismissedAlerts = localStorageBackedAtom<{[key: string]: boolean}>(
28 'isl.dismissed-alerts',
29 {},
30);
31
32const activeAlerts = atom<Array<Alert>>([]);
33
34const ALERT_FETCH_INTERVAL_MS = 5 * 60 * 1000;
35
36const alertsAlreadyLogged = new Set<string>();
37
38serverAPI.onMessageOfType('fetchedActiveAlerts', event => {
39 writeAtom(activeAlerts, event.alerts);
40});
41serverAPI.onSetup(() => {
42 const fetchAlerts = () =>
43 serverAPI.postMessage({
44 type: 'fetchActiveAlerts',
45 });
46 const interval = setInterval(fetchAlerts, ALERT_FETCH_INTERVAL_MS);
47 fetchAlerts();
48 return () => clearInterval(interval);
49});
50
51export function TopLevelAlerts() {
52 const [dismissed, setDismissed] = useAtom(dismissedAlerts);
53 const alerts = useAtomValue(activeAlerts);
54 const info = useAtomValue(applicationinfo);
55 const version = info?.version;
56
57 useEffect(() => {
58 for (const {key} of alerts) {
59 if (!alertsAlreadyLogged.has(key)) {
60 tracker.track('AlertShown', {extras: {key}});
61 alertsAlreadyLogged.add(key);
62 }
63 }
64 }, [alerts]);
65
66 return (
67 <div>
68 {alerts
69 .filter(
70 alert =>
71 dismissed[alert.key] !== true &&
72 (alert['isl-version-regex'] == null ||
73 (version != null && new RegExp(alert['isl-version-regex']).test(version))),
74 )
75 .map((alert, i) => (
76 <TopLevelAlert
77 alert={alert}
78 key={i}
79 onDismiss={() => {
80 setDismissed(old => ({...old, [alert.key]: true}));
81 tracker.track('AlertDismissed', {extras: {key: alert.key}});
82 }}
83 />
84 ))}
85 </div>
86 );
87}
88
89const styles = stylex.create({
90 alertContainer: {
91 margin: spacing.pad,
92 position: 'relative',
93 },
94 alert: {
95 fontSize: font.bigger,
96 padding: spacing.pad,
97 gap: spacing.half,
98 alignItems: 'flex-start',
99 },
100 alertContent: {
101 verticalAlign: 'center',
102 gap: spacing.half,
103 fontWeight: 'bold',
104 },
105 sev: {
106 color: 'white',
107 paddingInline: spacing.half,
108 paddingBlock: spacing.quarter,
109 borderRadius: radius.small,
110 fontSize: font.small,
111 },
112 dismissX: {
113 position: 'absolute',
114 right: spacing.double,
115 top: spacing.pad,
116 },
117 'SEV 0': {backgroundColor: colors.purple},
118 'SEV 1': {backgroundColor: colors.red},
119 'SEV 2': {backgroundColor: colors.orange},
120 'SEV 3': {backgroundColor: colors.blue},
121 'SEV 4': {backgroundColor: colors.grey},
122 UBN: {backgroundColor: colors.purple},
123});
124
125function SevBadge({children, severity}: {children: ReactNode; severity: AlertSeverity}) {
126 return <span {...stylex.props(styles.sev, styles[severity])}>{children}</span>;
127}
128
129function TopLevelAlert({alert, onDismiss}: {alert: Alert; onDismiss: () => unknown}) {
130 const {title, description, url, severity} = alert;
131 return (
132 <div {...stylex.props(styles.alertContainer)}>
133 <Banner kind={BannerKind.default} icon={<Icon icon="flame" size="M" color="red" />}>
134 <div {...stylex.props(layout.flexCol, styles.alert)}>
135 <div {...stylex.props(styles.dismissX)}>
136 <Button onClick={onDismiss} data-testid="dismiss-alert">
137 <Icon icon="x" />
138 </Button>
139 </div>
140 <b>
141 <T>Ongoing Issue</T>
142 </b>
143 <span {...stylex.props(layout.flexRow, styles.alertContent)}>
144 <SevBadge severity={severity}>{severity}</SevBadge> <Link href={url}>{title}</Link>
145 <Icon icon="link-external" />
146 </span>
147 <Subtle>{description}</Subtle>
148 </div>
149 </Banner>
150 </div>
151 );
152}
153