From 97cd37acfc6dcda7e675dd673e4366fb9a98dc5a Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Thu, 19 Mar 2026 14:02:17 +0000 Subject: [PATCH 1/7] feat: "login as guest" --- .../app/core/components/Login/LoginCtrl.tsx | 25 ++++++++++++++++++- .../app/core/components/Login/LoginPage.tsx | 13 +++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/Login/LoginCtrl.tsx b/public/app/core/components/Login/LoginCtrl.tsx index 00a007b636018..d7917f9770a4d 100644 --- a/public/app/core/components/Login/LoginCtrl.tsx +++ b/public/app/core/components/Login/LoginCtrl.tsx @@ -52,6 +52,7 @@ interface Props { passwordHint: string; showDefaultPasswordWarning: boolean; loginErrorMessage: string | undefined; + pmmDemoCredentials: { username: string; password: string } | null; }) => JSX.Element; } @@ -60,6 +61,7 @@ interface State { isChangingPassword: boolean; showDefaultPasswordWarning: boolean; loginErrorMessage?: string; + pmmDemoCredentials: { username: string; password: string } | null; } export class LoginCtrl extends PureComponent { @@ -73,9 +75,15 @@ export class LoginCtrl extends PureComponent { showDefaultPasswordWarning: false, // oAuth unauthorized sets the redirect error message in the bootdata, hence we need to check the key here loginErrorMessage: getBootDataErrMessage(config.loginError), + pmmDemoCredentials: null, }; } + // @PERCONA + componentDidMount() { + this.getPmmDemoCredentials(); + } + changePassword = (password: string) => { const pw = { newPassword: password, @@ -245,9 +253,23 @@ export class LoginCtrl extends PureComponent { return appSubUrl + redirectUrl; }; + // @PERCONA + getPmmDemoCredentials = async (): Promise<{ username: string; password: string } | null> => { + try { + const response = await fetch('/v1/users/demo/credentials'); + const data = await response.json(); + this.setState({ pmmDemoCredentials: data }); + return data; + } catch (err) { + console.error('Error fetching PMM demo credentials', err); + this.setState({ pmmDemoCredentials: null }); + return null; + } + } + render() { const { children } = this.props; - const { isLoggingIn, isChangingPassword, showDefaultPasswordWarning, loginErrorMessage } = this.state; + const { isLoggingIn, isChangingPassword, showDefaultPasswordWarning, loginErrorMessage, pmmDemoCredentials } = this.state; const { login, toGrafana, changePassword, passwordlessStart, passwordlessConfirm } = this; const { loginHint, passwordHint, disableLoginForm, disableUserSignUp } = config; @@ -269,6 +291,7 @@ export class LoginCtrl extends PureComponent { isChangingPassword, showDefaultPasswordWarning, loginErrorMessage, + pmmDemoCredentials, })} ); diff --git a/public/app/core/components/Login/LoginPage.tsx b/public/app/core/components/Login/LoginPage.tsx index 0296e6dd803ff..5d29538726820 100644 --- a/public/app/core/components/Login/LoginPage.tsx +++ b/public/app/core/components/Login/LoginPage.tsx @@ -4,7 +4,7 @@ import { css } from '@emotion/css'; // Components import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { Alert, LinkButton, Stack, useStyles2 } from '@grafana/ui'; +import { Alert, Button, Divider, LinkButton, Stack, useStyles2, useTheme2 } from '@grafana/ui'; import { Branding } from 'app/core/components/Branding/Branding'; import { t, Trans } from 'app/core/internationalization'; @@ -20,6 +20,7 @@ import { UserSignup } from './UserSignup'; const LoginPage = () => { const styles = useStyles2(getStyles); + const theme = useTheme2(); document.title = Branding.AppTitle; return ( @@ -39,6 +40,7 @@ const LoginPage = () => { isChangingPassword, showDefaultPasswordWarning, loginErrorMessage, + pmmDemoCredentials, }) => ( {!isChangingPassword && !showPasswordlessConfirmation && ( @@ -67,6 +69,15 @@ const LoginPage = () => { {config.auth.passwordlessEnabled && ( )} + {/* @PERCONA */} + {pmmDemoCredentials && ( + <> + + + + )} {!disableUserSignUp && } From 68ef88df8b0efe5a28251481ecf3b516511b9136 Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Thu, 19 Mar 2026 14:21:27 +0000 Subject: [PATCH 2/7] chore: disable "login as guest" btn when loggin in already --- public/app/core/components/Login/LoginCtrl.tsx | 2 +- public/app/core/components/Login/LoginPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/Login/LoginCtrl.tsx b/public/app/core/components/Login/LoginCtrl.tsx index d7917f9770a4d..298d8cb783f20 100644 --- a/public/app/core/components/Login/LoginCtrl.tsx +++ b/public/app/core/components/Login/LoginCtrl.tsx @@ -265,7 +265,7 @@ export class LoginCtrl extends PureComponent { this.setState({ pmmDemoCredentials: null }); return null; } - } + }; render() { const { children } = this.props; diff --git a/public/app/core/components/Login/LoginPage.tsx b/public/app/core/components/Login/LoginPage.tsx index 5d29538726820..f570cf8b66640 100644 --- a/public/app/core/components/Login/LoginPage.tsx +++ b/public/app/core/components/Login/LoginPage.tsx @@ -73,7 +73,7 @@ const LoginPage = () => { {pmmDemoCredentials && ( <> - From 9da5737fe98cf95bd497a309a91a3455b07701c7 Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Thu, 19 Mar 2026 15:21:45 +0000 Subject: [PATCH 3/7] chore: prettier --- public/app/core/components/Login/LoginCtrl.tsx | 9 +++++---- public/app/core/components/Login/LoginPage.tsx | 9 ++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/public/app/core/components/Login/LoginCtrl.tsx b/public/app/core/components/Login/LoginCtrl.tsx index 298d8cb783f20..c5e2b2e2e40ec 100644 --- a/public/app/core/components/Login/LoginCtrl.tsx +++ b/public/app/core/components/Login/LoginCtrl.tsx @@ -79,7 +79,7 @@ export class LoginCtrl extends PureComponent { }; } - // @PERCONA + // @PERCONA componentDidMount() { this.getPmmDemoCredentials(); } @@ -253,9 +253,9 @@ export class LoginCtrl extends PureComponent { return appSubUrl + redirectUrl; }; - // @PERCONA + // @PERCONA getPmmDemoCredentials = async (): Promise<{ username: string; password: string } | null> => { - try { + try { const response = await fetch('/v1/users/demo/credentials'); const data = await response.json(); this.setState({ pmmDemoCredentials: data }); @@ -269,7 +269,8 @@ export class LoginCtrl extends PureComponent { render() { const { children } = this.props; - const { isLoggingIn, isChangingPassword, showDefaultPasswordWarning, loginErrorMessage, pmmDemoCredentials } = this.state; + const { isLoggingIn, isChangingPassword, showDefaultPasswordWarning, loginErrorMessage, pmmDemoCredentials } = + this.state; const { login, toGrafana, changePassword, passwordlessStart, passwordlessConfirm } = this; const { loginHint, passwordHint, disableLoginForm, disableUserSignUp } = config; diff --git a/public/app/core/components/Login/LoginPage.tsx b/public/app/core/components/Login/LoginPage.tsx index f570cf8b66640..bca6fd043fb25 100644 --- a/public/app/core/components/Login/LoginPage.tsx +++ b/public/app/core/components/Login/LoginPage.tsx @@ -73,7 +73,14 @@ const LoginPage = () => { {pmmDemoCredentials && ( <> - From 01084974ea1b02ad77727eb5a1e1103691b9a25b Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Thu, 19 Mar 2026 15:28:20 +0000 Subject: [PATCH 4/7] chore: unit tests --- .../core/components/Login/LoginPage.test.tsx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/Login/LoginPage.test.tsx b/public/app/core/components/Login/LoginPage.test.tsx index 0e2369b0a8d31..bdcb96ee93961 100644 --- a/public/app/core/components/Login/LoginPage.test.tsx +++ b/public/app/core/components/Login/LoginPage.test.tsx @@ -34,14 +34,20 @@ jest.mock('@grafana/runtime', () => ({ })); describe('Login Page', () => { + const originalFetch = global.fetch; + beforeEach(() => { jest.resetAllMocks(); + global.fetch = originalFetch; + }); + + afterAll(() => { + global.fetch = originalFetch; }); it('renders correctly', () => { render(); - expect(screen.getByRole('heading', { name: 'Welcome to Grafana' })).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: 'Email or username' })).toBeInTheDocument(); expect(screen.getByLabelText('Password')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument(); @@ -95,6 +101,30 @@ describe('Login Page', () => { expect(window.location.assign).toHaveBeenCalledWith('/'); }); + // @PERCONA + it('shows Log in as guest when PMM demo credentials are available', async () => { + global.fetch = jest.fn().mockResolvedValue({ + json: () => Promise.resolve({ username: 'guest', password: 'guest-demo' }), + } as unknown as Response); + + render(); + + const guestButton = await screen.findByRole('button', { name: 'Log in as guest' }); + expect(guestButton).toBeInTheDocument(); + expect(global.fetch).toHaveBeenCalledWith('/v1/users/demo/credentials'); + }); + + // @PERCONA + it('does not show Log in as guest when PMM demo credentials are not available', async () => { + global.fetch = jest.fn().mockRejectedValue(new Error('Not found')); + jest.spyOn(console, 'error').mockImplementation(() => {}); + + render(); + + const guestButton = await screen.queryByRole('button', { name: 'Log in as guest' }); + expect(guestButton).not.toBeInTheDocument(); + }); + it('renders social logins correctly', () => { runtimeMock.config.oauth = { okta: { From 80066d7278e5d2dd6db48b6fee9164f1f6d790c0 Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Tue, 24 Mar 2026 15:11:07 +0000 Subject: [PATCH 5/7] feat: use only frontend settings --- pkg/api/dtos/frontend_settings.go | 1 + pkg/api/frontendsettings.go | 1 + .../app/core/components/Login/LoginCtrl.tsx | 25 +--- .../core/components/Login/LoginPage.test.tsx | 24 ---- .../app/core/components/Login/LoginPage.tsx | 22 +-- .../PmmDemoAutoLogin/PmmDemoAutoLogin.tsx | 127 ++++++++++++++++++ 6 files changed, 132 insertions(+), 68 deletions(-) create mode 100644 public/app/percona/shared/components/PmmDemoAutoLogin/PmmDemoAutoLogin.tsx diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index a9581366b1328..7a100b7dbc8ac 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -222,6 +222,7 @@ type FrontendSettingsDTO struct { FeatureToggles map[string]bool `json:"featureToggles"` AnonymousEnabled bool `json:"anonymousEnabled"` + AnonymousOrgRole string `json:"anonymousOrgRole"` AnonymousDeviceLimit int64 `json:"anonymousDeviceLimit"` RendererAvailable bool `json:"rendererAvailable"` RendererVersion string `json:"rendererVersion"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index ac7086f0a2c34..a8cc3774375f8 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -274,6 +274,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro FeatureToggles: featureToggles, AnonymousEnabled: hs.Cfg.Anonymous.Enabled, + AnonymousOrgRole: hs.Cfg.Anonymous.OrgRole, AnonymousDeviceLimit: hs.Cfg.Anonymous.DeviceLimit, RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()), RendererVersion: hs.RenderService.Version(), diff --git a/public/app/core/components/Login/LoginCtrl.tsx b/public/app/core/components/Login/LoginCtrl.tsx index c5e2b2e2e40ec..3acb86a69420e 100644 --- a/public/app/core/components/Login/LoginCtrl.tsx +++ b/public/app/core/components/Login/LoginCtrl.tsx @@ -52,7 +52,6 @@ interface Props { passwordHint: string; showDefaultPasswordWarning: boolean; loginErrorMessage: string | undefined; - pmmDemoCredentials: { username: string; password: string } | null; }) => JSX.Element; } @@ -61,7 +60,6 @@ interface State { isChangingPassword: boolean; showDefaultPasswordWarning: boolean; loginErrorMessage?: string; - pmmDemoCredentials: { username: string; password: string } | null; } export class LoginCtrl extends PureComponent { @@ -75,15 +73,9 @@ export class LoginCtrl extends PureComponent { showDefaultPasswordWarning: false, // oAuth unauthorized sets the redirect error message in the bootdata, hence we need to check the key here loginErrorMessage: getBootDataErrMessage(config.loginError), - pmmDemoCredentials: null, }; } - // @PERCONA - componentDidMount() { - this.getPmmDemoCredentials(); - } - changePassword = (password: string) => { const pw = { newPassword: password, @@ -253,23 +245,9 @@ export class LoginCtrl extends PureComponent { return appSubUrl + redirectUrl; }; - // @PERCONA - getPmmDemoCredentials = async (): Promise<{ username: string; password: string } | null> => { - try { - const response = await fetch('/v1/users/demo/credentials'); - const data = await response.json(); - this.setState({ pmmDemoCredentials: data }); - return data; - } catch (err) { - console.error('Error fetching PMM demo credentials', err); - this.setState({ pmmDemoCredentials: null }); - return null; - } - }; - render() { const { children } = this.props; - const { isLoggingIn, isChangingPassword, showDefaultPasswordWarning, loginErrorMessage, pmmDemoCredentials } = + const { isLoggingIn, isChangingPassword, showDefaultPasswordWarning, loginErrorMessage } = this.state; const { login, toGrafana, changePassword, passwordlessStart, passwordlessConfirm } = this; const { loginHint, passwordHint, disableLoginForm, disableUserSignUp } = config; @@ -292,7 +270,6 @@ export class LoginCtrl extends PureComponent { isChangingPassword, showDefaultPasswordWarning, loginErrorMessage, - pmmDemoCredentials, })} ); diff --git a/public/app/core/components/Login/LoginPage.test.tsx b/public/app/core/components/Login/LoginPage.test.tsx index bdcb96ee93961..4ca2b1e937107 100644 --- a/public/app/core/components/Login/LoginPage.test.tsx +++ b/public/app/core/components/Login/LoginPage.test.tsx @@ -101,30 +101,6 @@ describe('Login Page', () => { expect(window.location.assign).toHaveBeenCalledWith('/'); }); - // @PERCONA - it('shows Log in as guest when PMM demo credentials are available', async () => { - global.fetch = jest.fn().mockResolvedValue({ - json: () => Promise.resolve({ username: 'guest', password: 'guest-demo' }), - } as unknown as Response); - - render(); - - const guestButton = await screen.findByRole('button', { name: 'Log in as guest' }); - expect(guestButton).toBeInTheDocument(); - expect(global.fetch).toHaveBeenCalledWith('/v1/users/demo/credentials'); - }); - - // @PERCONA - it('does not show Log in as guest when PMM demo credentials are not available', async () => { - global.fetch = jest.fn().mockRejectedValue(new Error('Not found')); - jest.spyOn(console, 'error').mockImplementation(() => {}); - - render(); - - const guestButton = await screen.queryByRole('button', { name: 'Log in as guest' }); - expect(guestButton).not.toBeInTheDocument(); - }); - it('renders social logins correctly', () => { runtimeMock.config.oauth = { okta: { diff --git a/public/app/core/components/Login/LoginPage.tsx b/public/app/core/components/Login/LoginPage.tsx index bca6fd043fb25..deec41fee7276 100644 --- a/public/app/core/components/Login/LoginPage.tsx +++ b/public/app/core/components/Login/LoginPage.tsx @@ -4,7 +4,7 @@ import { css } from '@emotion/css'; // Components import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { Alert, Button, Divider, LinkButton, Stack, useStyles2, useTheme2 } from '@grafana/ui'; +import { Alert, LinkButton, Stack, useStyles2 } from '@grafana/ui'; import { Branding } from 'app/core/components/Branding/Branding'; import { t, Trans } from 'app/core/internationalization'; @@ -20,7 +20,6 @@ import { UserSignup } from './UserSignup'; const LoginPage = () => { const styles = useStyles2(getStyles); - const theme = useTheme2(); document.title = Branding.AppTitle; return ( @@ -39,8 +38,7 @@ const LoginPage = () => { skipPasswordChange, isChangingPassword, showDefaultPasswordWarning, - loginErrorMessage, - pmmDemoCredentials, + loginErrorMessage }) => ( {!isChangingPassword && !showPasswordlessConfirmation && ( @@ -69,22 +67,6 @@ const LoginPage = () => { {config.auth.passwordlessEnabled && ( )} - {/* @PERCONA */} - {pmmDemoCredentials && ( - <> - - - - )} {!disableUserSignUp && } diff --git a/public/app/percona/shared/components/PmmDemoAutoLogin/PmmDemoAutoLogin.tsx b/public/app/percona/shared/components/PmmDemoAutoLogin/PmmDemoAutoLogin.tsx new file mode 100644 index 0000000000000..03e4d2304088e --- /dev/null +++ b/public/app/percona/shared/components/PmmDemoAutoLogin/PmmDemoAutoLogin.tsx @@ -0,0 +1,127 @@ +import { useEffect, useRef } from 'react'; +import { useLocation } from 'react-router-dom-v5-compat'; + +import { locationUtil } from '@grafana/data'; +import { getBackendSrv } from '@grafana/runtime'; +import { LoginDTO } from 'app/core/components/Login/types'; +import config from 'app/core/config'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getPmmAppSubUrl, isPmmNavEnabled } from 'app/percona/shared/helpers/plugin'; + +/** + * True when Grafana is configured for anonymous access (`auth.anonymous` → boot settings). + * Mirrors the anonymous check in `getSearchResultActions` (command palette): do not treat the user + * as needing credential-based access when anonymous mode already applies. + */ +export function isAnonymousAuthEnabled(): boolean { + return Boolean(config.anonymousEnabled || config.bootData.settings.anonymousEnabled); +} + +/** + * When PMM demo credentials exist, log in automatically only from the app home path. + * Skipped when anonymous auth is on (Grafana already provides access without credential login). + * Running this on the login page would re-authenticate users immediately after logout. + */ +export function PmmDemoAutoLogin() { + const location = useLocation(); + const startedRef = useRef(false); + + useEffect(() => { + if ( + startedRef.current || + contextSrv.isSignedIn || + isAnonymousAuthEnabled() || + !isAppHomePath(location.pathname) + ) { + return; + } + startedRef.current = true; + + let cancelled = false; + + (async () => { + try { + const response = await fetch('/v1/users/demo/credentials'); + const data = await response.json(); + if (cancelled || !data?.username || !data?.password) { + return; + } + + const formModel = { user: data.username, password: data.password, email: '' }; + const loginResult = await getBackendSrv().post('/login', formModel, { showErrorAlert: false }); + if (cancelled) { + return; + } + + redirectAfterDemoLogin(loginResult); + } catch { + // Demo mode unavailable or login failed — stay on the page. + } + })(); + + return () => { + cancelled = true; + }; + }, [location.pathname]); + + return null; +} + +function isAppHomePath(pathname: string): boolean { + const stripped = locationUtil.stripBaseFromUrl(pathname); + if (stripped === '' || stripped === '/') { + return true; + } + return stripped.replace(/\/$/, '') === ''; +} + +function redirectAfterDemoLogin(result: LoginDTO | undefined) { + if (isPmmNavEnabled()) { + const appSubUrl = getPmmAppSubUrl(); + + if (config.featureToggles.useSessionStorageForRedirection) { + window.location.assign(appSubUrl + '/'); + return; + } + + if (result?.redirectUrl) { + window.location.assign(normalizePmmRedirectUrl(result.redirectUrl)); + } else { + window.location.assign(appSubUrl + '/'); + } + return; + } + + if (config.featureToggles.useSessionStorageForRedirection) { + window.location.assign(config.appSubUrl + '/'); + return; + } + + if (result?.redirectUrl) { + if (config.appSubUrl !== '' && !result.redirectUrl.startsWith(config.appSubUrl)) { + window.location.assign(config.appSubUrl + result.redirectUrl); + } else { + window.location.assign(result.redirectUrl); + } + } else { + window.location.assign(config.appSubUrl + '/'); + } +} + +function normalizePmmRedirectUrl(redirectUrl: string) { + const appSubUrl = getPmmAppSubUrl(); + + if (!redirectUrl) { + return redirectUrl; + } + + if (redirectUrl.startsWith(appSubUrl)) { + return redirectUrl; + } + + if (redirectUrl.startsWith(config.appSubUrl)) { + return redirectUrl.replace(config.appSubUrl, appSubUrl); + } + + return appSubUrl + redirectUrl; +} From 21f6d96c0ff34f86c409708c9bf2891dd38861af Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Wed, 25 Mar 2026 10:42:52 +0000 Subject: [PATCH 6/7] chore: add @Percona comment --- pkg/api/dtos/frontend_settings.go | 5 +- pkg/api/frontendsettings.go | 5 +- .../PmmDemoAutoLogin/PmmDemoAutoLogin.tsx | 127 ------------------ 3 files changed, 6 insertions(+), 131 deletions(-) delete mode 100644 public/app/percona/shared/components/PmmDemoAutoLogin/PmmDemoAutoLogin.tsx diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 7a100b7dbc8ac..6bbb7bf52d161 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -220,8 +220,9 @@ type FrontendSettingsDTO struct { LicenseInfo FrontendSettingsLicenseInfoDTO `json:"licenseInfo"` - FeatureToggles map[string]bool `json:"featureToggles"` - AnonymousEnabled bool `json:"anonymousEnabled"` + FeatureToggles map[string]bool `json:"featureToggles"` + AnonymousEnabled bool `json:"anonymousEnabled"` + // @PERCONA: add anonymousOrgRole to the frontend settings AnonymousOrgRole string `json:"anonymousOrgRole"` AnonymousDeviceLimit int64 `json:"anonymousDeviceLimit"` RendererAvailable bool `json:"rendererAvailable"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index a8cc3774375f8..78cf64e2a8529 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -272,8 +272,9 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro EnabledFeatures: hs.License.EnabledFeatures(), }, - FeatureToggles: featureToggles, - AnonymousEnabled: hs.Cfg.Anonymous.Enabled, + FeatureToggles: featureToggles, + AnonymousEnabled: hs.Cfg.Anonymous.Enabled, + // @PERCONA: add anonymousOrgRole to the frontend settings AnonymousOrgRole: hs.Cfg.Anonymous.OrgRole, AnonymousDeviceLimit: hs.Cfg.Anonymous.DeviceLimit, RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()), diff --git a/public/app/percona/shared/components/PmmDemoAutoLogin/PmmDemoAutoLogin.tsx b/public/app/percona/shared/components/PmmDemoAutoLogin/PmmDemoAutoLogin.tsx deleted file mode 100644 index 03e4d2304088e..0000000000000 --- a/public/app/percona/shared/components/PmmDemoAutoLogin/PmmDemoAutoLogin.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { useLocation } from 'react-router-dom-v5-compat'; - -import { locationUtil } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; -import { LoginDTO } from 'app/core/components/Login/types'; -import config from 'app/core/config'; -import { contextSrv } from 'app/core/services/context_srv'; -import { getPmmAppSubUrl, isPmmNavEnabled } from 'app/percona/shared/helpers/plugin'; - -/** - * True when Grafana is configured for anonymous access (`auth.anonymous` → boot settings). - * Mirrors the anonymous check in `getSearchResultActions` (command palette): do not treat the user - * as needing credential-based access when anonymous mode already applies. - */ -export function isAnonymousAuthEnabled(): boolean { - return Boolean(config.anonymousEnabled || config.bootData.settings.anonymousEnabled); -} - -/** - * When PMM demo credentials exist, log in automatically only from the app home path. - * Skipped when anonymous auth is on (Grafana already provides access without credential login). - * Running this on the login page would re-authenticate users immediately after logout. - */ -export function PmmDemoAutoLogin() { - const location = useLocation(); - const startedRef = useRef(false); - - useEffect(() => { - if ( - startedRef.current || - contextSrv.isSignedIn || - isAnonymousAuthEnabled() || - !isAppHomePath(location.pathname) - ) { - return; - } - startedRef.current = true; - - let cancelled = false; - - (async () => { - try { - const response = await fetch('/v1/users/demo/credentials'); - const data = await response.json(); - if (cancelled || !data?.username || !data?.password) { - return; - } - - const formModel = { user: data.username, password: data.password, email: '' }; - const loginResult = await getBackendSrv().post('/login', formModel, { showErrorAlert: false }); - if (cancelled) { - return; - } - - redirectAfterDemoLogin(loginResult); - } catch { - // Demo mode unavailable or login failed — stay on the page. - } - })(); - - return () => { - cancelled = true; - }; - }, [location.pathname]); - - return null; -} - -function isAppHomePath(pathname: string): boolean { - const stripped = locationUtil.stripBaseFromUrl(pathname); - if (stripped === '' || stripped === '/') { - return true; - } - return stripped.replace(/\/$/, '') === ''; -} - -function redirectAfterDemoLogin(result: LoginDTO | undefined) { - if (isPmmNavEnabled()) { - const appSubUrl = getPmmAppSubUrl(); - - if (config.featureToggles.useSessionStorageForRedirection) { - window.location.assign(appSubUrl + '/'); - return; - } - - if (result?.redirectUrl) { - window.location.assign(normalizePmmRedirectUrl(result.redirectUrl)); - } else { - window.location.assign(appSubUrl + '/'); - } - return; - } - - if (config.featureToggles.useSessionStorageForRedirection) { - window.location.assign(config.appSubUrl + '/'); - return; - } - - if (result?.redirectUrl) { - if (config.appSubUrl !== '' && !result.redirectUrl.startsWith(config.appSubUrl)) { - window.location.assign(config.appSubUrl + result.redirectUrl); - } else { - window.location.assign(result.redirectUrl); - } - } else { - window.location.assign(config.appSubUrl + '/'); - } -} - -function normalizePmmRedirectUrl(redirectUrl: string) { - const appSubUrl = getPmmAppSubUrl(); - - if (!redirectUrl) { - return redirectUrl; - } - - if (redirectUrl.startsWith(appSubUrl)) { - return redirectUrl; - } - - if (redirectUrl.startsWith(config.appSubUrl)) { - return redirectUrl.replace(config.appSubUrl, appSubUrl); - } - - return appSubUrl + redirectUrl; -} From a4afedf886a5cc9389017e1eb8f4440cac80ccaf Mon Sep 17 00:00:00 2001 From: Fabio Silva Date: Wed, 25 Mar 2026 10:44:59 +0000 Subject: [PATCH 7/7] chore: remove test changes --- public/app/core/components/Login/LoginPage.test.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/public/app/core/components/Login/LoginPage.test.tsx b/public/app/core/components/Login/LoginPage.test.tsx index 4ca2b1e937107..b1aee71c00c66 100644 --- a/public/app/core/components/Login/LoginPage.test.tsx +++ b/public/app/core/components/Login/LoginPage.test.tsx @@ -34,15 +34,9 @@ jest.mock('@grafana/runtime', () => ({ })); describe('Login Page', () => { - const originalFetch = global.fetch; beforeEach(() => { jest.resetAllMocks(); - global.fetch = originalFetch; - }); - - afterAll(() => { - global.fetch = originalFetch; }); it('renders correctly', () => {