Skip to content
Draft
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
16 changes: 14 additions & 2 deletions ui/apps/pmm/src/contexts/grafana/grafana.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useLocation, useNavigate, useNavigationType } from 'react-router';
import { GrafanaContext } from './grafana.context';
import {
GRAFANA_SUB_PATH,
PMM_BASE_PATH,
PMM_NEW_NAV_GRAFANA_PATH,
PMM_NEW_NAV_PATH,
} from 'lib/constants';
Expand All @@ -15,7 +16,7 @@ import type {
import { updateDocumentTitle } from 'utils/document.utils';
import { useKioskMode } from 'hooks/utils/useKioskMode';
import { useColorMode } from 'hooks/theme';
import { getLocationUrl } from './grafana.utils';
import { getLocationUrl, isGrafanaLoginPath } from './grafana.utils';
import messenger from 'lib/messenger';
import { useSettings } from 'hooks/api/useSettings';
import { useServiceTypes } from 'hooks/api/useServices';
Expand Down Expand Up @@ -93,7 +94,18 @@ export const GrafanaProvider: FC<PropsWithChildren> = ({ children }) => {
return;
}

navigate(getLocationUrl(location), {
const nextPath = getLocationUrl(location);

// Full top-level navigation: SPA route still mounts MainWithNav for /graph/*,
// which would keep the PMM sidebar visible next to Grafana login (PMM-14971).
if (isGrafanaLoginPath(nextPath)) {
window.location.replace(
new URL(`${PMM_BASE_PATH}${nextPath}`, window.location.origin).href
);
return;
}

navigate(nextPath, {
state: { fromGrafana: true },
replace: true,
});
Expand Down
46 changes: 46 additions & 0 deletions ui/apps/pmm/src/contexts/grafana/grafana.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest';
import { getLocationUrl, isGrafanaLoginPath } from './grafana.utils';

describe('getLocationUrl', () => {
it('maps Grafana-relative /login to /graph/login', () => {
expect(
getLocationUrl({
pathname: '/login',
search: '',
hash: '',
})
).toBe('/graph/login');
});

it('keeps /graph/login when Grafana already uses full graph path', () => {
expect(
getLocationUrl({
pathname: '/graph/login',
search: '',
hash: '',
})
).toBe('/graph/login');
});

it('strips /pmm-ui prefix when present', () => {
expect(
getLocationUrl({
pathname: '/pmm-ui/graph/d/x',
search: '?a=1',
hash: '',
})
).toBe('/graph/d/x?a=1');
});
});

describe('isGrafanaLoginPath', () => {
it('detects /graph/login', () => {
expect(isGrafanaLoginPath('/graph/login')).toBe(true);
expect(isGrafanaLoginPath('/graph/login?redirect=/')).toBe(true);
});

it('rejects other graph routes', () => {
expect(isGrafanaLoginPath('/graph/d/pmm-home')).toBe(false);
expect(isGrafanaLoginPath('/graph/admin/users')).toBe(false);
});
});
25 changes: 20 additions & 5 deletions ui/apps/pmm/src/contexts/grafana/grafana.utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { PMM_NEW_NAV_GRAFANA_PATH, PMM_NEW_NAV_PATH } from 'lib/constants';
import {
GRAFANA_SUB_PATH,
PMM_LOGIN_URL,
PMM_NEW_NAV_GRAFANA_PATH,
PMM_NEW_NAV_PATH,
} from 'lib/constants';
import { Path } from 'react-router-dom';

/** True when the PMM URL (pathname + optional search/hash) is the Grafana login route. */
export const isGrafanaLoginPath = (pathnameSearchHash: string) => {
const path = pathnameSearchHash.split(/[?#]/)[0].replace(/\/$/, '') || '/';
return path === PMM_LOGIN_URL || path.endsWith(PMM_LOGIN_URL);
};

export const getLocationUrl = (location: Path) => {
const pathname = location.pathname.startsWith('/pmm-ui')
? location.pathname.replace('/pmm-ui', PMM_NEW_NAV_PATH)
: PMM_NEW_NAV_GRAFANA_PATH + location.pathname;
const { pathname: rawPath, search, hash } = location;

const pathname = rawPath.startsWith('/pmm-ui')
? rawPath.replace('/pmm-ui', PMM_NEW_NAV_PATH)
: rawPath.startsWith(GRAFANA_SUB_PATH)
? rawPath
: PMM_NEW_NAV_GRAFANA_PATH + rawPath;

return pathname + location.search + location.hash;
return pathname + search + hash;
};
33 changes: 33 additions & 0 deletions ui/apps/pmm/src/pages/grafana/GrafanaFullPageLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CircularProgress, Stack } from '@mui/material';
import { GrafanaPage } from './GrafanaPage';
import { useBootstrap } from 'hooks/utils/useBootstrap';

/**
* Full-viewport Grafana iframe without PMM shell (sidebar/header).
* Used for /graph/login so session expiry after password change does not show duplicate nav (PMM-14971).
*/
export const GrafanaFullPageLogin = () => {
const { isReady } = useBootstrap();

if (!isReady) {
return (
<Stack
alignItems="center"
justifyContent="center"
sx={{
flex: 1,
minHeight: '100vh',
padding: 10,
}}
>
<CircularProgress data-testid="pmm-grafana-login-fullpage-loading" />
</Stack>
);
}

return (
<Stack direction="column" flex={1} sx={{ minHeight: '100vh' }}>
<GrafanaPage />
</Stack>
);
};
1 change: 1 addition & 0 deletions ui/apps/pmm/src/pages/grafana/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { GrafanaPage } from './GrafanaPage';
export { GrafanaFullPageLogin } from './GrafanaFullPageLogin';
5 changes: 5 additions & 0 deletions ui/apps/pmm/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ import { RealtimeSessionsPage } from 'pages/rta/sessions';
import { Redirect } from 'components/redirect';
import RealtimeOverviewPage from 'pages/rta/overview/RealtimeOverview';
import RealtimeTab from 'pages/rta/tab/RealtimeTab';
import { GrafanaFullPageLogin } from 'pages/grafana';

const router = createBrowserRouter(
[
{
path: '',
element: <Providers />,
children: [
{
path: 'graph/login',
element: <GrafanaFullPageLogin />,
},
{
path: PMM_NEW_NAV_PATH,
element: <MainWithNav />,
Expand Down
Loading