Skip to content

Commit

Permalink
feat: add a notification banner to display for first time users (#3396)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
musale authored Dec 2, 2024
1 parent d239f8f commit 79e3b85
Show file tree
Hide file tree
Showing 15 changed files with 1,850 additions and 114 deletions.
1,569 changes: 1,558 additions & 11 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@augloop/types-core": "file:packages/types-core-2.16.189.tgz",
"@axe-core/webdriverjs": "4.10.0",
"@fluentui/react-components": "9.55.1",
"@azure/msal-browser": "3.26.1",
"@babel/core": "7.26.0",
"@babel/runtime": "7.26.0",
Expand Down
3 changes: 2 additions & 1 deletion src/app/middleware/localStorageMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { CURRENT_THEME } from '../services/graph-constants';
import { getUniquePaths } from '../services/reducers/collections-reducer.util';
import {
CHANGE_THEME_SUCCESS, COLLECTION_CREATE_SUCCESS,
RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS, SAMPLES_FETCH_SUCCESS
RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS,
SAMPLES_FETCH_SUCCESS
} from '../services/redux-constants';
import { saveToLocalStorage } from '../utils/local-storage';

Expand Down
3 changes: 2 additions & 1 deletion src/app/services/graph-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export const ADMIN_CONSENT_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/s
// eslint-disable-next-line max-len
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'
export const CURRENT_THEME='CURRENT_THEME';
export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas';
export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas'
export const BANNER_IS_VISIBLE = 'bannerIsVisible';
1 change: 1 addition & 0 deletions src/app/services/redux-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ export const REVOKE_SCOPES_PENDING = 'auth/revokeScopes/pending';
export const REVOKE_SCOPES_SUCCESS = 'auth/revokeScopes/fulfilled';
export const REVOKE_SCOPES_ERROR = 'auth/revokeScopes/rejected';
export const COLLECTION_CREATE_SUCCESS = 'collections/createCollection';
export const SET_BANNER_STATE = 'banner/setBannerState';
189 changes: 103 additions & 86 deletions src/app/views/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Announced, getTheme, ITheme, styled } from '@fluentui/react';
import { FluentProvider, teamsHighContrastTheme, Theme, webDarkTheme, webLightTheme } from '@fluentui/react-components';
import { bindActionCreators, Dispatch } from '@reduxjs/toolkit';
import { Resizable } from 're-resizable';
import { Component } from 'react';
Expand Down Expand Up @@ -31,6 +32,7 @@ import { StatusMessages, TermsOfUseMessage } from './app-sections';
import { headerMessaging } from './app-sections/HeaderMessaging';
import { appStyles } from './App.styles';
import { classNames } from './classnames';
import Notification from './common/banners/Notification';
import { KeyboardCopyEvent } from './common/copy-button/KeyboardCopyEvent';
import PopupsWrapper from './common/popups/PopupsWrapper';
import { createShareLink } from './common/share';
Expand All @@ -43,6 +45,7 @@ export interface IAppProps {
theme?: ITheme;
styles?: object;
profile: object;
appTheme: string;
graphExplorerMode: Mode;
sidebarProperties: ISidebarProps;
sampleQuery: IQuery;
Expand Down Expand Up @@ -404,99 +407,113 @@ class App extends Component<IAppProps, IAppState> {
this.removeFlexBasisProperty();
this.removeSidebarHeightProperty();

const fluentV9Themes: Record<string, Theme>= {
'light': webLightTheme,
'dark': webDarkTheme,
'high-contrast': teamsHighContrastTheme
}
return (
// @ts-ignore
<ThemeContext.Provider value={this.props.appTheme}>
<PopupsProvider>
<div className={`ms-Grid ${classes.app}`} style={{ paddingLeft: mobileScreen && '15px' }}>
<MainHeader
toggleSidebar={this.toggleSidebar}
/>
<Announced
message={
!showSidebar
? translateMessage('Sidebar minimized')
: translateMessage('Sidebar maximized')
}
/>
<div className={`ms-Grid-row ${classes.appRow}`} style={{
flexWrap: mobileScreen && 'wrap',
marginRight: showSidebar || (graphExplorerMode === Mode.TryIt) && '-20px',
flexDirection: (graphExplorerMode === Mode.TryIt) ? 'column' : 'row'
}}>
{graphExplorerMode === Mode.Complete && (
<Resizable
onResize={(e: any, direction: any, ref: any) => {
if (ref?.style?.width) {
this.resizeSideBar(ref.style.width);
}
}}
className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${sidebarWidth} resizable-sidebar`}
minWidth={'71'}
maxWidth={maxWidth}
enable={{
right: true
}}
handleClasses={{
right: classes.vResizeHandle
}}
bounds={'parent'}
size={{
width: sideWidth,
height: ''
}}
>
<Sidebar currentTab={this.state.sidebarTabSelection}
setSidebarTabSelection={this.setSidebarTabSelection} showSidebar={showSidebar}
toggleSidebar={this.toggleSidebar}
mobileScreen={mobileScreen} />
</Resizable>
)}
{graphExplorerMode === Mode.TryIt &&
<FluentProvider theme={fluentV9Themes[this.props.appTheme]}>
<ThemeContext.Provider value={this.props.appTheme}>
<PopupsProvider>
<div className={`ms-Grid ${classes.app}`} style={{ paddingLeft: mobileScreen && '15px' }}>
<MainHeader
toggleSidebar={this.toggleSidebar}
/>
<Announced
message={
!showSidebar
? translateMessage('Sidebar minimized')
: translateMessage('Sidebar maximized')
}
/>
<div className={`ms-Grid-row ${classes.appRow}`} style={{
flexWrap: mobileScreen && 'wrap',
marginRight: showSidebar || (graphExplorerMode === Mode.TryIt) && '-20px',
flexDirection: (graphExplorerMode === Mode.TryIt) ? 'column' : 'row'
}}>
{graphExplorerMode === Mode.Complete && (
<Resizable
onResize={(e: any, direction: any, ref: any) => {
if (ref?.style?.width) {
this.resizeSideBar(ref.style.width);
}
}}
className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${sidebarWidth} resizable-sidebar`}
minWidth={'71'}
maxWidth={maxWidth}
enable={{
right: true
}}
handleClasses={{
right: classes.vResizeHandle
}}
bounds={'parent'}
size={{
width: sideWidth,
height: ''
}}
>
<Sidebar currentTab={this.state.sidebarTabSelection}
setSidebarTabSelection={this.setSidebarTabSelection} showSidebar={showSidebar}
toggleSidebar={this.toggleSidebar}
mobileScreen={mobileScreen} />
</Resizable>
)}
{graphExplorerMode === Mode.TryIt &&
headerMessaging(query)}

{displayContent && (
<Resizable
bounds={'window'}
className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${layout}`}
enable={{
right: false
}}
size={{
width: graphExplorerMode === Mode.TryIt ? '100%' : contentWidth,
height: ''
}}
style={!sidebarProperties.showSidebar && !mobileScreen ? {
marginLeft: '8px', display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
} : {
display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
}}
>
<ValidationProvider>
<div style={{ marginBottom: 2 }} >
<QueryRunner onSelectVerb={this.handleSelectVerb} />
</div>
<div style={{
{displayContent && (
<Resizable
bounds={'window'}
className={`ms-Grid-col ms-sm12 ms-md4 ms-lg4 ${layout}`}
enable={{
right: false
}}
size={{
width: graphExplorerMode === Mode.TryIt ? '100%' : contentWidth,
height: ''
}}
style={!sidebarProperties.showSidebar && !mobileScreen ? {
marginLeft: '8px', display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
} : {
display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
}}>
<div style={mobileScreen ? this.statusAreaMobileStyle : this.statusAreaFullScreenStyle}>
<StatusMessages />
</div>
<QueryResponse />
}}
>
<div className='ms-Grid-row'>
<Notification
header={translateMessage('Banner notification 1 header')}
content={translateMessage('Banner notification 1 content')}
link={translateMessage('Banner notification 1 link')}
linkText={translateMessage('Banner notification 1 link text')}/>
</div>
</ValidationProvider>
</Resizable>
)}
</div>
<div style={mobileScreen ? this.statusAreaMobileStyle : this.statusAreaFullScreenStyle}>
<TermsOfUseMessage />
<ValidationProvider>
<div style={{ marginBottom: 2 }} >
<QueryRunner onSelectVerb={this.handleSelectVerb} />
</div>
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'stretch', flex: 1
}}>
<div style={mobileScreen ? this.statusAreaMobileStyle : this.statusAreaFullScreenStyle}>
<StatusMessages />
</div>
<QueryResponse />
</div>
</ValidationProvider>
</Resizable>
)}
</div>
<div style={mobileScreen ? this.statusAreaMobileStyle : this.statusAreaFullScreenStyle}>
<TermsOfUseMessage />
</div>
</div>
</div>
<CollectionPermissionsProvider>
<PopupsWrapper />
</CollectionPermissionsProvider>
</PopupsProvider>
</ThemeContext.Provider>
<CollectionPermissionsProvider>
<PopupsWrapper />
</CollectionPermissionsProvider>
</PopupsProvider>
</ThemeContext.Provider>
</FluentProvider>
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/views/classnames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ interface IClassNames {
export function classNames({ styles, theme }: IClassNames): any {
const getClassNames = classNamesFunction();
return getClassNames(styles, theme);
}
}
31 changes: 31 additions & 0 deletions src/app/views/common/banners/Notification.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { makeStyles } from '@fluentui/react-components';
import polygons from './bgPolygons.svg';

export const useNotificationStyles = makeStyles({
container: {
padding: '8px',
marginBottom: '8px',
backgroundImage: `url(${polygons})`,
backgroundRepeat: 'no-repeat',
backgroundSize: 'contain',
backgroundPosition: 'right',
'&light': {
backgroundColor: '#E8EFFF',
color: '#000000'
},
'&.dark': {
backgroundColor: '#1D202A',
color: '#ffffff'
},
'&.highContrast': {
backgroundColor: '#0C3B5E',
color: '#ffffff'
}
},
body: {
width: '100%',
'@media (min-width: 720px)': {
width: '70%'
}
}
});
71 changes: 71 additions & 0 deletions src/app/views/common/banners/Notification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
Button,
Link,
MessageBar,
MessageBarActions,
MessageBarBody,
MessageBarTitle
} from '@fluentui/react-components';
import { DismissRegular, OpenRegular } from '@fluentui/react-icons';
import { useState } from 'react';
import { useAppSelector } from '../../../../store';
import { componentNames, eventTypes, telemetry } from '../../../../telemetry';
import { BANNER_IS_VISIBLE } from '../../../services/graph-constants';
import { translateMessage } from '../../../utils/translate-messages';
import { useNotificationStyles } from './Notification.styles';

interface NotificationProps {
header: string;
content: string;
link: string;
linkText: string;
}

const handleOnClickLink = (e: React.MouseEvent<HTMLAnchorElement>)=>{
telemetry.trackLinkClickEvent(
(e.currentTarget as HTMLAnchorElement).href, componentNames.GRAPH_EXPLORER_TUTORIAL_LINK)
}

const Notification: React.FunctionComponent<NotificationProps> = (props: NotificationProps) => {
const styles = useNotificationStyles();
const storageBanner = localStorage.getItem(BANNER_IS_VISIBLE);
const [isVisible, setIsVisible] = useState(storageBanner === null || storageBanner === 'true');
const theme = useAppSelector(s => s.theme);

const handleDismiss = () => {
telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, {
ComponentName: componentNames.NOTIFICATION_BANNER_DISMISS_BUTTON
});
localStorage.setItem(BANNER_IS_VISIBLE, 'false');
setIsVisible(false);
};

if (!isVisible) {
return null;
}

return (
<MessageBar className={`${styles.container} ${theme}`} icon={''}>
<MessageBarBody className={styles.body}>
<MessageBarTitle>{props.header}</MessageBarTitle><br></br>
{props.content}{' '}
<Link
onClick={handleOnClickLink}
href={props.link}
target='_blank'>{props.linkText} <OpenRegular /></Link>
</MessageBarBody>
<MessageBarActions
containerAction={
<Button
onClick={handleDismiss}
aria-label={translateMessage('Dismiss banner')}
appearance="transparent"
icon={<DismissRegular />}
/>
}
/>
</MessageBar>
);
};

export default telemetry.trackReactComponent(Notification, componentNames.NOTIFICATION_COMPONENT)
Loading

0 comments on commit 79e3b85

Please sign in to comment.