mirror of
https://github.com/grafana/grafana.git
synced 2024-12-01 21:19:28 -06:00
ManageDashboards: Fixes and improvements (#23879)
* ManageDashboards: Fixes and improvements * Fixed tests * Fixed issue with item height and margin
This commit is contained in:
parent
d6ba6440e4
commit
e505babbf4
@ -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};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 }) => {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>}
|
||||
|
@ -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>
|
||||
|
@ -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 () => {
|
||||
|
@ -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" />
|
||||
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}
|
||||
|
@ -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%;
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -66,7 +66,6 @@ export const SearchResultsFilter: FC<Props> = ({
|
||||
query,
|
||||
}}
|
||||
showStarredFilter
|
||||
hideSelectedTags
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user