Skip to content
Closed
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
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"author": "",
"license": "GPL-3.0-or-later",
"dependencies": {
"@freeipa/plugin-sdk": "file:../freeipa-webui-plugin-sdk",
"@patternfly/patternfly": "^6.4.0",
"@patternfly/react-core": "^6.4.0",
"@patternfly/react-icons": "^6.4.0",
Expand Down
57 changes: 48 additions & 9 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
import * as ReactRouter from "react-router";
import * as ReduxToolkit from "@reduxjs/toolkit";
import * as ReactRedux from "react-redux";
import * as PatternFlyReactCore from "@patternfly/react-core";
import * as PatternFlyReactIcons from "@patternfly/react-icons";
import * as PatternFlyReactTable from "@patternfly/react-table";
import App from "./App";
import "./main.css";
// react router dom
Expand All @@ -15,17 +21,50 @@
import "@patternfly/patternfly/utilities/Accessibility/accessibility.css";
// Navigation
import { URL_PREFIX } from "./navigation/NavRoutes";
// Plugin infrastructure
import {
exposeSharedDependencies,
loadPlugins,
pluginRegistry,
} from "@freeipa/plugin-sdk";

Check failure on line 29 in src/main.tsx

View workflow job for this annotation

GitHub Actions / Check i18n types are up to date

Cannot find module '@freeipa/plugin-sdk' or its corresponding type declarations.

Check failure on line 29 in src/main.tsx

View workflow job for this annotation

GitHub Actions / Integration tests

Cannot find module '@freeipa/plugin-sdk' or its corresponding type declarations.
import { api } from "./services/rpc";
import { injectPluginReducer } from "./store/store";

exposeSharedDependencies({
React,
ReactDOM,
ReactRouter,
ReduxToolkit,
ReactRedux,
PatternFlyReactCore,
PatternFlyReactIcons,
PatternFlyReactTable,
});

pluginRegistry.setEndpointInjector((endpoints) => {
api.injectEndpoints({ endpoints });
});
pluginRegistry.setReducerInjector((key, reducer) => {
injectPluginReducer(key, reducer);
});

const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);

root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter basename={URL_PREFIX}>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>
);
async function bootstrap() {
await loadPlugins();
await pluginRegistry.runPhase("ready");

root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter basename={URL_PREFIX}>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>
);
}
Comment on lines +55 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider adding basic error handling around the async bootstrap pipeline.

loadPlugins() and pluginRegistry.runPhase("ready") are awaited without any error handling, so a failure in any plugin will prevent the app from rendering and leave users on a blank page. Wrap these calls in a try/catch that logs the error and still renders either the core app without the failing plugin(s) or a dedicated error screen, so one bad plugin doesn’t break the whole UI.

Suggested change
async function bootstrap() {
await loadPlugins();
await pluginRegistry.runPhase("ready");
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter basename={URL_PREFIX}>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>
);
}
async function bootstrap() {
try {
await loadPlugins();
await pluginRegistry.runPhase("ready");
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter basename={URL_PREFIX}>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>
);
} catch (error) {
// Ensure a plugin failure doesn't leave the user on a blank screen
console.error("Failed to initialize plugins during bootstrap:", error);
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter basename={URL_PREFIX}>
<div role="alert" style={{ padding: 24 }}>
<h1>We couldn’t load all extensions</h1>
<p>
Some parts of the application failed to start correctly. You can try
refreshing the page. If the problem persists, please contact support.
</p>
</div>
</BrowserRouter>
</Provider>
</React.StrictMode>
);
}
}


bootstrap();
16 changes: 13 additions & 3 deletions src/navigation/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@
import IdRangesTabs from "src/pages/IdRanges/IdRangesTabs";
import GlobalTrustConfig from "src/pages/Trusts/GlobalTrustConfig";
import OtpTokens from "src/pages/OtpTokens/OtpTokens";
import Dashboard from "src/pages/Dashboard/Dashboard";
import { usePluginRoutes } from "@freeipa/plugin-sdk";

Check failure on line 80 in src/navigation/AppRoutes.tsx

View workflow job for this annotation

GitHub Actions / Check i18n types are up to date

Cannot find module '@freeipa/plugin-sdk' or its corresponding type declarations.

Check failure on line 80 in src/navigation/AppRoutes.tsx

View workflow job for this annotation

GitHub Actions / Integration tests

Cannot find module '@freeipa/plugin-sdk' or its corresponding type declarations.

// Renders routes (React)
export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => {
// Redux: Get if user is logged in
const userLoggedIn = useAppSelector((state) => state.auth.isUserLoggedIn);
const pluginRoutes = usePluginRoutes();

const configurationSettings = useConfigurationSettings();
const dnsIsEnabled = configurationSettings.dnsIsEnabled;
Expand Down Expand Up @@ -546,11 +548,19 @@
<Route path="" element={<GlobalTrustConfig />} />
</Route>
<Route path="configuration" element={<Configuration />} />
{/* Redirect to Active users page if user is logged in and navigates to the root page */}
<Route path="dashboard" element={<Dashboard />} />
{/* Plugin-registered routes */}
{pluginRoutes.map((r) => (
<Route
key={`plugin-${r.pluginId}-${r.path}`}
path={r.path}
element={<r.component />}
/>
))}
<Route path="login" element={<Navigate to={"/"} replace />} />
<Route
path=""
element={<Navigate to={"active-users"} replace />}
element={<Navigate to={"dashboard"} replace />}
/>
{/* 404 page */}
<Route path="*" element={<NotFound />} />
Expand Down
38 changes: 37 additions & 1 deletion src/navigation/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
updateBrowserTitle,
} from "src/store/Global/routes-slice";
import { useConfigurationSettings } from "src/utils/configurationSettings";
import { usePluginNavItems } from "@freeipa/plugin-sdk";

