diff --git a/ui/apps/pmm/src/contexts/navigation/navigation.provider.tsx b/ui/apps/pmm/src/contexts/navigation/navigation.provider.tsx index 8b78a26306d..90e57967ac3 100644 --- a/ui/apps/pmm/src/contexts/navigation/navigation.provider.tsx +++ b/ui/apps/pmm/src/contexts/navigation/navigation.provider.tsx @@ -101,8 +101,11 @@ export const NavigationProvider: FC = ({ children }) => { // is established; role/flag gating comes with real auth (Option B). items.push(...addSepApps()); - // Served by pmm-managed, so it is not gated with the SEP group. - items.push(...addOm()); + // Served by pmm-managed, so it is not gated with the SEP group -- gated + // on the settings flag alone. + if (settings?.omEnabled) { + items.push(...addOm()); + } if (settings?.backupManagementEnabled) { items.push(NAV_BACKUPS); diff --git a/ui/apps/pmm/src/contexts/settings/settings.provider.tsx b/ui/apps/pmm/src/contexts/settings/settings.provider.tsx index 2926b7ff38c..662fa19700c 100644 --- a/ui/apps/pmm/src/contexts/settings/settings.provider.tsx +++ b/ui/apps/pmm/src/contexts/settings/settings.provider.tsx @@ -38,6 +38,7 @@ export const SettingsProvider: FC = ({ children }) => { backupManagementEnabled: false, azurediscoverEnabled: false, enableAccessControl: false, + omEnabled: false, frontend: frontendSettings.data, // check if pmm-compat-app plugin is enabled newUIEnabled: frontendSettings.data.apps['pmm-compat-app']?.preload, diff --git a/ui/apps/pmm/src/om/OmPage.tsx b/ui/apps/pmm/src/om/OmPage.tsx index 53c4d9ef5b7..a6c30261c8e 100644 --- a/ui/apps/pmm/src/om/OmPage.tsx +++ b/ui/apps/pmm/src/om/OmPage.tsx @@ -16,11 +16,27 @@ */ import { FC, PropsWithChildren } from 'react'; +import Alert from '@mui/material/Alert'; +import Card from '@mui/material/Card'; +import CircularProgress from '@mui/material/CircularProgress'; import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; import { Page } from 'components/page'; import { useUser } from 'contexts/user'; +import { useReadonlySettings } from 'hooks/api/useSettings'; +import { useLocalStorage } from 'hooks/utils/useLocalStorage'; import { OrgRole } from 'types/user.types'; +const TECHNICAL_PREVIEW_DISMISSED_KEY = 'pmm-ui.om.technicalPreviewDismissed'; + +const Messages = { + switchedOff: + 'OpenManager is switched off. A PMM admin can turn it on in Configuration → Settings → Advanced Settings.', + technicalPreview: 'Technical preview', + technicalPreviewBody: + 'OpenManager is a technical preview. It is still under development and may change.', +}; + /** * Host chrome for the OM page. * @@ -37,9 +53,31 @@ import { OrgRole } from 'types/user.types'; * `isGrafanaAdmin || orgRole === Admin`, and `roles` (org-role only) cannot express the * Grafana-admin half on its own, so it gates the remaining case and `Page` renders its * standard unauthorized card. + * + * The settings gate is enforced here for the same reason: a saved or shared link to + * this page has to answer "switched off", not the API's raw FailedPrecondition, and + * not the unauthorized card above, which would misreport a disabled feature as a + * permissions problem to an admin who has every right to be here (PMM-15360 AC1/AC2/AC7). + * + * The technical-preview banner is dismissible, and remembered per browser rather than + * per PMM account or installation -- it is a "you've seen this" acknowledgement, not a + * setting with a right answer for every viewer, so localStorage is enough and needs no + * round trip to pmm-managed. + * + * The close button's `sx` override exists because `@percona/peak-ui`'s MuiAlert theme + * (`styleOverrides.icon`/`.message`) sets `color: theme.palette[severity].contrastText` + * on the icon and message slots, but not on `.MuiAlert-action` -- so the close button + * MUI renders for `onClose` falls back to the alert root's own `color`, which this + * theme leaves close to the warning background itself. Nothing else in this app uses a + * dismissible Alert, which is presumably why that gap was never hit before. */ export const OmPage: FC = ({ children }) => { const { user } = useUser(); + const { data: settings, isLoading } = useReadonlySettings(); + const [previewDismissed, setPreviewDismissed] = useLocalStorage( + TECHNICAL_PREVIEW_DISMISSED_KEY, + false + ); return ( = ({ children }) => { roles={user?.isPMMAdmin ? undefined : [OrgRole.Admin]} > -
{children}
+ {isLoading ? ( + + + + ) : settings?.omEnabled ? ( + <> + {!previewDismissed && ( + setPreviewDismissed(true)} + data-testid="om-technical-preview" + sx={{ + '& .MuiAlert-action': { + color: (theme) => theme.palette.warning.contrastText, + }, + }} + > + + {Messages.technicalPreview}{' '} + {Messages.technicalPreviewBody} + + + )} +
{children}
+ + ) : ( + + + {Messages.switchedOff} + + + )}
);