Search/migrate search results (#22930)

* Search: Setup SearchResults.tsx

* Search: add watchDepth

* Search: Use SearchResults.tsx in Angular template

* Search: Render search result header

* Search: Move new search components to features/search

* Search: Render nested dashboards

* Search: Expand dashboard folder

* Search: Remove fa prefix from icon names

* Search: Enable search results toggling

* Search: Add onItemClick handler

* Search: Add missing aria-label

* Search: Add no results message

* Search: Fix e2e selectors

* Search: Update SearchField imports

* Search: Add conditional classes

* Search: Abstract DashboardCheckbox

* Search: Separate ResultItem

* Search: Style ResultItem

* Search: Separate search components

* Search: Tweak checkbox styling

* Search: Simplify component names

* Search: Separate tag component

* Search: Checkbox docs

* Search: Remove inline on click

* Add Tag component

* Add Tag story

* Add TagList

* Group Tab and TabList

* Fix typechecks

* Remove Meta

* Use forwardRef for the Tag

* Search: Use TagList from grafana/ui

* Search: Add media query for TagList

* Search: Add types

* Search: Remove selectThemeVariant from SearchItem.tsx

* Search: Style section + header

* Search: Use semantic html

* Search: Adjust section padding

* Search: Setup tests

* Search: Fix tests

* Search: tweak result styles

* Search: Expand SearchResults tests

* Search: Add SearchItem tests

* Search: Use SearchResults in search.html

* Search: Toggle search result sections

* Search: Make selected prop optional

* Search: Fix tag selection

* Search: Fix tag filter onChange

* Search: Fix uncontrolled state change warning

* Search: Update icon names

* Search: memoize SearchCheckbox.tsx

* Search: Update types

* Search: Cleanup events

* Search: Semantic html

* Use styleMixins

* Search: Tweak styling

* Search: useCallback for checkbox toggle

* Search: Add stylesFactory

Co-authored-by: CirceCI <circleci@grafana.com>
This commit is contained in:
Alex Khomenko
2020-03-26 11:09:08 +02:00
committed by GitHub
parent 77459ae8d8
commit 85dc4e565e
20 changed files with 608 additions and 58 deletions

View File

@@ -39,7 +39,9 @@ export class TagFilter extends React.Component<Props, any> {
};
onChange = (newTags: any[]) => {
this.props.onChange(newTags.map(tag => tag.value));
// On remove with 1 item returns null, so we need to make sure it's an empty array in that case
// https://github.com/JedWatson/react-select/issues/3632
this.props.onChange((newTags || []).map(tag => tag.value));
};
render() {

View File

@@ -102,11 +102,12 @@
</div>
</div>
<div class="search-results-container">
<dashboard-search-results
<search-results
results="ctrl.sections"
editable="true"
on-selection-changed="ctrl.selectionChanged()"
on-tag-selected="ctrl.filterByTag($tag)"
on-selection-changed="ctrl.selectionChanged"
on-tag-selected="ctrl.filterByTag"
on-toggle-selection="ctrl.toggleSelection"
/>
</div>
</div>
@@ -114,14 +115,14 @@
</div>
<div ng-if="ctrl.canSave && ctrl.folderId && !ctrl.hasFilters && ctrl.sections.length === 0">
<empty-list-cta
<empty-list-cta
title="'This folder doesn\'t have any dashboards yet'"
buttonIcon="'gicon gicon-dashboard-new'"
buttonIcon="'gicon gicon-dashboard-new'"
buttonLink="'dashboard/new?folderId={{ctrl.folderId}}'"
buttonTitle="'Create Dashboard'"
proTip="'Add/move dashboards to your folder at ->'"
proTipLink="'dashboards'"
proTipLinkTitle="'Manage dashboards'"
buttonTitle="'Create Dashboard'"
proTip="'Add/move dashboards to your folder at ->'"
proTipLink="'dashboards'"
proTipLinkTitle="'Manage dashboards'"
proTipTarget=""
/>
</div>

View File

@@ -143,17 +143,19 @@ export class ManageDashboardsCtrl {
}
}
selectionChanged() {
selectionChanged = () => {
let selectedDashboards = 0;
for (const section of this.sections) {
selectedDashboards += _.filter(section.items, { checked: true } as any).length;
}
if (this.sections) {
for (const section of this.sections) {
selectedDashboards += _.filter(section.items, { checked: true } as any).length;
}
const selectedFolders = _.filter(this.sections, { checked: true }).length;
this.canMove = selectedDashboards > 0;
this.canDelete = selectedDashboards > 0 || selectedFolders > 0;
}
const selectedFolders = _.filter(this.sections, { checked: true }).length;
this.canMove = selectedDashboards > 0;
this.canDelete = selectedDashboards > 0 || selectedFolders > 0;
}
};
getFoldersAndDashboardsToDelete(): FoldersAndDashboardUids {
const selectedDashboards: FoldersAndDashboardUids = {
@@ -254,13 +256,14 @@ export class ManageDashboardsCtrl {
});
}
filterByTag(tag: any) {
if (_.indexOf(this.query.tag, tag) === -1) {
this.query.tag.push(tag);
filterByTag = (tag: any) => {
if (tag) {
if (_.indexOf(this.query.tag, tag) === -1) {
this.query.tag.push(tag);
}
}
return this.refreshList();
}
};
onQueryChange() {
return this.refreshList();
@@ -333,6 +336,26 @@ export class ManageDashboardsCtrl {
return url;
}
// TODO handle this inside SearchResults component
toggleSelection = (item: any, evt: any) => {
if (evt) {
evt.stopPropagation();
evt.preventDefault();
}
item.checked = !item.checked;
if (item.items) {
_.each(item.items, i => {
i.checked = item.checked;
});
}
if (this.selectionChanged) {
this.selectionChanged();
}
};
}
export function manageDashboardsDirective() {

View File

@@ -1,79 +0,0 @@
import React, { useContext } from 'react';
// @ts-ignore
import tinycolor from 'tinycolor2';
import { SearchQuery } from './search';
import { css, cx } from 'emotion';
import { ThemeContext } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
interface SearchFieldProps extends Omit<React.HTMLAttributes<HTMLInputElement>, 'onChange'> {
query: SearchQuery;
onChange: (query: string) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
}
const getSearchFieldStyles = (theme: GrafanaTheme) => ({
wrapper: css`
width: 100%;
height: 55px; /* this variable is not part of GrafanaTheme yet*/
display: flex;
background-color: ${theme.colors.formInputBg};
position: relative;
box-shadow: 0 0 10px ${theme.isLight ? theme.colors.gray85 : theme.colors.black};
`,
input: css`
max-width: 653px;
padding: ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.sm} ${theme.spacing.md};
height: 51px;
box-sizing: border-box;
outline: none;
background-color: ${theme.colors.formInputBg};
background: ${theme.colors.formInputBg};
flex-grow: 10;
`,
spacer: css`
flex-grow: 1;
`,
icon: cx(
css`
color: ${theme.colors.textWeak};
font-size: ${theme.typography.size.lg};
padding: ${theme.spacing.md} ${theme.spacing.md} ${theme.spacing.sm} ${theme.spacing.md};
`,
'pointer'
),
});
export const SearchField: React.FunctionComponent<SearchFieldProps> = ({ query, onChange, ...inputProps }) => {
const theme = useContext(ThemeContext);
const styles = getSearchFieldStyles(theme);
return (
<>
{/* search-field-wrapper class name left on purpose until we migrate entire search to React */}
{/* based on it GrafanaCtrl (L256) decides whether or not hide search */}
<div className={`${styles.wrapper} search-field-wrapper`}>
<div className={styles.icon}>
<i className="fa fa-search" />
</div>
<input
type="text"
placeholder="Find dashboards by name"
value={query.query}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onChange(event.currentTarget.value);
}}
tabIndex={1}
spellCheck={false}
{...inputProps}
className={styles.input}
/>
<div className={styles.spacer} />
</div>
</>
);
};

View File

@@ -16,11 +16,12 @@
<div class="search-results-scroller">
<div class="search-results-container" grafana-scrollbar>
<h6 ng-show="!ctrl.isLoading && ctrl.results.length === 0">No dashboards matching your query were found.</h6>
<dashboard-search-results
<search-results
results="ctrl.results"
on-tag-selected="ctrl.filterByTag($tag)"
on-folder-expanding="ctrl.folderExpanding()"
on-folder-expanded="ctrl.folderExpanded($folder)" />
on-tag-selected="ctrl.filterByTag"
on-folder-expanding="ctrl.folderExpanding"
on-selection-changed="ctrl.selectionChanged"
/>
</div>
</div>
</div>

View File

@@ -261,12 +261,14 @@ export class SearchCtrl {
return query.query === '' && query.starred === false && query.tags.length === 0;
}
filterByTag(tag: string) {
if (_.indexOf(this.query.tags, tag) === -1) {
this.query.tags.push(tag);
this.search();
filterByTag = (tag: string) => {
if (tag) {
if (_.indexOf(this.query.tags, tag) === -1) {
this.query.tags.push(tag);
this.search();
}
}
}
};
removeTag(tag: string, evt: any) {
this.query.tags = _.without(this.query.tags, tag);
@@ -297,15 +299,20 @@ export class SearchCtrl {
this.search();
}
selectionChanged = () => {
// TODO remove after React-side state management is implemented
// This method is only used as a callback after toggling section, to trigger results rerender
};
search() {
this.showImport = false;
this.selectedIndex = -1;
this.searchDashboards(this.query.parsedQuery['folder']);
}
folderExpanding() {
folderExpanding = () => {
this.moveSelection(0);
}
};
private getFlattenedResultForNavigation(): SelectedIndicies[] {
let folderIndex = 0;

View File

@@ -44,15 +44,6 @@ export class SearchResultsCtrl {
}
}
navigateToFolder(section: any, evt: any) {
this.$location.path(section.url);
if (evt) {
evt.stopPropagation();
evt.preventDefault();
}
}
toggleSelection(item: any, evt: any) {
item.checked = !item.checked;