From 2341a91937d2ed63da5896a84dc19959d953feee Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 18 Dec 2025 13:09:26 +0100 Subject: [PATCH 1/3] PMM-14651 Improve & fix dashboard menu variables sharing --- ui/apps/pmm-compat/src/lib/utils/variables.ts | 21 +++++++++++++-- .../components/sidebar/nav-item/NavItem.tsx | 18 +++++-------- .../navigation/navigation.constants.ts | 26 +++---------------- ui/apps/pmm/src/types/navigation.types.ts | 1 - 4 files changed, 28 insertions(+), 38 deletions(-) diff --git a/ui/apps/pmm-compat/src/lib/utils/variables.ts b/ui/apps/pmm-compat/src/lib/utils/variables.ts index 28441337cb..0d2f386eda 100644 --- a/ui/apps/pmm-compat/src/lib/utils/variables.ts +++ b/ui/apps/pmm-compat/src/lib/utils/variables.ts @@ -43,13 +43,30 @@ export const getLinkWithVariables = (url?: string): string => { const isDashboardUrl = (url?: string) => url?.includes('/d/'); const checkDbType = (url: string): boolean => { - const currentDB = window.location.pathname?.split('/')[3]?.split('-')[0]; - const targetDB = url?.split('/')[3]?.split('-')[0]; + const currentDB = getDbType(window.location.pathname); + const targetDB = getDbType(url); // enable variable sharing between same db types and db type -> os/node return (currentDB !== undefined && currentDB === targetDB) || targetDB === 'node'; }; +const getDbType = (url: string): string => { + const pathname = new URL(url, window.location.origin).pathname; + // normalize to the dashboard uid + const dashboardUid = pathname + .replace('/pmm-ui', '') + .replace('/next', '') + .replace('/graph', '') + .replace('/d/', '') + .split('/')[0]; + + if (dashboardUid.includes('-')) { + return dashboardUid.split('-')[0]; + } + + return dashboardUid; +}; + const cleanupVariables = (urlWithLinks: string) => { const [base, params] = urlWithLinks.split('?'); diff --git a/ui/apps/pmm/src/components/sidebar/nav-item/NavItem.tsx b/ui/apps/pmm/src/components/sidebar/nav-item/NavItem.tsx index 36e230c3d7..bfd3453338 100644 --- a/ui/apps/pmm/src/components/sidebar/nav-item/NavItem.tsx +++ b/ui/apps/pmm/src/components/sidebar/nav-item/NavItem.tsx @@ -1,6 +1,5 @@ import { useLinkWithVariables } from 'hooks/utils/useLinkWithVariables'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; import { NavItemProps } from './NavItem.types'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import { getLinkProps, hasChildMatch, shouldShowBadge } from './NavItem.utils'; @@ -33,13 +32,13 @@ const NavItem: FC = ({ [activeItem, item] ); const [open, setIsOpen] = useState(active); - const url = useLinkWithVariables(item.url); + const url = useLinkWithVariables( + item.children?.length ? item.children[0].url : item.url + ); const linkProps = getLinkProps(item, url); const theme = useTheme(); const styles = getStyles(theme, active, drawerOpen, level); - const children = item.children?.filter((i) => !i.hidden); const dataTestid = `navitem-${item.id}`; - const navigate = useNavigate(); const showBadge = shouldShowBadge(item, open); useEffect(() => { @@ -59,19 +58,13 @@ const NavItem: FC = ({ }, [drawerOpen]); const handleOpenCollapsible = () => { - const firstChild = (item.children || [])[0]; - // prevent opening when sidebar collapsed if (drawerOpen) { setIsOpen(true); } - - if (firstChild?.url) { - navigate(firstChild.url); - } }; - if (children?.length) { + if (item.children?.length) { return ( <> = ({ level === 0 && styles.navItemRootCollapsible, ]} onClick={handleOpenCollapsible} + {...linkProps} data-testid={dataTestid} data-navlevel={level} > @@ -141,7 +135,7 @@ const NavItem: FC = ({ data-testid={`${dataTestid}-collapse`} > - {children.map((item) => ( + {item.children.map((item) => ( void; - hidden?: boolean; badge?: ChipProps; matches?: string[]; } From e37ddf80e0478a176b20cdade3b6f2a0ca2f8206 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Fri, 19 Dec 2025 09:20:42 +0100 Subject: [PATCH 2/3] PMM-14651 Add unit tests --- .../src/lib/utils/variables.test.ts | 76 +++++++++++++++++++ ui/apps/pmm-compat/src/lib/utils/variables.ts | 4 +- 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 ui/apps/pmm-compat/src/lib/utils/variables.test.ts diff --git a/ui/apps/pmm-compat/src/lib/utils/variables.test.ts b/ui/apps/pmm-compat/src/lib/utils/variables.test.ts new file mode 100644 index 0000000000..75067e2ad4 --- /dev/null +++ b/ui/apps/pmm-compat/src/lib/utils/variables.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, beforeEach, jest } from '@jest/globals'; +import { getLinkWithVariables, shouldIncludeVars } from './variables'; + +const prefixes = { + grafana: '/graph', + pmm: '/pmm-ui/next', +}; + +const dashboards = { + pg: '/d/postgresql-instance-overview/postgresql-instances-overview', + pgSummary: '/d/postgresql-instance-summary/postgresql-instance-summary', + mysql: '/d/mysql-instance-summary/mysql-instance-summary', + node: '/d/node-overview/node-overview', +}; + +const mockLocation = (pathname: string) => { + Object.defineProperty(window, 'location', { + value: { + pathname, + origin: 'https://percona.com', + }, + writable: true, + }); +}; + +describe('getLinkWithVariables', () => { + beforeEach(() => { + mockLocation('/percona.com'); + }); + + it('should return the same url if it is not a dashboard url', () => { + const url = 'https://percona.com'; + const result = getLinkWithVariables(url); + expect(result).toBe(url); + }); +}); + +describe('shouldIncludeVars', () => { + it('should handle different prefixes', () => { + const urls = [ + dashboards.pg, + `${prefixes.grafana}${dashboards.pg}`, + `${prefixes.pmm}${prefixes.grafana}${dashboards.pg}`, + ]; + + urls.forEach((url) => { + mockLocation(dashboards.pg); + const result = shouldIncludeVars(url); + expect(result).toBe(true); + }); + }); + + it('should return true if the db type matches the current one', () => { + mockLocation(dashboards.pg); + const result = shouldIncludeVars(dashboards.pgSummary); + expect(result).toBe(true); + }); + + it('should return true if the target db type is node', () => { + mockLocation(dashboards.pg); + const result = shouldIncludeVars(dashboards.node); + expect(result).toBe(true); + }); + + it('should return false if the target db type is not the same as the current one', () => { + mockLocation(dashboards.pg); + const result = shouldIncludeVars(dashboards.mysql); + expect(result).toBe(false); + }); + + it('should return false if current db type is node and target db type is not node', () => { + mockLocation(dashboards.node); + const result = shouldIncludeVars(dashboards.pg); + expect(result).toBe(false); + }); +}); diff --git a/ui/apps/pmm-compat/src/lib/utils/variables.ts b/ui/apps/pmm-compat/src/lib/utils/variables.ts index 0d2f386eda..839b19b271 100644 --- a/ui/apps/pmm-compat/src/lib/utils/variables.ts +++ b/ui/apps/pmm-compat/src/lib/utils/variables.ts @@ -25,7 +25,7 @@ export const getLinkWithVariables = (url?: string): string => { url: url, keepTime: true, // Check if the DB type matches the current one used - includeVars: checkDbType(url), + includeVars: shouldIncludeVars(url), asDropdown: false, icon: '', tags: [], @@ -42,7 +42,7 @@ export const getLinkWithVariables = (url?: string): string => { const isDashboardUrl = (url?: string) => url?.includes('/d/'); -const checkDbType = (url: string): boolean => { +export const shouldIncludeVars = (url: string): boolean => { const currentDB = getDbType(window.location.pathname); const targetDB = getDbType(url); From 632979b7e3c3bfbd1a281166fc62e754d21cc1a0 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Mon, 5 Jan 2026 15:20:38 +0100 Subject: [PATCH 3/3] PMM-14651 Improve db type parsing --- ui/apps/pmm-compat/src/lib/utils/variables.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/ui/apps/pmm-compat/src/lib/utils/variables.ts b/ui/apps/pmm-compat/src/lib/utils/variables.ts index 839b19b271..f77a35efaa 100644 --- a/ui/apps/pmm-compat/src/lib/utils/variables.ts +++ b/ui/apps/pmm-compat/src/lib/utils/variables.ts @@ -46,19 +46,29 @@ export const shouldIncludeVars = (url: string): boolean => { const currentDB = getDbType(window.location.pathname); const targetDB = getDbType(url); + if (currentDB === undefined || targetDB === undefined) { + return false; + } + // enable variable sharing between same db types and db type -> os/node - return (currentDB !== undefined && currentDB === targetDB) || targetDB === 'node'; + return currentDB === targetDB || targetDB === 'node'; }; -const getDbType = (url: string): string => { +const getDbType = (url: string): string | undefined => { const pathname = new URL(url, window.location.origin).pathname; // normalize to the dashboard uid - const dashboardUid = pathname + const pathParts = pathname .replace('/pmm-ui', '') .replace('/next', '') .replace('/graph', '') .replace('/d/', '') - .split('/')[0]; + .split('/'); + + if (pathParts.length < 1 || !pathParts[0]) { + return undefined; + } + + const dashboardUid = pathParts[0]; if (dashboardUid.includes('-')) { return dashboardUid.split('-')[0];