diff --git a/packages/grafana-ui/src/components/Select/SingleValue.tsx b/packages/grafana-ui/src/components/Select/SingleValue.tsx index 84fdddf98b7..b76a8df5c35 100644 --- a/packages/grafana-ui/src/components/Select/SingleValue.tsx +++ b/packages/grafana-ui/src/components/Select/SingleValue.tsx @@ -13,7 +13,6 @@ import { SlideOutTransition } from '../transitions/SlideOutTransition'; const getStyles = (theme: GrafanaTheme2) => { const singleValue = css` label: singleValue; - color: ${theme.components.input.text}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx index 9b15d46e4da..2fda7a1c086 100644 --- a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx +++ b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx @@ -32,6 +32,9 @@ export interface ValuePickerProps { menuPlacement?: 'auto' | 'bottom' | 'top'; /** Which ButtonFill to use */ fill?: ButtonFill; + + /** custom css applied to the button */ + buttonCss?: string; } export function ValuePicker({ @@ -46,6 +49,7 @@ export function ValuePicker({ isFullWidth = true, menuPlacement, fill, + buttonCss, }: ValuePickerProps) { const [isPicking, setIsPicking] = useState(false); const theme = useTheme2(); @@ -55,6 +59,7 @@ export function ValuePicker({ {!isPicking && ( + + ); +}; + +describe('TopSearchBarSection', () => { + it('should use a wrapper on non mobile screen', () => { + (window.matchMedia as jest.Mock).mockImplementation(() => ({ + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + matches: () => false, + })); + + const { container } = renderComponent(); + + expect(container.querySelector('[data-test-id="wrapper"]')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /test item/i })).toBeInTheDocument(); + }); + + it('should not use a wrapper on mobile screen', () => { + (window.matchMedia as jest.Mock).mockImplementation(() => ({ + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + matches: () => true, + })); + + const { container } = renderComponent(); + + expect(container.querySelector('[data-test-id="wrapper"]')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: /test item/i })).toBeInTheDocument(); + }); +}); diff --git a/public/app/core/components/AppChrome/TopBar/TopSearchBarSection.tsx b/public/app/core/components/AppChrome/TopBar/TopSearchBarSection.tsx new file mode 100644 index 00000000000..5f3b0419bc9 --- /dev/null +++ b/public/app/core/components/AppChrome/TopBar/TopSearchBarSection.tsx @@ -0,0 +1,50 @@ +import { css, cx } from '@emotion/css'; +import React, { useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2, useTheme2 } from '@grafana/ui'; +import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; + +export interface TopSearchBarSectionProps { + children: React.ReactNode; + align?: 'left' | 'center' | 'right'; +} + +export function TopSearchBarSection({ children, align = 'left' }: TopSearchBarSectionProps) { + const styles = useStyles2(getStyles); + const theme = useTheme2(); + const breakpoint = theme.breakpoints.values.sm; + + const [isSmallScreen, setIsSmallScreen] = useState(window.matchMedia(`(max-width: ${breakpoint}px)`).matches); + + useMediaQueryChange({ + breakpoint, + onChange: (e: MediaQueryListEvent) => { + setIsSmallScreen(e.matches); + }, + }); + + if (isSmallScreen) { + return <>{children}; + } + + return ( +
+ {children} +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + wrapper: css({ + display: 'flex', + gap: theme.spacing(0.5), + alignItems: 'center', + }), + + right: css({ + justifyContent: 'flex-end', + }), + left: css({}), + center: css({}), +}); diff --git a/public/app/core/components/AppChrome/TopSearchBar.tsx b/public/app/core/components/AppChrome/TopSearchBar.tsx index 5981404c6a7..679f7b388ae 100644 --- a/public/app/core/components/AppChrome/TopSearchBar.tsx +++ b/public/app/core/components/AppChrome/TopSearchBar.tsx @@ -10,6 +10,7 @@ import { NewsContainer } from './News/NewsContainer'; import { OrganizationSwitcher } from './Organization/OrganizationSwitcher'; import { SignInLink } from './TopBar/SignInLink'; import { TopNavBarMenu } from './TopBar/TopNavBarMenu'; +import { TopSearchBarSection } from './TopBar/TopSearchBarSection'; import { TopSearchBarInput } from './TopSearchBarInput'; import { TOP_BAR_LEVEL_HEIGHT } from './types'; @@ -21,23 +22,23 @@ export function TopSearchBar() { const profileNode = navIndex['profile']; return ( -
-
+
+ -
-
+ + -
-
+ + {helpNode && ( }> )} - + {!contextSrv.user.isSignedIn && } {profileNode && ( }> @@ -49,45 +50,43 @@ export function TopSearchBar() { /> )} -
+
); } -const getStyles = (theme: GrafanaTheme2) => { - return { - container: css({ - height: TOP_BAR_LEVEL_HEIGHT, - display: 'grid', - gap: theme.spacing(0.5), +const getStyles = (theme: GrafanaTheme2) => ({ + layout: css({ + height: TOP_BAR_LEVEL_HEIGHT, + display: 'flex', + gap: theme.spacing(0.5), + alignItems: 'center', + padding: theme.spacing(0, 2), + borderBottom: `1px solid ${theme.colors.border.weak}`, + justifyContent: 'space-between', + + [theme.breakpoints.up('sm')]: { gridTemplateColumns: '1fr 2fr 1fr', - padding: theme.spacing(0, 2), - alignItems: 'center', - borderBottom: `1px solid ${theme.colors.border.weak}`, - }), - leftContent: css({ - display: 'flex', - alignItems: 'center', - gap: theme.spacing(1), - }), - logo: css({ - display: 'flex', - }), - searchWrapper: css({}), - searchInput: css({}), - actions: css({ - display: 'flex', - gap: theme.spacing(0.5), - justifyContent: 'flex-end', - alignItems: 'center', - }), - profileButton: css({ - img: { - borderRadius: '50%', - height: '24px', - marginRight: 0, - width: '24px', - }, - }), - }; -}; + display: 'grid', + + justifyContent: 'flex-start', + }, + }), + logo: css({ + display: 'flex', + }), + profileButton: css({ + img: { + borderRadius: '50%', + height: '24px', + marginRight: 0, + width: '24px', + }, + }), + + newsButton: css({ + [theme.breakpoints.down('sm')]: { + display: 'none', + }, + }), +}); diff --git a/public/app/core/components/AppChrome/TopSearchBarInput.tsx b/public/app/core/components/AppChrome/TopSearchBarInput.tsx index f007c66329c..35cc8a157d8 100644 --- a/public/app/core/components/AppChrome/TopSearchBarInput.tsx +++ b/public/app/core/components/AppChrome/TopSearchBarInput.tsx @@ -1,12 +1,24 @@ -import React from 'react'; +import React, { useState } from 'react'; import { locationService } from '@grafana/runtime'; -import { FilterInput } from '@grafana/ui'; +import { FilterInput, ToolbarButton, useTheme2 } from '@grafana/ui'; +import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; import { t } from 'app/core/internationalization'; import { useSearchQuery } from 'app/features/search/hooks/useSearchQuery'; export function TopSearchBarInput() { + const theme = useTheme2(); const { query, onQueryChange } = useSearchQuery({}); + const breakpoint = theme.breakpoints.values.sm; + + const [isSmallScreen, setIsSmallScreen] = useState(window.matchMedia(`(max-width: ${breakpoint}px)`).matches); + + useMediaQueryChange({ + breakpoint, + onChange: (e) => { + setIsSmallScreen(e.matches); + }, + }); const onOpenSearch = () => { locationService.partial({ search: 'open' }); @@ -18,6 +30,11 @@ export function TopSearchBarInput() { onOpenSearch(); } }; + + if (isSmallScreen) { + return ; + } + return ( void; +}) { + useEffect(() => { + const mediaQuery = window.matchMedia(`(max-width: ${breakpoint}px)`); + const onMediaQueryChange = (e: MediaQueryListEvent) => onChange(e); + mediaQuery.addEventListener('change', onMediaQueryChange); + + return () => mediaQuery.removeEventListener('change', onMediaQueryChange); + }, [breakpoint, onChange]); +}