ManageDashboards: Fixes and improvements (#23879)

* ManageDashboards: Fixes and improvements

* Fixed tests

* Fixed issue with item height and margin
This commit is contained in:
Torkel Ödegaard 2020-04-25 13:08:23 +02:00 committed by GitHub
parent d6ba6440e4
commit e505babbf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 62 additions and 131 deletions

View File

@ -19,6 +19,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => {
labelStyles.label,
css`
padding-left: ${theme.spacing.formSpacingBase}px;
white-space: nowrap;
`
),
description: cx(
@ -50,11 +51,13 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => {
* */
&:checked + span {
background: blue;
background: ${theme.colors.formInputBg};
background: ${theme.colors.formCheckboxBgChecked};
border: none;
&:hover {
background: ${theme.colors.formCheckboxBgCheckedHover};
}
&:after {
content: '';
position: absolute;
@ -79,6 +82,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => {
position: absolute;
top: 1px;
left: 0;
&:hover {
cursor: pointer;
border-color: ${theme.colors.formInputBorderHover};

View File

@ -20,7 +20,12 @@ export const simple = () => {
return (
<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('header', theme, ['sm', 'md', 'lg', 'xl', 'xxl'], ['search', 'trash-alt', 'arrow-left', 'times'])}
</div>
@ -31,8 +36,8 @@ function renderScenario(surface: string, theme: GrafanaTheme, sizes: IconSize[],
let bg: string = 'red';
switch (surface) {
case 'body':
bg = theme.colors.bodyBg;
case 'dashboard':
bg = theme.colors.dashboardBg;
break;
case 'panel':
bg = theme.colors.bodyBg;

View File

@ -18,7 +18,7 @@ export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
tooltipPlacement?: TooltipPlacement;
}
type SurfaceType = 'body' | 'panel' | 'header';
type SurfaceType = 'dashboard' | 'panel' | 'header';
export const IconButton = React.forwardRef<HTMLButtonElement, Props>(
({ name, size = 'md', surface = 'panel', iconType, tooltip, tooltipPlacement, className, ...restProps }, ref) => {
@ -47,7 +47,7 @@ IconButton.displayName = 'IconButton';
function getHoverColor(theme: GrafanaTheme, surface: SurfaceType): string {
switch (surface) {
case 'body':
case 'dashboard':
return theme.isLight ? theme.palette.gray95 : theme.palette.gray15;
case 'panel':
return theme.isLight ? theme.palette.gray6 : theme.palette.gray15;

View File

@ -3,7 +3,7 @@ import { IconButton } from '@grafana/ui';
import { e2e } from '@grafana/e2e';
export interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
surface: 'body' | 'panel';
surface: 'dashboard' | 'panel' | 'header';
}
export const BackButton: React.FC<Props> = ({ surface, onClick }) => {

View File

@ -143,7 +143,7 @@ class DashNav extends PureComponent<Props> {
renderBackButton() {
return (
<div className="navbar-edit">
<BackButton surface="body" onClick={this.onClose} />
<BackButton surface="dashboard" onClick={this.onClose} />
</div>
);
}

View File

@ -50,7 +50,7 @@ export class DashboardSettings extends PureComponent<Props> {
<div className="dashboard-settings">
<div className="navbar navbar--edit">
<div className="navbar-edit">
<BackButton surface="body" onClick={this.onClose} />
<BackButton surface="panel" onClick={this.onClose} />
</div>
<div className="navbar-page-btn">
{haveFolder && <div className="navbar-page-btn__folder">{folderTitle} / </div>}

View File

@ -1,6 +1,6 @@
import React, { Dispatch, FC, SetStateAction } from 'react';
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 { SortPicker } from 'app/core/components/Select/SortPicker';
import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
@ -8,11 +8,6 @@ import { SearchSrv } from 'app/core/services/search_srv';
import { layoutOptions } from '../hooks/useSearchLayout';
import { DashboardQuery } from '../types';
const starredFilterOptions = [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
];
const searchSrv = new SearchSrv();
type onSelectChange = (value: SelectableValue) => void;
@ -24,7 +19,6 @@ interface Props {
onTagFilterChange: onSelectChange;
query: DashboardQuery;
showStarredFilter?: boolean;
hideSelectedTags?: boolean;
hideLayout?: boolean;
}
@ -36,7 +30,6 @@ export const ActionRow: FC<Props> = ({
onTagFilterChange,
query,
showStarredFilter,
hideSelectedTags,
hideLayout,
}) => {
const theme = useTheme();
@ -48,24 +41,13 @@ export const ActionRow: FC<Props> = ({
{!hideLayout ? <RadioButtonGroup options={layoutOptions} onChange={onLayoutChange} value={layout} /> : null}
<SortPicker onChange={onSortChange} value={query.sort} />
</HorizontalGroup>
<HorizontalGroup spacing="md" justify="space-between">
{showStarredFilter && (
<Select
width={20}
placeholder="Filter by starred"
key={starredFilterOptions?.find(f => f.value === query.starred)?.label}
options={starredFilterOptions}
onChange={onStarredFilterChange}
/>
)}
<HorizontalGroup spacing="md" width="auto">
{showStarredFilter && <Checkbox label="Filter by starred" onChange={onStarredFilterChange} />}
<TagFilter
placeholder="Filter by tag"
tags={query.tag}
tagOptions={searchSrv.getDashboardTags}
onChange={onTagFilterChange}
hideValues={hideSelectedTags}
isClearable={!hideSelectedTags}
/>
</HorizontalGroup>
</div>

View File

@ -76,8 +76,10 @@ describe('DashboardSearch', () => {
wrapper.update();
expect(
wrapper.findWhere((c: any) => c.type() === 'h6' && c.text() === 'No dashboards matching your query were found.')
).toHaveLength(1);
wrapper
.findWhere((c: any) => c.type() === 'div' && c.text() === 'No dashboards matching your query were found.')
.exists()
).toBe(true);
});
it('should render search results', async () => {

View File

@ -1,6 +1,6 @@
import React, { FC, memo, useState } from 'react';
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 { contextSrv } from 'app/core/services/context_srv';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
@ -32,9 +32,6 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
query,
hasFilters,
onQueryChange,
onRemoveStarred,
onTagRemove,
onClearFilters,
onTagFilterChange,
onStarredFilterChange,
onTagAdd,
@ -102,72 +99,24 @@ export const ManageDashboards: FC<Props> = memo(({ folderId, folderUid }) => {
/>
<DashboardActions isEditor={isEditor} canEdit={hasEditPermissionInFolders || canSave} folderId={folderId} />
</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 className={styles.results}>
{results?.length > 0 && (
<SearchResultsFilter
allChecked={allChecked}
canDelete={canDelete}
canMove={canMove}
deleteItem={onItemDelete}
moveTo={onMoveTo}
onToggleAllChecked={onToggleAllChecked}
onStarredFilterChange={onStarredFilterChange}
onSortChange={onSortChange}
onTagFilterChange={onTagFilterChange}
query={query}
layout={layout}
hideLayout={!!folderUid}
onLayoutChange={onLayoutChange}
/>
)}
<SearchResultsFilter
allChecked={allChecked}
canDelete={canDelete}
canMove={canMove}
deleteItem={onItemDelete}
moveTo={onMoveTo}
onToggleAllChecked={onToggleAllChecked}
onStarredFilterChange={onStarredFilterChange}
onSortChange={onSortChange}
onTagFilterChange={onTagFilterChange}
query={query}
layout={layout}
hideLayout={!!folderUid}
onLayoutChange={onLayoutChange}
/>
<SearchResults
loading={loading}
results={results}

View File

@ -68,7 +68,13 @@ export const SearchResults: FC<Props> = ({
>
{({ index, style }) => {
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>
)}
@ -80,7 +86,7 @@ export const SearchResults: FC<Props> = ({
if (loading) {
return <Spinner className={styles.spinner} />;
} 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 (
@ -120,6 +126,11 @@ const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
border-radius: 3px;
height: 100%;
`,
noResults: css`
padding: ${md};
background: ${theme.colors.bg2};
text-style: italic;
`,
listModeWrapper: css`
position: relative;
height: 100%;

View File

@ -38,24 +38,21 @@ describe('SearchResultsFilter', () => {
it('should render "filter by starred" and "filter by tag" filters by default', () => {
const { wrapper } = setup();
const ActionRow = wrapper.find('ActionRow').shallow();
expect(ActionRow.find({ placeholder: 'Filter by starred' })).toHaveLength(1);
expect(ActionRow.find({ placeholder: 'Filter by tag' })).toHaveLength(1);
expect(ActionRow.find('Checkbox')).toHaveLength(1);
expect(findBtnByText(wrapper, 'Move')).toHaveLength(0);
expect(findBtnByText(wrapper, 'Delete')).toHaveLength(0);
});
it('should render Move and Delete buttons when canDelete is true', () => {
const { wrapper } = setup({ canDelete: true });
expect(wrapper.find({ placeholder: 'Filter by starred' })).toHaveLength(0);
expect(wrapper.find({ placeholder: 'Filter by tag' })).toHaveLength(0);
expect(wrapper.find('Checkbox')).toHaveLength(1);
expect(findBtnByText(wrapper, 'Move')).toHaveLength(1);
expect(findBtnByText(wrapper, 'Delete')).toHaveLength(1);
});
it('should render Move and Delete buttons when canMove is true', () => {
const { wrapper } = setup({ canMove: true });
expect(wrapper.find({ placeholder: 'Filter by starred' })).toHaveLength(0);
expect(wrapper.find({ placeholder: 'Filter by tag' })).toHaveLength(0);
expect(wrapper.find('Checkbox')).toHaveLength(1);
expect(findBtnByText(wrapper, 'Move')).toHaveLength(1);
expect(findBtnByText(wrapper, 'Delete')).toHaveLength(1);
});
@ -66,9 +63,10 @@ describe('SearchResultsFilter', () => {
//@ts-ignore
const { wrapper } = setup({ onStarredFilterChange: mockFilterStarred }, mount);
wrapper
.find({ placeholder: 'Filter by starred' })
.at(0)
.prop('onChange')(option);
.find('Checkbox')
.at(1)
.prop('onChange')(option as any);
expect(mockFilterStarred).toHaveBeenCalledTimes(1);
expect(mockFilterStarred).toHaveBeenCalledWith(option);
});

View File

@ -66,7 +66,6 @@ export const SearchResultsFilter: FC<Props> = ({
query,
}}
showStarredFilter
hideSelectedTags
/>
)}
</div>

View File

@ -1,16 +1,7 @@
import { useReducer } from 'react';
import { SelectableValue } from '@grafana/data';
import { defaultQuery, queryReducer } from '../reducers/searchQueryReducer';
import {
ADD_TAG,
CLEAR_FILTERS,
QUERY_CHANGE,
REMOVE_STARRED,
REMOVE_TAG,
SET_TAGS,
TOGGLE_SORT,
TOGGLE_STARRED,
} from '../reducers/actionTypes';
import { ADD_TAG, CLEAR_FILTERS, QUERY_CHANGE, SET_TAGS, TOGGLE_SORT, TOGGLE_STARRED } from '../reducers/actionTypes';
import { DashboardQuery } from '../types';
import { hasFilters } from '../utils';
@ -22,14 +13,6 @@ export const useSearchQuery = (queryParams: Partial<DashboardQuery>) => {
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[]) => {
dispatch({ type: SET_TAGS, payload: tags });
};
@ -54,8 +37,6 @@ export const useSearchQuery = (queryParams: Partial<DashboardQuery>) => {
query,
hasFilters: hasFilters(query),
onQueryChange,
onRemoveStarred,
onTagRemove,
onClearFilters,
onTagFilterChange,
onStarredFilterChange,