Skip to content

Commit 69628ee

Browse files
committed
PMM-15326: Mount OM in the PMM app and its navigation
Adds the OpenManager entry with its four children, and mounts OmApp as a splat since the plugin composes its own routes. Deliberately not part of addSepApps: those entries are gated as a group on SEP, and OM is served by pmm-managed over PMM's own data, so hiding it when SEP is off would hide a working page. For the same reason the chrome is OmPage rather than SepPage, whose SepAuthGate fails closed and would blank a page that has no SEP call on any browser path. OmPage keeps the PMM-admin restriction and enforces it on the route, because NavigationProvider only hides the entry while the route still matches on direct navigation. Signed-off-by: Pawel Lebioda <pawel.lebioda@percona.com>
1 parent 6209c16 commit 69628ee

7 files changed

Lines changed: 134 additions & 1 deletion

File tree

ui/apps/pmm/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@sep/api": "workspace:*",
3434
"@sep/framework": "workspace:*",
3535
"@sep/plugins-atw": "workspace:*",
36+
"@sep/plugins-om": "workspace:*",
3637
"@tanstack/react-query": "^5.100.7",
3738
"axios": "^1.13.5",
3839
"axios-case-converter": "^1.1.1",

ui/apps/pmm/src/contexts/navigation/navigation.provider.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
addUsersAndAccess,
1414
addHomePage,
1515
addSepApps,
16+
addOm,
1617
} from './navigation.utils';
1718
import { useUser } from 'contexts/user';
1819
import { useAdvisors } from 'hooks/api/useAdvisors';
@@ -94,6 +95,9 @@ export const NavigationProvider: FC<PropsWithChildren> = ({ children }) => {
9495
// is established; role/flag gating comes with real auth (Option B).
9596
items.push(...addSepApps());
9697

98+
// Served by pmm-managed, so it is not gated with the SEP group.
99+
items.push(...addOm());
100+
97101
if (settings.backupManagementEnabled) {
98102
items.push(NAV_BACKUPS);
99103
}

ui/apps/pmm/src/contexts/navigation/navigation.utils.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined';
22
import LightModeOutlined from '@mui/icons-material/LightModeOutlined';
33
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
4-
import { MySqlIcon } from '@percona/peak-ui';
4+
import { MongoIcon, MySqlIcon } from '@percona/peak-ui';
55
import { NavItem } from 'types/navigation.types';
66
import { ServiceType } from 'types/services.types';
77
import { User, UserPreferences } from 'types/user.types';
@@ -11,6 +11,7 @@ import {
1111
PMM_NEW_NAV_GRAFANA_PATH,
1212
SEP_ATW_PATH,
1313
SEP_MYSQL_BACKUPS_PATH,
14+
OM_PATH,
1415
} from 'lib/constants';
1516
import { ColorMode } from '@pmm/shared';
1617
import {
@@ -319,3 +320,47 @@ export const addSepApps = (): NavItem[] => [
319320
matches: [SEP_MYSQL_BACKUPS_PATH],
320321
},
321322
];
323+
324+
/**
325+
* OM's navigation, deliberately not part of `addSepApps`.
326+
*
327+
* Those entries are gated as a group on SEP, and the group is expected to gain a
328+
* flag gate with real auth. OM is served by pmm-managed and reads PMM's own data,
329+
* so hiding it when SEP is off or unreachable would hide a working page.
330+
*/
331+
export const addOm = (): NavItem[] => [
332+
{
333+
id: 'om',
334+
text: 'OpenManager',
335+
icon: MongoIcon,
336+
url: OM_PATH,
337+
matches: [OM_PATH],
338+
children: [
339+
{
340+
id: 'om-overview',
341+
text: 'Overview',
342+
url: OM_PATH,
343+
},
344+
{
345+
id: 'om-services',
346+
text: 'Services',
347+
url: `${OM_PATH}/services`,
348+
matches: [`${OM_PATH}/services`],
349+
},
350+
{
351+
// The page a host with no database appears on, which no other OM page can
352+
// show: it has no service to be listed through.
353+
id: 'om-hosts',
354+
text: 'Hosts',
355+
url: `${OM_PATH}/hosts`,
356+
matches: [`${OM_PATH}/hosts`],
357+
},
358+
{
359+
id: 'om-inventory',
360+
text: 'Inventory',
361+
url: `${OM_PATH}/inventory`,
362+
matches: [`${OM_PATH}/inventory`],
363+
},
364+
],
365+
},
366+
];

ui/apps/pmm/src/lib/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export const PMM_NEW_NAV_HOME_URL = `${PMM_NEW_NAV_PATH}/graph/d/pmm-home`;
2323
export const SEP_ATW_PATH = `${PMM_NEW_NAV_PATH}/sep/atw`;
2424
export const SEP_MYSQL_BACKUPS_PATH = `${PMM_NEW_NAV_PATH}/sep/mysql-backups`;
2525

26+
// OM (OpenManager) is a PMM page, not a SEP one. Its API is pmm-managed's own
27+
// `/v1/om`, authorised by the Grafana session, and no request it makes needs a SEP
28+
// bearer -- which is why it sits beside the SEP mounts above rather than under them.
29+
export const OM_PATH = `${PMM_NEW_NAV_PATH}/om`;
30+
2631
export const INTERVALS_MS = {
2732
// 5 mins
2833
SERVICE_TYPES: 300000,

ui/apps/pmm/src/om/OmPage.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright (C) 2026 Percona LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
import { FC, PropsWithChildren } from 'react';
19+
import Stack from '@mui/material/Stack';
20+
import { Page } from 'components/page';
21+
import { useUser } from 'contexts/user';
22+
import { OrgRole } from 'types/user.types';
23+
24+
/**
25+
* Host chrome for the OM page.
26+
*
27+
* Deliberately not `SepPage`, which OM used while its backend was a SEP app. That
28+
* wrapper holds its children behind `SepAuthGate` until a SEP bearer has been minted
29+
* from the PMM session, and fails closed — so a SEP that is down, unconfigured or
30+
* refusing the exchange would blank a page whose data comes from pmm-managed's own
31+
* inventory, VictoriaMetrics and stored snapshot. OM has no SEP call on any browser
32+
* path, so it must not inherit SEP's availability.
33+
*
34+
* The PMM-admin restriction is kept, and enforced here rather than left to the
35+
* sidebar: NavigationProvider only *hides* the entry for non-admins, while the route
36+
* still matches on direct navigation. The predicate is the nav's own — `isPMMAdmin` is
37+
* `isGrafanaAdmin || orgRole === Admin`, and `roles` (org-role only) cannot express the
38+
* Grafana-admin half on its own, so it gates the remaining case and `Page` renders its
39+
* standard unauthorized card.
40+
*/
41+
export const OmPage: FC<PropsWithChildren> = ({ children }) => {
42+
const { user } = useUser();
43+
44+
return (
45+
<Page
46+
maxWidth="full"
47+
roles={user?.isPMMAdmin ? undefined : [OrgRole.Admin]}
48+
>
49+
<Stack gap={3} sx={{ flex: 1 }}>
50+
<div>{children}</div>
51+
</Stack>
52+
</Page>
53+
);
54+
};

ui/apps/pmm/src/router.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ import {
1212
PMM_NEW_NAV_PATH,
1313
SEP_ATW_PATH,
1414
SEP_MYSQL_BACKUPS_PATH,
15+
OM_PATH,
1516
} from 'lib/constants';
1617
import { RealtimeSessionsPage } from 'pages/rta/sessions';
1718
import { Redirect, SettingsRedirect } from 'components/redirect';
1819
import RealtimeOverviewPage from 'pages/rta/overview/RealtimeOverview';
1920
import RealtimeTab from 'pages/rta/tab/RealtimeTab';
2021
import { AlertsPage } from 'pages/alerting/status';
2122
import { AtwApp } from '@sep/plugins-atw';
23+
import { OmApp } from '@sep/plugins-om';
24+
import { OmPage } from 'om/OmPage';
2225
import { SchemaDrivenPlugin } from '@sep/framework';
2326
import { SepPage } from './sep/SepPage';
2427

@@ -101,6 +104,24 @@ const router = createBrowserRouter(
101104
</SepPage>
102105
),
103106
},
107+
{
108+
// OM (OpenManager) is bespoke like ATW: OmApp composes its
109+
// own <Routes>, so this must be a splat.
110+
//
111+
// All four pages are under it now. Inventory used to be mounted
112+
// separately inside SepPage, because it read SEP's app directly and
113+
// needed a bearer minted from the PMM session; pmm-managed proxies the
114+
// estate at /v1/om/inventory, so no OM path in the browser talks to SEP
115+
// any more. That is why OmPage rather than SepPage, and why a sick SEP
116+
// now shows an error inside a page that still renders instead of
117+
// blanking it -- SepAuthGate fails closed.
118+
path: `${relativeToNav(OM_PATH)}/*`,
119+
element: (
120+
<OmPage>
121+
<OmApp />
122+
</OmPage>
123+
),
124+
},
104125
{
105126
path: `${relativeToNav(SEP_MYSQL_BACKUPS_PATH)}/*`,
106127
// routeBase must match the mount path (basename-stripped) so the

ui/pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)