mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Refactoring and simplifying TopSearchBar (#55341)
This commit is contained in:
parent
80c0dc890a
commit
40bc140a9a
@ -1,18 +1,25 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||||
import { Menu, MenuItem, useStyles2 } from '@grafana/ui';
|
import { Menu, MenuItem, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import menuItemTranslations from '../../NavBar/navBarItem-translations';
|
import menuItemTranslations from '../../NavBar/navBarItem-translations';
|
||||||
|
import { enrichConfigItems, enrichWithInteractionTracking } from '../../NavBar/utils';
|
||||||
|
|
||||||
export interface TopNavBarMenuProps {
|
export interface TopNavBarMenuProps {
|
||||||
node: NavModelItem;
|
node: NavModelItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopNavBarMenu({ node }: TopNavBarMenuProps) {
|
export function TopNavBarMenu({ node: nodePlain }: TopNavBarMenuProps) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const location = useLocation();
|
||||||
|
const enriched = enrichConfigItems([cloneDeep(nodePlain)], location);
|
||||||
|
const node = enrichWithInteractionTracking(enriched[0], false);
|
||||||
|
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,22 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { cloneDeep } from 'lodash';
|
import React from 'react';
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { GrafanaTheme2, NavSection } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { locationService } from '@grafana/runtime';
|
import { Dropdown, Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
import { Dropdown, FilterInput, Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { useSearchQuery } from 'app/features/search/hooks/useSearchQuery';
|
|
||||||
import { useSelector } from 'app/types';
|
import { useSelector } from 'app/types';
|
||||||
|
|
||||||
import { enrichConfigItems, enrichWithInteractionTracking } from '../NavBar/utils';
|
|
||||||
import { OrgSwitcher } from '../OrgSwitcher';
|
|
||||||
|
|
||||||
import { TopNavBarMenu } from './TopBar/TopNavBarMenu';
|
import { TopNavBarMenu } from './TopBar/TopNavBarMenu';
|
||||||
|
import { TopSearchBarInput } from './TopSearchBarInput';
|
||||||
import { TOP_BAR_LEVEL_HEIGHT } from './types';
|
import { TOP_BAR_LEVEL_HEIGHT } from './types';
|
||||||
|
|
||||||
export function TopSearchBar() {
|
export function TopSearchBar() {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const location = useLocation();
|
const navIndex = useSelector((state) => state.navIndex);
|
||||||
const { query, onQueryChange } = useSearchQuery({});
|
|
||||||
const navBarTree = useSelector((state) => state.navBarTree);
|
|
||||||
const navTree = cloneDeep(navBarTree);
|
|
||||||
const [showSwitcherModal, setShowSwitcherModal] = useState(false);
|
|
||||||
const toggleSwitcherModal = () => {
|
|
||||||
setShowSwitcherModal(!showSwitcherModal);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onOpenSearch = () => {
|
const helpNode = navIndex['help'];
|
||||||
locationService.partial({ search: 'open' });
|
const profileNode = navIndex['profile'];
|
||||||
};
|
const signInNode = navIndex['signin'];
|
||||||
const onSearchChange = (value: string) => {
|
|
||||||
onQueryChange(value);
|
|
||||||
if (value) {
|
|
||||||
onOpenSearch();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const configItems = enrichConfigItems(
|
|
||||||
navTree.filter((item) => item.section === NavSection.Config),
|
|
||||||
location,
|
|
||||||
toggleSwitcherModal
|
|
||||||
).map((item) => enrichWithInteractionTracking(item, false));
|
|
||||||
|
|
||||||
const helpNode = configItems.find((item) => item.id === 'help');
|
|
||||||
const profileNode = configItems.find((item) => item.id === 'profile');
|
|
||||||
const signInNode = configItems.find((item) => item.id === 'signin');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@ -55,17 +26,11 @@ export function TopSearchBar() {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.searchWrapper}>
|
<div className={styles.searchWrapper}>
|
||||||
<FilterInput
|
<TopSearchBarInput />
|
||||||
onClick={onOpenSearch}
|
|
||||||
placeholder="Search Grafana"
|
|
||||||
value={query.query ?? ''}
|
|
||||||
onChange={onSearchChange}
|
|
||||||
className={styles.searchInput}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
{helpNode && (
|
{helpNode && (
|
||||||
<Dropdown overlay={<TopNavBarMenu node={helpNode} />}>
|
<Dropdown overlay={() => <TopNavBarMenu node={helpNode} />}>
|
||||||
<button className={styles.actionItem}>
|
<button className={styles.actionItem}>
|
||||||
<Icon name="question-circle" size="lg" />
|
<Icon name="question-circle" size="lg" />
|
||||||
</button>
|
</button>
|
||||||
@ -91,7 +56,6 @@ export function TopSearchBar() {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showSwitcherModal && <OrgSwitcher onDismiss={toggleSwitcherModal} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
28
public/app/core/components/AppChrome/TopSearchBarInput.tsx
Normal file
28
public/app/core/components/AppChrome/TopSearchBarInput.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
import { FilterInput } from '@grafana/ui';
|
||||||
|
import { useSearchQuery } from 'app/features/search/hooks/useSearchQuery';
|
||||||
|
|
||||||
|
export function TopSearchBarInput() {
|
||||||
|
const { query, onQueryChange } = useSearchQuery({});
|
||||||
|
|
||||||
|
const onOpenSearch = () => {
|
||||||
|
locationService.partial({ search: 'open' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearchChange = (value: string) => {
|
||||||
|
onQueryChange(value);
|
||||||
|
if (value) {
|
||||||
|
onOpenSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<FilterInput
|
||||||
|
onClick={onOpenSearch}
|
||||||
|
placeholder="Search Grafana"
|
||||||
|
value={query.query ?? ''}
|
||||||
|
onChange={onSearchChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data';
|
import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data';
|
||||||
@ -22,11 +22,6 @@ export const MegaMenu = React.memo<Props>(({ onClose, searchBarHidden }) => {
|
|||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [showSwitcherModal, setShowSwitcherModal] = useState(false);
|
|
||||||
|
|
||||||
const toggleSwitcherModal = () => {
|
|
||||||
setShowSwitcherModal(!showSwitcherModal);
|
|
||||||
};
|
|
||||||
|
|
||||||
const homeItem: NavModelItem = enrichWithInteractionTracking(
|
const homeItem: NavModelItem = enrichWithInteractionTracking(
|
||||||
{
|
{
|
||||||
@ -48,8 +43,7 @@ export const MegaMenu = React.memo<Props>(({ onClose, searchBarHidden }) => {
|
|||||||
.map((item) => enrichWithInteractionTracking(item, true));
|
.map((item) => enrichWithInteractionTracking(item, true));
|
||||||
const configItems = enrichConfigItems(
|
const configItems = enrichConfigItems(
|
||||||
navTree.filter((item) => item.section === NavSection.Config && item && item.id !== 'help' && item.id !== 'profile'),
|
navTree.filter((item) => item.section === NavSection.Config && item && item.id !== 'help' && item.id !== 'profile'),
|
||||||
location,
|
location
|
||||||
toggleSwitcherModal
|
|
||||||
).map((item) => enrichWithInteractionTracking(item, true));
|
).map((item) => enrichWithInteractionTracking(item, true));
|
||||||
|
|
||||||
const activeItem = getActiveItem(navTree, location.pathname);
|
const activeItem = getActiveItem(navTree, location.pathname);
|
||||||
|
@ -11,8 +11,6 @@ import { Icon, useTheme2, CustomScrollbar } from '@grafana/ui';
|
|||||||
import { getKioskMode } from 'app/core/navigation/kiosk';
|
import { getKioskMode } from 'app/core/navigation/kiosk';
|
||||||
import { useSelector } from 'app/types';
|
import { useSelector } from 'app/types';
|
||||||
|
|
||||||
import { OrgSwitcher } from '../OrgSwitcher';
|
|
||||||
|
|
||||||
import NavBarItem from './NavBarItem';
|
import NavBarItem from './NavBarItem';
|
||||||
import { NavBarItemIcon } from './NavBarItemIcon';
|
import { NavBarItemIcon } from './NavBarItemIcon';
|
||||||
import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu';
|
import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu';
|
||||||
@ -38,15 +36,10 @@ export const NavBar = React.memo(() => {
|
|||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [showSwitcherModal, setShowSwitcherModal] = useState(false);
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [menuAnimationInProgress, setMenuAnimationInProgress] = useState(false);
|
const [menuAnimationInProgress, setMenuAnimationInProgress] = useState(false);
|
||||||
const [menuIdOpen, setMenuIdOpen] = useState<string | undefined>(undefined);
|
const [menuIdOpen, setMenuIdOpen] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const toggleSwitcherModal = () => {
|
|
||||||
setShowSwitcherModal(!showSwitcherModal);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Here we need to hack in a "home" and "search" NavModelItem since this is constructed in the frontend
|
// Here we need to hack in a "home" and "search" NavModelItem since this is constructed in the frontend
|
||||||
const searchItem: NavModelItem = enrichWithInteractionTracking(
|
const searchItem: NavModelItem = enrichWithInteractionTracking(
|
||||||
{
|
{
|
||||||
@ -78,8 +71,7 @@ export const NavBar = React.memo(() => {
|
|||||||
.map((item) => enrichWithInteractionTracking(item, menuOpen));
|
.map((item) => enrichWithInteractionTracking(item, menuOpen));
|
||||||
const configItems = enrichConfigItems(
|
const configItems = enrichConfigItems(
|
||||||
navTree.filter((item) => item.section === NavSection.Config),
|
navTree.filter((item) => item.section === NavSection.Config),
|
||||||
location,
|
location
|
||||||
toggleSwitcherModal
|
|
||||||
).map((item) => enrichWithInteractionTracking(item, menuOpen));
|
).map((item) => enrichWithInteractionTracking(item, menuOpen));
|
||||||
|
|
||||||
const activeItem = isSearchActive(location) ? searchItem : getActiveItem(navTree, location.pathname);
|
const activeItem = isSearchActive(location) ? searchItem : getActiveItem(navTree, location.pathname);
|
||||||
@ -158,7 +150,6 @@ export const NavBar = React.memo(() => {
|
|||||||
</FocusScope>
|
</FocusScope>
|
||||||
</NavBarContext.Provider>
|
</NavBarContext.Provider>
|
||||||
</nav>
|
</nav>
|
||||||
{showSwitcherModal && <OrgSwitcher onDismiss={toggleSwitcherModal} />}
|
|
||||||
{(menuOpen || menuAnimationInProgress) && (
|
{(menuOpen || menuAnimationInProgress) && (
|
||||||
<div className={styles.menuWrapper}>
|
<div className={styles.menuWrapper}>
|
||||||
<NavBarMenu
|
<NavBarMenu
|
||||||
|
@ -62,7 +62,7 @@ describe('enrichConfigItems', () => {
|
|||||||
const contextSrv = new ContextSrv();
|
const contextSrv = new ContextSrv();
|
||||||
contextSrv.user.isSignedIn = false;
|
contextSrv.user.isSignedIn = false;
|
||||||
setContextSrv(contextSrv);
|
setContextSrv(contextSrv);
|
||||||
const enrichedConfigItems = enrichConfigItems(mockItems, mockLocation, jest.fn());
|
const enrichedConfigItems = enrichConfigItems(mockItems, mockLocation);
|
||||||
const signInNode = enrichedConfigItems.find((item) => item.id === 'signin');
|
const signInNode = enrichedConfigItems.find((item) => item.id === 'signin');
|
||||||
expect(signInNode).toBeDefined();
|
expect(signInNode).toBeDefined();
|
||||||
});
|
});
|
||||||
@ -71,7 +71,7 @@ describe('enrichConfigItems', () => {
|
|||||||
const contextSrv = new ContextSrv();
|
const contextSrv = new ContextSrv();
|
||||||
contextSrv.user.isSignedIn = true;
|
contextSrv.user.isSignedIn = true;
|
||||||
setContextSrv(contextSrv);
|
setContextSrv(contextSrv);
|
||||||
const enrichedConfigItems = enrichConfigItems(mockItems, mockLocation, jest.fn());
|
const enrichedConfigItems = enrichConfigItems(mockItems, mockLocation);
|
||||||
const signInNode = enrichedConfigItems.find((item) => item.id === 'signin');
|
const signInNode = enrichedConfigItems.find((item) => item.id === 'signin');
|
||||||
expect(signInNode).toBeDefined();
|
expect(signInNode).toBeDefined();
|
||||||
});
|
});
|
||||||
@ -80,7 +80,7 @@ describe('enrichConfigItems', () => {
|
|||||||
const contextSrv = new ContextSrv();
|
const contextSrv = new ContextSrv();
|
||||||
contextSrv.user.orgCount = 1;
|
contextSrv.user.orgCount = 1;
|
||||||
setContextSrv(contextSrv);
|
setContextSrv(contextSrv);
|
||||||
const enrichedConfigItems = enrichConfigItems(mockItems, mockLocation, jest.fn());
|
const enrichedConfigItems = enrichConfigItems(mockItems, mockLocation);
|
||||||
const profileNode = enrichedConfigItems.find((item) => item.id === 'profile');
|
const profileNode = enrichedConfigItems.find((item) => item.id === 'profile');
|
||||||
expect(profileNode!.children).toBeUndefined();
|
expect(profileNode!.children).toBeUndefined();
|
||||||
});
|
});
|
||||||
@ -89,7 +89,7 @@ describe('enrichConfigItems', () => {
|
|||||||
const contextSrv = new ContextSrv();
|
const contextSrv = new ContextSrv();
|
||||||
contextSrv.user.orgCount = 2;
|
contextSrv.user.orgCount = 2;
|
||||||
setContextSrv(contextSrv);
|
setContextSrv(contextSrv);
|
||||||
const enrichedConfigItems = enrichConfigItems(mockItems, mockLocation, jest.fn());
|
const enrichedConfigItems = enrichConfigItems(mockItems, mockLocation);
|
||||||
const profileNode = enrichedConfigItems.find((item) => item.id === 'profile');
|
const profileNode = enrichedConfigItems.find((item) => item.id === 'profile');
|
||||||
expect(profileNode!.children).toContainEqual(
|
expect(profileNode!.children).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -101,7 +101,7 @@ describe('enrichConfigItems', () => {
|
|||||||
it('enhances the help node with extra child links', () => {
|
it('enhances the help node with extra child links', () => {
|
||||||
const contextSrv = new ContextSrv();
|
const contextSrv = new ContextSrv();
|
||||||
setContextSrv(contextSrv);
|
setContextSrv(contextSrv);
|
||||||
const enrichedConfigItems = enrichConfigItems(mockItems, mockLocation, jest.fn());
|
const enrichedConfigItems = enrichConfigItems(mockItems, mockLocation);
|
||||||
const helpNode = enrichedConfigItems.find((item) => item.id === 'help');
|
const helpNode = enrichedConfigItems.find((item) => item.id === 'help');
|
||||||
expect(helpNode!.children).toContainEqual(
|
expect(helpNode!.children).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
@ -8,6 +8,7 @@ import { contextSrv } from 'app/core/services/context_srv';
|
|||||||
import { ShowModalReactEvent } from '../../../types/events';
|
import { ShowModalReactEvent } from '../../../types/events';
|
||||||
import appEvents from '../../app_events';
|
import appEvents from '../../app_events';
|
||||||
import { getFooterLinks } from '../Footer/Footer';
|
import { getFooterLinks } from '../Footer/Footer';
|
||||||
|
import { OrgSwitcher } from '../OrgSwitcher';
|
||||||
import { HelpModal } from '../help/HelpModal';
|
import { HelpModal } from '../help/HelpModal';
|
||||||
|
|
||||||
export const SEARCH_ITEM_ID = 'search';
|
export const SEARCH_ITEM_ID = 'search';
|
||||||
@ -22,21 +23,21 @@ export const getForcedLoginUrl = (url: string) => {
|
|||||||
return `${getConfig().appSubUrl}${url.split('?')[0]}?${queryParams.toString()}`;
|
return `${getConfig().appSubUrl}${url.split('?')[0]}?${queryParams.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enrichConfigItems = (
|
export const enrichConfigItems = (items: NavModelItem[], location: Location<unknown>) => {
|
||||||
items: NavModelItem[],
|
|
||||||
location: Location<unknown>,
|
|
||||||
toggleOrgSwitcher: () => void
|
|
||||||
) => {
|
|
||||||
const { isSignedIn, user } = contextSrv;
|
const { isSignedIn, user } = contextSrv;
|
||||||
const onOpenShortcuts = () => {
|
const onOpenShortcuts = () => {
|
||||||
appEvents.publish(new ShowModalReactEvent({ component: HelpModal }));
|
appEvents.publish(new ShowModalReactEvent({ component: HelpModal }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onOpenOrgSwitcher = () => {
|
||||||
|
appEvents.publish(new ShowModalReactEvent({ component: OrgSwitcher }));
|
||||||
|
};
|
||||||
|
|
||||||
if (user && user.orgCount > 1) {
|
if (user && user.orgCount > 1) {
|
||||||
const profileNode = items.find((bottomNavItem) => bottomNavItem.id === 'profile');
|
const profileNode = items.find((bottomNavItem) => bottomNavItem.id === 'profile');
|
||||||
if (profileNode) {
|
if (profileNode) {
|
||||||
profileNode.showOrgSwitcher = true;
|
profileNode.showOrgSwitcher = true;
|
||||||
profileNode.subTitle = `Current Org.: ${user?.orgName}`;
|
profileNode.subTitle = `Organization: ${user?.orgName}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ export const enrichConfigItems = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
items.forEach((link, index) => {
|
items.forEach((link) => {
|
||||||
let menuItems = link.children || [];
|
let menuItems = link.children || [];
|
||||||
|
|
||||||
if (link.id === 'help') {
|
if (link.id === 'help') {
|
||||||
@ -75,7 +76,7 @@ export const enrichConfigItems = (
|
|||||||
id: 'switch-organization',
|
id: 'switch-organization',
|
||||||
text: 'Switch organization',
|
text: 'Switch organization',
|
||||||
icon: 'arrow-random',
|
icon: 'arrow-random',
|
||||||
onClick: toggleOrgSwitcher,
|
onClick: onOpenOrgSwitcher,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user