mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Remove search_srv (#62964)
* Chore: Remove Search Service * use getSortOptions from grafana searcher * wip * fix library panels search tests
This commit is contained in:
@@ -2347,9 +2347,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/core/components/Select/SortPicker.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/core/components/TagFilter/TagFilter.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
@@ -2478,17 +2475,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
],
|
||||
"public/app/core/services/search_srv.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
|
||||
],
|
||||
"public/app/core/specs/backend_srv.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
@@ -2503,14 +2489,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/core/specs/search_srv.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
],
|
||||
"public/app/core/specs/ticks.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
||||
@@ -4,8 +4,7 @@ import { useAsync } from 'react-use';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Icon, Select } from '@grafana/ui';
|
||||
import { DEFAULT_SORT } from 'app/features/search/constants';
|
||||
|
||||
import { SearchSrv } from '../../services/search_srv';
|
||||
import { getGrafanaSearcher } from 'app/features/search/service';
|
||||
|
||||
export interface Props {
|
||||
onChange: (sortValue: SelectableValue) => void;
|
||||
@@ -17,9 +16,7 @@ export interface Props {
|
||||
}
|
||||
|
||||
const defaultSortOptionsGetter = (): Promise<SelectableValue[]> => {
|
||||
return new SearchSrv().getSortOptions().then(({ sortOptions }) => {
|
||||
return sortOptions.map((opt: any) => ({ label: opt.displayName, value: opt.name }));
|
||||
});
|
||||
return getGrafanaSearcher().getSortOptions();
|
||||
};
|
||||
|
||||
export function SortPicker({ onChange, value, placeholder, filter, getSortOptions, isClearable }: Props) {
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import { clone, keys, sortBy, take, values } from 'lodash';
|
||||
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import impressionSrv from 'app/core/services/impression_srv';
|
||||
import store from 'app/core/store';
|
||||
import { SECTION_STORAGE_KEY } from 'app/features/search/constants';
|
||||
import {
|
||||
DashboardSection,
|
||||
DashboardSearchItemType,
|
||||
DashboardSearchItem,
|
||||
SearchLayout,
|
||||
} from 'app/features/search/types';
|
||||
import { hasFilters } from 'app/features/search/utils';
|
||||
|
||||
import { backendSrv } from './backend_srv';
|
||||
|
||||
interface Sections {
|
||||
[key: string]: Partial<DashboardSection>;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export class SearchSrv {
|
||||
private getRecentDashboards(sections: DashboardSection[] | any) {
|
||||
return this.queryForRecentDashboards().then((result: any[]) => {
|
||||
if (result.length > 0) {
|
||||
sections['recent'] = {
|
||||
title: 'Recent',
|
||||
icon: 'clock-nine',
|
||||
score: -1,
|
||||
expanded: store.getBool(`${SECTION_STORAGE_KEY}.recent`, true),
|
||||
items: result,
|
||||
type: DashboardSearchItemType.DashFolder,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private queryForRecentDashboards(): Promise<DashboardSearchItem[]> {
|
||||
return new Promise((resolve) => {
|
||||
impressionSrv.getDashboardOpened().then((uids) => {
|
||||
const dashUIDs: string[] = take(uids, 30);
|
||||
if (dashUIDs.length === 0) {
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
backendSrv.search({ dashboardUIDs: dashUIDs }).then((result) => {
|
||||
return resolve(
|
||||
dashUIDs
|
||||
.map((orderId) => result.find((result) => result.uid === orderId))
|
||||
.filter((hit) => hit && !hit.isStarred) as DashboardSearchItem[]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getStarred(sections: DashboardSection): Promise<any> {
|
||||
if (!contextSrv.isSignedIn) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return backendSrv.search({ starred: true, limit: 30 }).then((result) => {
|
||||
if (result.length > 0) {
|
||||
(sections as any)['starred'] = {
|
||||
title: 'Starred',
|
||||
icon: 'star',
|
||||
score: -2,
|
||||
expanded: store.getBool(`${SECTION_STORAGE_KEY}.starred`, true),
|
||||
items: result,
|
||||
type: DashboardSearchItemType.DashFolder,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
search(options: any) {
|
||||
const sections: any = {};
|
||||
const promises = [];
|
||||
const query = clone(options);
|
||||
const filters = hasFilters(options) || query.folderIds?.length > 0;
|
||||
|
||||
query.folderIds = query.folderIds || [];
|
||||
|
||||
if (query.layout === SearchLayout.List) {
|
||||
return backendSrv
|
||||
.search({ ...query, type: DashboardSearchItemType.DashDB })
|
||||
.then((results) => (results.length ? [{ title: '', items: results }] : []));
|
||||
}
|
||||
|
||||
if (!filters) {
|
||||
query.folderIds = [0];
|
||||
}
|
||||
|
||||
if (!options.skipRecent && !filters) {
|
||||
promises.push(this.getRecentDashboards(sections));
|
||||
}
|
||||
|
||||
if (!options.skipStarred && !filters) {
|
||||
promises.push(this.getStarred(sections));
|
||||
}
|
||||
|
||||
promises.push(
|
||||
backendSrv.search(query).then((results) => {
|
||||
return this.handleSearchResult(sections, results);
|
||||
})
|
||||
);
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return sortBy(values(sections), 'score');
|
||||
});
|
||||
}
|
||||
|
||||
private handleSearchResult(sections: Sections, results: DashboardSearchItem[]): any {
|
||||
if (results.length === 0) {
|
||||
return sections;
|
||||
}
|
||||
|
||||
// create folder index
|
||||
for (const hit of results) {
|
||||
if (hit.type === 'dash-folder') {
|
||||
sections[hit.uid!] = {
|
||||
uid: hit.uid,
|
||||
title: hit.title,
|
||||
expanded: false,
|
||||
items: [],
|
||||
url: hit.url,
|
||||
icon: 'folder',
|
||||
score: keys(sections).length,
|
||||
type: hit.type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const hit of results) {
|
||||
if (hit.type === 'dash-folder') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let section = sections[hit.folderUid || 0];
|
||||
if (!section) {
|
||||
if (hit.folderUid) {
|
||||
section = {
|
||||
uid: hit.folderUid,
|
||||
title: hit.folderTitle,
|
||||
url: hit.folderUrl,
|
||||
items: [],
|
||||
icon: 'folder-open',
|
||||
score: keys(sections).length,
|
||||
type: DashboardSearchItemType.DashFolder,
|
||||
};
|
||||
} else {
|
||||
section = {
|
||||
uid: '',
|
||||
title: 'General',
|
||||
items: [],
|
||||
icon: 'folder-open',
|
||||
score: keys(sections).length,
|
||||
type: DashboardSearchItemType.DashFolder,
|
||||
};
|
||||
}
|
||||
// add section
|
||||
sections[hit.folderUid || 0] = section;
|
||||
}
|
||||
|
||||
section.expanded = true;
|
||||
section.items && section.items.push(hit);
|
||||
}
|
||||
}
|
||||
|
||||
getDashboardTags() {
|
||||
return backendSrv.get('/api/dashboards/tags');
|
||||
}
|
||||
|
||||
getSortOptions() {
|
||||
return backendSrv.get('/api/search/sorting');
|
||||
}
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import impressionSrv from 'app/core/services/impression_srv';
|
||||
import { SearchSrv } from 'app/core/services/search_srv';
|
||||
import { DashboardSearchItem } from 'app/features/search/types';
|
||||
|
||||
import { backendSrv } from '../services/backend_srv';
|
||||
|
||||
jest.mock('app/core/store', () => {
|
||||
return {
|
||||
getBool: jest.fn(),
|
||||
set: jest.fn(),
|
||||
getObject: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('app/core/services/impression_srv', () => {
|
||||
return {
|
||||
getDashboardOpened: jest.fn,
|
||||
};
|
||||
});
|
||||
|
||||
describe('SearchSrv', () => {
|
||||
let searchSrv: SearchSrv;
|
||||
const searchMock = jest.spyOn(backendSrv, 'search'); // will use the mock in __mocks__
|
||||
|
||||
beforeEach(() => {
|
||||
searchSrv = new SearchSrv();
|
||||
|
||||
contextSrv.isSignedIn = true;
|
||||
impressionSrv.getDashboardOpened = jest.fn().mockResolvedValue([]);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('With recent dashboards', () => {
|
||||
let results: any;
|
||||
|
||||
beforeEach(() => {
|
||||
searchMock.mockImplementation((options) => {
|
||||
if (options.dashboardUIDs) {
|
||||
return Promise.resolve([
|
||||
{ uid: 'DSNdW0gVk', title: 'second but first' },
|
||||
{ uid: 'srx16xR4z', title: 'first but second' },
|
||||
] as DashboardSearchItem[]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
impressionSrv.getDashboardOpened = jest.fn().mockResolvedValue(['srx16xR4z', 'DSNdW0gVk']);
|
||||
|
||||
return searchSrv.search({ query: '' }).then((res) => {
|
||||
results = res;
|
||||
});
|
||||
});
|
||||
|
||||
it('should include recent dashboards section', () => {
|
||||
expect(results[0].title).toBe('Recent');
|
||||
});
|
||||
|
||||
it('should return order decided by impressions store not api', () => {
|
||||
expect(results[0].items[0].title).toBe('first but second');
|
||||
expect(results[0].items[1].title).toBe('second but first');
|
||||
});
|
||||
|
||||
describe('and 3 recent dashboards removed in backend', () => {
|
||||
let results: any;
|
||||
|
||||
beforeEach(() => {
|
||||
searchMock.mockImplementation((options) => {
|
||||
if (options.dashboardUIDs) {
|
||||
return Promise.resolve([
|
||||
{ uid: 'DSNdW0gVk', title: 'two' },
|
||||
{ uid: 'srx16xR4z', title: 'one' },
|
||||
] as DashboardSearchItem[]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
impressionSrv.getDashboardOpened = jest
|
||||
.fn()
|
||||
.mockResolvedValue(['Xrx16x4z', 'CSxdW0gYA', 'srx16xR4z', 'DSNdW0gVk', 'xSxdW0gYA']);
|
||||
|
||||
return searchSrv.search({ query: '' }).then((res) => {
|
||||
results = res;
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 2 dashboards', () => {
|
||||
expect(results[0].items.length).toBe(2);
|
||||
expect(results[0].items[0].uid).toBe('srx16xR4z');
|
||||
expect(results[0].items[1].uid).toBe('DSNdW0gVk');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('With starred dashboards', () => {
|
||||
let results: any;
|
||||
|
||||
beforeEach(() => {
|
||||
searchMock.mockImplementation((options) => {
|
||||
if (options.starred) {
|
||||
return Promise.resolve([{ uid: '1', title: 'starred' }] as DashboardSearchItem[]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
return searchSrv.search({ query: '' }).then((res) => {
|
||||
results = res;
|
||||
});
|
||||
});
|
||||
|
||||
it('should include starred dashboards section', () => {
|
||||
expect(results[0].title).toBe('Starred');
|
||||
expect(results[0].items.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With starred dashboards and recent', () => {
|
||||
let results: any;
|
||||
|
||||
beforeEach(() => {
|
||||
searchMock.mockImplementation((options) => {
|
||||
if (options.dashboardUIDs) {
|
||||
return Promise.resolve([
|
||||
{ uid: 'srx16xR4z', title: 'starred and recent', isStarred: true },
|
||||
{ uid: 'DSNdW0gVk', title: 'recent' },
|
||||
] as DashboardSearchItem[]);
|
||||
}
|
||||
return Promise.resolve([{ uid: 'srx16xR4z', title: 'starred and recent' }] as DashboardSearchItem[]);
|
||||
});
|
||||
|
||||
impressionSrv.getDashboardOpened = jest.fn().mockResolvedValue(['srx16xR4z', 'DSNdW0gVk']);
|
||||
return searchSrv.search({ query: '' }).then((res) => {
|
||||
results = res;
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show starred in recent', () => {
|
||||
expect(results[1].title).toBe('Recent');
|
||||
expect(results[1].items[0].title).toBe('recent');
|
||||
});
|
||||
|
||||
it('should show starred', () => {
|
||||
expect(results[0].title).toBe('Starred');
|
||||
expect(results[0].items[0].title).toBe('starred and recent');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no query string and dashboards with folders returned', () => {
|
||||
let results: any;
|
||||
|
||||
beforeEach(() => {
|
||||
searchMock.mockImplementation(
|
||||
jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(Promise.resolve([]))
|
||||
.mockResolvedValue(
|
||||
Promise.resolve([
|
||||
{
|
||||
title: 'folder1',
|
||||
type: 'dash-folder',
|
||||
uid: 'folder-1',
|
||||
},
|
||||
{
|
||||
title: 'dash with no folder',
|
||||
type: 'dash-db',
|
||||
uid: '2',
|
||||
},
|
||||
{
|
||||
title: 'dash in folder1 1',
|
||||
type: 'dash-db',
|
||||
uid: '3',
|
||||
folderUid: 'folder-1',
|
||||
},
|
||||
{
|
||||
title: 'dash in folder1 2',
|
||||
type: 'dash-db',
|
||||
uid: '4',
|
||||
folderUid: 'folder-1',
|
||||
},
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
return searchSrv.search({ query: '' }).then((res) => {
|
||||
results = res;
|
||||
});
|
||||
});
|
||||
|
||||
it('should create sections for each folder and root', () => {
|
||||
expect(results).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should place folders first', () => {
|
||||
expect(results[0].title).toBe('folder1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with query string and dashboards with folders returned', () => {
|
||||
let results: any;
|
||||
|
||||
beforeEach(() => {
|
||||
searchMock.mockImplementation(
|
||||
jest.fn().mockResolvedValue([
|
||||
{
|
||||
folderUid: 'dash-with-no-folder-uid',
|
||||
title: 'dash with no folder',
|
||||
type: 'dash-db',
|
||||
},
|
||||
{
|
||||
title: 'dash in folder1 1',
|
||||
type: 'dash-db',
|
||||
folderUid: 'uid',
|
||||
folderTitle: 'folder1',
|
||||
folderUrl: '/dashboards/f/uid/folder1',
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
return searchSrv.search({ query: 'search' }).then((res) => {
|
||||
results = res;
|
||||
});
|
||||
});
|
||||
|
||||
it('should not specify folder ids', () => {
|
||||
expect(searchMock.mock.calls[0][0].folderIds).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should group results by folder', () => {
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0].uid).toEqual('dash-with-no-folder-uid');
|
||||
expect(results[1].uid).toEqual('uid');
|
||||
expect(results[1].title).toEqual('folder1');
|
||||
expect(results[1].url).toEqual('/dashboards/f/uid/folder1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with tags', () => {
|
||||
beforeEach(() => {
|
||||
searchMock.mockImplementation(jest.fn().mockResolvedValue(Promise.resolve([])));
|
||||
|
||||
return searchSrv.search({ tag: ['atag'] }).then(() => {});
|
||||
});
|
||||
|
||||
it('should send tags query to backend search', () => {
|
||||
expect(searchMock.mock.calls[0][0].tag).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with starred', () => {
|
||||
beforeEach(() => {
|
||||
searchMock.mockImplementation(jest.fn().mockResolvedValue(Promise.resolve([])));
|
||||
|
||||
return searchSrv.search({ starred: true }).then(() => {});
|
||||
});
|
||||
|
||||
it('should send starred query to backend search', () => {
|
||||
expect(searchMock.mock.calls[0][0].starred).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when skipping recent dashboards', () => {
|
||||
let getRecentDashboardsCalled = false;
|
||||
|
||||
beforeEach(() => {
|
||||
searchMock.mockImplementation(jest.fn().mockResolvedValue(Promise.resolve([])));
|
||||
|
||||
searchSrv['getRecentDashboards'] = () => {
|
||||
getRecentDashboardsCalled = true;
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
return searchSrv.search({ skipRecent: true }).then(() => {});
|
||||
});
|
||||
|
||||
it('should not fetch recent dashboards', () => {
|
||||
expect(getRecentDashboardsCalled).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when skipping starred dashboards', () => {
|
||||
let getStarredCalled = false;
|
||||
|
||||
beforeEach(() => {
|
||||
searchMock.mockImplementation(jest.fn().mockResolvedValue(Promise.resolve([])));
|
||||
impressionSrv.getDashboardOpened = jest.fn().mockResolvedValue([]);
|
||||
|
||||
searchSrv['getStarred'] = () => {
|
||||
getStarredCalled = true;
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
return searchSrv.search({ skipStarred: true }).then(() => {});
|
||||
});
|
||||
|
||||
it('should not fetch starred dashboards', () => {
|
||||
expect(getStarredCalled).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,9 @@ import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { PanelPluginMeta, PluginType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Panel } from '@grafana/schema';
|
||||
import { getGrafanaSearcher } from 'app/features/search/service';
|
||||
|
||||
import { backendSrv } from '../../../../core/services/backend_srv';
|
||||
import * as panelUtils from '../../../panel/state/util';
|
||||
@@ -63,9 +65,21 @@ async function getTestContext(
|
||||
module: '',
|
||||
sort: 1,
|
||||
};
|
||||
const getSpy = jest
|
||||
.spyOn(backendSrv, 'get')
|
||||
.mockResolvedValue({ sortOptions: [{ displaName: 'Desc', name: 'alpha-desc' }] });
|
||||
|
||||
config.featureToggles = { panelTitleSearch: false };
|
||||
const getSpy = jest.spyOn(backendSrv, 'get');
|
||||
|
||||
jest.spyOn(getGrafanaSearcher(), 'getSortOptions').mockResolvedValue([
|
||||
{
|
||||
label: 'Alphabetically (A–Z)',
|
||||
value: 'alpha-asc',
|
||||
},
|
||||
{
|
||||
label: 'Alphabetically (Z–A)',
|
||||
value: 'alpha-desc',
|
||||
},
|
||||
]);
|
||||
|
||||
const getLibraryPanelsSpy = jest.spyOn(api, 'getLibraryPanels').mockResolvedValue(searchResult);
|
||||
const getAllPanelPluginMetaSpy = jest.spyOn(panelUtils, 'getAllPanelPluginMeta').mockReturnValue([graph, timeseries]);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type Props = Pick<SearchResultsProps, 'selection' | 'selectionToggle' | 'onTagSe
|
||||
tags?: string[];
|
||||
hidePseudoFolders?: boolean;
|
||||
};
|
||||
|
||||
export const FolderView = ({
|
||||
selection,
|
||||
selectionToggle,
|
||||
@@ -31,6 +32,7 @@ export const FolderView = ({
|
||||
|
||||
const results = useAsync(async () => {
|
||||
const folders: DashboardSection[] = [];
|
||||
|
||||
if (!hidePseudoFolders) {
|
||||
if (contextSrv.isSignedIn) {
|
||||
const stars = await getBackendSrv().get('api/user/stars');
|
||||
@@ -44,6 +46,7 @@ export const FolderView = ({
|
||||
folders.push({ title: 'Recent', icon: 'clock-nine', kind: 'query-recent', uid: '__recent', itemsUIDs });
|
||||
}
|
||||
}
|
||||
|
||||
folders.push({ title: 'General', url: '/dashboards', kind: 'folder', uid: GENERAL_FOLDER_UID });
|
||||
|
||||
const searcher = getGrafanaSearcher();
|
||||
|
||||
Reference in New Issue
Block a user