Skip to content

Commit 3af7c25

Browse files
AbdelHedhiliTheMaskedTurtleTristan-WorkGH
authored
Add announcement banner (#751)
Signed-off-by: Abdelsalem <[email protected]> Co-authored-by: Abdelsalem <[email protected]> Co-authored-by: Joris Mancini <[email protected]> Signed-off-by: Tristan Chuine <[email protected]> Co-authored-by: Tristan Chuine <[email protected]>
1 parent 0b5f930 commit 3af7c25

File tree

16 files changed

+366
-130
lines changed

16 files changed

+366
-130
lines changed

demo/src/app.jsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -869,9 +869,7 @@ function AppContent({ language, onLanguageClick }) {
869869
onEquipmentLabellingClick={handleEquipmentLabellingClick}
870870
equipmentLabelling={equipmentLabelling}
871871
withElementsSearch
872-
searchingLabel={intl.formatMessage({
873-
id: 'equipment_search/label',
874-
})}
872+
searchingLabel={intl.formatMessage({ id: 'equipment_search/label' })}
875873
onSearchTermChange={searchMatchingEquipments}
876874
onSelectionChange={displayEquipment}
877875
searchDisabled={searchDisabled}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright © 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
import type { UUID } from 'crypto';
8+
import { type PropsWithChildren, type ReactNode, useCallback, useEffect, useState } from 'react';
9+
import {
10+
Alert,
11+
type AlertColor,
12+
type AlertProps,
13+
AlertTitle,
14+
Collapse,
15+
type SxProps,
16+
type Theme,
17+
Tooltip,
18+
useTheme,
19+
} from '@mui/material';
20+
import { Campaign as CampaignIcon } from '@mui/icons-material';
21+
import type { User } from 'oidc-client';
22+
import { AnnouncementSeverity } from '../../utils/types';
23+
24+
// Pick the same color than Snackbar
25+
const snackbarInfo = '#2196f3';
26+
const snackbarWarning = '#ff9800';
27+
// const snackbarError = '#d32f2f';
28+
29+
const styles = {
30+
alert: (theme) => ({
31+
'&.MuiAlert-colorWarning': {
32+
color: theme.palette.getContrastText(snackbarWarning),
33+
backgroundColor: snackbarWarning,
34+
},
35+
'&.MuiAlert-colorInfo': {
36+
color: theme.palette.getContrastText(snackbarInfo),
37+
backgroundColor: snackbarInfo,
38+
},
39+
}),
40+
} as const satisfies Record<string, SxProps<Theme>>;
41+
42+
export type AnnouncementBannerProps = PropsWithChildren<{
43+
// message only visible if user logged
44+
user?: User | {};
45+
/** only field used to detect if msg change */
46+
id?: UUID;
47+
duration?: number;
48+
severity?: AnnouncementSeverity;
49+
title?: ReactNode;
50+
sx?: AlertProps['sx'];
51+
}>;
52+
53+
const iconMapping: AlertProps['iconMapping'] = {
54+
info: <CampaignIcon />,
55+
};
56+
57+
function convertSeverity(severity: AnnouncementSeverity): AlertColor | undefined {
58+
switch (severity) {
59+
case AnnouncementSeverity.INFO:
60+
return 'info';
61+
case AnnouncementSeverity.WARN:
62+
return 'warning';
63+
default:
64+
return undefined;
65+
}
66+
}
67+
68+
export function AnnouncementBanner({
69+
user,
70+
id,
71+
severity = AnnouncementSeverity.INFO,
72+
title,
73+
children,
74+
duration,
75+
sx,
76+
}: Readonly<AnnouncementBannerProps>) {
77+
const theme = useTheme();
78+
const [visible, setVisible] = useState(false);
79+
80+
useEffect(
81+
() => {
82+
// If the message to show changed
83+
if (id) {
84+
setVisible(true);
85+
if (duration) {
86+
// the previous timer is normally already cleared
87+
const bannerTimer = setTimeout(() => {
88+
setVisible(false);
89+
}, duration);
90+
return () => {
91+
clearTimeout(bannerTimer); // clear previous timer
92+
};
93+
}
94+
} else {
95+
setVisible(false);
96+
}
97+
return () => {};
98+
},
99+
// eslint-disable-next-line react-hooks/exhaustive-deps -- we check msg id in cas of an announcement
100+
[id]
101+
);
102+
103+
const handleClose = useCallback(() => setVisible(false), []);
104+
105+
return (
106+
<Collapse in={user !== undefined && visible} unmountOnExit sx={sx} style={{ margin: theme.spacing(1) }}>
107+
<Alert
108+
variant="filled"
109+
elevation={0}
110+
severity={convertSeverity(severity)}
111+
onClose={handleClose}
112+
iconMapping={iconMapping}
113+
hidden={!visible}
114+
className={title ? undefined : 'no-title'}
115+
sx={styles.alert}
116+
>
117+
{title && <AlertTitle>{title}</AlertTitle>}
118+
<Tooltip title={children} placement="bottom">
119+
<span>{children}</span>
120+
</Tooltip>
121+
</Alert>
122+
</Collapse>
123+
);
124+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright © 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
import type { User } from 'oidc-client';
9+
import { useGlobalAnnouncement } from './useGlobalAnnouncement';
10+
import { AnnouncementBanner, type AnnouncementBannerProps } from './AnnouncementBanner';
11+
12+
export type AnnouncementNotificationProps = {
13+
user: User | null;
14+
sx?: AnnouncementBannerProps['sx'];
15+
};
16+
17+
export function AnnouncementNotification({ user, sx }: Readonly<AnnouncementNotificationProps>) {
18+
const { id, severity, message, duration } = useGlobalAnnouncement(user) ?? {};
19+
return (
20+
<AnnouncementBanner user={user ?? undefined} id={id} severity={severity} duration={duration} sx={sx}>
21+
{message}
22+
</AnnouncementBanner>
23+
);
24+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright © 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
export * from './AnnouncementBanner';
8+
export * from './AnnouncementNotification';
9+
export * from './useGlobalAnnouncement';
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright © 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
import type { UUID } from 'crypto';
8+
import { v4 } from 'uuid';
9+
import { useCallback, useEffect, useState } from 'react';
10+
import type { User } from 'oidc-client';
11+
import { fetchCurrentAnnouncement } from '../../services/userAdmin';
12+
import { NotificationsUrlKeys } from '../../utils/constants/notificationsProvider';
13+
import { useNotificationsListener } from '../notifications/hooks/useNotificationsListener';
14+
import type { AnnouncementSeverity } from '../../utils/types';
15+
16+
export type AnnouncementProps = {
17+
id: UUID; // keep the id to refresh the component when the message is updated
18+
message: string;
19+
duration: number;
20+
severity: AnnouncementSeverity;
21+
};
22+
23+
export function useGlobalAnnouncement(user: User | null) {
24+
const [announcementInfos, setAnnouncementInfos] = useState<AnnouncementProps>();
25+
26+
useEffect(() => {
27+
if (user) {
28+
fetchCurrentAnnouncement()
29+
.then((announcementDto) => {
30+
if (announcementDto) {
31+
setAnnouncementInfos({
32+
id: announcementDto.id,
33+
message: announcementDto.message,
34+
duration: announcementDto.remainingDuration,
35+
severity: announcementDto.severity,
36+
});
37+
} else {
38+
setAnnouncementInfos(undefined); // no message currently shown
39+
// TODO: is events still coming even if user disconnect by token expiration? if yes then no need for this "else" case
40+
}
41+
})
42+
.catch((error: unknown) => console.error('Failed to retrieve global announcement:', error));
43+
} else {
44+
setAnnouncementInfos(undefined); // user disconnected
45+
}
46+
}, [user]);
47+
48+
const handleAnnouncementMessage = useCallback((event: MessageEvent) => {
49+
const eventData = JSON.parse(event.data);
50+
if (eventData.headers.messageType === 'announcement') {
51+
const announcement: AnnouncementProps = {
52+
id: eventData.id ?? v4(),
53+
message: eventData.payload,
54+
severity: eventData.headers.severity,
55+
duration: eventData.headers.duration,
56+
};
57+
console.debug('announcement incoming:', announcement);
58+
setAnnouncementInfos(announcement);
59+
} else if (eventData.headers.messageType === 'cancelAnnouncement') {
60+
setAnnouncementInfos(undefined);
61+
}
62+
}, []);
63+
64+
useNotificationsListener(NotificationsUrlKeys.GLOBAL_CONFIG, {
65+
listenerCallbackMessage: handleAnnouncementMessage,
66+
});
67+
68+
return announcementInfos;
69+
}

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* License, v. 2.0. If a copy of the MPL was not distributed with this
55
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
66
*/
7+
export * from './announcement';
78
export * from './authentication';
89
export * from './cardErrorBoundary';
910
export * from './checkBoxList';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Copyright (c) 2025, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
import { useCallback, useState } from 'react';
8+
import { Alert, Collapse, type SxProps, type Theme, Typography } from '@mui/material';
9+
import { FormattedMessage } from 'react-intl';
10+
11+
const styles = {
12+
alert: {
13+
'& .MuiAlert-colorWarning': {
14+
backgroundColor: '#f6b26b',
15+
color: 'black',
16+
'& .MuiAlert-action .MuiIconButton-root:hover': {
17+
backgroundColor: '#e39648',
18+
},
19+
'& .MuiAlert-icon': {
20+
color: 'red',
21+
},
22+
},
23+
// MUI IconButton: square ripple
24+
'& .MuiAlert-action .MuiIconButton-root': {
25+
borderRadius: 1,
26+
'& .MuiTouchRipple-root .MuiTouchRipple-child ': {
27+
borderRadius: '8px',
28+
},
29+
},
30+
},
31+
} as const satisfies Record<string, SxProps<Theme>>;
32+
33+
export function DevModeBanner() {
34+
const [visible, setVisible] = useState(true);
35+
36+
return (
37+
<Collapse in={visible} sx={styles.alert}>
38+
<Alert
39+
square
40+
severity="warning"
41+
variant="filled"
42+
elevation={0}
43+
onClose={useCallback(() => setVisible(false), [])}
44+
>
45+
<Typography variant="body1">
46+
<FormattedMessage id="top-bar/developerModeWarning" defaultMessage="Developer mode" />
47+
</Typography>
48+
</Alert>
49+
</Collapse>
50+
);
51+
}

src/components/topBar/MessageBanner.tsx

Lines changed: 0 additions & 73 deletions
This file was deleted.

0 commit comments

Comments
 (0)