grafana/public/app/features/search/service/sql.ts

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 '';
};
}