diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 725e9395a65..ecc76556591 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -419,6 +419,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/playlist/ @grafana/dashboards-squad /public/app/features/plugins/ @grafana/plugins-platform-frontend /public/app/features/profile/ @grafana/grafana-frontend-platform +/public/app/features/query-library/ @grafana/explore-squad /public/app/features/runtime/ @ryantxu /public/app/features/query/ @grafana/dashboards-squad /public/app/features/sandbox/ @grafana/grafana-frontend-platform diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 54e8c634072..ec33626bc43 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -28,6 +28,7 @@ import usersReducers from 'app/features/users/state/reducers'; import templatingReducers from 'app/features/variables/state/keyedVariablesReducer'; import { alertingApi } from '../../features/alerting/unified/api/alertingApi'; +import { queryLibraryApi } from '../../features/query-library/api/factory'; import { cleanUpAction } from '../actions/cleanUp'; const rootReducers = { @@ -57,6 +58,7 @@ const rootReducers = { [publicDashboardApi.reducerPath]: publicDashboardApi.reducer, [browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer, [cloudMigrationAPI.reducerPath]: cloudMigrationAPI.reducer, + [queryLibraryApi.reducerPath]: queryLibraryApi.reducer, }; const addedReducers = {}; diff --git a/public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx b/public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx index 238cfc06da1..3cd4c211236 100644 --- a/public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx +++ b/public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx @@ -40,6 +40,7 @@ export function QueriesDrawerDropdown({ variant }: Props) { icon="book" variant={drawerOpened ? 'active' : 'canvas'} onClick={() => setDrawerOpened(!drawerOpened)} + aria-label={selectedTab} > {variant === 'full' ? selectedTab : undefined} diff --git a/public/app/features/explore/QueryLibrary/QueryLibrary.tsx b/public/app/features/explore/QueryLibrary/QueryLibrary.tsx new file mode 100644 index 00000000000..11173ca0384 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryLibrary.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +import { QueryTemplatesList } from './QueryTemplatesList'; + +export function QueryLibrary() { + return ; +} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx new file mode 100644 index 00000000000..a1e8bb5c568 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import { EmptyState, Spinner } from '@grafana/ui'; +import { useAllQueryTemplatesQuery } from 'app/features/query-library'; +import { QueryTemplate } from 'app/features/query-library/types'; + +import { getDatasourceSrv } from '../../plugins/datasource_srv'; + +import QueryTemplatesTable from './QueryTemplatesTable'; +import { QueryTemplateRow } from './QueryTemplatesTable/types'; + +export function QueryTemplatesList() { + const { data, isLoading, error } = useAllQueryTemplatesQuery(); + + if (error) { + return ( + + {error.message} + + ); + } + + if (isLoading) { + return ; + } + + if (!data || data.length === 0) { + return ( + +

+ { + "You haven't saved any queries to your library yet. Start adding them from Explore or your Query History tab." + } +

+
+ ); + } + + const queryTemplateRows: QueryTemplateRow[] = data.map((queryTemplate: QueryTemplate, index: number) => { + const datasourceRef = queryTemplate.targets[0]?.datasource; + const datasourceType = getDatasourceSrv().getInstanceSettings(datasourceRef)?.meta.name || ''; + return { + index: index.toString(), + datasourceRef, + datasourceType, + createdAtTimestamp: queryTemplate?.createdAtTimestamp || 0, + query: queryTemplate.targets[0], + description: queryTemplate.title, + }; + }); + + return ; +} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx new file mode 100644 index 00000000000..ccc18049a78 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import { Button } from '@grafana/ui'; + +export function ActionsCell() { + return ( + <> + + + ); +} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/AddedByCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/AddedByCell.tsx new file mode 100644 index 00000000000..4f344fc7002 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/AddedByCell.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { Avatar } from '@grafana/ui'; + +import { useQueryLibraryListStyles } from './styles'; + +export function AddedByCell() { + const styles = useQueryLibraryListStyles(); + + return ( +
+ + + + Unknown +
+ ); +} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DatasourceTypeCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DatasourceTypeCell.tsx new file mode 100644 index 00000000000..2c9db92cd04 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DatasourceTypeCell.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { CellProps } from 'react-table'; + +import { useDatasource } from '../utils/useDatasource'; + +import { useQueryLibraryListStyles } from './styles'; +import { QueryTemplateRow } from './types'; + +export function DatasourceTypeCell(props: CellProps) { + const datasourceApi = useDatasource(props.row.original.datasourceRef); + const styles = useQueryLibraryListStyles(); + + return

{datasourceApi?.meta.name}

; +} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DateAddedCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DateAddedCell.tsx new file mode 100644 index 00000000000..3a8b59f08df --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DateAddedCell.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { CellProps } from 'react-table'; + +import { dateTime } from '@grafana/data'; + +import { useQueryLibraryListStyles } from './styles'; +import { QueryTemplateRow } from './types'; + +export function DateAddedCell(props: CellProps) { + const styles = useQueryLibraryListStyles(); + const formattedTime = dateTime(props.row.original.createdAtTimestamp).format('YYYY-MM-DD HH:mm:ss'); + + return

{formattedTime}

; +} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx new file mode 100644 index 00000000000..95b8a7d9221 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx @@ -0,0 +1,41 @@ +import { cx } from '@emotion/css'; +import React from 'react'; +import { CellProps } from 'react-table'; + +import { Spinner } from '@grafana/ui'; + +import { useDatasource } from '../utils/useDatasource'; + +import { useQueryLibraryListStyles } from './styles'; +import { QueryTemplateRow } from './types'; + +export function QueryDescriptionCell(props: CellProps) { + const datasourceApi = useDatasource(props.row.original.datasourceRef); + const styles = useQueryLibraryListStyles(); + + if (!datasourceApi) { + return ; + } + + if (!props.row.original.query) { + return
No queries
; + } + const query = props.row.original.query; + const description = props.row.original.description; + const dsName = datasourceApi?.name || ''; + + return ( +
+

+ {datasourceApi?.meta.info.description} + {dsName} +

+

{datasourceApi?.getQueryDisplayText?.(query)}

+

{description}

+
+ ); +} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx new file mode 100644 index 00000000000..53a54848d24 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx @@ -0,0 +1,49 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { SortByFn } from 'react-table'; + +import { Column, InteractiveTable } from '@grafana/ui'; + +import { ActionsCell } from './ActionsCell'; +import { AddedByCell } from './AddedByCell'; +import { DatasourceTypeCell } from './DatasourceTypeCell'; +import { DateAddedCell } from './DateAddedCell'; +import { QueryDescriptionCell } from './QueryDescriptionCell'; +import { QueryTemplateRow } from './types'; + +const timestampSort: SortByFn = (rowA, rowB, _, desc) => { + const timeA = rowA.original.createdAtTimestamp || 0; + const timeB = rowB.original.createdAtTimestamp || 0; + return desc ? timeA - timeB : timeB - timeA; +}; + +const columns: Array> = [ + { id: 'description', header: 'Data source and query', cell: QueryDescriptionCell }, + { id: 'addedBy', header: 'Added by', cell: AddedByCell }, + { id: 'datasourceType', header: 'Datasource type', cell: DatasourceTypeCell, sortType: 'string' }, + { id: 'createdAtTimestamp', header: 'Date added', cell: DateAddedCell, sortType: timestampSort }, + { id: 'actions', header: '', cell: ActionsCell }, +]; + +const styles = { + tableWithSpacing: css({ + 'th:first-child': { + width: '50%', + }, + }), +}; + +type Props = { + queryTemplateRows: QueryTemplateRow[]; +}; + +export default function QueryTemplatesTable({ queryTemplateRows }: Props) { + return ( + row.index} + /> + ); +} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx new file mode 100644 index 00000000000..34b9ff33d4e --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx @@ -0,0 +1,37 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data/'; +import { useStyles2 } from '@grafana/ui/'; + +export const useQueryLibraryListStyles = () => { + return useStyles2(getStyles); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + logo: css({ + marginRight: theme.spacing(2), + width: '16px', + }), + header: css({ + margin: 0, + fontSize: theme.typography.h5.fontSize, + color: theme.colors.text.secondary, + }), + mainText: css({ + margin: 0, + fontSize: theme.typography.body.fontSize, + textOverflow: 'ellipsis', + }), + otherText: css({ + margin: 0, + fontSize: theme.typography.body.fontSize, + color: theme.colors.text.secondary, + textOverflow: 'ellipsis', + }), + singleLine: css({ + display: '-webkit-box', + '-webkit-box-orient': 'vertical', + '-webkit-line-clamp': '1', + overflow: 'hidden', + }), +}); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts new file mode 100644 index 00000000000..62ddcf12420 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts @@ -0,0 +1,10 @@ +import { DataQuery, DataSourceRef } from '@grafana/schema'; + +export type QueryTemplateRow = { + index: string; + description?: string; + query?: DataQuery; + datasourceRef?: DataSourceRef | null; + datasourceType?: string; + createdAtTimestamp?: number; +}; diff --git a/public/app/features/explore/QueryLibrary/utils/useDatasource.tsx b/public/app/features/explore/QueryLibrary/utils/useDatasource.tsx new file mode 100644 index 00000000000..1ea4fd29b76 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/utils/useDatasource.tsx @@ -0,0 +1,9 @@ +import { useAsync } from 'react-use'; + +import { getDataSourceSrv } from '@grafana/runtime'; +import { DataSourceRef } from '@grafana/schema'; + +export function useDatasource(dataSourceRef?: DataSourceRef | null) { + const { value } = useAsync(async () => await getDataSourceSrv().get(dataSourceRef), [dataSourceRef]); + return value; +} diff --git a/public/app/features/explore/RichHistory/RichHistory.tsx b/public/app/features/explore/RichHistory/RichHistory.tsx index 8382edc323b..297a6a578f4 100644 --- a/public/app/features/explore/RichHistory/RichHistory.tsx +++ b/public/app/features/explore/RichHistory/RichHistory.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react'; import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { EmptyState, TabbedContainer, TabConfig } from '@grafana/ui'; +import { TabbedContainer, TabConfig } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { SortOrder, RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistory'; import { RichHistoryQuery } from 'app/types/explore'; @@ -11,6 +11,7 @@ import { RichHistoryQuery } from 'app/types/explore'; import { supportedFeatures } from '../../../core/history/richHistoryStorageProvider'; import { Tabs, useQueriesDrawerContext } from '../QueriesDrawer/QueriesDrawerContext'; import { i18n } from '../QueriesDrawer/utils'; +import { QueryLibrary } from '../QueryLibrary/QueryLibrary'; import { RichHistoryQueriesTab } from './RichHistoryQueriesTab'; import { RichHistorySettingsTab } from './RichHistorySettingsTab'; @@ -85,7 +86,7 @@ export function RichHistory(props: RichHistoryProps) { const QueryLibraryTab: TabConfig = { label: i18n.queryLibrary, value: Tabs.QueryLibrary, - content: , + content: , icon: 'book', }; diff --git a/public/app/features/explore/spec/helper/assert.ts b/public/app/features/explore/spec/helper/assert.ts index 6e8f97d15e3..f68e477254d 100644 --- a/public/app/features/explore/spec/helper/assert.ts +++ b/public/app/features/explore/spec/helper/assert.ts @@ -22,6 +22,17 @@ export const assertQueryHistory = async (expectedQueryTexts: string[]) => { }); }; +export const assertQueryLibraryTemplateExists = async (datasource: string, description: string) => { + const selector = withinQueryHistory(); + await waitFor(() => { + const cell = selector.getByRole('cell', { + name: new RegExp(`query template for ${datasource.toLowerCase()}: ${description.toLowerCase()}`, 'i'), + }); + + expect(cell).toBeInTheDocument(); + }); +}; + export const assertQueryHistoryIsEmpty = async () => { const selector = withinQueryHistory(); const queryTexts = selector.queryAllByLabelText('Query text'); diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts index 5304115ead0..19db9f932b5 100644 --- a/public/app/features/explore/spec/helper/interactions.ts +++ b/public/app/features/explore/spec/helper/interactions.ts @@ -32,6 +32,12 @@ export const openQueryHistory = async () => { expect(await screen.findByPlaceholderText('Search queries')).toBeInTheDocument(); }; +export const openQueryLibrary = async () => { + const explore = withinExplore('left'); + const button = explore.getByRole('button', { name: 'Query library' }); + await userEvent.click(button); +}; + export const closeQueryHistory = async () => { const selector = withinQueryHistory(); const closeButton = selector.getByRole('button', { name: 'Close query history' }); diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index 2611e1c30f1..da69dba6881 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -34,6 +34,7 @@ import { GrafanaContext } from 'app/core/context/GrafanaContext'; import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute'; import { Echo } from 'app/core/services/echo/Echo'; import { setLastUsedDatasourceUID } from 'app/core/utils/explore'; +import { QueryLibraryMocks } from 'app/features/query-library'; import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { configureStore } from 'app/store/configureStore'; @@ -70,12 +71,14 @@ export function setupExplore(options?: SetupOptions): { datasourceRequest: jest.fn().mockRejectedValue(undefined), delete: jest.fn().mockRejectedValue(undefined), fetch: jest.fn().mockImplementation((req) => { - const data: Record = {}; + let data: Record = {}; if (req.url.startsWith('/api/datasources/correlations') && req.method === 'GET') { data.correlations = []; data.totalCount = 0; } else if (req.url.startsWith('/api/query-history') && req.method === 'GET') { data.result = options?.queryHistory || {}; + } else if (req.url.startsWith(QueryLibraryMocks.data.all.url)) { + data = QueryLibraryMocks.data.all.response; } return of({ data }); }), diff --git a/public/app/features/explore/spec/queryLibrary.test.tsx b/public/app/features/explore/spec/queryLibrary.test.tsx new file mode 100644 index 00000000000..33badc3fd61 --- /dev/null +++ b/public/app/features/explore/spec/queryLibrary.test.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { Props } from 'react-virtualized-auto-sizer'; + +import { EventBusSrv } from '@grafana/data'; +import { config } from '@grafana/runtime'; + +import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput'; + +import { assertQueryLibraryTemplateExists } from './helper/assert'; +import { openQueryLibrary } from './helper/interactions'; +import { setupExplore, waitForExplore } from './helper/setup'; + +const reportInteractionMock = jest.fn(); +const testEventBus = new EventBusSrv(); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: (...args: object[]) => { + reportInteractionMock(...args); + }, + getAppEvents: () => testEventBus, +})); + +jest.mock('app/core/core', () => ({ + contextSrv: { + hasPermission: () => true, + isSignedIn: true, + getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, + }, +})); + +jest.mock('app/core/services/PreferencesService', () => ({ + PreferencesService: function () { + return { + patch: jest.fn(), + load: jest.fn().mockResolvedValue({ + queryHistory: { + homeTab: 'query', + }, + }), + }; + }, +})); + +jest.mock('../hooks/useExplorePageTitle', () => ({ + useExplorePageTitle: jest.fn(), +})); + +jest.mock('react-virtualized-auto-sizer', () => { + return { + __esModule: true, + default(props: Props) { + return
{props.children({ height: 1, scaledHeight: 1, scaledWidth: 1000, width: 1000 })}
; + }, + }; +}); + +describe('QueryLibrary', () => { + silenceConsoleOutput(); + + beforeAll(() => { + config.featureToggles.queryLibrary = true; + }); + + afterAll(() => { + config.featureToggles.queryLibrary = false; + }); + + it('Load query templates', async () => { + setupExplore(); + await waitForExplore(); + await openQueryLibrary(); + await assertQueryLibraryTemplateExists('loki', 'Loki Query Template'); + await assertQueryLibraryTemplateExists('elastic', 'Elastic Query Template'); + }); +}); diff --git a/public/app/features/query-library/api/factory.ts b/public/app/features/query-library/api/factory.ts new file mode 100644 index 00000000000..083680e8855 --- /dev/null +++ b/public/app/features/query-library/api/factory.ts @@ -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({ + query: () => undefined, + transformResponse: convertDataQueryResponseToQueryTemplates, + }), + }), + reducerPath: 'queryLibrary', +}); diff --git a/public/app/features/query-library/api/mappers.ts b/public/app/features/query-library/api/mappers.ts new file mode 100644 index 00000000000..9dce0c19bfa --- /dev/null +++ b/public/app/features/query-library/api/mappers.ts @@ -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(), + }; + }); +}; diff --git a/public/app/features/query-library/api/mocks.ts b/public/app/features/query-library/api/mocks.ts new file mode 100644 index 00000000000..470dae005ae --- /dev/null +++ b/public/app/features/query-library/api/mocks.ts @@ -0,0 +1,9 @@ +import { BASE_URL } from './query'; +import { getTestQueryList } from './testdata/testQueryList'; + +export const mockData = { + all: { + url: BASE_URL, + response: getTestQueryList(), + }, +}; diff --git a/public/app/features/query-library/api/query.ts b/public/app/features/query-library/api/query.ts new file mode 100644 index 00000000000..b3f39234579 --- /dev/null +++ b/public/app/features/query-library/api/query.ts @@ -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 = async () => { + try { + const responseObservable = getBackendSrv().fetch({ + 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') }; + } + } +}; diff --git a/public/app/features/query-library/api/testdata/testQueryList.ts b/public/app/features/query-library/api/testdata/testQueryList.ts new file mode 100644 index 00000000000..2a941716b7d --- /dev/null +++ b/public/app/features/query-library/api/testdata/testQueryList.ts @@ -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"}', + }, + }, + ], + }, + }, + ], +}); diff --git a/public/app/features/query-library/api/types.ts b/public/app/features/query-library/api/types.ts new file mode 100644 index 00000000000..e2feb99057f --- /dev/null +++ b/public/app/features/query-library/api/types.ts @@ -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[]; +}; diff --git a/public/app/features/query-library/index.ts b/public/app/features/query-library/index.ts new file mode 100644 index 00000000000..b9b339b096b --- /dev/null +++ b/public/app/features/query-library/index.ts @@ -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, +}; diff --git a/public/app/features/query-library/types.ts b/public/app/features/query-library/types.ts new file mode 100644 index 00000000000..0629b327d1a --- /dev/null +++ b/public/app/features/query-library/types.ts @@ -0,0 +1,8 @@ +import { DataQuery } from '@grafana/schema'; + +export type QueryTemplate = { + uid: string; + title: string; + targets: DataQuery[]; + createdAtTimestamp: number; +}; diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 3fed0e7644f..a8dfc096915 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -9,6 +9,7 @@ import { StoreState } from 'app/types/store'; import { buildInitialState } from '../core/reducers/navModel'; import { addReducer, createRootReducer } from '../core/reducers/root'; import { alertingApi } from '../features/alerting/unified/api/alertingApi'; +import { queryLibraryApi } from '../features/query-library/api/factory'; import { setStore } from './store'; @@ -30,7 +31,8 @@ export function configureStore(initialState?: Partial) { alertingApi.middleware, publicDashboardApi.middleware, browseDashboardsAPI.middleware, - cloudMigrationAPI.middleware + cloudMigrationAPI.middleware, + queryLibraryApi.middleware ), devTools: process.env.NODE_ENV !== 'production', preloadedState: {