Commit e505babb by Torkel Ödegaard Committed by GitHub

ManageDashboards: Fixes and improvements (#23879)

* ManageDashboards: Fixes and improvements

* Fixed tests

* Fixed issue with item height and margin
parent d6ba6440
...@@ -19,6 +19,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -19,6 +19,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => {
labelStyles.label, labelStyles.label,
css` css`
padding-left: ${theme.spacing.formSpacingBase}px; padding-left: ${theme.spacing.formSpacingBase}px;
white-space: nowrap;
` `
), ),
description: cx( description: cx(
...@@ -50,11 +51,13 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -50,11 +51,13 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => {
* */ * */
&:checked + span { &:checked + span {
background: blue; background: blue;
background: ${theme.colors.formInputBg}; background: ${theme.colors.formCheckboxBgChecked};
border: none; border: none;
&:hover { &:hover {
background: ${theme.colors.formCheckboxBgCheckedHover}; background: ${theme.colors.formCheckboxBgCheckedHover};
} }
&:after { &:after {
content: ''; content: '';
position: absolute; position: absolute;
...@@ -79,6 +82,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -79,6 +82,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => {
position: absolute; position: absolute;
top: 1px; top: 1px;
left: 0; left: 0;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
border-color: ${theme.colors.formInputBorderHover}; border-color: ${theme.colors.formInputBorderHover};
......
...@@ -20,7 +20,12 @@ export const simple = () => { ...@@ -20,7 +20,12 @@ export const simple = () => {
return ( return (
<div> <div>
{renderScenario('body', theme, ['sm', 'md', 'lg', 'xl', 'xxl'], ['search', 'trash-alt', 'arrow-left', 'times'])} {renderScenario(
'dashboard',
theme,
['sm', 'md', 'lg', 'xl', 'xxl'],
['search', 'trash-alt', 'arrow-left', 'times']
)}
{renderScenario('panel', theme, ['sm', 'md', 'lg', 'xl', 'xxl'], ['search', 'trash-alt', 'arrow-left', 'times'])} {renderScenario('panel', theme, ['sm', 'md', 'lg', 'xl', 'xxl'], ['search', 'trash-alt', 'arrow-left', 'times'])}
{renderScenario('header', theme, ['sm', 'md', 'lg', 'xl', 'xxl'], ['search', 'trash-alt', 'arrow-left', 'times'])} {renderScenario('header', theme, ['sm', 'md', 'lg', 'xl', 'xxl'], ['search', 'trash-alt', 'arrow-left', 'times'])}
</div> </div>
...@@ -31,8 +36,8 @@ function renderScenario(surface: string, theme: GrafanaTheme, sizes: IconSize[], ...@@ -31,8 +36,8 @@ function renderScenario(surface: string, theme: GrafanaTheme, sizes: IconSize[],
let bg: string = 'red'; let bg: string = 'red';
switch (surface) { switch (surface) {
case 'body': case 'dashboard':
bg = theme.colors.bodyBg; bg = theme.colors.dashboardBg;
break; break;
case 'panel': case 'panel':
bg = theme.colors.bodyBg; bg = theme.colors.bodyBg;
......
...@@ -18,7 +18,7 @@ export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { ...@@ -18,7 +18,7 @@ export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
tooltipPlacement?: TooltipPlacement; tooltipPlacement?: TooltipPlacement;
} }
type SurfaceType = 'body' | 'panel' | 'header'; type SurfaceType = 'dashboard' | 'panel' | 'header';
export const IconButton = React.forwardRef<HTMLButtonElement, Props>( export const IconButton = React.forwardRef<HTMLButtonElement, Props>(
({ name, size = 'md', surface = 'panel', iconType, tooltip, tooltipPlacement, className, ...restProps }, ref) => { ({ name, size = 'md', surface = 'panel', iconType, tooltip, tooltipPlacement, className, ...restProps }, ref) => {
...@@ -47,7 +47,7 @@ IconButton.displayName = 'IconButton'; ...@@ -47,7 +47,7 @@ IconButton.displayName = 'IconButton';
function getHoverColor(theme: GrafanaTheme, surface: SurfaceType): string { function getHoverColor(theme: GrafanaTheme, surface: SurfaceType): string {
switch (surface) { switch (surface) {
case 'body': case 'dashboard':
return theme.isLight ? theme.palette.gray95 : theme.palette.gray15; return theme.isLight ? theme.palette.gray95 : theme.palette.gray15;
case 'panel': case 'panel':
return theme.isLight ? theme.palette.gray6 : theme.palette.gray15; return theme.isLight ? theme.palette.gray6 : theme.palette.gray15;
......
...@@ -3,7 +3,7 @@ import { IconButton } from '@grafana/ui'; ...@@ -3,7 +3,7 @@ import { IconButton } from '@grafana/ui';
import { e2e } from '@grafana/e2e'; import { e2e } from '@grafana/e2e';
export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> { export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
surface: 'body' | 'panel'; surface: 'dashboard' | 'panel' | 'header';
} }
export const BackButton: React.FC<Props> = ({ surface, onClick }) => { export const BackButton: React.FC<Props> = ({ surface, onClick }) => {
......
...@@ -143,7 +143,7 @@ class DashNav extends PureComponent<Props> { ...@@ -143,7 +143,7 @@ class DashNav extends PureComponent<Props> {
renderBackButton() { renderBackButton() {
return ( return (
<div className="navbar-edit"> <div className="navbar-edit">
<BackButton surface="body" onClick={this.onClose} /> <BackButton surface="dashboard" onClick={this.onClose} />
</div> </div>
); );
} }
......
...@@ -50,7 +50,7 @@ export class DashboardSettings extends PureComponent<Props> { ...@@ -50,7 +50,7 @@ export class DashboardSettings extends PureComponent<Props> {
<div className="dashboard-settings"> <div className="dashboard-settings">
<div className="navbar navbar--edit"> <div className="navbar navbar--edit">
<div className="navbar-edit"> <div className="navbar-edit">
<BackButton surface="body" onClick={this.onClose} /> <BackButton surface="panel" onClick={this.onClose} />
</div> </div>
<div className="navbar-page-btn"> <div className="navbar-page-btn">
{haveFolder && <div className="navbar-page-btn__folder">{folderTitle} / </div>} {haveFolder && <div className="navbar-page-btn__folder">{folderTitle} / </div>}
......
import React, { Dispatch, FC, SetStateAction } from 'react'; import React, { Dispatch, FC, SetStateAction } from 'react';
import { css } from 'emotion'; import { css } from 'emotion';
import { HorizontalGroup, RadioButtonGroup, Select, stylesFactory, useTheme } from '@grafana/ui'; import { HorizontalGroup, RadioButtonGroup, stylesFactory, useTheme, Checkbox } from '@grafana/ui';
import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { SortPicker } from 'app/core/components/Select/SortPicker'; import { SortPicker } from 'app/core/components/Select/SortPicker';
import { TagFilter } from 'app/core/components/TagFilter/TagFilter'; import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
...@@ -8,11 +8,6 @@ import { SearchSrv } from 'app/core/services/search_srv'; ...@@ -8,11 +8,6 @@ import { SearchSrv } from 'app/core/services/search_srv';
import { layoutOptions } from '../hooks/useSearchLayout'; import { layoutOptions } from '../hooks/useSearchLayout';
import { DashboardQuery } from '../types'; import { DashboardQuery } from '../types';
const starredFilterOptions = [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
];
const searchSrv = new SearchSrv(); const searchSrv = new SearchSrv();
type onSelectChange = (value: SelectableValue) => void; type onSelectChange = (value: SelectableValue) => void;
...@@ -24,7 +19,6 @@ interface Props { ...@@ -24,7 +19,6 @@ interface Props {
onTagFilterChange: onSelectChange; onTagFilterChange: onSelectChange;
query: DashboardQuery; query: DashboardQuery;
showStarredFilter?: boolean; showStarredFilter?: boolean;
hideSelectedTags?: boolean;
hideLayout?: boolean; hideLayout?: boolean;
} }
...@@ -36,7 +30,6 @@ export const ActionRow: FC<Props> = ({ ...@@ -36,7 +30,6 @@ export const ActionRow: FC<Props> = ({
onTagFilterChange, onTagFilterChange,
query, query,
showStarredFilter, showStarredFilter,
hideSelectedTags,
hideLayout, hideLayout,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
...@@ -48,24 +41,13 @@ export const ActionRow: FC<Props> = ({ ...@@ -48,24 +41,13 @@ export const ActionRow: FC<Props> = ({
{!hideLayout ? <RadioButtonGroup options={layoutOptions} onChange={onLayoutChange} value={layout} /> : null} {!hideLayout ? <RadioButtonGroup options={layoutOptions} onChange={onLayoutChange} value={layout} /> : null}
<SortPicker onChange={onSortChange} value={query.sort} /> <SortPicker onChange={onSortChange} value={query.sort} />
</HorizontalGroup> </HorizontalGroup>
<HorizontalGroup spacing="md" justify="space-between"> <HorizontalGroup spacing="md" width="auto">
{showStarredFilter && ( {showStarredFilter && <Checkbox label="Filter by starred" onChange={onStarredFilterChange} />}
<Select
width={20}
placeholder="Filter by starred"
key={starredFilterOptions?.find(f => f.value === query.starred)?.label}
options={starredFilterOptions}
onChange={onStarredFilterChange}
/>
)}
<TagFilter <TagFilter
placeholder="Filter by tag" placeholder="Filter by tag"
tags={query.tag} tags={query.tag}
tagOptions={searchSrv.getDashboardTags} tagOptions={searchSrv.getDashboardTags}
onChange={onTagFilterChange} onChange={onTagFilterChange}
hideValues={hideSelectedTags}
isClearable={!hideSelectedTags}
/> />
</HorizontalGroup> </HorizontalGroup>
</div> </div>
......
...@@ -76,8 +76,10 @@ describe('DashboardSearch', () => { ...@@ -76,8 +76,10 @@ describe('DashboardSearch', () => {
wrapper.update(); wrapper.update();
expect( expect(
wrapper.findWhere((c: any) => c.type() === 'h6' && c.text() === 'No dashboards matching your query were found.') wrapper
).toHaveLength(1); .findWhere((c: any) => c.type() === 'div' && c.text() === 'No dashboards matching your query were found.')
.exists()
).toBe(true);
}); });
it('should render search results', async () => { it('should render search results', async () => {
......
import React, { FC, memo, useState } from 'react'; import React, { FC, memo, useState } from 'react';
import { css } from 'emotion'; import { css } from 'emotion';
import { HorizontalGroup, Icon, stylesFactory, TagList, useTheme } from '@grafana/ui'; import { HorizontalGroup, stylesFactory, useTheme } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
...@@ -32,9 +32,6 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => { ...@@ -32,9 +32,6 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
query, query,
hasFilters, hasFilters,
onQueryChange, onQueryChange,
onRemoveStarred,
onTagRemove,
onClearFilters,
onTagFilterChange, onTagFilterChange,
onStarredFilterChange, onStarredFilterChange,
onTagAdd, onTagAdd,
...@@ -102,72 +99,24 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => { ...@@ -102,72 +99,24 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
/> />
<DashboardActions isEditor={isEditor} canEdit={hasEditPermissionInFolders || canSave} folderId={folderId} /> <DashboardActions isEditor={isEditor} canEdit={hasEditPermissionInFolders || canSave} folderId={folderId} />
</HorizontalGroup> </HorizontalGroup>
{hasFilters && (
<HorizontalGroup>
<div className="gf-form-inline">
{query.tag.length > 0 && (
<div className="gf-form">
<label className="gf-form-label width-4">Tags</label>
<TagList tags={query.tag} onClick={onTagRemove} />
</div>
)}
{query.starred && (
<div className="gf-form">
<label className="gf-form-label">
<a className="pointer" onClick={onRemoveStarred}>
<Icon name="check" />
Starred
</a>
</label>
</div>
)}
{query.sort && (
<div className="gf-form">
<label className="gf-form-label">
<a className="pointer" onClick={() => onSortChange(null)}>
Sort: {query.sort.label}
</a>
</label>
</div>
)}
<div className="gf-form">
<label className="gf-form-label">
<a
className="pointer"
onClick={() => {
onClearFilters();
setLayout(SearchLayout.Folders);
}}
>
<Icon name="times" />
&nbsp;Clear
</a>
</label>
</div>
</div>
</HorizontalGroup>
)}
</div> </div>
<div className={styles.results}> <div className={styles.results}>
{results?.length > 0 && ( <SearchResultsFilter
<SearchResultsFilter allChecked={allChecked}
allChecked={allChecked} canDelete={canDelete}
canDelete={canDelete} canMove={canMove}
canMove={canMove} deleteItem={onItemDelete}
deleteItem={onItemDelete} moveTo={onMoveTo}
moveTo={onMoveTo} onToggleAllChecked={onToggleAllChecked}
onToggleAllChecked={onToggleAllChecked} onStarredFilterChange={onStarredFilterChange}
onStarredFilterChange={onStarredFilterChange} onSortChange={onSortChange}
onSortChange={onSortChange} onTagFilterChange={onTagFilterChange}
onTagFilterChange={onTagFilterChange} query={query}
query={query} layout={layout}
layout={layout} hideLayout={!!folderUid}
hideLayout={!!folderUid} onLayoutChange={onLayoutChange}
onLayoutChange={onLayoutChange} />
/>
)}
<SearchResults <SearchResults
loading={loading} loading={loading}
results={results} results={results}
......
...@@ -68,7 +68,13 @@ export const SearchResults: FC<Props> = ({ ...@@ -68,7 +68,13 @@ export const SearchResults: FC<Props> = ({
> >
{({ index, style }) => { {({ index, style }) => {
const item = items[index]; const item = items[index];
return <SearchItem key={item.id} {...itemProps} item={item} style={style} />; // The wrapper div is needed as the inner SearchItem has margin-bottom spacing
// And without this wrapper there is no room for that margin
return (
<div style={style}>
<SearchItem key={item.id} {...itemProps} item={item} />
</div>
);
}} }}
</FixedSizeList> </FixedSizeList>
)} )}
...@@ -80,7 +86,7 @@ export const SearchResults: FC<Props> = ({ ...@@ -80,7 +86,7 @@ export const SearchResults: FC<Props> = ({
if (loading) { if (loading) {
return <Spinner className={styles.spinner} />; return <Spinner className={styles.spinner} />;
} else if (!results || !results.length) { } else if (!results || !results.length) {
return <h6>No dashboards matching your query were found.</h6>; return <div className={styles.noResults}>No dashboards matching your query were found.</div>;
} }
return ( return (
...@@ -120,6 +126,11 @@ const getSectionStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -120,6 +126,11 @@ const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
border-radius: 3px; border-radius: 3px;
height: 100%; height: 100%;
`, `,
noResults: css`
padding: ${md};
background: ${theme.colors.bg2};
text-style: italic;
`,
listModeWrapper: css` listModeWrapper: css`
position: relative; position: relative;
height: 100%; height: 100%;
......
...@@ -38,24 +38,21 @@ describe('SearchResultsFilter', () => { ...@@ -38,24 +38,21 @@ describe('SearchResultsFilter', () => {
it('should render "filter by starred" and "filter by tag" filters by default', () => { it('should render "filter by starred" and "filter by tag" filters by default', () => {
const { wrapper } = setup(); const { wrapper } = setup();
const ActionRow = wrapper.find('ActionRow').shallow(); const ActionRow = wrapper.find('ActionRow').shallow();
expect(ActionRow.find({ placeholder: 'Filter by starred' })).toHaveLength(1); expect(ActionRow.find('Checkbox')).toHaveLength(1);
expect(ActionRow.find({ placeholder: 'Filter by tag' })).toHaveLength(1);
expect(findBtnByText(wrapper, 'Move')).toHaveLength(0); expect(findBtnByText(wrapper, 'Move')).toHaveLength(0);
expect(findBtnByText(wrapper, 'Delete')).toHaveLength(0); expect(findBtnByText(wrapper, 'Delete')).toHaveLength(0);
}); });
it('should render Move and Delete buttons when canDelete is true', () => { it('should render Move and Delete buttons when canDelete is true', () => {
const { wrapper } = setup({ canDelete: true }); const { wrapper } = setup({ canDelete: true });
expect(wrapper.find({ placeholder: 'Filter by starred' })).toHaveLength(0); expect(wrapper.find('Checkbox')).toHaveLength(1);
expect(wrapper.find({ placeholder: 'Filter by tag' })).toHaveLength(0);
expect(findBtnByText(wrapper, 'Move')).toHaveLength(1); expect(findBtnByText(wrapper, 'Move')).toHaveLength(1);
expect(findBtnByText(wrapper, 'Delete')).toHaveLength(1); expect(findBtnByText(wrapper, 'Delete')).toHaveLength(1);
}); });
it('should render Move and Delete buttons when canMove is true', () => { it('should render Move and Delete buttons when canMove is true', () => {
const { wrapper } = setup({ canMove: true }); const { wrapper } = setup({ canMove: true });
expect(wrapper.find({ placeholder: 'Filter by starred' })).toHaveLength(0); expect(wrapper.find('Checkbox')).toHaveLength(1);
expect(wrapper.find({ placeholder: 'Filter by tag' })).toHaveLength(0);
expect(findBtnByText(wrapper, 'Move')).toHaveLength(1); expect(findBtnByText(wrapper, 'Move')).toHaveLength(1);
expect(findBtnByText(wrapper, 'Delete')).toHaveLength(1); expect(findBtnByText(wrapper, 'Delete')).toHaveLength(1);
}); });
...@@ -66,9 +63,10 @@ describe('SearchResultsFilter', () => { ...@@ -66,9 +63,10 @@ describe('SearchResultsFilter', () => {
//@ts-ignore //@ts-ignore
const { wrapper } = setup({ onStarredFilterChange: mockFilterStarred }, mount); const { wrapper } = setup({ onStarredFilterChange: mockFilterStarred }, mount);
wrapper wrapper
.find({ placeholder: 'Filter by starred' }) .find('Checkbox')
.at(0) .at(1)
.prop('onChange')(option); .prop('onChange')(option as any);
expect(mockFilterStarred).toHaveBeenCalledTimes(1); expect(mockFilterStarred).toHaveBeenCalledTimes(1);
expect(mockFilterStarred).toHaveBeenCalledWith(option); expect(mockFilterStarred).toHaveBeenCalledWith(option);
}); });
......
...@@ -66,7 +66,6 @@ export const SearchResultsFilter: FC<Props> = ({ ...@@ -66,7 +66,6 @@ export const SearchResultsFilter: FC<Props> = ({
query, query,
}} }}
showStarredFilter showStarredFilter
hideSelectedTags
/> />
)} )}
</div> </div>
......
import { useReducer } from 'react'; import { useReducer } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { defaultQuery, queryReducer } from '../reducers/searchQueryReducer'; import { defaultQuery, queryReducer } from '../reducers/searchQueryReducer';
import { import { ADD_TAG, CLEAR_FILTERS, QUERY_CHANGE, SET_TAGS, TOGGLE_SORT, TOGGLE_STARRED } from '../reducers/actionTypes';
ADD_TAG,
CLEAR_FILTERS,
QUERY_CHANGE,
REMOVE_STARRED,
REMOVE_TAG,
SET_TAGS,
TOGGLE_SORT,
TOGGLE_STARRED,
} from '../reducers/actionTypes';
import { DashboardQuery } from '../types'; import { DashboardQuery } from '../types';
import { hasFilters } from '../utils'; import { hasFilters } from '../utils';
...@@ -22,14 +13,6 @@ export const useSearchQuery = (queryParams: Partial<DashboardQuery>) => { ...@@ -22,14 +13,6 @@ export const useSearchQuery = (queryParams: Partial<DashboardQuery>) => {
dispatch({ type: QUERY_CHANGE, payload: query }); dispatch({ type: QUERY_CHANGE, payload: query });
}; };
const onRemoveStarred = () => {
dispatch({ type: REMOVE_STARRED });
};
const onTagRemove = (tag: string) => {
dispatch({ type: REMOVE_TAG, payload: tag });
};
const onTagFilterChange = (tags: string[]) => { const onTagFilterChange = (tags: string[]) => {
dispatch({ type: SET_TAGS, payload: tags }); dispatch({ type: SET_TAGS, payload: tags });
}; };
...@@ -54,8 +37,6 @@ export const useSearchQuery = (queryParams: Partial<DashboardQuery>) => { ...@@ -54,8 +37,6 @@ export const useSearchQuery = (queryParams: Partial<DashboardQuery>) => {
query, query,
hasFilters: hasFilters(query), hasFilters: hasFilters(query),
onQueryChange, onQueryChange,
onRemoveStarred,
onTagRemove,
onClearFilters, onClearFilters,
onTagFilterChange, onTagFilterChange,
onStarredFilterChange, onStarredFilterChange,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment