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