Skip to content

Commit

Permalink
Merge branch 'main' into feat/jetstream-authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
paustint committed Nov 15, 2024
2 parents 89b841e + bfbc749 commit e1e735d
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 24 deletions.
23 changes: 23 additions & 0 deletions apps/api/src/app/announcements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { logger } from '@jetstream/api-config';
import { getErrorMessageAndStackObj } from '@jetstream/shared/utils';
import { Announcement } from '@jetstream/types';

export function getAnnouncements(): Announcement[] {
try {
// This is a placeholder for the announcements that will be stored in the database eventually
return [
{
id: 'auth-downtime-2024-11-15T15:00:00.000Z',
title: 'Downtime',
content:
'We will be upgrading our authentication system with an expected start time of {start} in your local timezone. During this time, you will not be able to log in or use Jetstream. We expect the upgrade to take less than one hour.',
replacementDates: [{ key: '{start}', value: '2024-11-16T18:00:00.000Z' }],
expiresAt: '2024-11-16T20:00:00.000Z',
createdAt: '2024-11-15T15:00:00.000Z',
},
].filter(({ expiresAt }) => new Date(expiresAt) > new Date());
} catch (ex) {
logger.error({ ...getErrorMessageAndStackObj(ex) }, 'Failed to get announcements');
return [];
}
}
3 changes: 2 additions & 1 deletion apps/api/src/app/routes/api.routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ENV } from '@jetstream/api-config';
import express from 'express';
import Router from 'express-promise-router';
import { getAnnouncements } from '../announcements';
import { routeDefinition as imageController } from '../controllers/image.controller';
import { routeDefinition as jetstreamOrganizationsController } from '../controllers/jetstream-organizations.controller';
import { routeDefinition as orgsController } from '../controllers/orgs.controller';
Expand All @@ -23,7 +24,7 @@ routes.use(addOrgsToLocal);
// used to make sure the user is authenticated and can communicate with the server
routes.get('/heartbeat', (req: express.Request, res: express.Response) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sendJson(res as any, { version: ENV.GIT_VERSION || null });
sendJson(res as any, { version: ENV.GIT_VERSION || null, announcements: getAnnouncements() });
});