Check failure on line 16 in src/navigation/Nav.tsx

View workflow job for this annotation

GitHub Actions / Check i18n types are up to date

Cannot find module '@freeipa/plugin-sdk' or its corresponding type declarations.

Check failure on line 16 in src/navigation/Nav.tsx

View workflow job for this annotation

GitHub Actions / Integration tests

Cannot find module '@freeipa/plugin-sdk' or its corresponding type declarations.

// Renders NavItem
const renderNavItem = (
Expand Down Expand Up @@ -43,7 +44,6 @@

// Renders 'Navigation'
const Navigation = () => {
// The first level will determine if the section is expanded and highligted
const activeFirstLevel = useAppSelector(
(state) => state.routes.activeFirstLevel
);
Expand All @@ -52,6 +52,7 @@
const activePageName = useAppSelector((state) => state.routes.activePageName);

const configurationSettings = useConfigurationSettings();
const pluginNavItems = usePluginNavItems();

const navigationRoutes = React.useMemo(() => {
return getNavigationRoutes(configurationSettings);
Expand Down Expand Up @@ -109,6 +110,41 @@
</NavExpandable>
);
})}
{pluginNavItems.length > 0 && (
<NavExpandable
title="Plugins"
isActive={pluginNavItems.some(
(item) => item.path === activePageName
)}
isExpanded={pluginNavItems.some(
(item) => item.path === activeFirstLevel
)}
>
{pluginNavItems.map((item) => (
<NavItem
key={`plugin-nav-${item.pluginId}-${item.path}`}
isActive={activePageName === item.path}
onClick={() => {
dispatch(updateActiveSecondLevel(item.path));
dispatch(updateActivePageName(item.path));
dispatch(
updateBrowserTitle(
item.title || `Identity Management - ${item.label}`
)
);
dispatch(updateActiveFirstLevel(item.path));
dispatch(
updateBreadCrumbPath([
{ name: item.label, url: item.path },
])
);
}}
>
<NavLink to={item.path}>{item.label}</NavLink>
</NavItem>
))}
</NavExpandable>
)}
</NavList>
</Nav>
);
Expand Down
46 changes: 46 additions & 0 deletions src/pages/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from "react";
import {
PageSection,
Content,
ContentVariants,
Gallery,
GalleryItem,
Card,
CardTitle,
CardBody,
CardHeader,
Icon,
} from "@patternfly/react-core";
import { ServerIcon } from "@patternfly/react-icons";
import { ExtensionSlot, EXTENSION_POINTS } from "@freeipa/plugin-sdk";

Check failure on line 15 in src/pages/Dashboard/Dashboard.tsx

View workflow job for this annotation

GitHub Actions / Check i18n types are up to date

Cannot find module '@freeipa/plugin-sdk' or its corresponding type declarations.

Check failure on line 15 in src/pages/Dashboard/Dashboard.tsx

View workflow job for this annotation

GitHub Actions / Integration tests

Cannot find module '@freeipa/plugin-sdk' or its corresponding type declarations.

const Dashboard: React.FC = () => {
return (
<PageSection>
<Content component={ContentVariants.h1}>Dashboard</Content>
<Gallery hasGutter>
<GalleryItem>
<Card isCompact>
<CardHeader>
<CardTitle>
<Icon isInline>
<ServerIcon />
</Icon>{" "}
System Status
</CardTitle>
</CardHeader>
<CardBody>
FreeIPA server is running. Use the navigation to manage identity,
policy, and authentication.
</CardBody>
</Card>
</GalleryItem>
<ExtensionSlot
extensionPointId={EXTENSION_POINTS.DASHBOARD_CONTENT}
/>
</Gallery>
</PageSection>
);
};

export default Dashboard;
44 changes: 31 additions & 13 deletions src/store/store.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
import { configureStore } from "@reduxjs/toolkit";
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import type { Reducer } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import globalReducer from "./Global/global-slice";
import { api } from "../services/rpc";
import routesReducer from "./Global/routes-slice";
import authReducer from "./Global/auth-slice";
import alertsReducer from "./Global/alerts-slice";

const staticReducers = {
api: api.reducer,
global: globalReducer,
routes: routesReducer,
auth: authReducer,
alerts: alertsReducer,
};

const pluginReducers: Record<string, Reducer> = {};

function createRootReducer() {
return combineReducers({
...staticReducers,
...pluginReducers,
});
}

export const setupStore = () => {
const store = configureStore({
reducer: {
api: api.reducer,
global: globalReducer,
routes: routesReducer,
auth: authReducer,
alerts: alertsReducer,
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
reducer: createRootReducer(),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false, // Removes the warning about non-serializable data
serializableCheck: false,
}).concat(api.middleware),
});

// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch);
return store;
};

const store = setupStore();

/** Called by the plugin registry to inject a new reducer at runtime. */
export function injectPluginReducer(key: string, reducer: Reducer): void {
if (pluginReducers[key]) {
return;
}
pluginReducers[key] = reducer;
store.replaceReducer(createRootReducer());
}

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
Loading
Loading