mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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() {
|
||||
|
@@ -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>
|
||||
|
@@ -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() {
|
||||
|
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
||||
|
Reference in New Issue
Block a user