mirror of https://github.com/grafana/grafana
Navigation: Add quick actions button (#58707)
* initial implementation for quick add * add new isCreateAction prop on NavModel * adjust separator margin * switch to primary button * undo changes to plugin.json * remove unused props from interface * use a consistent dropdown overlay type * memoize findCreateActions * add prop description * use a function so that menus are only rendered when the dropdown is openpull/58750/head
parent
84a69135a7
commit
028751a18a
@ -0,0 +1,73 @@ |
||||
import { render, screen } from '@testing-library/react'; |
||||
import userEvent from '@testing-library/user-event'; |
||||
import React from 'react'; |
||||
import { Provider } from 'react-redux'; |
||||
|
||||
import { NavModelItem, NavSection } from '@grafana/data'; |
||||
import { configureStore } from 'app/store/configureStore'; |
||||
|
||||
import { QuickAdd } from './QuickAdd'; |
||||
|
||||
const setup = () => { |
||||
const navBarTree: NavModelItem[] = [ |
||||
{ |
||||
text: 'Section 1', |
||||
section: NavSection.Core, |
||||
id: 'section1', |
||||
url: 'section1', |
||||
children: [ |
||||
{ text: 'New child 1', id: 'child1', url: 'section1/child1', isCreateAction: true }, |
||||
{ text: 'Child2', id: 'child2', url: 'section1/child2' }, |
||||
], |
||||
}, |
||||
{ |
||||
text: 'Section 2', |
||||
id: 'section2', |
||||
section: NavSection.Config, |
||||
url: 'section2', |
||||
children: [{ text: 'New child 3', id: 'child3', url: 'section2/child3', isCreateAction: true }], |
||||
}, |
||||
]; |
||||
|
||||
const store = configureStore({ navBarTree }); |
||||
|
||||
return render( |
||||
<Provider store={store}> |
||||
<QuickAdd /> |
||||
</Provider> |
||||
); |
||||
}; |
||||
|
||||
describe('QuickAdd', () => { |
||||
it('renders a `New` button', () => { |
||||
setup(); |
||||
expect(screen.getByRole('button', { name: 'New' })).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('renders the `New` text on a larger viewport', () => { |
||||
(window.matchMedia as jest.Mock).mockImplementation(() => ({ |
||||
addEventListener: jest.fn(), |
||||
removeEventListener: jest.fn(), |
||||
matches: () => false, |
||||
})); |
||||
setup(); |
||||
expect(screen.getByText('New')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('does not render the text on a smaller viewport', () => { |
||||
(window.matchMedia as jest.Mock).mockImplementation(() => ({ |
||||
addEventListener: jest.fn(), |
||||
removeEventListener: jest.fn(), |
||||
matches: () => true, |
||||
})); |
||||
setup(); |
||||
expect(screen.queryByText('New')).not.toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('shows isCreateAction options when clicked', async () => { |
||||
setup(); |
||||
await userEvent.click(screen.getByRole('button', { name: 'New' })); |
||||
expect(screen.getByRole('link', { name: 'New child 1' })).toBeInTheDocument(); |
||||
expect(screen.getByRole('link', { name: 'New child 3' })).toBeInTheDocument(); |
||||
}); |
||||
}); |
@ -0,0 +1,76 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React, { useMemo, useState } from 'react'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { Menu, Dropdown, Button, Icon, useStyles2, useTheme2, ToolbarButton } from '@grafana/ui'; |
||||
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; |
||||
import { useSelector } from 'app/types'; |
||||
|
||||
import { NavToolbarSeparator } from '../NavToolbarSeparator'; |
||||
|
||||
import { findCreateActions } from './utils'; |
||||
|
||||
export interface Props {} |
||||
|
||||
export const QuickAdd = ({}: Props) => { |
||||
const styles = useStyles2(getStyles); |
||||
const theme = useTheme2(); |
||||
const navBarTree = useSelector((state) => state.navBarTree); |
||||
const breakpoint = theme.breakpoints.values.sm; |
||||
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(window.matchMedia(`(max-width: ${breakpoint}px)`).matches); |
||||
const createActions = useMemo(() => findCreateActions(navBarTree), [navBarTree]); |
||||
|
||||
useMediaQueryChange({ |
||||
breakpoint, |
||||
onChange: (e) => { |
||||
setIsSmallScreen(e.matches); |
||||
}, |
||||
}); |
||||
|
||||
const MenuActions = () => { |
||||
return ( |
||||
<Menu> |
||||
{createActions.map((createAction, index) => ( |
||||
<Menu.Item key={index} url={createAction.url} label={createAction.text} /> |
||||
))} |
||||
</Menu> |
||||
); |
||||
}; |
||||
|
||||
return createActions.length > 0 ? ( |
||||
<> |
||||
<Dropdown overlay={MenuActions} placement="bottom-end"> |
||||
{isSmallScreen ? ( |
||||
<ToolbarButton iconOnly icon="plus-circle" aria-label="New" /> |
||||
) : ( |
||||
<Button variant="primary" size="sm" icon="plus"> |
||||
<div className={styles.buttonContent}> |
||||
<span className={styles.buttonText}>New</span> |
||||
<Icon name="angle-down" /> |
||||
</div> |
||||
</Button> |
||||
)} |
||||
</Dropdown> |
||||
<NavToolbarSeparator className={styles.separator} /> |
||||
</> |
||||
) : null; |
||||
}; |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
buttonContent: css({ |
||||
alignItems: 'center', |
||||
display: 'flex', |
||||
}), |
||||
buttonText: css({ |
||||
[theme.breakpoints.down('md')]: { |
||||
display: 'none', |
||||
}, |
||||
}), |
||||
separator: css({ |
||||
marginLeft: theme.spacing(1), |
||||
[theme.breakpoints.down('md')]: { |
||||
display: 'none', |
||||
}, |
||||
}), |
||||
}); |
@ -0,0 +1,14 @@ |
||||
import { NavModelItem } from '@grafana/data'; |
||||
|
||||
export function findCreateActions(navTree: NavModelItem[]): NavModelItem[] { |
||||
const results: NavModelItem[] = []; |
||||
for (const navItem of navTree) { |
||||
if (navItem.isCreateAction) { |
||||
results.push(navItem); |
||||
} |
||||
if (navItem.children) { |
||||
results.push(...findCreateActions(navItem.children)); |
||||
} |
||||
} |
||||
return results; |
||||
} |
Loading…
Reference in new issue