Skip to content

Commit 79e3b85

Browse files
authored
feat: add a notification banner to display for first time users (#3396)
* WIP: set basics for the Notification banner * Revert to previous functionality * Use v8 styling construct and a MessageBar * Use fluent v9 and get messages for notification from GE.json * Add fluent v9 components dependencies * Update styles and components for Notification * Add text for notification * Hook theming of v9 provider to prop value * Track the tutorial link with telemetry * Add slice banner * Persist banner state to localstorage * Update state usage of banner state * Use local storage to track banner visibility * Add the background and custom types * Fix theming and sizing of text in the body * Handle dismissing of the banner * Lock the v9 packages version * Update the banner notification link * Enhance type safety for trackReactComponent method * Add telemetry tracking for Notification component * Refactor Notification import and update App component structure * Add telemetry tracking for notification dismiss button * fix: correct typo in notification link text * fix: type in notification message
1 parent d239f8f commit 79e3b85

File tree

15 files changed

+1850
-114
lines changed

15 files changed

+1850
-114
lines changed

package-lock.json

Lines changed: 1558 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"dependencies": {
66
"@augloop/types-core": "file:packages/types-core-2.16.189.tgz",
77
"@axe-core/webdriverjs": "4.10.0",
8+
"@fluentui/react-components": "9.55.1",
89
"@azure/msal-browser": "3.26.1",
910
"@babel/core": "7.26.0",
1011
"@babel/runtime": "7.26.0",

src/app/middleware/localStorageMiddleware.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { CURRENT_THEME } from '../services/graph-constants';
77
import { getUniquePaths } from '../services/reducers/collections-reducer.util';
88
import {
99
CHANGE_THEME_SUCCESS, COLLECTION_CREATE_SUCCESS,
10-
RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS, SAMPLES_FETCH_SUCCESS
10+
RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS,
11+
SAMPLES_FETCH_SUCCESS
1112
} from '../services/redux-constants';
1213
import { saveToLocalStorage } from '../utils/local-storage';
1314

src/app/services/graph-constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ export const ADMIN_CONSENT_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/s
3232
// eslint-disable-next-line max-len
3333
export const CONSENT_TYPE_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/api/resources/oauth2permissiongrant?view=graph-rest-1.0#:~:text=(eq%20only).-,consentType,-String'
3434
export const CURRENT_THEME='CURRENT_THEME';
35-
export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas';
35+
export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas'
36+
export const BANNER_IS_VISIBLE = 'bannerIsVisible';

src/app/services/redux-constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,4 @@ export const REVOKE_SCOPES_PENDING = 'auth/revokeScopes/pending';
5656
export const REVOKE_SCOPES_SUCCESS = 'auth/revokeScopes/fulfilled';
5757
export const REVOKE_SCOPES_ERROR = 'auth/revokeScopes/rejected';
5858
export const COLLECTION_CREATE_SUCCESS = 'collections/createCollection';
59+
export const SET_BANNER_STATE = 'banner/setBannerState';

src/app/views/App.tsx

Lines changed: 103 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Announced, getTheme, ITheme, styled } from '@fluentui/react';
2+
import { FluentProvider, teamsHighContrastTheme, Theme, webDarkTheme, webLightTheme } from '@fluentui/react-components';
23
import { bindActionCreators, Dispatch } from '@reduxjs/toolkit';
34
import { Resizable } from 're-resizable';
45
import { Component } from 'react';
@@ -31,6 +32,7 @@ import { StatusMessages, TermsOfUseMessage } from './app-sections';
3132
import { headerMessaging } from './app-sections/HeaderMessaging';
3233
import { appStyles } from './App.styles';
3334
import { classNames } from './classnames';
35+
import Notification from './common/banners/Notification';
3436
import { KeyboardCopyEvent } from './common/copy-button/KeyboardCopyEvent';
3537
import PopupsWrapper from './common/popups/PopupsWrapper';
3638
import { createShareLink } from './common/share';
@@ -43,6 +45,7 @@ export interface IAppProps {
4345
theme?: ITheme;
4446
styles?: object;
4547
profile: object;
48+
appTheme: string;
4649
graphExplorerMode: Mode;
4750
sidebarProperties: ISidebarProps;
4851
sampleQuery: IQuery;
@@ -404,99 +407,113 @@ class App extends Component<IAppProps, IAppState> {
404407
this.removeFlexBasisProperty();
405408
this.removeSidebarHeightProperty();
406409

410+
const fluentV9Themes: Record<string, Theme>= {
411+
'light': webLightTheme,
412+
'dark': webDarkTheme,
413+
'high-contrast': teamsHighContrastTheme
414+
}
407415
return (
408416
// @ts-ignore
409-
<ThemeContext.Provider value={this.props.appTheme}>
410-
<PopupsProvider>
411-
<div className={`ms-Grid ${classes.app}`} style={{ paddingLeft: mobileScreen && '15px' }}>
412-
<MainHeader
413-
toggleSidebar={this.toggleSidebar}
414-
/>
415-
<Announced
416-
message={
417-
!showSidebar
418-
? translateMessage('Sidebar minimized')
419-
: translateMessage('Sidebar maximized')
420-
}
421-
/>
422-
<div className={`ms-Grid-row ${classes.appRow}`} style={{
423-
flexWrap: mobileScreen && 'wrap',
424-
marginRight: showSidebar || (graphExplorerMode === Mode.TryIt) && '-20px',
425-
flexDirection: (graphExplorerMode === Mode.TryIt) ? 'column' : 'row'
426-
}}>
427-
{graphExplorerMode === Mode.Complete && (
428-
<Resizable
429-
onResize={(e: any, direction: any, ref: any) => {
430-
if (ref?.style?.width) {
431-
this.resizeSideBar(ref.style.width);
432-
}
433-
}}
434-
className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${sidebarWidth} resizable-sidebar`}
435-
minWidth={'71'}
436-
maxWidth={maxWidth}
437-
enable={{
438-
right: true
439-
}}
440-
handleClasses={{
441-
right: classes.vResizeHandle
442-
}}
443-
bounds={'parent'}
444-
size={{
445-
width: sideWidth,
446-
height: ''
447-
}}
448-
>
449-
<Sidebar currentTab={this.state.sidebarTabSelection}
450-
setSidebarTabSelection={this.setSidebarTabSelection} showSidebar={showSidebar}
451-
toggleSidebar={this.toggleSidebar}
452-
mobileScreen={mobileScreen} />
453-
</Resizable>
454-
)}
455-
{graphExplorerMode === Mode.TryIt &&
417+
<FluentProvider theme={fluentV9Themes[this.props.appTheme]}>
418+
<ThemeContext.Provider value={this.props.appTheme}>
419+
<PopupsProvider>
420+
<div className={`ms-Grid ${classes.app}`} style={{ paddingLeft: mobileScreen && '15px' }}>
421+
<MainHeader
422+
toggleSidebar={this.toggleSidebar}
423+
/>
424+
<Announced
425+
message={
426+
!showSidebar
427+
? translateMessage('Sidebar minimized')
428+
: translateMessage('Sidebar maximized')
429+
}
430+
/>
431+
<div className={`ms-Grid-row ${classes.appRow}`} style={{
432+
flexWrap: mobileScreen && 'wrap',
433+
marginRight: showSidebar || (graphExplorerMode === Mode.TryIt) && '-20px',
434+
flexDirection: (graphExplorerMode === Mode.TryIt) ? 'column' : 'row'
435+
}}>
436+
{graphExplorerMode === Mode.Complete && (
437+
<Resizable
438+
onResize={(e: any, direction: any, ref: any) => {
439+
if (ref?.style?.width) {
440+
this.resizeSideBar(ref.style.width);
441+
}
442+
}}
443+
className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${sidebarWidth} resizable-sidebar`}
444+
minWidth={'71'}
445+
maxWidth={maxWidth}
446+
enable={{
447+
right: true
448+
}}
449+
handleClasses={{
450+
right: classes.vResizeHandle
451+
}}
452+
bounds={'parent'}
453+
size={{
454+
width: sideWidth,
455+
height: ''
456+
}}
457+
>
458+
<Sidebar currentTab={this.state.sidebarTabSelection}
459+
setSidebarTabSelection={this.setSidebarTabSelection} showSidebar={showSidebar}
460+
toggleSidebar={this.toggleSidebar}
461+
mobileScreen={mobileScreen} />
462+
</Resizable>
463+
)}
464+
{graphExplorerMode === Mode.TryIt &&
456465
headerMessaging(query)}
457466

458-
{displayContent && (
459-
<Resizable
460-
bounds={'window'}
461-
className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${layout}`}
462-
enable={{
463-
right: false
464-
}}
465-
size={{
466-
width: graphExplorerMode === Mode.TryIt ? '100%' : contentWidth,
467-
height: ''
468-
}}
469-
style={!sidebarProperties.showSidebar && !mobileScreen ? {
470-
marginLeft: '8px', display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
471-
} : {
472-
display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
473-
}}
474-
>
475-
<ValidationProvider>
476-
<div style={{ marginBottom: 2 }} >
477-
<QueryRunner onSelectVerb={this.handleSelectVerb} />
478-
</div>
479-
<div style={{
467+
{displayContent && (
468+
<Resizable
469+
bounds={'window'}
470+
className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${layout}`}
471+
enable={{
472+
right: false
473+
}}
474+
size={{
475+
width: graphExplorerMode === Mode.TryIt ? '100%' : contentWidth,
476+
height: ''
477+
}}
478+
style={!sidebarProperties.showSidebar && !mobileScreen ? {
479+
marginLeft: '8px', display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
480+
} : {
480481
display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
481-
}}>
482-
<div style={mobileScreen ? this.statusAreaMobileStyle : this.statusAreaFullScreenStyle}>
483-
<StatusMessages />
484-
</div>
485-
<QueryResponse />
482+
}}
483+
>
484+
<div className='ms-Grid-row'>
485+
<Notification
486+
header={translateMessage('Banner notification 1 header')}
487+
content={translateMessage('Banner notification 1 content')}
488+
link={translateMessage('Banner notification 1 link')}
489+
linkText={translateMessage('Banner notification 1 link text')}/>
486490
</div>
487-
</ValidationProvider>
488-
</Resizable>
489-
)}
490-
</div>
491-
<div style={mobileScreen ? this.statusAreaMobileStyle : this.statusAreaFullScreenStyle}>
492-
<TermsOfUseMessage />
491+
<ValidationProvider>
492+
<div style={{ marginBottom: 2 }} >
493+
<QueryRunner onSelectVerb={this.handleSelectVerb} />
494+
</div>
495+
<div style={{
496+
display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
497+
}}>
498+
<div style={mobileScreen ? this.statusAreaMobileStyle : this.statusAreaFullScreenStyle}>
499+
<StatusMessages />
500+
</div>
501+
<QueryResponse />
502+
</div>
503+
</ValidationProvider>
504+
</Resizable>
505+
)}
506+
</div>
507+
<div style={mobileScreen ? this.statusAreaMobileStyle : this.statusAreaFullScreenStyle}>
508+
<TermsOfUseMessage />
509+
</div>
493510
</div>
494-
</div>
495-
<CollectionPermissionsProvider>
496-
<PopupsWrapper />
497-
</CollectionPermissionsProvider>
498-
</PopupsProvider>
499-
</ThemeContext.Provider>
511+
<CollectionPermissionsProvider>
512+
<PopupsWrapper />
513+
</CollectionPermissionsProvider>
514+
</PopupsProvider>
515+
</ThemeContext.Provider>
516+
</FluentProvider>
500517
);
501518
}
502519
}

