Explore: List query templates (#86897)

* Create basic feature toggle

* Rename context to reflect it contains query history and query library

* Update icons and variants

* Rename hooks

* Update tests

* Fix mock

* Add tracking

* Turn button into a toggle

* Make dropdown active as well

This is required to have better UI and an indication of selected state in split view

* Update Query Library icon

This is to make it consistent with the toolbar button

* Hide query history button when query library is available

This is to avoid confusing UX with 2 button triggering the drawer but with slightly different behavior

* Make the drawer bigger for query library

To avoid confusion for current users and test it internally a bit more it's behind a feature toggle. Bigger drawer may obstruct the view and add more friction in the UX.

* Fix tests

The test was failing because queryLibraryAvailable was set to true for tests. This change makes it more explicit what use case is being tested

* Remove active state underline from the dropdown

* Add basic types and api methods

This is just moved from the app. To be cleaned up and refactored later.

* Move API utils from Query Library app to Grafana packages

* Move API utils from Query Library app to Grafana packages

* Move API utils from Query Library app to Grafana packages

* Add basic table for query templates

* Add sorting

* Style cells

* Style table cells

* Allow closing Query Library drawer from the toolbar

* Remove Private Query toggle

It will be moved to the kebab

* Add empty state

* Remove variables detection for now

Just to simplify the PR, it's not needed for Explore yet.

* Simplify getting useDatasource.tsx

* Rename cell

* Move QueryTemplatesTable to a separate folder

* Use RTK Query to get list of query templates

* Clean up query templates table

* Simplify useDatasource hook

* Add a test

* Retrigger the build

* Remove unused code

* Small clean up

* Update import

* Add reduxjs/toolkit as a peer dependecy

* Revert "Add reduxjs/toolkit as a peer dependecy"

This reverts commit aa9da6e442.

* Update import

* Add reduxjs/toolkit as a peer dependecy

* Revert "Add reduxjs/toolkit as a peer dependecy"

This reverts commit 2e68a62ab6.

* Add @reduxjs/toolkit as peer dependency

* Add @reduxjs/toolkit as peer dependecy

* Move reactjs/toolkit to dev dependecies

* Minor clean up and use react-redux as a peer dependency

* Move query library code to core features

* Update code owners

* Update export

* Update exports

* Use Redux store instead of APIProvider

* Await for query templates to show during the test

* Add more explicit docs that the feature is experimental

---------

Co-authored-by: Kristina Durivage <kristina.durivage@grafana.com>
This commit is contained in:
Piotr Jamróz
2024-05-14 10:05:39 +02:00
committed by GitHub
parent 6b1a662f6b
commit fd218edca4
28 changed files with 622 additions and 4 deletions

View File

@@ -0,0 +1,17 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { QueryTemplate } from '../types';
import { convertDataQueryResponseToQueryTemplates } from './mappers';
import { baseQuery } from './query';
export const queryLibraryApi = createApi({
baseQuery,
endpoints: (builder) => ({
allQueryTemplates: builder.query<QueryTemplate[], void>({
query: () => undefined,
transformResponse: convertDataQueryResponseToQueryTemplates,
}),
}),
reducerPath: 'queryLibrary',
});

View File

@@ -0,0 +1,17 @@
import { QueryTemplate } from '../types';
import { DataQuerySpecResponse, DataQueryTarget } from './types';
export const convertDataQueryResponseToQueryTemplates = (result: DataQuerySpecResponse): QueryTemplate[] => {
if (!result.items) {
return [];
}
return result.items.map((spec) => {
return {
uid: spec.metadata.name || '',
title: spec.spec.title,
targets: spec.spec.targets.map((target: DataQueryTarget) => target.properties),
createdAtTimestamp: new Date(spec.metadata.creationTimestamp || '').getTime(),
};
});
};

View File

@@ -0,0 +1,9 @@
import { BASE_URL } from './query';
import { getTestQueryList } from './testdata/testQueryList';
export const mockData = {
all: {
url: BASE_URL,
response: getTestQueryList(),
},
};

View File

@@ -0,0 +1,34 @@
import { BaseQueryFn } from '@reduxjs/toolkit/query/react';
import { lastValueFrom } from 'rxjs';
import { getBackendSrv, isFetchError } from '@grafana/runtime/src/services/backendSrv';
import { DataQuerySpecResponse } from './types';
/**
* Query Library is an experimental feature. API (including the URL path) will likely change.
*
* @alpha
*/
export const BASE_URL = '/apis/peakq.grafana.app/v0alpha1/namespaces/default/querytemplates/';
/**
* TODO: similar code is duplicated in many places. To be unified in #86960
*/
export const baseQuery: BaseQueryFn<void, DataQuerySpecResponse, Error> = async () => {
try {
const responseObservable = getBackendSrv().fetch<DataQuerySpecResponse>({
url: BASE_URL,
showErrorAlert: true,
});
return await lastValueFrom(responseObservable);
} catch (error) {
if (isFetchError(error)) {
return { error: new Error(error.data.message) };
} else if (error instanceof Error) {
return { error };
} else {
return { error: new Error('Unknown error') };
}
}
};

View File

@@ -0,0 +1,122 @@
export const getTestQueryList = () => ({
kind: 'QueryTemplateList',
apiVersion: 'peakq.grafana.app/v0alpha1',
metadata: {
resourceVersion: '1783293408052252672',
remainingItemCount: 0,
},
items: [
{
kind: 'QueryTemplate',
apiVersion: 'peakq.grafana.app/v0alpha1',
metadata: {
name: 'AElastic2nkf9',
generateName: 'AElastic',
namespace: 'default',
uid: '65327fce-c545-489d-ada5-16f909453d12',
resourceVersion: '1783293341664808960',
creationTimestamp: '2024-04-25T20:32:58Z',
},
spec: {
title: 'Elastic Query Template',
targets: [
{
variables: {},
properties: {
refId: 'A',
datasource: {
type: 'elasticsearch',
uid: 'elastic-uid',
},
alias: '',
metrics: [
{
id: '1',
type: 'count',
},
],
bucketAggs: [
{
field: '@timestamp',
id: '2',
settings: {
interval: 'auto',
},
type: 'date_histogram',
},
],
timeField: '@timestamp',
query: 'test:test ',
},
},
],
},
},
{
kind: 'QueryTemplate',
apiVersion: 'peakq.grafana.app/v0alpha1',
metadata: {
name: 'ALoki296tj',
generateName: 'ALoki',
namespace: 'default',
uid: '3e71de65-efa7-40e3-8f23-124212cca455',
resourceVersion: '1783214217151647744',
creationTimestamp: '2024-04-25T11:05:55Z',
},
spec: {
title: 'Loki Query Template',
vars: [
{
key: '__value',
defaultValues: [''],
valueListDefinition: {
customValues: '',
},
},
],
targets: [
{
variables: {
__value: [
{
path: '$.datasource.jsonData.derivedFields.0.url',
position: {
start: 0,
end: 14,
},
format: 'raw',
},
{
path: '$.datasource.jsonData.derivedFields.1.url',
position: {
start: 0,
end: 14,
},
format: 'raw',
},
{
path: '$.datasource.jsonData.derivedFields.2.url',
position: {
start: 0,
end: 14,
},
format: 'raw',
},
],
},
properties: {
refId: 'A',
datasource: {
type: 'loki',
uid: 'loki-uid',
},
queryType: 'range',
editorMode: 'code',
expr: '{test="test"}',
},
},
],
},
},
],
});

View File

@@ -0,0 +1,26 @@
import { DataQuery } from '@grafana/schema/dist/esm/index';
export type DataQueryTarget = {
variables: object; // TODO: Detect variables in #86838
properties: DataQuery;
};
export type DataQuerySpec = {
apiVersion: string;
kind: string;
metadata: {
generateName: string;
name?: string;
creationTimestamp?: string;
};
spec: {
title: string;
vars: object[]; // TODO: Detect variables in #86838
targets: DataQueryTarget[];
};
};
export type DataQuerySpecResponse = {
apiVersion: string;
items: DataQuerySpec[];
};

View File

@@ -0,0 +1,17 @@
/**
* This is a temporary place for Query Library API and data types.
* To be exposed via grafana-runtime/data in the future.
*
* Query Library is an experimental feature, the API and components are subject to change
*
* @alpha
*/
import { queryLibraryApi } from './api/factory';
import { mockData } from './api/mocks';
export const { useAllQueryTemplatesQuery } = queryLibraryApi;
export const QueryLibraryMocks = {
data: mockData,
};

View File

@@ -0,0 +1,8 @@
import { DataQuery } from '@grafana/schema';
export type QueryTemplate = {
uid: string;
title: string;
targets: DataQuery[];
createdAtTimestamp: number;
};