Skip to content

Commit

Permalink
Merge pull request #431 from PagerDuty/better-async
Browse files Browse the repository at this point in the history
  • Loading branch information
gsreynolds authored May 22, 2024
2 parents 6997ce5 + 2fbe85c commit 07ee3c0
Show file tree
Hide file tree
Showing 50 changed files with 3,990 additions and 517 deletions.
10 changes: 10 additions & 0 deletions cypress/e2e/app.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,14 @@ describe('PagerDuty Live', { failFast: { enabled: true } }, () => {
cy.get('iframe[title="TestExtra"]');
// would need to enable cross-domain iframe javascript access to test further
});

it('Application correctly renders the catastrophe modal', () => {
cy
.window()
.its('store')
.invoke('dispatch', { type: 'CATASTROPHE' });

cy.get('header').contains('Catastrophic Error');
cy.get('p').contains('The application will restart');
});
});
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"dependencies": {
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.0",
"@datadog/browser-rum": "^4.47.0",
"@datadog/browser-rum": "^5.14.0",
"@datadog/datadog-ci": "^2.33.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
Expand Down Expand Up @@ -43,6 +44,7 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18",
"react-error-boundary": "^4.0.13",
"react-i18next": "^13.2.0",
"react-icons": "^4.9.0",
"react-minimal-pie-chart": "^8.4.0",
Expand All @@ -61,6 +63,7 @@
"scripts": {
"start": "vite",
"build": "npx genversion src/config/version.js --semi --es6 && vite build",
"sourcemaps": "datadog-ci sourcemaps upload build --service=pd-live-react --release-version=$(node -e 'console.log(require(__dirname + `/package.json`).version)') --project-path=./ --minified-path-prefix=/pd-live-react/",
"genversion": "npx genversion src/config/version.js --semi --es6",
"jest": "npx jest",
"cypress:open": "npx cypress open --browser chrome --e2e",
Expand Down
228 changes: 97 additions & 131 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {
useEffect, useRef,
useEffect, useMemo, useState,
} from 'react';
import {
useSelector, useDispatch,
Expand All @@ -12,12 +12,19 @@ import {
HTML5Backend,
} from 'react-dnd-html5-backend';

import {
ErrorBoundary,
} from 'react-error-boundary';

import RealUserMonitoring from 'src/config/monitoring';

import {
Box, Flex,
} from '@chakra-ui/react';

import moment from 'moment/min/moment-with-locales';

import CatastropheModal from 'src/components/CatastropheModal/CatastropheModal';
import AuthComponent from 'src/components/Auth/AuthComponent';
import UnauthorizedModalComponent from 'src/components/UnauthorizedModal/UnauthorizedModalComponent';
import DisclaimerModalComponent from 'src/components/DisclaimerModal/DisclaimerModalComponent';
Expand All @@ -40,11 +47,12 @@ import {
} from 'src/redux/extensions/actions';
import {
getIncidentsAsync as getIncidentsAsyncConnected,
// refreshIncidentsAsync as refreshIncidentsAsyncConnected,
} from 'src/redux/incidents/actions';
import {
getLogEntriesAsync as getLogEntriesAsyncConnected,
cleanRecentLogEntriesAsync as cleanRecentLogEntriesAsyncConnected,
START_LOG_ENTRIES_POLLING,
STOP_LOG_ENTRIES_POLLING,
START_CLEAN_RECENT_LOG_ENTRIES_POLLING,
STOP_CLEAN_RECENT_LOG_ENTRIES_POLLING,
} from 'src/redux/log_entries/actions';
import {
getPrioritiesAsync as getPrioritiesAsyncConnected,
Expand All @@ -56,63 +64,61 @@ import {
getResponsePlaysAsync as getResponsePlaysAsyncConnected,
} from 'src/redux/response_plays/actions';
import {
checkConnectionStatus as checkConnectionStatusConnected,
updateQueueStats as updateQueueStatsConnected,
checkAbilities as checkAbilitiesConnected,
START_CONNECTION_STATUS_POLLING,
START_ABILITIES_POLLING,
STOP_ABILITIES_POLLING,
START_QUEUE_STATS_POLLING,
START_OAUTH_REFRESH_POLLING,
STOP_OAUTH_REFRESH_POLLING,
CATASTROPHE,
} from 'src/redux/connection/actions';
import {
startMonitoring as startMonitoringConnected,
} from 'src/redux/monitoring/actions';

import {
getLimiterStats,
} from 'src/util/pd-api-wrapper';

import {
PD_USER_TOKEN,
PD_OAUTH_CLIENT_ID,
PD_OAUTH_CLIENT_SECRET,
PD_REQUIRED_ABILITY,
LOG_ENTRIES_POLLING_INTERVAL_SECONDS,
// TODO: Implement log entries clearing
// LOG_ENTRIES_CLEARING_INTERVAL_SECONDS,
DEBUG_DISABLE_POLLING,
} from 'src/config/constants';

import 'src/App.scss';
import 'moment/min/locales.min';

const App = () => {
// Verify if session token is present
const token = sessionStorage.getItem('pd_access_token');

const dispatch = useDispatch();
const startMonitoring = () => dispatch(startMonitoringConnected());
const userAuthorize = () => dispatch(userAuthorizeConnected());
const checkAbilities = () => dispatch(checkAbilitiesConnected());
const checkConnectionStatus = () => dispatch(checkConnectionStatusConnected());
const updateQueueStats = (queueStats) => dispatch(updateQueueStatsConnected(queueStats));
const getExtensionsAsync = () => dispatch(getExtensionsAsyncConnected());
const getPrioritiesAsync = () => dispatch(getPrioritiesAsyncConnected());
const getResponsePlaysAsync = () => dispatch(getResponsePlaysAsyncConnected());
const getLogEntriesAsync = (since) => dispatch(getLogEntriesAsyncConnected(since));
const cleanRecentLogEntriesAsync = () => dispatch(cleanRecentLogEntriesAsyncConnected());
const getIncidentsAsync = () => dispatch(getIncidentsAsyncConnected());

const dispatchCatastrophe = (connectionStatusMessage) => dispatch({
type: CATASTROPHE,
connectionStatusMessage,
});

const darkMode = useSelector((state) => state.settings.darkMode);
const abilities = useSelector((state) => state.connection.abilities);
const {
fetchingIncidents, lastFetchDate,
} = useSelector((state) => state.incidents);

const {
userAuthorized, userAcceptedDisclaimer, currentUserLocale,
} = useSelector(
(state) => state.users,
);

const {
fetchingData: fetchingLogEntries, latestLogEntryDate,
} = useSelector(
(state) => state.logEntries,
);
status: connectionStatus,
connectionStatusMessage,
} = useSelector((state) => state.connection);
const [catastrophe, setCatastrophe] = useState(false);
useEffect(() => {
if (connectionStatus === CATASTROPHE) {
setCatastrophe(true);
}
}, [connectionStatus]);

const token = useMemo(() => sessionStorage.getItem('pd_access_token'), [userAuthorized]);

if (darkMode) {
document.body.classList.add('dark-mode');
Expand All @@ -123,12 +129,10 @@ const App = () => {
userAuthorize();
if (token && userAuthorized) {
startMonitoring();
checkAbilities();
getPrioritiesAsync();
getIncidentsAsync();
getExtensionsAsync();
getResponsePlaysAsync();
checkConnectionStatus();
}
}, [userAuthorized]);

Expand All @@ -137,86 +141,39 @@ const App = () => {
moment.locale(currentUserLocale);
}, [currentUserLocale]);

// use these refs in the polling interval to avoid stale values
// without having to add them to the dependency array
const latestLogEntryDateRef = useRef(latestLogEntryDate);
useEffect(() => {
latestLogEntryDateRef.current = latestLogEntryDate;
}, [latestLogEntryDate]);
const fetchingIncidentsRef = useRef(fetchingIncidents);
useEffect(() => {
fetchingIncidentsRef.current = fetchingIncidents;
}, [fetchingIncidents]);
const fetchingLogEntriesRef = useRef(fetchingLogEntries);
useEffect(() => {
fetchingLogEntriesRef.current = fetchingLogEntries;
}, [fetchingLogEntries]);

// Set up log entry polling
useEffect(
() => {
const pollingInterval = setInterval(() => {
checkConnectionStatus();
if (userAuthorized && abilities.includes(PD_REQUIRED_ABILITY)) {
if (fetchingLogEntriesRef.current) {
// eslint-disable-next-line no-console
console.warn('skipping log entries fetch because already fetching log entries');
return;
}

if (!fetchingIncidentsRef.current && !DEBUG_DISABLE_POLLING) {
// Determine lookback based on last fetch/refresh of incidents
// 2x polling interval is a good lookback if we don't have a last fetch date
let since = new Date(new Date() - 2000 * LOG_ENTRIES_POLLING_INTERVAL_SECONDS);
// If we have a last fetch date, use that
if (lastFetchDate) {
since = new Date(lastFetchDate - 1000);
}
// If we have a latest log entry date and it's newer than last fetch date, use that
if (latestLogEntryDateRef.current && latestLogEntryDateRef.current > since) {
since = new Date(latestLogEntryDateRef.current - 1000);
}
getLogEntriesAsync(since);
} else if (fetchingIncidentsRef.current) {
// eslint-disable-next-line no-console
console.warn('skipping log entries fetch because already fetching incidents');
}
}
}, LOG_ENTRIES_POLLING_INTERVAL_SECONDS * 1000);
return () => clearInterval(pollingInterval);
},
// Changes to any of these in the store resets log entries timer
[userAuthorized, fetchingIncidents, lastFetchDate],
);

// Setup log entry clearing
// Set up API polling
useEffect(() => {
const clearingInterval = setInterval(
() => {
if (userAuthorized) {
cleanRecentLogEntriesAsync();
}
},
60 * 60 * 1000,
);
return () => clearInterval(clearingInterval);
}, [userAuthorized]);
if (token && userAuthorized && userAcceptedDisclaimer) {
dispatch({ type: START_LOG_ENTRIES_POLLING });
dispatch({ type: START_CLEAN_RECENT_LOG_ENTRIES_POLLING });
dispatch({ type: START_ABILITIES_POLLING });
dispatch({ type: START_OAUTH_REFRESH_POLLING });
} else {
dispatch({ type: STOP_LOG_ENTRIES_POLLING });
dispatch({ type: STOP_CLEAN_RECENT_LOG_ENTRIES_POLLING });
dispatch({ type: STOP_ABILITIES_POLLING });
dispatch({ type: STOP_OAUTH_REFRESH_POLLING });
}
}, [userAuthorized, userAcceptedDisclaimer]);

// Setup queue stats update for status beacon tooltip
// Set up connection status polling
useEffect(() => {
const queueStateInterval = setInterval(() => {
if (userAuthorized) {
updateQueueStats(getLimiterStats());
}
}, 2000);
return () => clearInterval(queueStateInterval);
}, [userAuthorized]);
dispatch({ type: START_CONNECTION_STATUS_POLLING });
dispatch({ type: START_QUEUE_STATS_POLLING });
}, []);