src/app/views/classnames.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ interface IClassNames {
99
export function classNames({ styles, theme }: IClassNames): any {
1010
const getClassNames = classNamesFunction();
1111
return getClassNames(styles, theme);
12-
}
12+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { makeStyles } from '@fluentui/react-components';
2+
import polygons from './bgPolygons.svg';
3+
4+
export const useNotificationStyles = makeStyles({
5+
container: {
6+
padding: '8px',
7+
marginBottom: '8px',
8+
backgroundImage: `url(${polygons})`,
9+
backgroundRepeat: 'no-repeat',
10+
backgroundSize: 'contain',
11+
backgroundPosition: 'right',
12+
'&light': {
13+
backgroundColor: '#E8EFFF',
14+
color: '#000000'
15+
},
16+
'&.dark': {
17+
backgroundColor: '#1D202A',
18+
color: '#ffffff'
19+
},
20+
'&.highContrast': {
21+
backgroundColor: '#0C3B5E',
22+
color: '#ffffff'
23+
}
24+
},
25+
body: {
26+
width: '100%',
27+
'@media (min-width: 720px)': {
28+
width: '70%'
29+
}
30+
}
31+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
Button,
3+
Link,
4+
MessageBar,
5+
MessageBarActions,
6+
MessageBarBody,
7+
MessageBarTitle
8+
} from '@fluentui/react-components';
9+
import { DismissRegular, OpenRegular } from '@fluentui/react-icons';
10+
import { useState } from 'react';
11+
import { useAppSelector } from '../../../../store';
12+
import { componentNames, eventTypes, telemetry } from '../../../../telemetry';
13+
import { BANNER_IS_VISIBLE } from '../../../services/graph-constants';
14+
import { translateMessage } from '../../../utils/translate-messages';
15+
import { useNotificationStyles } from './Notification.styles';
16+
17+
interface NotificationProps {
18+
header: string;
19+
content: string;
20+
link: string;
21+
linkText: string;
22+
}
23+
24+
const handleOnClickLink = (e: React.MouseEvent<HTMLAnchorElement>)=>{
25+
telemetry.trackLinkClickEvent(
26+
(e.currentTarget as HTMLAnchorElement).href, componentNames.GRAPH_EXPLORER_TUTORIAL_LINK)
27+
}
28+
29+
const Notification: React.FunctionComponent<NotificationProps> = (props: NotificationProps) => {
30+
const styles = useNotificationStyles();
31+
const storageBanner = localStorage.getItem(BANNER_IS_VISIBLE);
32+
const [isVisible, setIsVisible] = useState(storageBanner === null || storageBanner === 'true');
33+
const theme = useAppSelector(s => s.theme);
34+
35+
const handleDismiss = () => {
36+
telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, {
37+
ComponentName: componentNames.NOTIFICATION_BANNER_DISMISS_BUTTON
38+
});
39+
localStorage.setItem(BANNER_IS_VISIBLE, 'false');
40+
setIsVisible(false);
41+
};
42+
43+
if (!isVisible) {
44+
return null;
45+
}
46+
47+
return (
48+
<MessageBar className={`${styles.container} ${theme}`} icon={''}>
49+
<MessageBarBody className={styles.body}>
50+
<MessageBarTitle>{props.header}</MessageBarTitle><br></br>
51+
{props.content}{' '}
52+
<Link
53+
onClick={handleOnClickLink}
54+
href={props.link}
55+
target='_blank'>{props.linkText} <OpenRegular /></Link>
56+
</MessageBarBody>
57+
<MessageBarActions
58+
containerAction={
59+
<Button
60+
onClick={handleDismiss}
61+
aria-label={translateMessage('Dismiss banner')}
62+
appearance="transparent"
63+
icon={<DismissRegular />}
64+
/>
65+
}
66+
/>
67+
</MessageBar>
68+
);
69+
};
70+
71+
export default telemetry.trackReactComponent(Notification, componentNames.NOTIFICATION_COMPONENT)

0 commit comments

Comments
 (0)