mirror of https://github.com/grafana/grafana.git
249 lines
7.5 KiB
TypeScript
249 lines
7.5 KiB
TypeScript
import { DataFrame, DataFrameView, FieldType, getDisplayProcessor, SelectableValue } from '@grafana/data';
|
|
import { config } from '@grafana/runtime';
|
|
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
|
|
import { backendSrv } from 'app/core/services/backend_srv';
|
|
import { PermissionLevel } from 'app/types/acl';
|
|
|
|
import { DEFAULT_MAX_VALUES, GENERAL_FOLDER_UID, TYPE_KIND_MAP } from '../constants';
|
|
import { DashboardSearchHit, DashboardSearchItemType } from '../types';
|
|
|
|
import { deletedDashboardsCache } from './deletedDashboardsCache';
|
|
import { DashboardQueryResult, GrafanaSearcher, LocationInfo, QueryResponse, SearchQuery, SortOptions } from './types';
|
|
import { filterSearchResults, replaceCurrentFolderQuery, searchHitsToDashboardSearchHits } from './utils';
|
|
|
|
interface APIQuery {
|
|
query?: string;
|
|
tag?: string[];
|
|
limit?: number;
|
|
page?: number;
|
|
type?: DashboardSearchItemType;
|
|
dashboardUID?: string[];
|
|
folderUIDs?: string[];
|
|
sort?: string;
|
|
starred?: boolean;
|
|
permission?: PermissionLevel;
|
|
deleted?: boolean;
|
|
}
|
|
|
|
// Internal object to hold folderId
|
|
interface LocationInfoEXT extends LocationInfo {
|
|
folderUid?: string;
|
|
}
|
|
|
|
export class SQLSearcher implements GrafanaSearcher {
|
|
locationInfo: Record<string, LocationInfoEXT> = {
|
|
general: {
|
|
kind: 'folder',
|
|
name: 'Dashboards',
|
|
url: '/dashboards',
|
|
},
|
|
}; // share location info with everyone
|
|
|
|
private async composeQuery(apiQuery: APIQuery, searchOptions: SearchQuery): Promise<APIQuery> {
|
|
const query = await replaceCurrentFolderQuery(searchOptions);
|
|
|
|
if (query.query?.length && query.query !== '*') {
|
|
apiQuery.query = query.query;
|
|
}
|
|
|
|
// search v1 supports only one kind
|
|
if (query.kind?.length === 1 && TYPE_KIND_MAP[query.kind[0]]) {
|
|
apiQuery.type = TYPE_KIND_MAP[query.kind[0]];
|
|
}
|
|
|
|
if (query.uid) {
|
|
apiQuery.dashboardUID = query.uid;
|
|
} else if (query.location?.length) {
|
|
apiQuery.folderUIDs = [query.location];
|
|
}
|
|
|
|
return apiQuery;
|
|
}
|
|
|
|
async search(query: SearchQuery): Promise<QueryResponse> {
|
|
if (query.facet?.length) {
|
|
throw new Error('facets not supported!');
|
|
}
|
|
|
|
if (query.from !== undefined) {
|
|
if (!query.limit) {
|
|
throw new Error('Must specify non-zero limit parameter when using from');
|
|
}
|
|
|
|
if ((query.from / query.limit) % 1 !== 0) {
|
|
throw new Error('From parameter must be a multiple of limit');
|
|
}
|
|
}
|
|
|
|
const limit = query.limit ?? (query.from !== undefined ? 1 : DEFAULT_MAX_VALUES);
|
|
const page =
|
|
query.from !== undefined
|
|
? // prettier-ignore
|
|
(query.from / limit) + 1 // pages are 1-indexed, so need to +1 to get there
|
|
: undefined;
|
|
|
|
const q = await this.composeQuery(
|
|
{
|
|
limit: limit,
|
|
tag: query.tags,
|
|
sort: query.sort,
|
|
permission: query.permission,
|
|
page,
|
|
deleted: query.deleted,
|
|
},
|
|
query
|
|
);
|
|
|
|
return this.doAPIQuery(q);
|
|
}
|
|
|
|
async starred(query: SearchQuery): Promise<QueryResponse> {
|
|
if (query.facet?.length) {
|
|
throw new Error('facets not supported!');
|
|
}
|
|
|
|
const q = await this.composeQuery(
|
|
{
|
|
limit: query.limit ?? DEFAULT_MAX_VALUES, // default 1k max values
|
|
tag: query.tags,
|
|
sort: query.sort,
|
|
starred: query.starred,
|
|
},
|
|
query
|
|
);
|
|
|
|
return this.doAPIQuery(q);
|
|
}
|
|
|
|
// returns the appropriate sorting options
|
|
async getSortOptions(): Promise<SelectableValue[]> {
|
|
const opts = await backendSrv.get<SortOptions>('/api/search/sorting');
|
|
return opts.sortOptions.map((v) => ({
|
|
value: v.name,
|
|
label: v.displayName,
|
|
}));
|
|
}
|
|
|
|
// NOTE: the bluge query will find tags within the current results, the SQL based one does not
|
|
async tags(query: SearchQuery): Promise<TermCount[]> {
|
|
const terms = await backendSrv.get<TermCount[]>('/api/dashboards/tags');
|
|
return terms.sort((a, b) => b.count - a.count);
|
|
}
|
|
|
|
async getLocationInfo() {
|
|
return this.locationInfo;
|
|
}
|
|
|
|
async doAPIQuery(query: APIQuery): Promise<QueryResponse> {
|
|
let rsp: DashboardSearchHit[];
|
|
|
|
if (query.deleted) {
|
|
const allDeletedHits = await deletedDashboardsCache.get();
|
|
rsp = searchHitsToDashboardSearchHits(filterSearchResults(allDeletedHits, query));
|
|
} else {
|
|
rsp = await backendSrv.get<DashboardSearchHit[]>('/api/search', query);
|
|
}
|
|
|
|
// Field values (columnar)
|
|
const kind: string[] = [];
|
|
const name: string[] = [];
|
|
const uid: string[] = [];
|
|
const url: string[] = [];
|
|
const tags: string[][] = [];
|
|
const location: string[] = [];
|
|
const sortBy: number[] = [];
|
|
const isDeleted: boolean[] = [];
|
|
const permanentlyDeleteDate: Array<Date | undefined> = [];
|
|
let sortMetaName: string | undefined;
|
|
|
|
for (let hit of rsp) {
|
|
const k = hit.type === 'dash-folder' ? 'folder' : 'dashboard';
|
|
kind.push(k);
|
|
name.push(hit.title);
|
|
uid.push(hit.uid);
|
|
url.push(hit.url);
|
|
tags.push(hit.tags);
|
|
sortBy.push(hit.sortMeta!);
|
|
isDeleted.push(hit.isDeleted ?? false);
|
|
permanentlyDeleteDate.push(hit.permanentlyDeleteDate ? new Date(hit.permanentlyDeleteDate) : undefined);
|
|
|
|
let v = hit.folderUid;
|
|
if (!v && k === 'dashboard') {
|
|
v = GENERAL_FOLDER_UID;
|
|
}
|
|
location.push(v!);
|
|
|
|
if (hit.sortMetaName?.length) {
|
|
sortMetaName = hit.sortMetaName;
|
|
}
|
|
|
|
if (hit.folderUid && hit.folderTitle) {
|
|
this.locationInfo[hit.folderUid] = {
|
|
kind: 'folder',
|
|
name: hit.folderTitle,
|
|
url: hit.folderUrl!,
|
|
folderUid: hit.folderUid,
|
|
};
|
|
} else if (k === 'folder') {
|
|
this.locationInfo[hit.uid] = {
|
|
kind: k,
|
|
name: hit.title!,
|
|
url: hit.url,
|
|
folderUid: hit.folderUid,
|
|
};
|
|
}
|
|
}
|
|
|
|
const data: DataFrame = {
|
|
fields: [
|
|
{ name: 'kind', type: FieldType.string, config: {}, values: kind },
|
|
{ name: 'name', type: FieldType.string, config: {}, values: name },
|
|
{ name: 'uid', type: FieldType.string, config: {}, values: uid },
|
|
{ name: 'url', type: FieldType.string, config: {}, values: url },
|
|
{ name: 'tags', type: FieldType.other, config: {}, values: tags },
|
|
{ name: 'location', type: FieldType.string, config: {}, values: location },
|
|
{ name: 'isDeleted', type: FieldType.boolean, config: {}, values: isDeleted },
|
|
{ name: 'permanentlyDeleteDate', type: FieldType.time, config: {}, values: permanentlyDeleteDate },
|
|
],
|
|
length: name.length,
|
|
meta: {
|
|
custom: {
|
|
count: name.length,
|
|
max_score: 1,
|
|
locationInfo: this.locationInfo,
|
|
},
|
|
},
|
|
};
|
|
|
|
// Add enterprise sort fields as a field in the frame
|
|
if (sortMetaName?.length && sortBy.length) {
|
|
data.meta!.custom!.sortBy = sortMetaName;
|
|
data.fields.push({
|
|
name: sortMetaName, // Used in display
|
|
type: FieldType.number,
|
|
config: {},
|
|
values: sortBy,
|
|
});
|
|
}
|
|
|
|
for (const field of data.fields) {
|
|
field.display = getDisplayProcessor({ field, theme: config.theme2 });
|
|
}
|
|
|
|
const view = new DataFrameView<DashboardQueryResult>(data);
|
|
return {
|
|
totalRows: data.length,
|
|
view,
|
|
|
|
// Paging not supported with this version
|
|
loadMoreItems: async (startIndex: number, stopIndex: number): Promise<void> => {},
|
|
isItemLoaded: (index: number): boolean => true,
|
|
};
|
|
}
|
|
|
|
getFolderViewSort = () => {
|
|
// sorts alphabetically in memory after retrieving the folders from the database
|
|
return '';
|
|
};
|
|
}
|