mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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 commitaa9da6e442. * Update import * Add reduxjs/toolkit as a peer dependecy * Revert "Add reduxjs/toolkit as a peer dependecy" This reverts commit2e68a62ab6. * 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:
@@ -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 = {};
|
||||
|
||||
@@ -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}
|
||||
</ToolbarButton>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { QueryTemplatesList } from './QueryTemplatesList';
|
||||
|
||||
export function QueryLibrary() {
|
||||
return <QueryTemplatesList />;
|
||||
}
|
||||
@@ -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 (
|
||||
<EmptyState variant="not-found" message={`Something went wrong`}>
|
||||
{error.message}
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<EmptyState message={`Query Library`} variant="not-found">
|
||||
<p>
|
||||
{
|
||||
"You haven't saved any queries to your library yet. Start adding them from Explore or your Query History tab."
|
||||
}
|
||||
</p>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
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 <QueryTemplatesTable queryTemplateRows={queryTemplateRows} />;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@grafana/ui';
|
||||
|
||||
export function ActionsCell() {
|
||||
return (
|
||||
<>
|
||||
<Button disabled={true} variant="primary">
|
||||
Run
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Avatar } from '@grafana/ui';
|
||||
|
||||
import { useQueryLibraryListStyles } from './styles';
|
||||
|
||||
export function AddedByCell() {
|
||||
const styles = useQueryLibraryListStyles();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className={styles.logo}>
|
||||
<Avatar src="https://secure.gravatar.com/avatar" alt="unknown" />
|
||||
</span>
|
||||
<span className={styles.otherText}>Unknown</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<QueryTemplateRow>) {
|
||||
const datasourceApi = useDatasource(props.row.original.datasourceRef);
|
||||
const styles = useQueryLibraryListStyles();
|
||||
|
||||
return <p className={styles.otherText}>{datasourceApi?.meta.name}</p>;
|
||||
}
|
||||
@@ -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<QueryTemplateRow>) {
|
||||
const styles = useQueryLibraryListStyles();
|
||||
const formattedTime = dateTime(props.row.original.createdAtTimestamp).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
return <p className={styles.otherText}>{formattedTime}</p>;
|
||||
}
|
||||
@@ -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<QueryTemplateRow>) {
|
||||
const datasourceApi = useDatasource(props.row.original.datasourceRef);
|
||||
const styles = useQueryLibraryListStyles();
|
||||
|
||||
if (!datasourceApi) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!props.row.original.query) {
|
||||
return <div>No queries</div>;
|
||||
}
|
||||
const query = props.row.original.query;
|
||||
const description = props.row.original.description;
|
||||
const dsName = datasourceApi?.name || '';
|
||||
|
||||
return (
|
||||
<div aria-label={`Query template for ${dsName}: ${description}`}>
|
||||
<p className={styles.header}>
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={datasourceApi?.meta.info.logos.small || 'public/img/icn-datasource.svg'}
|
||||
alt={datasourceApi?.meta.info.description}
|
||||
/>
|
||||
{dsName}
|
||||
</p>
|
||||
<p className={cx(styles.mainText, styles.singleLine)}>{datasourceApi?.getQueryDisplayText?.(query)}</p>
|
||||
<p className={cx(styles.otherText, styles.singleLine)}>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<QueryTemplateRow> = (rowA, rowB, _, desc) => {
|
||||
const timeA = rowA.original.createdAtTimestamp || 0;
|
||||
const timeB = rowB.original.createdAtTimestamp || 0;
|
||||
return desc ? timeA - timeB : timeB - timeA;
|
||||
};
|
||||
|
||||
const columns: Array<Column<QueryTemplateRow>> = [
|
||||
{ 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 (
|
||||
<InteractiveTable
|
||||
className={styles.tableWithSpacing}
|
||||
columns={columns}
|
||||
data={queryTemplateRows}
|
||||
getRowId={(row: { index: string }) => row.index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
}),
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: <EmptyState message="Coming soon!" variant="not-found" />,
|
||||
content: <QueryLibrary />,
|
||||
icon: 'book',
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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<string, object | number> = {};
|
||||
let data: Record<string, string | object | number> = {};
|
||||
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 });
|
||||
}),
|
||||
|
||||
76
public/app/features/explore/spec/queryLibrary.test.tsx
Normal file
76
public/app/features/explore/spec/queryLibrary.test.tsx
Normal file
@@ -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 <div>{props.children({ height: 1, scaledHeight: 1, scaledWidth: 1000, width: 1000 })}</div>;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
17
public/app/features/query-library/api/factory.ts
Normal file
17
public/app/features/query-library/api/factory.ts
Normal 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',
|
||||
});
|
||||
17
public/app/features/query-library/api/mappers.ts
Normal file
17
public/app/features/query-library/api/mappers.ts
Normal 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(),
|
||||
};
|
||||
});
|
||||
};
|
||||
9
public/app/features/query-library/api/mocks.ts
Normal file
9
public/app/features/query-library/api/mocks.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { BASE_URL } from './query';
|
||||
import { getTestQueryList } from './testdata/testQueryList';
|
||||
|
||||
export const mockData = {
|
||||
all: {
|
||||
url: BASE_URL,
|
||||
response: getTestQueryList(),
|
||||
},
|
||||
};
|
||||
34
public/app/features/query-library/api/query.ts
Normal file
34
public/app/features/query-library/api/query.ts
Normal 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') };
|
||||
}
|
||||
}
|
||||
};
|
||||
122
public/app/features/query-library/api/testdata/testQueryList.ts
vendored
Normal file
122
public/app/features/query-library/api/testdata/testQueryList.ts
vendored
Normal 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"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
26
public/app/features/query-library/api/types.ts
Normal file
26
public/app/features/query-library/api/types.ts
Normal 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[];
|
||||
};
|
||||
17
public/app/features/query-library/index.ts
Normal file
17
public/app/features/query-library/index.ts
Normal 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,
|
||||
};
|
||||
8
public/app/features/query-library/types.ts
Normal file
8
public/app/features/query-library/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
|
||||
export type QueryTemplate = {
|
||||
uid: string;
|
||||
title: string;
|
||||
targets: DataQuery[];
|
||||
createdAtTimestamp: number;
|
||||
};
|
||||
@@ -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<StoreState>) {
|
||||
alertingApi.middleware,
|
||||
publicDashboardApi.middleware,
|
||||
browseDashboardsAPI.middleware,
|
||||
cloudMigrationAPI.middleware
|
||||
cloudMigrationAPI.middleware,
|
||||
queryLibraryApi.middleware
|
||||
),
|
||||
devTools: process.env.NODE_ENV !== 'production',
|
||||
preloadedState: {
|
||||
|
||||
Reference in New Issue
Block a user