The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/core/components/NavBar/utils.ts

166 lines
5.1 KiB

import { Location } from 'history';
import { NavModelItem, NavSection } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { getConfig } from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv';
import { ShowModalReactEvent } from '../../../types/events';
import appEvents from '../../app_events';
import { getFooterLinks } from '../Footer/Footer';
import { HelpModal } from '../help/HelpModal';
export const SEARCH_ITEM_ID = 'search';
export const NAV_MENU_PORTAL_CONTAINER_ID = 'navbar-menu-portal-container';
export const getNavMenuPortalContainer = () => document.getElementById(NAV_MENU_PORTAL_CONTAINER_ID) ?? document.body;
export const getForcedLoginUrl = (url: string) => {
const queryParams = new URLSearchParams(url.split('?')[1]);
queryParams.append('forceLogin', 'true');
return `${getConfig().appSubUrl}${url.split('?')[0]}?${queryParams.toString()}`;
};
export const enrichConfigItems = (
items: NavModelItem[],
location: Location<unknown>,
toggleOrgSwitcher: () => void
) => {
const { isSignedIn, user } = contextSrv;
const onOpenShortcuts = () => {
appEvents.publish(new ShowModalReactEvent({ component: HelpModal }));
};
if (user && user.orgCount > 1) {
const profileNode = items.find((bottomNavItem) => bottomNavItem.id === 'profile');
if (profileNode) {
profileNode.showOrgSwitcher = true;
profileNode.subTitle = `Current Org.: ${user?.orgName}`;
}
}
if (!isSignedIn) {
const forcedLoginUrl = getForcedLoginUrl(location.pathname + location.search);
items.unshift({
icon: 'signout',
id: 'signin',
section: NavSection.Config,
target: '_self',
text: 'Sign in',
url: forcedLoginUrl,
});
}
items.forEach((link, index) => {
let menuItems = link.children || [];
if (link.id === 'help') {
link.children = [
...getFooterLinks(),
{
id: 'keyboard-shortcuts',
text: 'Keyboard shortcuts',
icon: 'keyboard',
onClick: onOpenShortcuts,
},
];
}
if (link.showOrgSwitcher) {
link.children = [
...menuItems,
{
id: 'switch-organization',
text: 'Switch organization',
icon: 'arrow-random',
onClick: toggleOrgSwitcher,
},
];
}
});
return items;
};
export const enrichWithInteractionTracking = (item: NavModelItem, expandedState: boolean) => {
const onClick = item.onClick;
item.onClick = () => {
reportInteraction('grafana_navigation_item_clicked', {
path: item.url ?? item.id,
state: expandedState ? 'expanded' : 'collapsed',
});
onClick?.();
};
if (item.children) {
item.children = item.children.map((item) => enrichWithInteractionTracking(item, expandedState));
}
return item;
};
export const isMatchOrChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelItem) => {
return Boolean(itemToCheck === searchItem || itemToCheck.children?.some((child) => child === searchItem));
};
const stripQueryParams = (url?: string) => {
return url?.split('?')[0] ?? '';
};
const isBetterMatch = (newMatch: NavModelItem, currentMatch?: NavModelItem) => {
const currentMatchUrl = stripQueryParams(currentMatch?.url);
const newMatchUrl = stripQueryParams(newMatch.url);
return newMatchUrl && newMatchUrl.length > currentMatchUrl?.length;
};
export const getActiveItem = (
navTree: NavModelItem[],
pathname: string,
currentBestMatch?: NavModelItem
): NavModelItem | undefined => {
const newNavigationEnabled = getConfig().featureToggles.newNavigation;
const dashboardLinkMatch = newNavigationEnabled ? '/dashboards' : '/';
for (const link of navTree) {
const linkPathname = stripQueryParams(link.url);
if (linkPathname) {
if (linkPathname === pathname) {
// exact match
currentBestMatch = link;
break;
} else if (linkPathname !== '/' && pathname.startsWith(linkPathname)) {
// partial match
if (isBetterMatch(link, currentBestMatch)) {
currentBestMatch = link;
}
} else if (linkPathname === '/alerting/list' && pathname.startsWith('/alerting/notification/')) {
// alert channel match
// TODO refactor routes such that we don't need this custom logic
currentBestMatch = link;
break;
} else if (linkPathname === dashboardLinkMatch && pathname.startsWith('/d/')) {
// dashboard match
// TODO refactor routes such that we don't need this custom logic
if (isBetterMatch(link, currentBestMatch)) {
currentBestMatch = link;
}
}
}
if (link.children) {
currentBestMatch = getActiveItem(link.children, pathname, currentBestMatch);
}
if (stripQueryParams(currentBestMatch?.url) === pathname) {
return currentBestMatch;
}
}
return currentBestMatch;
};
export const isSearchActive = (location: Location<unknown>) => {
const query = new URLSearchParams(location.search);
return query.get('search') === 'open';
};
Navigation: Implement Keyboard Navigation (#41618) * Navigation: Start creating new NavBarMenu component * Navigation: Apply new NavBarMenu to NavBarNext * Navigation: Remove everything to do with .sidemenu-open--xs * Navigation: Ensure search is passed to NavBarMenu * Navigation: Standardise NavBarMenuItem * This extra check isn't needed anymore * Navigation: Refactor <li> out of NavBarMenu * Navigation: Combine NavBarMenuItem with DropdownChild * use spread syntax since performance shouldn't be a concern for such small arrays * Improve active item logic * Ensure unique keys * Remove this duplicate code * Add unit tests for getActiveItem * Add tests for NavBarMenu * Rename mobileMenuOpen -> menuOpen in NavBarNext (since it can be used for mobile menu or megamenu) * just use index to key the items * Use exact versions of @react-aria packages * Navigation: Make the dropdown header a NavBarMenuItem * Navigation: Stop using dropdown-menu for styles * Navigation: Add react-aria relevant packages * Navigation: Refactor NavBarDropdown to support react aria * Navigation: apply keyboard navigation to NavBar component * Navigation: UseHover hook for triggering submenu on navbar * Navigation: rename testMenu component to NavBarItemButton * WIP * some hacks * Refactor: clean up keybinding events * Navigation: render subtitle on item menu and disable it * Navigation: Adds react-aria types (#42113) * Refactor: refactor out to NavBarItemWithoutMenu * Refactor: cleaning up stuff * Refactor: comment out unused code * Chore: Removes section and uses items only * Chore: fix NavBarNext * Chore: adds tests * Refactor: minimize props api * Refactor: various refactors * Refactor: rename enableAllItems * Refactor: remove unused code * Refactor: fix clicking on menuitems * Refactor: use recommended onAction instead * Navigation: Fix a11y issues on NavBar * Navigation: Fix a11y navBar Next * Navigation: Remove unnecessary label prop, use link.text instead * Apply suggestions from code review Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> * Apply unit tests suggestions from code review Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Update react-aria/menu package to latest version and apply PR suggestion Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
4 years ago
export function getNavModelItemKey(item: NavModelItem) {
return item.id ?? item.text;
}