const headerRef = useRef(null);
const mainRef = useRef(null);
const footerRef = useRef(null);
if (catastrophe) {
return (
<CatastropheModal
errorMessage={connectionStatusMessage}
/>
);
}

if (!token) {
if (
!PD_USER_TOKEN
&& (typeof token !== 'string' || !token.startsWith('pd'))
) {
return (
<div className="App">
<AuthComponent clientId={PD_OAUTH_CLIENT_ID} clientSecret={PD_OAUTH_CLIENT_SECRET} />
Expand Down Expand Up @@ -244,29 +201,38 @@ const App = () => {

return (
<div className="App">
<Box position="fixed" w="100%" h="100%" overflow="hidden">
<Box as="header" top={0} w="100%" pb={1} ref={headerRef}>
<NavigationBarComponent />
</Box>
<Box ref={mainRef} as="main" id="main">
<IncidentTableComponent headerRef={headerRef} mainRef={mainRef} footerRef={footerRef} />
<SettingsModalComponent />
<LoadSavePresetsModal />
<DndProvider backend={HTML5Backend}>
<ColumnsModalComponent />
</DndProvider>
<ActionAlertsModalComponent />
<CustomSnoozeModalComponent />
<AddNoteModalComponent />
<ReassignModalComponent />
<AddResponderModalComponent />
<MergeModalComponent />
<IncidentAlertsModal />
<ErrorBoundary fallbackRender={
(details) => {
RealUserMonitoring.trackError(details.error);
dispatchCatastrophe(`UI Render error: ${details.error.message}`);
return null;
}
}
>
<Box position="fixed" w="100%" h="100%" overflow="hidden">
<Box as="header" top={0} w="100%" pb={1}>
<NavigationBarComponent />
</Box>
<Box as="main" id="main">
<IncidentTableComponent />
<SettingsModalComponent />
<LoadSavePresetsModal />
<DndProvider backend={HTML5Backend}>
<ColumnsModalComponent />
</DndProvider>
<ActionAlertsModalComponent />
<CustomSnoozeModalComponent />
<AddNoteModalComponent />
<ReassignModalComponent />
<AddResponderModalComponent />
<MergeModalComponent />
<IncidentAlertsModal />
</Box>
<Flex as="footer" position="fixed" bottom={0} w="100%" zIndex="1" pt={1}>
<IncidentActionsComponent />
</Flex>
</Box>
<Flex as="footer" position="fixed" bottom={0} w="100%" zIndex="1" pt={1} ref={footerRef}>
<IncidentActionsComponent />
</Flex>
</Box>
</ErrorBoundary>
</div>
);
};
Expand Down
23 changes: 17 additions & 6 deletions src/components/Auth/AuthComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,25 @@ const AuthComponent = (props) => {

useEffect(() => {
if (code && codeVerifier && !accessToken) {
// if there were button params on the first load, load the button params and put them back on the URL
const savedButtonsStr = sessionStorage.getItem('pd_buttons');
const savedButtons = savedButtonsStr ? JSON.parse(savedButtonsStr) : [];
const buttonParams = savedButtons ? `?button=${savedButtons.join('&button=')}` : '';

exchangeCodeForToken(clientId, clientSecret, redirectURL, codeVerifier, code).then(
(token) => {
(data) => {
const {
access_token: newAccessToken,
refresh_token: newRefreshToken,
expires_in: expiresIn,
} = data;
if (!newAccessToken || !newRefreshToken || !expiresIn) {
window.location.assign(redirectURL + buttonParams);
}
sessionStorage.removeItem('code_verifier');
sessionStorage.setItem('pd_access_token', token);
// if there were button params on the first load, load the button params and put them back on the URL
const savedButtonsStr = sessionStorage.getItem('pd_buttons');
const savedButtons = savedButtonsStr ? JSON.parse(savedButtonsStr) : [];
const buttonParams = savedButtons ? `?button=${savedButtons.join('&button=')}` : '';
sessionStorage.setItem('pd_access_token', newAccessToken);
sessionStorage.setItem('pd_refresh_token', newRefreshToken);
sessionStorage.setItem('pd_token_expires_at', new Date().getTime() + (expiresIn * 1000));
window.location.assign(redirectURL + buttonParams);
},
);
Expand Down
Loading

0 comments on commit 07ee3c0

Please sign in to comment.