Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions apps/ui-community/src/App.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ApolloProvider } from '@apollo/client';
import { FeatureFlagProvider } from '@ocom/ui-shared';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { AuthProvider } from 'react-oidc-context';
import { MemoryRouter } from 'react-router-dom';
import { expect } from 'storybook/test';
import App from './App.tsx';
import { client } from './components/ui/organisms/apollo-connection/apollo-client-links.tsx';
import { featureFlagConfig } from './config/feature-flag-config.ts';

// Mock environment variables
const mockEnv = {
Expand Down Expand Up @@ -64,17 +66,19 @@ const meta = {
},
decorators: [
(Story) => (
<AuthProvider
authority={mockEnv.VITE_APP_UI_COMMUNITY_END_USER_B2C_AUTHORITY}
client_id={mockEnv.VITE_APP_UI_COMMUNITY_END_USER_B2C_CLIENTID}
redirect_uri={globalThis.location.origin}
post_logout_redirect_uri={globalThis.location.origin}
userStore={mockStorage}
>
<ApolloProvider client={client}>
<Story />
</ApolloProvider>
</AuthProvider>
<FeatureFlagProvider config={featureFlagConfig}>
<AuthProvider
authority={mockEnv.VITE_APP_UI_COMMUNITY_END_USER_B2C_AUTHORITY}
client_id={mockEnv.VITE_APP_UI_COMMUNITY_END_USER_B2C_CLIENTID}
redirect_uri={globalThis.location.origin}
post_logout_redirect_uri={globalThis.location.origin}
userStore={mockStorage}
>
<ApolloProvider client={client}>
<Story />
</ApolloProvider>
</AuthProvider>
</FeatureFlagProvider>
),
],
} satisfies Meta<typeof App>;
Expand Down
73 changes: 40 additions & 33 deletions apps/ui-community/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RequireAuth } from '@cellix/ui-core';
import { Accounts } from '@ocom/ui-community-route-accounts';
import { Admin } from '@ocom/ui-community-route-admin';
import { Root } from '@ocom/ui-community-route-root';
import { MaintenanceMessage, MaintenanceMessageProvider, useMaintenanceMessage } from '@ocom/ui-shared';
import { Route, Routes } from 'react-router-dom';
import './App.css';
import { AuthLanding } from './components/ui/molecules/auth-landing/index.tsx';
Expand All @@ -16,41 +17,47 @@ export default function App() {

const rootSection = <Root />;

const communitySection = (
<RequireAuth forceLogin={false}>
<Routes>
<Route
path="/"
element={<Accounts />}
/>
<Route
path="/accounts/*"
element={<Accounts />}
/>
<Route
path="/:communityId/admin/:memberId/*"
element={<Admin />}
/>
</Routes>
</RequireAuth>
);

return (
<ApolloConnection>
<Routes>
<Route
path="*"
element={rootSection}
/>
<Route
path="/auth-redirect"
element={authSection}
/>
<Route
path="/community/*"
element={communitySection}
/>
</Routes>
<MaintenanceMessageProvider portalKey="UI_COMMUNITY_PORTAL">
<Routes>
<Route
path="*"
element={rootSection}
/>
<Route
path="/auth-redirect"
element={authSection}
/>
<Route
path="/community/*"
element={<CommunitySection />}
/>
</Routes>
</MaintenanceMessageProvider>
</ApolloConnection>
);
}

function CommunitySection() {
const { isMaintenance } = useMaintenanceMessage();

const routes = (
<Routes>
<Route
path="/"
element={<Accounts />}
/>
<Route
path="/accounts/*"
element={<Accounts />}
/>
<Route
path="/:communityId/admin/:memberId/*"
element={<Admin />}
/>
</Routes>
);

return <RequireAuth forceLogin={false}>{isMaintenance ? <MaintenanceMessage portalKey="UI_COMMUNITY_PORTAL" /> : routes}</RequireAuth>;
}
48 changes: 25 additions & 23 deletions apps/ui-staff/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RequireAuth } from '@cellix/ui-core';
import { HandleLogout } from '@ocom/ui-shared';
import { HandleLogout, MaintenanceMessage, MaintenanceMessageProvider, useMaintenanceMessage } from '@ocom/ui-shared';
import { Root as CommunityManagement } from '@ocom/ui-staff-route-community-management';
import { Root as Finance } from '@ocom/ui-staff-route-finance';
import { Root } from '@ocom/ui-staff-route-root';
Expand Down Expand Up @@ -115,34 +115,33 @@ export default function App() {

return (
<ApolloConnection>
<Routes>
<Route
path="*"
element={rootSection}
/>
<Route
path="/auth-redirect"
element={authSection}
/>
<Route
path="/unauthorized"
element={<Unauthorized />}
/>

{/* StaffSection renders StaffAuthProvider + StaffRoutes which handles all
authenticated sub-routes with permission guards. No nested Route children
are needed here because StaffRoutes defines its own Routes block. */}
<Route
path="/staff/*"
element={staffSectionElement}
/>
</Routes>
<MaintenanceMessageProvider>
<Routes>
<Route
path="/auth-redirect"
element={authSection}
/>
<Route
path="/staff/*"
element={staffSectionElement}
/>
<Route
path="/unauthorized"
element={<Unauthorized />}
/>
<Route
path="*"
element={rootSection}
/>
</Routes>
</MaintenanceMessageProvider>
</ApolloConnection>
);
}

