mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Refactor Search/Folder view types into DashboardViewItem (#63162)
* Chore: Refactor Search/Folder view types into DashboardViewItem * uid is not optional in api * rename queryResultToNestedFolderItem function * Fix error from locationInfo being empty * change queryResultToViewItem to take view instead * Fix sortMeta fields not showing on search cards * Show correct parent for panel search results
This commit is contained in:
parent
a48793b542
commit
0c36b247af
@ -4075,13 +4075,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/search/page/components/MoveToFolderModal.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/search/page/components/SearchResultsCards.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/search/page/components/SearchResultsGrid.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/search/page/components/columns.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
@ -4096,6 +4089,9 @@ exports[`better eslint`] = {
|
||||
"public/app/features/search/service/sql.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/search/service/utils.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/search/state/SearchStateManager.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
|
@ -5,7 +5,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { AsyncMultiSelect, Icon, Button, useStyles2 } from '@grafana/ui';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { DashboardSearchItemType } from 'app/features/search/types';
|
||||
import { FolderInfo, PermissionLevelString } from 'app/types';
|
||||
|
||||
export interface FolderFilterProps {
|
||||
@ -72,7 +72,7 @@ async function getFoldersAsOptions(
|
||||
};
|
||||
|
||||
// FIXME: stop using id from search and use UID instead
|
||||
const searchHits: DashboardSearchHit[] = await getBackendSrv().search(params);
|
||||
const searchHits = await getBackendSrv().search(params);
|
||||
const options = searchHits.map((d) => ({ label: d.title, value: { uid: d.uid, title: d.title } }));
|
||||
if (!searchString || 'general'.includes(searchString.toLowerCase())) {
|
||||
options.unshift({ label: 'General', value: { uid: 'general', title: 'General' } });
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
|
||||
import * as api from '../../../../features/manage-dashboards/state/actions';
|
||||
import { DashboardSearchItem } from '../../../../features/search/types';
|
||||
import { DashboardSearchHit } from '../../../../features/search/types';
|
||||
import { PermissionLevelString } from '../../../../types';
|
||||
|
||||
import { ALL_FOLDER, GENERAL_FOLDER } from './ReadonlyFolderPicker';
|
||||
import { getFolderAsOption, getFoldersAsOptions } from './api';
|
||||
|
||||
function getTestContext(
|
||||
searchHits: DashboardSearchItem[] = [],
|
||||
searchHits: DashboardSearchHit[] = [],
|
||||
folderById: { id: number; title: string } = { id: 1, title: 'Folder 1' }
|
||||
) {
|
||||
jest.clearAllMocks();
|
||||
|
@ -59,7 +59,6 @@ describe('resolveLinks', () => {
|
||||
title: 'DashLinks',
|
||||
url: '/d/6ieouugGk/DashLinks',
|
||||
isStarred: false,
|
||||
items: [],
|
||||
tags: [],
|
||||
uri: 'db/DashLinks',
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
|
@ -201,7 +201,6 @@ describe('AddToDashboardButton', () => {
|
||||
{
|
||||
uid: 'someUid',
|
||||
isStarred: false,
|
||||
items: [],
|
||||
title: 'Dashboard Title',
|
||||
tags: [],
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
@ -243,7 +242,6 @@ describe('AddToDashboardButton', () => {
|
||||
{
|
||||
uid: 'someUid',
|
||||
isStarred: false,
|
||||
items: [],
|
||||
title: 'Dashboard Title',
|
||||
tags: [],
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
@ -359,7 +357,6 @@ describe('AddToDashboardButton', () => {
|
||||
{
|
||||
uid: 'someUid',
|
||||
isStarred: false,
|
||||
items: [],
|
||||
title: 'Dashboard Title',
|
||||
tags: [],
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
|
@ -9,7 +9,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Icon, Portal, TagList, useTheme2 } from '@grafana/ui';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
|
||||
import { DashboardSectionItem, OnToggleChecked } from '../types';
|
||||
import { DashboardViewItem, OnToggleChecked } from '../types';
|
||||
|
||||
import { SearchCardExpanded } from './SearchCardExpanded';
|
||||
import { SearchCheckbox } from './SearchCheckbox';
|
||||
@ -18,7 +18,8 @@ const DELAY_BEFORE_EXPANDING = 500;
|
||||
|
||||
export interface Props {
|
||||
editable?: boolean;
|
||||
item: DashboardSectionItem;
|
||||
item: DashboardViewItem;
|
||||
isSelected?: boolean;
|
||||
onTagSelected?: (name: string) => any;
|
||||
onToggleChecked?: OnToggleChecked;
|
||||
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
@ -28,7 +29,7 @@ export function getThumbnailURL(uid: string, isLight?: boolean) {
|
||||
return `/api/dashboards/uid/${uid}/img/thumb/${isLight ? 'light' : 'dark'}`;
|
||||
}
|
||||
|
||||
export function SearchCard({ editable, item, onTagSelected, onToggleChecked, onClick }: Props) {
|
||||
export function SearchCard({ editable, item, isSelected, onTagSelected, onToggleChecked, onClick }: Props) {
|
||||
const [hasImage, setHasImage] = useState(true);
|
||||
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
|
||||
const [showExpandedView, setShowExpandedView] = useState(false);
|
||||
@ -130,7 +131,7 @@ export function SearchCard({ editable, item, onTagSelected, onToggleChecked, onC
|
||||
className={styles.checkbox}
|
||||
aria-label={`Select dashboard ${item.title}`}
|
||||
editable={editable}
|
||||
checked={item.checked}
|
||||
checked={isSelected}
|
||||
onClick={onCheckboxClick}
|
||||
/>
|
||||
{hasImage ? (
|
||||
@ -153,7 +154,7 @@ export function SearchCard({ editable, item, onTagSelected, onToggleChecked, onC
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.title}>{item.title}</div>
|
||||
<TagList displayMax={1} tags={item.tags} onClick={onTagClick} />
|
||||
<TagList displayMax={1} tags={item.tags ?? []} onClick={onTagClick} />
|
||||
</div>
|
||||
{showExpandedView && (
|
||||
<Portal className={styles.portal}>
|
||||
|
@ -6,7 +6,7 @@ import SVG from 'react-inlinesvg';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, Spinner, TagList, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { DashboardSectionItem } from '../types';
|
||||
import { DashboardViewItem } from '../types';
|
||||
|
||||
import { getThumbnailURL } from './SearchCard';
|
||||
|
||||
@ -14,7 +14,7 @@ export interface Props {
|
||||
className?: string;
|
||||
imageHeight: number;
|
||||
imageWidth: number;
|
||||
item: DashboardSectionItem;
|
||||
item: DashboardViewItem;
|
||||
lastUpdated?: string | null;
|
||||
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
}
|
||||
@ -25,7 +25,7 @@ export function SearchCardExpanded({ className, imageHeight, imageWidth, item, l
|
||||
const imageSrc = getThumbnailURL(item.uid!, theme.isLight);
|
||||
const styles = getStyles(theme, imageHeight, imageWidth);
|
||||
|
||||
const folderTitle = item.folderTitle || 'General';
|
||||
const folderTitle = item.parentTitle || 'General';
|
||||
|
||||
return (
|
||||
<a className={classNames(className, styles.card)} key={item.uid} href={item.url} onClick={onClick}>
|
||||
@ -66,7 +66,7 @@ export function SearchCardExpanded({ className, imageHeight, imageWidth, item, l
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<TagList className={styles.tagList} tags={item.tags} />
|
||||
<TagList className={styles.tagList} tags={item.tags ?? []} />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { DashboardSearchItemType } from '../types';
|
||||
import { DashboardViewItem } from '../types';
|
||||
|
||||
import { Props, SearchItem } from './SearchItem';
|
||||
|
||||
@ -11,17 +11,12 @@ beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const data = {
|
||||
id: 1,
|
||||
const data: DashboardViewItem = {
|
||||
kind: 'dashboard' as const,
|
||||
uid: 'lBdLINUWk',
|
||||
title: 'Test 1',
|
||||
uri: 'db/test1',
|
||||
url: '/d/lBdLINUWk/test1',
|
||||
slug: '',
|
||||
type: DashboardSearchItemType.DashDB,
|
||||
tags: ['Tag1', 'Tag2'],
|
||||
isStarred: false,
|
||||
checked: false,
|
||||
};
|
||||
|
||||
const setup = (propOverrides?: Partial<Props>) => {
|
||||
@ -54,7 +49,7 @@ describe('SearchItem', () => {
|
||||
});
|
||||
|
||||
it('should mark items as checked', () => {
|
||||
setup({ editable: true, item: { ...data, checked: true } });
|
||||
setup({ editable: true, isSelected: true });
|
||||
expect(screen.getByRole('checkbox')).toBeChecked();
|
||||
});
|
||||
|
||||
|
@ -6,12 +6,14 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { Card, Icon, IconName, TagList, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { SEARCH_ITEM_HEIGHT } from '../constants';
|
||||
import { DashboardSectionItem, OnToggleChecked } from '../types';
|
||||
import { getIconForKind } from '../service/utils';
|
||||
import { DashboardViewItem, OnToggleChecked } from '../types';
|
||||
|
||||
import { SearchCheckbox } from './SearchCheckbox';
|
||||
|
||||
export interface Props {
|
||||
item: DashboardSectionItem;
|
||||
item: DashboardViewItem;
|
||||
isSelected?: boolean;
|
||||
editable?: boolean;
|
||||
onTagSelected: (name: string) => any;
|
||||
onToggleChecked?: OnToggleChecked;
|
||||
@ -30,7 +32,7 @@ const getIconFromMeta = (meta = ''): IconName => {
|
||||
};
|
||||
|
||||
/** @deprecated */
|
||||
export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected, onClickItem }) => {
|
||||
export const SearchItem: FC<Props> = ({ item, isSelected, editable, onToggleChecked, onTagSelected, onClickItem }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const tagSelected = useCallback(
|
||||
(tag: string, event: React.MouseEvent<HTMLElement>) => {
|
||||
@ -53,7 +55,6 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
|
||||
[item, onToggleChecked]
|
||||
);
|
||||
|
||||
const folderTitle = item.folderTitle || 'General';
|
||||
return (
|
||||
<Card
|
||||
data-testid={selectors.dashboardItem(item.title)}
|
||||
@ -67,15 +68,16 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
|
||||
<SearchCheckbox
|
||||
aria-label="Select dashboard"
|
||||
editable={editable}
|
||||
checked={item.checked}
|
||||
checked={isSelected}
|
||||
onClick={handleCheckboxClick}
|
||||
/>
|
||||
</Card.Figure>
|
||||
<Card.Meta separator={''}>
|
||||
<span className={styles.metaContainer}>
|
||||
<Icon name={'folder'} aria-hidden />
|
||||
{folderTitle}
|
||||
<Icon name={getIconForKind(item.parentKind ?? 'folder')} aria-hidden />
|
||||
{item.parentTitle || 'General'}
|
||||
</span>
|
||||
|
||||
{item.sortMetaName && (
|
||||
<span className={styles.metaContainer}>
|
||||
<Icon name={getIconFromMeta(item.sortMetaName)} />
|
||||
@ -84,7 +86,7 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
|
||||
)}
|
||||
</Card.Meta>
|
||||
<Card.Tags>
|
||||
<TagList tags={item.tags} onClick={tagSelected} getAriaLabel={(tag) => `Filter by tag "${tag}"`} />
|
||||
<TagList tags={item.tags ?? []} onClick={tagSelected} getAriaLabel={(tag) => `Filter by tag "${tag}"`} />
|
||||
</Card.Tags>
|
||||
</Card>
|
||||
);
|
||||
|
@ -5,7 +5,7 @@ import React from 'react';
|
||||
import { ArrayVector, DataFrame, DataFrameView, FieldType } from '@grafana/data';
|
||||
|
||||
import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from '../../service';
|
||||
import { DashboardSearchItemType } from '../../types';
|
||||
import { DashboardSearchItemType, DashboardViewItem } from '../../types';
|
||||
|
||||
import { FolderSection } from './FolderSection';
|
||||
|
||||
@ -14,8 +14,8 @@ describe('FolderSection', () => {
|
||||
const mockOnTagSelected = jest.fn();
|
||||
const mockSelectionToggle = jest.fn();
|
||||
const mockSelection = jest.fn();
|
||||
const mockSection = {
|
||||
kind: 'folder',
|
||||
const mockSection: DashboardViewItem = {
|
||||
kind: 'folder' as const,
|
||||
uid: 'my-folder',
|
||||
title: 'My folder',
|
||||
};
|
||||
@ -218,7 +218,7 @@ describe('FolderSection', () => {
|
||||
});
|
||||
|
||||
describe('when in a pseudo-folder (i.e. Starred/Recent)', () => {
|
||||
const mockRecentSection = {
|
||||
const mockRecentSection: DashboardViewItem = {
|
||||
kind: 'folder',
|
||||
uid: '__recent',
|
||||
title: 'Recent',
|
||||
|
@ -1,35 +1,26 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useAsync, useLocalStorage } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { GrafanaTheme2, toIconName } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Card, Checkbox, CollapsableSection, Icon, IconName, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { Card, Checkbox, CollapsableSection, Icon, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { getSectionStorageKey } from 'app/features/search/utils';
|
||||
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
|
||||
|
||||
import { SearchItem } from '../..';
|
||||
import { getGrafanaSearcher, SearchQuery } from '../../service';
|
||||
import { DashboardSearchItemType, DashboardSectionItem } from '../../types';
|
||||
import { queryResultToViewItem } from '../../service/utils';
|
||||
import { DashboardViewItem } from '../../types';
|
||||
import { SelectionChecker, SelectionToggle } from '../selection';
|
||||
|
||||
export interface DashboardSection {
|
||||
kind: string; // folder | query!
|
||||
uid: string;
|
||||
title: string;
|
||||
selected?: boolean; // not used ? keyboard
|
||||
url?: string;
|
||||
icon?: IconName;
|
||||
itemsUIDs?: string[]; // for pseudo folders
|
||||
}
|
||||
|
||||
interface SectionHeaderProps {
|
||||
selection?: SelectionChecker;
|
||||
selectionToggle?: SelectionToggle;
|
||||
onClickItem?: (e: React.MouseEvent<HTMLElement>) => void;
|
||||
onTagSelected: (tag: string) => void;
|
||||
section: DashboardSection;
|
||||
section: DashboardViewItem;
|
||||
renderStandaloneBody?: boolean; // render the body on its own
|
||||
tags?: string[];
|
||||
}
|
||||
@ -44,20 +35,13 @@ export const FolderSection = ({
|
||||
tags,
|
||||
}: SectionHeaderProps) => {
|
||||
const editable = selectionToggle != null;
|
||||
const styles = useStyles2(
|
||||
useCallback(
|
||||
(theme: GrafanaTheme2) => getSectionHeaderStyles(theme, section.selected, editable),
|
||||
[section.selected, editable]
|
||||
)
|
||||
);
|
||||
const styles = useStyles2(useCallback((theme: GrafanaTheme2) => getSectionHeaderStyles(theme, editable), [editable]));
|
||||
const [sectionExpanded, setSectionExpanded] = useLocalStorage(getSectionStorageKey(section.title), false);
|
||||
|
||||
const results = useAsync(async () => {
|
||||
if (!sectionExpanded && !renderStandaloneBody) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
let folderUid: string | undefined = section.uid;
|
||||
let folderTitle: string | undefined = section.title;
|
||||
let query: SearchQuery = {
|
||||
query: '*',
|
||||
kind: ['dashboard'],
|
||||
@ -69,24 +53,11 @@ export const FolderSection = ({
|
||||
query = {
|
||||
uid: section.itemsUIDs, // array of UIDs
|
||||
};
|
||||
folderUid = undefined;
|
||||
folderTitle = undefined;
|
||||
}
|
||||
|
||||
const raw = await getGrafanaSearcher().search({ ...query, tags });
|
||||
const v = raw.view.map<DashboardSectionItem>((item) => ({
|
||||
uid: item.uid,
|
||||
title: item.name,
|
||||
url: item.url,
|
||||
uri: item.url,
|
||||
type: item.kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB,
|
||||
id: 666, // do not use me!
|
||||
isStarred: false,
|
||||
tags: item.tags ?? [],
|
||||
folderUid: folderUid || item.location,
|
||||
folderTitle: folderTitle || raw.view.dataFrame.meta?.custom?.locationInfo[item.location].name,
|
||||
}));
|
||||
return v;
|
||||
const items = raw.view.map((v) => queryResultToViewItem(v, raw.view));
|
||||
return items;
|
||||
}, [sectionExpanded, tags]);
|
||||
|
||||
const onSectionExpand = () => {
|
||||
@ -111,7 +82,7 @@ export const FolderSection = ({
|
||||
const id = useUniqueId();
|
||||
const labelId = `section-header-label-${id}`;
|
||||
|
||||
let icon = section.icon;
|
||||
let icon = toIconName(section.icon ?? '');
|
||||
if (!icon) {
|
||||
icon = sectionExpanded ? 'folder-open' : 'folder';
|
||||
}
|
||||
@ -127,18 +98,11 @@ export const FolderSection = ({
|
||||
);
|
||||
}
|
||||
|
||||
return results.value.map((v) => {
|
||||
if (selection && selectionToggle) {
|
||||
const type = v.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard';
|
||||
v = {
|
||||
...v,
|
||||
checked: selection(type, v.uid!),
|
||||
};
|
||||
}
|
||||
return results.value.map((item) => {
|
||||
return (
|
||||
<SearchItem
|
||||
key={v.uid}
|
||||
item={v}
|
||||
key={item.uid}
|
||||
item={item}
|
||||
onTagSelected={onTagSelected}
|
||||
onToggleChecked={(item) => {
|
||||
if (selectionToggle) {
|
||||
@ -147,6 +111,7 @@ export const FolderSection = ({
|
||||
}}
|
||||
editable={Boolean(selection != null)}
|
||||
onClickItem={onClickItem}
|
||||
isSelected={selectionToggle && selection?.(item.kind, item.uid)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -203,34 +168,30 @@ export const FolderSection = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getSectionHeaderStyles = (theme: GrafanaTheme2, selected = false, editable: boolean) => {
|
||||
const getSectionHeaderStyles = (theme: GrafanaTheme2, editable: boolean) => {
|
||||
const sm = theme.spacing(1);
|
||||
return {
|
||||
wrapper: cx(
|
||||
css`
|
||||
align-items: center;
|
||||
font-size: ${theme.typography.size.base};
|
||||
padding: 12px;
|
||||
border-bottom: none;
|
||||
color: ${theme.colors.text.secondary};
|
||||
z-index: 1;
|
||||
wrapper: css`
|
||||
align-items: center;
|
||||
font-size: ${theme.typography.size.base};
|
||||
padding: 12px;
|
||||
border-bottom: none;
|
||||
color: ${theme.colors.text.secondary};
|
||||
z-index: 1;
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
color: ${theme.colors.text};
|
||||
}
|
||||
&:hover,
|
||||
&.selected {
|
||||
color: ${theme.colors.text};
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible,
|
||||
&:focus-within {
|
||||
a {
|
||||
opacity: 1;
|
||||
}
|
||||
&:hover,
|
||||
&:focus-visible,
|
||||
&:focus-within {
|
||||
a {
|
||||
opacity: 1;
|
||||
}
|
||||
`,
|
||||
'pointer',
|
||||
{ selected }
|
||||
),
|
||||
}
|
||||
`,
|
||||
sectionItems: css`
|
||||
margin: 0 24px 0 32px;
|
||||
`,
|
||||
|
@ -11,9 +11,11 @@ import { contextSrv } from '../../../../core/services/context_srv';
|
||||
import impressionSrv from '../../../../core/services/impression_srv';
|
||||
import { GENERAL_FOLDER_UID } from '../../constants';
|
||||
import { getGrafanaSearcher } from '../../service';
|
||||
import { queryResultToViewItem } from '../../service/utils';
|
||||
import { DashboardViewItem } from '../../types';
|
||||
import { SearchResultsProps } from '../components/SearchResultsTable';
|
||||
|
||||
import { DashboardSection, FolderSection } from './FolderSection';
|
||||
import { FolderSection } from './FolderSection';
|
||||
|
||||
type Props = Pick<SearchResultsProps, 'selection' | 'selectionToggle' | 'onTagSelected' | 'onClickItem'> & {
|
||||
tags?: string[];
|
||||
@ -31,38 +33,33 @@ export const FolderView = ({
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const results = useAsync(async () => {
|
||||
const folders: DashboardSection[] = [];
|
||||
const folders: DashboardViewItem[] = [];
|
||||
|
||||
if (!hidePseudoFolders) {
|
||||
if (contextSrv.isSignedIn) {
|
||||
const stars = await getBackendSrv().get('api/user/stars');
|
||||
if (stars.length > 0) {
|
||||
folders.push({ title: 'Starred', icon: 'star', kind: 'query-star', uid: '__starred', itemsUIDs: stars });
|
||||
folders.push({ title: 'Starred', icon: 'star', kind: 'folder', uid: '__starred', itemsUIDs: stars });
|
||||
}
|
||||
}
|
||||
|
||||
const itemsUIDs = await impressionSrv.getDashboardOpened();
|
||||
if (itemsUIDs.length) {
|
||||
folders.push({ title: 'Recent', icon: 'clock-nine', kind: 'query-recent', uid: '__recent', itemsUIDs });
|
||||
folders.push({ title: 'Recent', icon: 'clock-nine', kind: 'folder', uid: '__recent', itemsUIDs });
|
||||
}
|
||||
}
|
||||
|
||||
folders.push({ title: 'General', url: '/dashboards', kind: 'folder', uid: GENERAL_FOLDER_UID });
|
||||
|
||||
const searcher = getGrafanaSearcher();
|
||||
const rsp = await searcher.search({
|
||||
const results = await searcher.search({
|
||||
query: '*',
|
||||
kind: ['folder'],
|
||||
sort: searcher.getFolderViewSort(),
|
||||
limit: 1000,
|
||||
});
|
||||
for (const row of rsp.view) {
|
||||
folders.push({
|
||||
title: row.name,
|
||||
url: row.url,
|
||||
uid: row.uid,
|
||||
kind: row.kind,
|
||||
});
|
||||
for (const row of results.view) {
|
||||
folders.push(queryResultToViewItem(row, results.view));
|
||||
}
|
||||
|
||||
return folders;
|
||||
|
@ -9,8 +9,7 @@ import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { SearchItem } from '../../components/SearchItem';
|
||||
import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection';
|
||||
import { SearchResultMeta } from '../../service';
|
||||
import { DashboardSearchItemType, DashboardSectionItem } from '../../types';
|
||||
import { queryResultToViewItem } from '../../service/utils';
|
||||
|
||||
import { SearchResultsProps } from './SearchResultsTable';
|
||||
|
||||
@ -42,45 +41,18 @@ export const SearchResultsCards = React.memo(
|
||||
|
||||
const RenderRow = useCallback(
|
||||
({ index: rowIndex, style }: { index: number; style: CSSProperties }) => {
|
||||
const meta = response.view.dataFrame.meta?.custom as SearchResultMeta;
|
||||
|
||||
let className = '';
|
||||
if (rowIndex === highlightIndex.y) {
|
||||
className += ' ' + styles.selectedRow;
|
||||
}
|
||||
|
||||
const item = response.view.get(rowIndex);
|
||||
let v: DashboardSectionItem = {
|
||||
uid: item.uid,
|
||||
title: item.name,
|
||||
url: item.url,
|
||||
uri: item.url,
|
||||
type: item.kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB,
|
||||
id: 666, // do not use me!
|
||||
isStarred: false,
|
||||
tags: item.tags ?? [],
|
||||
};
|
||||
const searchItem = queryResultToViewItem(item, response.view);
|
||||
|
||||
if (item.location) {
|
||||
const first = item.location.split('/')[0];
|
||||
const finfo = meta.locationInfo[first];
|
||||
if (finfo) {
|
||||
v.folderUid = item.location;
|
||||
v.folderTitle = finfo.name;
|
||||
}
|
||||
}
|
||||
|
||||
if (selection && selectionToggle) {
|
||||
const type = v.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard';
|
||||
v = {
|
||||
...v,
|
||||
checked: selection(type, v.uid!),
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div style={style} key={item.uid} className={className} role="row">
|
||||
<SearchItem
|
||||
item={v}
|
||||
item={searchItem}
|
||||
onTagSelected={onTagSelected}
|
||||
onToggleChecked={(item) => {
|
||||
if (selectionToggle) {
|
||||
@ -89,6 +61,7 @@ export const SearchResultsCards = React.memo(
|
||||
}}
|
||||
editable={Boolean(selection != null)}
|
||||
onClickItem={onClickItem}
|
||||
isSelected={selectionToggle && selection?.(searchItem.kind, searchItem.uid)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -9,7 +9,8 @@ import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { SearchCard } from '../../components/SearchCard';
|
||||
import { useSearchKeyboardNavigation } from '../../hooks/useSearchKeyboardSelection';
|
||||
import { DashboardSearchItemType, DashboardSectionItem } from '../../types';
|
||||
import { queryResultToViewItem } from '../../service/utils';
|
||||
import { DashboardViewItem } from '../../types';
|
||||
|
||||
import { SearchResultsProps } from './SearchResultsTable';
|
||||
|
||||
@ -28,11 +29,9 @@ export const SearchResultsGrid = ({
|
||||
// Hacked to reuse existing SearchCard (and old DashboardSectionItem)
|
||||
const itemProps = {
|
||||
editable: selection != null,
|
||||
onToggleChecked: (item: any) => {
|
||||
const d = item as DashboardSectionItem;
|
||||
const t = d.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard';
|
||||
onToggleChecked: (item: DashboardViewItem) => {
|
||||
if (selectionToggle) {
|
||||
selectionToggle(t, d.uid!);
|
||||
selectionToggle(item.kind, item.uid);
|
||||
}
|
||||
},
|
||||
onTagSelected,
|
||||
@ -77,17 +76,7 @@ export const SearchResultsGrid = ({
|
||||
const item = view.get(index);
|
||||
const kind = item.kind ?? 'dashboard';
|
||||
|
||||
const facade: DashboardSectionItem = {
|
||||
uid: item.uid,
|
||||
title: item.name,
|
||||
url: item.url,
|
||||
uri: item.url,
|
||||
type: kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB,
|
||||
id: 666, // do not use me!
|
||||
isStarred: false,
|
||||
tags: item.tags ?? [],
|
||||
checked: selection ? selection(kind, item.uid) : false,
|
||||
};
|
||||
const facade = queryResultToViewItem(item, view);
|
||||
|
||||
if (kind === 'panel') {
|
||||
const type = item.panel_type;
|
||||
@ -112,7 +101,12 @@ export const SearchResultsGrid = ({
|
||||
// And without this wrapper there is no room for that margin
|
||||
return item ? (
|
||||
<li style={style} className={className}>
|
||||
<SearchCard key={item.uid} {...itemProps} item={facade} />
|
||||
<SearchCard
|
||||
key={item.uid}
|
||||
{...itemProps}
|
||||
item={facade}
|
||||
isSelected={selection ? selection(facade.kind, facade.uid) : false}
|
||||
/>
|
||||
</li>
|
||||
) : null;
|
||||
}}
|
||||
|
@ -17,6 +17,7 @@ import { PluginIconName } from 'app/features/plugins/admin/types';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import { QueryResponse, SearchResultMeta } from '../../service';
|
||||
import { getIconForKind } from '../../service/utils';
|
||||
import { SelectionChecker, SelectionToggle } from '../selection';
|
||||
|
||||
import { ExplainScorePopup } from './ExplainScorePopup';
|
||||
@ -249,16 +250,6 @@ export const generateColumns = (
|
||||
return columns;
|
||||
};
|
||||
|
||||
function getIconForKind(v: string): IconName {
|
||||
if (v === 'dashboard') {
|
||||
return 'apps';
|
||||
}
|
||||
if (v === 'folder') {
|
||||
return 'folder';
|
||||
}
|
||||
return 'question-circle';
|
||||
}
|
||||
|
||||
function hasValue(f: Field): boolean {
|
||||
for (let i = 0; i < f.values.length; i++) {
|
||||
if (f.values.get(i) != null) {
|
||||
|
@ -148,7 +148,7 @@ export class SQLSearcher implements GrafanaSearcher {
|
||||
const k = hit.type === 'dash-folder' ? 'folder' : 'dashboard';
|
||||
kind.push(k);
|
||||
name.push(hit.title);
|
||||
uid.push(hit.uid!);
|
||||
uid.push(hit.uid);
|
||||
url.push(hit.url);
|
||||
tags.push(hit.tags);
|
||||
sortBy.push(hit.sortMeta!);
|
||||
@ -171,7 +171,7 @@ export class SQLSearcher implements GrafanaSearcher {
|
||||
folderId: hit.folderId,
|
||||
};
|
||||
} else if (k === 'folder') {
|
||||
this.locationInfo[hit.uid!] = {
|
||||
this.locationInfo[hit.uid] = {
|
||||
kind: k,
|
||||
name: hit.title!,
|
||||
url: hit.url,
|
||||
|
@ -40,6 +40,9 @@ export interface DashboardQueryResult {
|
||||
// debugging fields
|
||||
score: number;
|
||||
explain: {};
|
||||
|
||||
// enterprise sends extra properties through for sorting (views, errors, etc)
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface LocationInfo {
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { DataFrameView, IconName } from '@grafana/data';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
|
||||
import { SearchQuery } from './types';
|
||||
import { DashboardViewItem } from '../types';
|
||||
|
||||
import { DashboardQueryResult, SearchQuery, SearchResultMeta } from './types';
|
||||
|
||||
/** prepare the query replacing folder:current */
|
||||
export async function replaceCurrentFolderQuery(query: SearchQuery): Promise<SearchQuery> {
|
||||
@ -34,3 +37,54 @@ async function getCurrentFolderUID(): Promise<string | undefined> {
|
||||
function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function getIconForKind(kind: string): IconName {
|
||||
if (kind === 'dashboard') {
|
||||
return 'apps';
|
||||
}
|
||||
|
||||
if (kind === 'folder') {
|
||||
return 'folder';
|
||||
}
|
||||
|
||||
return 'question-circle';
|
||||
}
|
||||
|
||||
export function queryResultToViewItem(
|
||||
item: DashboardQueryResult,
|
||||
view?: DataFrameView<DashboardQueryResult>,
|
||||
index = -1
|
||||
): DashboardViewItem {
|
||||
const meta = view?.dataFrame.meta?.custom as SearchResultMeta | undefined;
|
||||
|
||||
const viewItem: DashboardViewItem = {
|
||||
kind: 'dashboard',
|
||||
uid: item.uid,
|
||||
title: item.name,
|
||||
url: item.url,
|
||||
tags: item.tags ?? [],
|
||||
};
|
||||
|
||||
// Set enterprise sort value property
|
||||
const sortFieldName = meta?.sortBy;
|
||||
if (sortFieldName) {
|
||||
console.log('have sortFieldName', sortFieldName);
|
||||
const sortFieldValue = item[sortFieldName];
|
||||
if (typeof sortFieldValue === 'string' || typeof sortFieldValue === 'number') {
|
||||
viewItem.sortMetaName = sortFieldName;
|
||||
viewItem.sortMeta = sortFieldValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.location) {
|
||||
const ancestors = item.location.split('/');
|
||||
const parentUid = ancestors[ancestors.length - 1];
|
||||
const parentInfo = meta?.locationInfo[parentUid];
|
||||
if (parentInfo) {
|
||||
viewItem.parentTitle = parentInfo.name;
|
||||
viewItem.parentKind = parentInfo.kind;
|
||||
}
|
||||
}
|
||||
|
||||
return viewItem;
|
||||
}
|
||||
|
@ -11,59 +11,67 @@ export enum DashboardSearchItemType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @deprecated Use DashboardSearchItem and use UIDs instead of IDs
|
||||
* DTO type for search API result items, but with deprecated IDs
|
||||
* This type was previously also used heavily for views, so contains lots of
|
||||
* extraneous properties
|
||||
*/
|
||||
export interface DashboardSection {
|
||||
id?: number;
|
||||
uid?: string;
|
||||
title: string;
|
||||
expanded?: boolean;
|
||||
url: string;
|
||||
icon?: string;
|
||||
score?: number;
|
||||
checked?: boolean;
|
||||
items: DashboardSectionItem[];
|
||||
toggle?: (section: DashboardSection) => Promise<DashboardSection>;
|
||||
selected?: boolean;
|
||||
type: DashboardSearchItemType;
|
||||
slug?: string;
|
||||
itemsFetching?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export interface DashboardSectionItem {
|
||||
checked?: boolean;
|
||||
export interface DashboardSearchHit extends WithAccessControlMetadata {
|
||||
folderId?: number;
|
||||
folderTitle?: string;
|
||||
folderUid?: string;
|
||||
folderUrl?: string;
|
||||
id?: number;
|
||||
isStarred: boolean;
|
||||
selected?: boolean;
|
||||
tags: string[];
|
||||
title: string;
|
||||
type: DashboardSearchItemType;
|
||||
icon?: string; // used for grid view
|
||||
uid?: string;
|
||||
uri: string;
|
||||
uid: string;
|
||||
url: string;
|
||||
sortMeta?: number;
|
||||
sortMetaName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated - It uses dashboard ID which is deprecated in favor of dashboard UID. Please, use DashboardSearchItem instead.
|
||||
* DTO type for search API result items
|
||||
* This should not be used directly - use GrafanaSearcher instead and get a DashboardQueryResult
|
||||
*/
|
||||
export interface DashboardSearchHit extends DashboardSectionItem, DashboardSection, WithAccessControlMetadata {}
|
||||
|
||||
export interface DashboardSearchItem
|
||||
extends Omit<
|
||||
DashboardSearchHit,
|
||||
'id' | 'uid' | 'expanded' | 'selected' | 'checked' | 'folderId' | 'icon' | 'sortMeta' | 'sortMetaName'
|
||||
> {
|
||||
export interface DashboardSearchItem {
|
||||
uid: string;
|
||||
title: string;
|
||||
uri: string;
|
||||
url: string;
|
||||
type: string; // dash-db, dash-home
|
||||
tags: string[];
|
||||
isStarred: boolean;
|
||||
|
||||
// Only on dashboards in folders results
|
||||
folderUid?: string;
|
||||
folderTitle?: string;
|
||||
folderUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type used in the folder view components
|
||||
*/
|
||||
export interface DashboardViewItem {
|
||||
kind: 'folder' | 'dashboard' | 'panel';
|
||||
uid: string;
|
||||
title: string;
|
||||
url?: string;
|
||||
tags?: string[];
|
||||
|
||||
icon?: string;
|
||||
|
||||
// Most commonly parent folder title, but can be dashboard if panelTitleSearch is enabled
|
||||
parentTitle?: string;
|
||||
parentKind?: string;
|
||||
|
||||
// Used only for psuedo-folders, such as Starred or Recent
|
||||
itemsUIDs?: string[];
|
||||
|
||||
// For enterprise sort options
|
||||
sortMeta?: number | string; // value sorted by
|
||||
sortMetaName?: string; // name of the value being sorted e.g. 'Views'
|
||||
}
|
||||
|
||||
export interface SearchAction extends Action {
|
||||
@ -88,7 +96,7 @@ export interface SearchState {
|
||||
eventTrackingNamespace: EventTrackingNamespace;
|
||||
}
|
||||
|
||||
export type OnToggleChecked = (item: DashboardSectionItem | DashboardSection) => void;
|
||||
export type OnToggleChecked = (item: DashboardViewItem) => void;
|
||||
|
||||
export enum SearchLayout {
|
||||
List = 'list',
|
||||
|
@ -170,7 +170,7 @@ export function DashList(props: PanelProps<PanelOptions>) {
|
||||
<ul className={css.gridContainer}>
|
||||
{dashboards.map((dash) => (
|
||||
<li key={dash.uid}>
|
||||
<SearchCard item={dash} />
|
||||
<SearchCard item={{ ...dash, kind: 'folder' }} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
Loading…
Reference in New Issue
Block a user