/**
Expand Down
7 changes: 5 additions & 2 deletions apps/jetstream/src/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Maybe, UserProfileUi } from '@jetstream/types';
import { Announcement, Maybe, UserProfileUi } from '@jetstream/types';
import { AppToast, ConfirmationServiceProvider } from '@jetstream/ui';
import { AppLoading, DownloadFileStream, ErrorBoundaryFallback, HeaderNavbar } from '@jetstream/ui-core';
import { OverlayProvider } from '@react-aria/overlays';
Expand All @@ -9,6 +9,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import ModalContainer from 'react-modal-promise';
import { RecoilRoot } from 'recoil';
import { AppRoutes } from './AppRoutes';
import { AnnouncementAlerts } from './components/core/AnnouncementAlerts';
import AppInitializer from './components/core/AppInitializer';
import AppStateResetOnOrgChange from './components/core/AppStateResetOnOrgChange';
import LogInitializer from './components/core/LogInitializer';
Expand All @@ -18,12 +19,13 @@ import NotificationsRequestModal from './components/core/NotificationsRequestMod
export const App = () => {
const [userProfile, setUserProfile] = useState<Maybe<UserProfileUi>>();
const [featureFlags, setFeatureFlags] = useState<Set<string>>(new Set(['all']));
const [announcements, setAnnouncements] = useState<Announcement[]>([]);

return (
<ConfirmationServiceProvider>
<RecoilRoot>
<Suspense fallback={<AppLoading />}>
<AppInitializer onUserProfile={setUserProfile}>
<AppInitializer onAnnouncements={setAnnouncements} onUserProfile={setUserProfile}>
<OverlayProvider>
<DndProvider backend={HTML5Backend}>
<ModalContainer />
Expand All @@ -37,6 +39,7 @@ export const App = () => {
<HeaderNavbar userProfile={userProfile} featureFlags={featureFlags} />
</div>
<div className="app-container slds-p-horizontal_xx-small slds-p-vertical_xx-small" data-testid="content">
<AnnouncementAlerts announcements={announcements} />
<Suspense fallback={<AppLoading />}>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<AppRoutes featureFlags={featureFlags} userProfile={userProfile} />
Expand Down
46 changes: 46 additions & 0 deletions apps/jetstream/src/app/components/core/AnnouncementAlerts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Announcement } from '@jetstream/types';
import { Alert } from '@jetstream/ui';
import { useState } from 'react';

interface UnverifiedEmailAlertProps {
announcements: Announcement[];
}

const LS_KEY_PREFIX = 'announcement_dismissed_';

export function AnnouncementAlerts({ announcements }: UnverifiedEmailAlertProps) {
if (!announcements || !announcements.length) {
return null;
}

return (
<>
{announcements.map((announcement) => (
<AnnouncementAlert key={announcement.id} announcement={announcement} />
))}
</>
);
}

export function AnnouncementAlert({ announcement }: { announcement: Announcement }) {
const key = `${LS_KEY_PREFIX}${announcement.id}`;
const [dismissed, setDismissed] = useState(() => localStorage.getItem(key) === 'true');

if (dismissed || !announcement || !announcement.id || !announcement.content) {
return null;
}

return (
<Alert
type="warning"
leadingIcon="warning"
allowClose
onClose={() => {
localStorage.setItem(key, 'true');
setDismissed(true);
}}
>
<span className="text-bold">{announcement.title}:</span> {announcement.content}
</Alert>
);
}
41 changes: 26 additions & 15 deletions apps/jetstream/src/app/components/core/AppInitializer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { logger } from '@jetstream/shared/client-logger';
import { HTTP } from '@jetstream/shared/constants';
import { checkHeartbeat, registerMiddleware } from '@jetstream/shared/data';
import { useObservable, useRollbar } from '@jetstream/shared/ui-utils';
import { ApplicationCookie, SalesforceOrgUi, UserProfileUi } from '@jetstream/types';
import { Announcement, ApplicationCookie, SalesforceOrgUi, UserProfileUi } from '@jetstream/types';
import { fromAppState, useAmplitude, usePageViews } from '@jetstream/ui-core';
import { AxiosResponse } from 'axios';
import localforage from 'localforage';
Expand All @@ -30,13 +30,14 @@ localforage.config({
});

export interface AppInitializerProps {
onAnnouncements?: (announcements: Announcement[]) => void;
onUserProfile: (userProfile: UserProfileUi) => void;
children?: React.ReactNode;
}

export const AppInitializer: FunctionComponent<AppInitializerProps> = ({ onUserProfile, children }) => {
export const AppInitializer: FunctionComponent<AppInitializerProps> = ({ onAnnouncements, onUserProfile, children }) => {
const userProfile = useRecoilValue<UserProfileUi>(fromAppState.userProfileState);
const { version } = useRecoilValue(fromAppState.appVersionState);
const { version, announcements } = useRecoilValue(fromAppState.appVersionState);
const appCookie = useRecoilValue<ApplicationCookie>(fromAppState.applicationCookieState);
const [orgs, setOrgs] = useRecoilState(fromAppState.salesforceOrgsState);
const invalidOrg = useObservable(orgConnectionError$);
Expand All @@ -54,6 +55,10 @@ APP VERSION ${version}
`);
}, [version]);

useEffect(() => {
announcements && onAnnouncements && onAnnouncements(announcements);
}, [announcements, onAnnouncements]);

useRollbar({
accessToken: environment.rollbarClientAccessToken,
environment: appCookie.environment,
Expand Down Expand Up @@ -90,20 +95,26 @@ APP VERSION ${version}
* 1. ensure user is still authenticated
* 2. make sure the app version has not changed, if it has then refresh the page
*/
const handleWindowFocus = useCallback(async (event: FocusEvent) => {
try {
if (document.visibilityState === 'visible') {
const { version: serverVersion } = await checkHeartbeat();
// TODO: inform user that there is a new version and that they should refresh their browser.
// We could force refresh, but don't want to get into some weird infinite refresh state
if (version !== serverVersion) {
console.log('VERSION MISMATCH', { serverVersion, version });
const handleWindowFocus = useCallback(
async (event: FocusEvent) => {
try {
if (document.visibilityState === 'visible') {
const { version: serverVersion, announcements } = await checkHeartbeat();
// TODO: inform user that there is a new version and that they should refresh their browser.
// We could force refresh, but don't want to get into some weird infinite refresh state
if (version !== serverVersion) {
console.log('VERSION MISMATCH', { serverVersion, version });
}
if (announcements && onAnnouncements) {
onAnnouncements(announcements);
}
}
} catch (ex) {
// ignore error, but user should have been logged out if this failed
}
} catch (ex) {
// ignore error, but user should have been logged out if this failed
}
}, []);
},
[onAnnouncements, version]
);

useEffect(() => {
document.addEventListener('visibilitychange', handleWindowFocus);
Expand Down
18 changes: 16 additions & 2 deletions libs/shared/data/src/lib/client-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import type {
UserProfileUiWithIdentities,
UserSessionWithLocation,
} from '@jetstream/auth/types';
import { logger } from '@jetstream/shared/client-logger';
import { HTTP, MIME_TYPES } from '@jetstream/shared/constants';
import {
Announcement,
AnonymousApexResponse,
ApexCompletionResponse,
ApiResponse,
Expand Down Expand Up @@ -71,8 +73,20 @@ function convertDateToLocale(dateOrIsoDateString?: string | Date, options?: Intl

//// APPLICATION ROUTES

export async function checkHeartbeat(): Promise<{ version: string }> {
return handleRequest({ method: 'GET', url: '/api/heartbeat' }).then(unwrapResponseIgnoreCache);
export async function checkHeartbeat(): Promise<{ version: string; announcements?: Announcement[] }> {
const heartbeat = await handleRequest<{ version: string; announcements?: Announcement[] }>({ method: 'GET', url: '/api/heartbeat' }).then(
unwrapResponseIgnoreCache
);
try {
heartbeat?.announcements?.forEach((item) => {
item?.replacementDates?.forEach(({ key, value }) => {
item.content = item.content.replaceAll(key, new Date(value).toLocaleString());
});
});
} catch (ex) {
logger.warn('Unable to parse announcements');
}
return heartbeat;
}

export async function emailSupport(emailBody: string, attachments: InputReadFileContent[]): Promise<void> {
Expand Down
5 changes: 3 additions & 2 deletions libs/shared/ui-core/src/state-management/app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { checkHeartbeat, getJetstreamOrganizations, getOrgs, getUserProfile } fr
import { getChromeExtensionVersion, getOrgType, isChromeExtension, parseCookie } from '@jetstream/shared/ui-utils';
import { groupByFlat, orderObjectsBy } from '@jetstream/shared/utils';
import {
Announcement,
ApplicationCookie,
JetstreamOrganization,
JetstreamOrganizationWithOrgs,
Expand Down Expand Up @@ -135,7 +136,7 @@ function setSelectedJetstreamOrganizationFromStorage(id: Maybe<string>) {

async function fetchAppVersion() {
try {
return isChromeExtension() ? { version: getChromeExtensionVersion() } : await checkHeartbeat();
return isChromeExtension() ? { version: getChromeExtensionVersion(), announcements: [] } : await checkHeartbeat();
} catch (ex) {
return { version: 'unknown' };
}
Expand All @@ -162,7 +163,7 @@ export const applicationCookieState = atom<ApplicationCookie>({
default: getAppCookie(),
});

export const appVersionState = atom<{ version: string }>({
export const appVersionState = atom<{ version: string; announcements?: Announcement[] }>({
key: 'appVersionState',
default: fetchAppVersion(),
});
Expand Down
9 changes: 9 additions & 0 deletions libs/types/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import { SalesforceOrgEdition } from './salesforce/misc.types';
import { QueryResult } from './salesforce/query.types';
import { InsertUpdateUpsertDeleteQuery } from './salesforce/record.types';

export interface Announcement {
id: string;
title: string;
content: string;
replacementDates: { key: string; value: string }[];
expiresAt: string;
createdAt: string;
}

export type CopyAsDataType = 'excel' | 'csv' | 'json';

export interface RequestResult<T> {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"url": "https://github.com/jetstreamapp/jetstream"
},
"author": "Jetstream",
"version": "4.17.2",
"version": "4.18.0",
"license": "GNU Lesser General Public License v3.0",
"engines": {
"node": ">=20 <22",
Expand Down Expand Up @@ -56,7 +56,7 @@
"email:build": "email build --dir ./libs/email/src/lib/email-templates --packageManager yarn",
"scripts:replace-deps": "node ./scripts/replace-package-deps.mjs",
"scripts:generate-env": "node ./scripts/generate.env.mjs",
"release": "dotenv -- release-it -V ${0}",
"release": "dotenv -- release-it -V",
"release:build": "zx ./scripts/build-release.mjs",
"release:web-extension": "dotenv -- release-it --config .release-it-web-ext.json",
"bundle-analyzer:client": "npx webpack-bundle-analyzer dist/apps/jetstream/stats.json",
Expand Down

0 comments on commit e1e735d

Please sign in to comment.