function StaffSection({ identity }: { identity: Parameters<typeof StaffAuthProvider>[0]['value'] }) {
const { permissions, enterpriseAppRole, user, loading } = useStaffPermissions();
const { isMaintenance } = useMaintenanceMessage();

if (loading) {
return (
Expand All @@ -151,6 +150,9 @@ function StaffSection({ identity }: { identity: Parameters<typeof StaffAuthProvi
</div>
);
}
if (isMaintenance) {
return <MaintenanceMessage />;
}

return (
<StaffAuthProvider value={{ ...identity, permissions, enterpriseAppRole, name: user?.displayName, email: user?.email }}>
Expand Down
3 changes: 3 additions & 0 deletions packages/ocom/graphql/src/schema/types/server-date.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
extend type Query {
serverDate: DateTime!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Resolvers } from '../builder/generated.ts';

const serverDate: Resolvers = {
Query: {
serverDate: () => new Date(),
},
};

export default serverDate;
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { LoggedInUserContainer } from '@ocom/ui-shared';
import { ImpendingMessage, LoggedInUserContainer, MaintenanceMessage, useMaintenanceMessage } from '@ocom/ui-shared';
import { Layout, theme } from 'antd';
import { Outlet } from 'react-router-dom';

const { Header } = Layout;

export const SectionLayout: React.FC = () => {
const { isImpending, isMaintenance } = useMaintenanceMessage();
const {
token: { colorBgContainer },
} = theme.useToken();
Expand All @@ -29,8 +30,10 @@ export const SectionLayout: React.FC = () => {
<LoggedInUserContainer autoLogin={true} />
</div>
</Header>
{isImpending && <ImpendingMessage portalKey="UI_COMMUNITY_PORTAL" />}
{isMaintenance && <MaintenanceMessage portalKey="UI_COMMUNITY_PORTAL" />}

<Layout>
<Layout style={{ marginTop: isImpending || isMaintenance ? '95px' : undefined }}>
<Layout
style={{
display: 'flex',
Expand Down
10 changes: 8 additions & 2 deletions packages/ocom/ui-community-route-admin/src/section-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CommunitiesDropdownContainer, LoggedInUserContainer, MenuComponent, type MenuComponentProps, type PageLayoutProps } from '@ocom/ui-shared';
import { CommunitiesDropdownContainer, ImpendingMessage, LoggedInUserContainer, MaintenanceMessage, MenuComponent, type MenuComponentProps, type PageLayoutProps, useMaintenanceMessage } from '@ocom/ui-shared';
import { Layout, theme } from 'antd';
import { useState } from 'react';
import { Link, Outlet, useParams } from 'react-router-dom';
Expand Down Expand Up @@ -35,6 +35,7 @@ interface AdminSectionLayoutProps {

export const SectionLayout: React.FC<AdminSectionLayoutProps> = (props) => {
const params = useParams();
const { isImpending, isMaintenance } = useMaintenanceMessage();
const sidebarCollapsed = localStorage.getItem(LocalSettingsKeys.SidebarCollapsed);
const [isExpanded, setIsExpanded] = useState(!sidebarCollapsed);
const {
Expand Down Expand Up @@ -86,8 +87,13 @@ export const SectionLayout: React.FC<AdminSectionLayoutProps> = (props) => {
<LoggedInUserContainer autoLogin={true} />
</div>
</Header>
{isImpending && <ImpendingMessage portalKey="UI_COMMUNITY_PORTAL" />}
{isMaintenance && <MaintenanceMessage portalKey="UI_COMMUNITY_PORTAL" />}

<Layout hasSider={true}>
<Layout
hasSider={true}
style={{ marginTop: isImpending || isMaintenance ? '95px' : undefined }}
>
<Sider
theme="light"
className="site-layout-background"
Expand Down
1 change: 1 addition & 0 deletions packages/ocom/ui-community-route-root/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"@ocom/ui-community-shared": "workspace:*",
"@ocom/ui-shared": "workspace:*",
"antd": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MaintenanceMessageContext } from '@ocom/ui-shared';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, within } from 'storybook/test';
import { Root } from '../index.tsx';
Expand All @@ -8,6 +9,22 @@ const meta = {
parameters: {
layout: 'fullscreen',
},
decorators: [
(Story) => (
<MaintenanceMessageContext.Provider
value={{
isMaintenance: false,
isImpending: false,
isApproachingMaintenance: false,
impendingMaintenanceStartTimestamp: '',
maintenanceStartTimestamp: '',
maintenanceEndTimestamp: '',
}}
>
<Story />
</MaintenanceMessageContext.Provider>
),
],
} satisfies Meta<typeof Root>;

export default meta;
Expand Down
2 changes: 2 additions & 0 deletions packages/ocom/ui-shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
"@graphql-typed-document-node/core": "^3.2.0",
"antd": "catalog:",
"async-retry": "^1.3.3",
"dayjs": "^1.11.19",
"graphql": "catalog:",
"html-react-parser": "^5.2.6",
"lru-cache": "^11.3.6",
"react": "catalog:",
"react-dom": "catalog:",
Expand Down
1 change: 1 addition & 0 deletions packages/ocom/ui-shared/src/components/organisms/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { CommunitiesDropdownContainer } from './dropdown-menu/communities-dropdown.container.tsx';
export { type FeatureFlagConfig, type FeatureFlagInterface, FeatureFlagProvider, type FeatureFlagProviderProps, type FeatureFlags, FeatureFlagsContext, useFeatureFlags } from './feature-flag/index.tsx';
export { HandleLogout, LoggedInUserContainer } from './header/index.tsx';
export { ImpendingMessage, MaintenanceMessage, MaintenanceMessageContext, type MaintenanceMessageInterface, MaintenanceMessageProvider, useMaintenanceMessage } from './maintinence-message/index.tsx';
export { MenuComponent, type MenuComponentProps, type PageLayoutProps } from './navigation/menu-component.tsx';
export { VerticalTabs } from './vertical-tabs/index.tsx';
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Alert } from 'antd';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone.js';
import utc from 'dayjs/plugin/utc.js';
import type { FC } from 'react';
import { useFeatureFlags } from '../feature-flag/index.tsx';
import parse from './parse-html.ts';
import useMaintenanceMessage from './use-maintenance-message.tsx';
import 'dayjs/locale/en';

dayjs.locale('en');
dayjs.extend(utc);
dayjs.extend(timezone);

interface ImpendingMessageProps {
isRootPage?: boolean;
portalKey?: string;
}

const ImpendingMessage: FC<ImpendingMessageProps> = (props) => {
const portalKey = props.portalKey ?? 'UI_STAFF_PORTAL';
const { GetFeatureFlagByName } = useFeatureFlags();
const replaceTokens = (str: string, mapObj: Record<string, string>) => {
const re = new RegExp(Object.keys(mapObj).join('|'), 'g');
return str.replace(re, (matched) => mapObj[matched] ?? matched);
};

const { isApproachingMaintenance } = useMaintenanceMessage();

const formatMessage = () => {
const maintenanceStartTimestamp = GetFeatureFlagByName(`MAINTENANCE_START_TIMESTAMP_${portalKey}`);
const maintenanceEndTimestamp = GetFeatureFlagByName(`MAINTENANCE_END_TIMESTAMP_${portalKey}`);
const impendingMessage = GetFeatureFlagByName(`MAINTENANCE_MSG_IMPENDING_${portalKey}`);
const startTimestampStr = dayjs(maintenanceStartTimestamp).locale('en').tz('America/New_York').format('h:mm a on dddd, MMMM DD, YYYY');
const endTimestampStr = dayjs(maintenanceEndTimestamp).locale('en').tz('America/New_York').format('h:mm a on dddd, MMMM DD, YYYY');
const startDateStr = dayjs(maintenanceStartTimestamp).locale('en').tz('America/New_York').format('MMMM DD');
const endDateStr = dayjs(maintenanceEndTimestamp).locale('en').tz('America/New_York').format('MMMM DD');
const timeRangeStr = startDateStr === endDateStr ? startDateStr : `${startDateStr} - ${endDateStr}`;
const mapObj = {
'##startTimestampStr##': startTimestampStr,
'##endTimestampStr##': endTimestampStr,
'##timeRangeStr##': timeRangeStr,
};
return replaceTokens(impendingMessage, mapObj);
};

return (
<>
{props.isRootPage ? (
<div data-testid="impending-message">
<Alert message={<div>{parse(formatMessage())}</div>} />
</div>
) : (
<div
style={{ textAlign: 'left', position: 'fixed', top: isApproachingMaintenance ? '100px' : '60px', zIndex: 1000 }}
data-testid="impending-message"
>
<Alert message={<div>{parse(formatMessage())}</div>} />
</div>
)}
</>
);
};

export default ImpendingMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { default as ImpendingMessage } from './impending-message.tsx';
export { default as MaintenanceMessage } from './maintenance-message.tsx';
export { default as MaintenanceMessageContext, type MaintenanceMessageInterface } from './maintenance-message-context.tsx';
export { default as MaintenanceMessageProvider } from './maintenance-message-provider.tsx';
export { default as useMaintenanceMessage } from './use-maintenance-message.tsx';
Loading