mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Query Library: Enable edit description (#90009)
* WIP
* Finish getting form to populate
* WIP API
* Get PUT to work although the edit doesnt apply yet
* Complete working PUT
* PATCH v1 no json patch
* This works!
* Fix headers to be configurable
* Fix translations
* Revert "This works!"
This reverts commit d57ffcbc4b
.
# Conflicts:
# public/app/features/query-library/api/factory.ts
# public/app/features/query-library/api/query.ts
# public/app/features/query-library/types.ts
* Use normal method of patching 😅
* Replace add to library form with generic version
* make translations generic
* Rename function, fix bad conflict resolution
This commit is contained in:
parent
da6b02a2b0
commit
7cc925d319
@ -28,12 +28,7 @@ import { TokenRevokedModal } from 'app/features/users/TokenRevokedModal';
|
||||
import { DashboardDTO, FolderDTO } from 'app/types';
|
||||
|
||||
import { ShowModalReactEvent } from '../../types/events';
|
||||
import {
|
||||
isContentTypeApplicationJson,
|
||||
parseInitFromOptions,
|
||||
parseResponseBody,
|
||||
parseUrlFromOptions,
|
||||
} from '../utils/fetch';
|
||||
import { isContentTypeJson, parseInitFromOptions, parseResponseBody, parseUrlFromOptions } from '../utils/fetch';
|
||||
import { isDataQuery, isLocalUrl } from '../utils/query';
|
||||
|
||||
import { FetchQueue } from './FetchQueue';
|
||||
@ -229,7 +224,7 @@ export class BackendSrv implements BackendService {
|
||||
mergeMap(async (response) => {
|
||||
const { status, statusText, ok, headers, url, type, redirected } = response;
|
||||
|
||||
const responseType = options.responseType ?? (isContentTypeApplicationJson(headers) ? 'json' : undefined);
|
||||
const responseType = options.responseType ?? (isContentTypeJson(headers) ? 'json' : undefined);
|
||||
|
||||
const data = await parseResponseBody<T>(response, responseType);
|
||||
const fetchResponse: FetchResponse<T> = {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {
|
||||
isContentTypeApplicationJson,
|
||||
isContentTypeJson,
|
||||
parseBody,
|
||||
parseCredentials,
|
||||
parseHeaders,
|
||||
@ -75,7 +75,7 @@ describe('parseHeaders', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isContentTypeApplicationJson', () => {
|
||||
describe('isContentTypeJson', () => {
|
||||
it.each`
|
||||
headers | expected
|
||||
${undefined} | ${false}
|
||||
@ -85,7 +85,7 @@ describe('isContentTypeApplicationJson', () => {
|
||||
${new Headers({ 'content-type': 'application/x-www-form-urlencoded' })} | ${false}
|
||||
${new Headers({ auth: 'Basic akdjasdkjalksdjasd' })} | ${false}
|
||||
`("when called with headers: 'headers' then the result should be '$expected'", ({ headers, expected }) => {
|
||||
expect(isContentTypeApplicationJson(headers)).toEqual(expected);
|
||||
expect(isContentTypeJson(headers)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { BackendSrvRequest } from '@grafana/runtime';
|
||||
export const parseInitFromOptions = (options: BackendSrvRequest): RequestInit => {
|
||||
const method = options.method;
|
||||
const headers = parseHeaders(options);
|
||||
const isAppJson = isContentTypeApplicationJson(headers);
|
||||
const isAppJson = isContentTypeJson(headers);
|
||||
const body = parseBody(options, isAppJson);
|
||||
const credentials = parseCredentials(options);
|
||||
|
||||
@ -68,13 +68,16 @@ export const parseHeaders = (options: BackendSrvRequest) => {
|
||||
return combinedHeaders;
|
||||
};
|
||||
|
||||
export const isContentTypeApplicationJson = (headers: Headers) => {
|
||||
export const isContentTypeJson = (headers: Headers) => {
|
||||
if (!headers) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const contentType = headers.get('content-type');
|
||||
if (contentType && contentType.toLowerCase() === 'application/json') {
|
||||
if (
|
||||
contentType &&
|
||||
(contentType.toLowerCase() === 'application/json' || contentType.toLowerCase() === 'application/merge-patch+json')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ import { ExploreActions } from './ExploreActions';
|
||||
import { ExploreDrawer } from './ExploreDrawer';
|
||||
import { ExplorePaneContainer } from './ExplorePaneContainer';
|
||||
import { QueriesDrawerContextProvider, useQueriesDrawerContext } from './QueriesDrawer/QueriesDrawerContext';
|
||||
import { AddToLibraryForm } from './QueryLibrary/AddToLibraryForm';
|
||||
import { QueryTemplateForm } from './QueryLibrary/QueryTemplateForm';
|
||||
import RichHistoryContainer from './RichHistory/RichHistoryContainer';
|
||||
import { useExplorePageTitle } from './hooks/useExplorePageTitle';
|
||||
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
|
||||
@ -132,11 +132,11 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
|
||||
</ExploreDrawer>
|
||||
)}
|
||||
<Modal
|
||||
title={t('explore.add-to-library-modal.title', 'Add query to Query Library')}
|
||||
title={t('explore.query-template-modal.add-title', 'Add query to Query Library')}
|
||||
isOpen={queryToAdd !== undefined}
|
||||
onDismiss={() => setQueryToAdd(undefined)}
|
||||
>
|
||||
<AddToLibraryForm
|
||||
<QueryTemplateForm
|
||||
onCancel={() => {
|
||||
setQueryToAdd(undefined);
|
||||
}}
|
||||
@ -145,7 +145,7 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
|
||||
setQueryToAdd(undefined);
|
||||
}
|
||||
}}
|
||||
query={queryToAdd!}
|
||||
queryToAdd={queryToAdd!}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
|
@ -1,116 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { AppEvents, dateTime } from '@grafana/data';
|
||||
import { DataSourcePicker, getAppEvents } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { Button, InlineSwitch, Modal, RadioButtonGroup, TextArea } from '@grafana/ui';
|
||||
import { Field } from '@grafana/ui/';
|
||||
import { Input } from '@grafana/ui/src/components/Input/Input';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { getQueryDisplayText } from 'app/core/utils/richHistory';
|
||||
import { useAddQueryTemplateMutation } from 'app/features/query-library';
|
||||
import { AddQueryTemplateCommand } from 'app/features/query-library/types';
|
||||
|
||||
import { useDatasource } from '../QueryLibrary/utils/useDatasource';
|
||||
|
||||
type Props = {
|
||||
onCancel: () => void;
|
||||
onSave: (isSuccess: boolean) => void;
|
||||
query: DataQuery;
|
||||
};
|
||||
|
||||
export type QueryDetails = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
const VisibilityOptions = [
|
||||
{ value: 'Public', label: t('explore.query-library.public', 'Public') },
|
||||
{ value: 'Private', label: t('explore.query-library.private', 'Private') },
|
||||
];
|
||||
|
||||
const info = t(
|
||||
'explore.add-to-library-modal.info',
|
||||
`You're about to save this query. Once saved, you can easily access it in the Query Library tab for future use and reference.`
|
||||
);
|
||||
|
||||
export const AddToLibraryForm = ({ onCancel, onSave, query }: Props) => {
|
||||
const { register, handleSubmit } = useForm<QueryDetails>();
|
||||
|
||||
const [addQueryTemplate] = useAddQueryTemplateMutation();
|
||||
|
||||
const handleAddQueryTemplate = async (addQueryTemplateCommand: AddQueryTemplateCommand) => {
|
||||
return addQueryTemplate(addQueryTemplateCommand)
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [
|
||||
t('explore.query-library.query-template-added', 'Query template successfully added to the library'),
|
||||
],
|
||||
});
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [
|
||||
t('explore.query-library.query-template-error', 'Error attempting to add this query to the library'),
|
||||
],
|
||||
});
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const datasource = useDatasource(query.datasource);
|
||||
|
||||
const displayText = useMemo(() => {
|
||||
return datasource?.getQueryDisplayText?.(query) || getQueryDisplayText(query);
|
||||
}, [datasource, query]);
|
||||
|
||||
const onSubmit = async (data: QueryDetails) => {
|
||||
const timestamp = dateTime().toISOString();
|
||||
const temporaryDefaultTitle =
|
||||
data.description || t('explore.query-library.default-description', 'Public', { timestamp: timestamp });
|
||||
handleAddQueryTemplate({ title: temporaryDefaultTitle, targets: [query] }).then((isSuccess) => {
|
||||
onSave(isSuccess);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<p>{info}</p>
|
||||
<Field label={t('explore.add-to-library-modal.query', 'Query')}>
|
||||
<TextArea readOnly={true} value={displayText}></TextArea>
|
||||
</Field>
|
||||
<Field label={t('explore.add-to-library-modal.data-source-name', 'Data source name')}>
|
||||
<DataSourcePicker current={datasource?.uid} disabled={true} />
|
||||
</Field>
|
||||
<Field label={t('explore.add-to-library-modal.data-source-type', 'Data source type')}>
|
||||
<Input disabled={true} defaultValue={datasource?.meta.name}></Input>
|
||||
</Field>
|
||||
<Field label={t('explore.add-to-library-modal.description', 'Description')}>
|
||||
<Input id="query-template-description" autoFocus={true} {...register('description')}></Input>
|
||||
</Field>
|
||||
<Field label={t('explore.add-to-library-modal.visibility', 'Visibility')}>
|
||||
<RadioButtonGroup options={VisibilityOptions} value={'Public'} disabled={true} />
|
||||
</Field>
|
||||
<InlineSwitch
|
||||
showLabel={true}
|
||||
disabled={true}
|
||||
label={t(
|
||||
'explore.add-to-library-modal.auto-star',
|
||||
'Auto-star this query to add it to your starred list in the Query Library.'
|
||||
)}
|
||||
/>
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" onClick={() => onCancel()} fill="outline">
|
||||
<Trans i18nKey="explore.query-library.cancel">Cancel</Trans>
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
<Trans i18nKey="explore.query-library.save">Save</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</form>
|
||||
);
|
||||
};
|
172
public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx
Normal file
172
public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { AppEvents, dateTime } from '@grafana/data';
|
||||
import { DataSourcePicker, getAppEvents, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { Button, InlineSwitch, Modal, RadioButtonGroup, TextArea } from '@grafana/ui';
|
||||
import { Field } from '@grafana/ui/';
|
||||
import { Input } from '@grafana/ui/src/components/Input/Input';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { getQueryDisplayText } from 'app/core/utils/richHistory';
|
||||
import { useAddQueryTemplateMutation, useEditQueryTemplateMutation } from 'app/features/query-library';
|
||||
import { AddQueryTemplateCommand, EditQueryTemplateCommand } from 'app/features/query-library/types';
|
||||
|
||||
import { useDatasource } from '../QueryLibrary/utils/useDatasource';
|
||||
|
||||
import { QueryTemplateRow } from './QueryTemplatesTable/types';
|
||||
|
||||
type Props = {
|
||||
onCancel: () => void;
|
||||
onSave: (isSuccess: boolean) => void;
|
||||
queryToAdd?: DataQuery;
|
||||
templateData?: QueryTemplateRow;
|
||||
};
|
||||
|
||||
export type QueryDetails = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
const VisibilityOptions = [
|
||||
{ value: 'Public', label: t('explore.query-library.public', 'Public') },
|
||||
{ value: 'Private', label: t('explore.query-library.private', 'Private') },
|
||||
];
|
||||
|
||||
const getInstuctions = (isAdd: boolean) => {
|
||||
return isAdd
|
||||
? t(
|
||||
'explore.query-template-modal.add-info',
|
||||
`You're about to save this query. Once saved, you can easily access it in the Query Library tab for future use and reference.`
|
||||
)
|
||||
: t(
|
||||
'explore.query-template-modal.edit-info',
|
||||
`You're about to edit this query. Once saved, you can easily access it in the Query Library tab for future use and reference.`
|
||||
);
|
||||
};
|
||||
|
||||
export const QueryTemplateForm = ({ onCancel, onSave, queryToAdd, templateData }: Props) => {
|
||||
const { register, handleSubmit } = useForm<QueryDetails>({
|
||||
defaultValues: {
|
||||
description: templateData?.description,
|
||||
},
|
||||
});
|
||||
|
||||
const [addQueryTemplate] = useAddQueryTemplateMutation();
|
||||
const [editQueryTemplate] = useEditQueryTemplateMutation();
|
||||
|
||||
const datasource = useDatasource(queryToAdd?.datasource);
|
||||
|
||||
// this is an array to support multi query templates sometime in the future
|
||||
const queries =
|
||||
queryToAdd !== undefined ? [queryToAdd] : templateData?.query !== undefined ? [templateData?.query] : [];
|
||||
|
||||
const handleAddQueryTemplate = async (addQueryTemplateCommand: AddQueryTemplateCommand) => {
|
||||
return addQueryTemplate(addQueryTemplateCommand)
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [
|
||||
t('explore.query-library.query-template-added', 'Query template successfully added to the library'),
|
||||
],
|
||||
});
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [
|
||||
t('explore.query-library.query-template-add-error', 'Error attempting to add this query to the library'),
|
||||
],
|
||||
});
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditQueryTemplate = async (editQueryTemplateCommand: EditQueryTemplateCommand) => {
|
||||
return editQueryTemplate(editQueryTemplateCommand)
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [t('explore.query-library.query-template-edited', 'Query template successfully edited')],
|
||||
});
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: [t('explore.query-library.query-template-edit-error', 'Error attempting to edit this query')],
|
||||
});
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (data: QueryDetails) => {
|
||||
const timestamp = dateTime().toISOString();
|
||||
const temporaryDefaultTitle =
|
||||
data.description || t('explore.query-library.default-description', 'Public', { timestamp: timestamp });
|
||||
|
||||
if (templateData?.uid) {
|
||||
handleEditQueryTemplate({ uid: templateData.uid, partialSpec: { title: data.description } }).then((isSuccess) => {
|
||||
onSave(isSuccess);
|
||||
});
|
||||
} else if (queryToAdd) {
|
||||
handleAddQueryTemplate({ title: temporaryDefaultTitle, targets: [queryToAdd] }).then((isSuccess) => {
|
||||
onSave(isSuccess);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { value: queryText } = useAsync(async () => {
|
||||
const promises = queries.map(async (query, i) => {
|
||||
const datasource = await getDataSourceSrv().get(query.datasource);
|
||||
return datasource?.getQueryDisplayText?.(query) || getQueryDisplayText(query);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<p>{getInstuctions(templateData === undefined)}</p>
|
||||
{queryText &&
|
||||
queryText.map((queryString, i) => (
|
||||
<Field key={`query-${i}`} label={t('explore.query-template-modal.query', 'Query')}>
|
||||
<TextArea readOnly={true} value={queryString}></TextArea>
|
||||
</Field>
|
||||
))}
|
||||
{queryToAdd && (
|
||||
<>
|
||||
<Field label={t('explore.query-template-modal.data-source-name', 'Data source name')}>
|
||||
<DataSourcePicker current={datasource?.uid} disabled={true} />
|
||||
</Field>
|
||||
<Field label={t('explore.query-template-modall.data-source-type', 'Data source type')}>
|
||||
<Input disabled={true} defaultValue={datasource?.meta.name}></Input>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<Field label={t('explore.query-template-modal.description', 'Description')}>
|
||||
<Input id="query-template-description" autoFocus={true} {...register('description')}></Input>
|
||||
</Field>
|
||||
<Field label={t('explore.query-template-modal.visibility', 'Visibility')}>
|
||||
<RadioButtonGroup options={VisibilityOptions} value={'Public'} disabled={true} />
|
||||
</Field>
|
||||
<InlineSwitch
|
||||
showLabel={true}
|
||||
disabled={true}
|
||||
label={t(
|
||||
'explore.query-template-modal.auto-star',
|
||||
'Auto-star this query to add it to your starred list in the Query Library.'
|
||||
)}
|
||||
/>
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" onClick={() => onCancel()} fill="outline">
|
||||
<Trans i18nKey="explore.query-library.cancel">Cancel</Trans>
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
<Trans i18nKey="explore.query-library.save">Save</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { reportInteraction, getAppEvents } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { IconButton } from '@grafana/ui';
|
||||
import { IconButton, Modal } from '@grafana/ui';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
import { t } from 'app/core/internationalization';
|
||||
@ -10,17 +11,20 @@ import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import ExploreRunQueryButton from '../../ExploreRunQueryButton';
|
||||
import { useQueriesDrawerContext } from '../../QueriesDrawer/QueriesDrawerContext';
|
||||
import { QueryTemplateForm } from '../QueryTemplateForm';
|
||||
|
||||
import { useQueryLibraryListStyles } from './styles';
|
||||
import { QueryTemplateRow } from './types';
|
||||
|
||||
interface ActionsCellProps {
|
||||
queryUid?: string;
|
||||
query?: DataQuery;
|
||||
queryTemplate: QueryTemplateRow;
|
||||
rootDatasourceUid?: string;
|
||||
}
|
||||
|
||||
function ActionsCell({ query, rootDatasourceUid, queryUid }: ActionsCellProps) {
|
||||
function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid }: ActionsCellProps) {
|
||||
const [deleteQueryTemplate] = useDeleteQueryTemplateMutation();
|
||||
const [editFormOpen, setEditFormOpen] = useState(false);
|
||||
const { setDrawerOpened } = useQueriesDrawerContext();
|
||||
const styles = useQueryLibraryListStyles();
|
||||
|
||||
@ -59,11 +63,34 @@ function ActionsCell({ query, rootDatasourceUid, queryUid }: ActionsCellProps) {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.actionButton}
|
||||
size="lg"
|
||||
name="comment-alt"
|
||||
title={t('explore.query-library.add-edit-description', 'Add/edit description')}
|
||||
tooltip={t('explore.query-library.add-edit-description', 'Add/edit description')}
|
||||
onClick={() => {
|
||||
setEditFormOpen(true);
|
||||
}}
|
||||
/>
|
||||
<ExploreRunQueryButton
|
||||
queries={query ? [query] : []}
|
||||
queries={queryTemplate.query ? [queryTemplate.query] : []}
|
||||
rootDatasourceUid={rootDatasourceUid}
|
||||
onClick={() => setDrawerOpened(false)}
|
||||
/>
|
||||
<Modal
|
||||
title={t('explore.query-template-modal.edit-title', 'Edit query')}
|
||||
isOpen={editFormOpen}
|
||||
onDismiss={() => setEditFormOpen(false)}
|
||||
>
|
||||
<QueryTemplateForm
|
||||
onCancel={() => setEditFormOpen(false)}
|
||||
templateData={queryTemplate}
|
||||
onSave={() => {
|
||||
setEditFormOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ const columns: Array<Column<QueryTemplateRow>> = [
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row: { original } }) => (
|
||||
<ActionsCell query={original.query} rootDatasourceUid={original.datasourceRef?.uid} queryUid={original.uid} />
|
||||
<ActionsCell queryTemplate={original} rootDatasourceUid={original.datasourceRef?.uid} queryUid={original.uid} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
@ -5,7 +5,7 @@ import { DataQuery } from '@grafana/schema';
|
||||
import { Button, Modal } from '@grafana/ui';
|
||||
import { isQueryLibraryEnabled } from 'app/features/query-library';
|
||||
|
||||
import { AddToLibraryForm } from '../QueryLibrary/AddToLibraryForm';
|
||||
import { QueryTemplateForm } from '../QueryLibrary/QueryTemplateForm';
|
||||
|
||||
type Props = {
|
||||
query: DataQuery;
|
||||
@ -23,13 +23,13 @@ export const RichHistoryAddToLibrary = ({ query }: Props) => {
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
<Modal
|
||||
title={t('explore.add-to-library-modal.title', 'Add query to Query Library')}
|
||||
title={t('explore.query-template-modal.add-title', 'Add query to Query Library')}
|
||||
isOpen={isOpen}
|
||||
onDismiss={() => setIsOpen(false)}
|
||||
>
|
||||
<AddToLibraryForm
|
||||
<QueryTemplateForm
|
||||
onCancel={() => setIsOpen(() => false)}
|
||||
query={query}
|
||||
queryToAdd={query}
|
||||
onSave={(isSuccess) => {
|
||||
if (isSuccess) {
|
||||
setIsOpen(false);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { AddQueryTemplateCommand, DeleteQueryTemplateCommand, QueryTemplate } from '../types';
|
||||
import { AddQueryTemplateCommand, DeleteQueryTemplateCommand, EditQueryTemplateCommand, QueryTemplate } from '../types';
|
||||
|
||||
import { convertAddQueryTemplateCommandToDataQuerySpec, convertDataQueryResponseToQueryTemplates } from './mappers';
|
||||
import { baseQuery } from './query';
|
||||
@ -28,6 +28,17 @@ export const queryLibraryApi = createApi({
|
||||
}),
|
||||
invalidatesTags: ['QueryTemplatesList'],
|
||||
}),
|
||||
editQueryTemplate: builder.mutation<void, EditQueryTemplateCommand>({
|
||||
query: (editQueryTemplateCommand) => ({
|
||||
url: `${editQueryTemplateCommand.uid}`,
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/merge-patch+json',
|
||||
},
|
||||
data: { spec: editQueryTemplateCommand.partialSpec },
|
||||
}),
|
||||
invalidatesTags: ['QueryTemplatesList'],
|
||||
}),
|
||||
}),
|
||||
reducerPath: 'queryLibrary',
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AddQueryTemplateCommand, QueryTemplate } from '../types';
|
||||
|
||||
import { API_VERSION, QueryTemplateKinds } from './query';
|
||||
import { CREATED_BY_KEY, DataQuerySpec, DataQuerySpecResponse, DataQueryTarget } from './types';
|
||||
import { CREATED_BY_KEY, DataQueryFullSpec, DataQuerySpecResponse, DataQueryTarget } from './types';
|
||||
|
||||
export const parseCreatedByValue = (value?: string) => {
|
||||
// https://github.com/grafana/grafana/blob/main/pkg/services/user/identity.go#L194
|
||||
@ -42,7 +42,7 @@ export const convertDataQueryResponseToQueryTemplates = (result: DataQuerySpecRe
|
||||
|
||||
export const convertAddQueryTemplateCommandToDataQuerySpec = (
|
||||
addQueryTemplateCommand: AddQueryTemplateCommand
|
||||
): DataQuerySpec => {
|
||||
): DataQueryFullSpec => {
|
||||
const { title, targets } = addQueryTemplateCommand;
|
||||
return {
|
||||
apiVersion: API_VERSION,
|
||||
|
@ -28,6 +28,7 @@ export const BASE_URL = `/apis/${API_VERSION}/namespaces/${config.namespace}/que
|
||||
// URL is optional for these requests
|
||||
interface QueryLibraryBackendRequest extends Pick<BackendSrvRequest, 'data' | 'method'> {
|
||||
url?: string;
|
||||
headers?: { [key: string]: string };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,6 +43,7 @@ export const baseQuery: BaseQueryFn<QueryLibraryBackendRequest, DataQuerySpecRes
|
||||
showErrorAlert: true,
|
||||
method: requestOptions.method || 'GET',
|
||||
data: requestOptions.data,
|
||||
headers: { ...requestOptions.headers },
|
||||
});
|
||||
return await lastValueFrom(responseObservable);
|
||||
} catch (error) {
|
||||
|
@ -6,6 +6,12 @@ export type DataQueryTarget = {
|
||||
};
|
||||
|
||||
export type DataQuerySpec = {
|
||||
title: string;
|
||||
vars: object[]; // TODO: Detect variables in #86838
|
||||
targets: DataQueryTarget[];
|
||||
};
|
||||
|
||||
export type DataQueryFullSpec = {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: {
|
||||
@ -14,16 +20,14 @@ export type DataQuerySpec = {
|
||||
creationTimestamp?: string;
|
||||
annotations?: { [key: string]: string };
|
||||
};
|
||||
spec: {
|
||||
title: string;
|
||||
vars: object[]; // TODO: Detect variables in #86838
|
||||
targets: DataQueryTarget[];
|
||||
};
|
||||
spec: DataQuerySpec;
|
||||
};
|
||||
|
||||
export type DataQueryPartialSpec = Partial<DataQuerySpec>;
|
||||
|
||||
export type DataQuerySpecResponse = {
|
||||
apiVersion: string;
|
||||
items: DataQuerySpec[];
|
||||
items: DataQueryFullSpec[];
|
||||
};
|
||||
|
||||
export const CREATED_BY_KEY = 'grafana.app/createdBy';
|
||||
|
@ -12,8 +12,12 @@ import { config } from '@grafana/runtime';
|
||||
import { queryLibraryApi } from './api/factory';
|
||||
import { mockData } from './api/mocks';
|
||||
|
||||
export const { useAllQueryTemplatesQuery, useAddQueryTemplateMutation, useDeleteQueryTemplateMutation } =
|
||||
queryLibraryApi;
|
||||
export const {
|
||||
useAllQueryTemplatesQuery,
|
||||
useAddQueryTemplateMutation,
|
||||
useDeleteQueryTemplateMutation,
|
||||
useEditQueryTemplateMutation,
|
||||
} = queryLibraryApi;
|
||||
|
||||
export function isQueryLibraryEnabled() {
|
||||
return config.featureToggles.queryLibrary;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
|
||||
import { DataQueryPartialSpec } from './api/types';
|
||||
|
||||
export type QueryTemplate = {
|
||||
uid: string;
|
||||
title: string;
|
||||
@ -13,6 +15,11 @@ export type AddQueryTemplateCommand = {
|
||||
targets: DataQuery[];
|
||||
};
|
||||
|
||||
export type EditQueryTemplateCommand = {
|
||||
uid: string;
|
||||
partialSpec: DataQueryPartialSpec;
|
||||
};
|
||||
|
||||
export type DeleteQueryTemplateCommand = {
|
||||
uid: string;
|
||||
};
|
||||
|
@ -702,16 +702,6 @@
|
||||
},
|
||||
"explore": {
|
||||
"add-to-dashboard": "Add to dashboard",
|
||||
"add-to-library-modal": {
|
||||
"auto-star": "Auto-star this query to add it to your starred list in the Query Library.",
|
||||
"data-source-name": "Data source name",
|
||||
"data-source-type": "Data source type",
|
||||
"description": "Description",
|
||||
"info": "You're about to save this query. Once saved, you can easily access it in the Query Library tab for future use and reference.",
|
||||
"query": "Query",
|
||||
"title": "Add query to Query Library",
|
||||
"visibility": "Visibility"
|
||||
},
|
||||
"logs": {
|
||||
"maximum-pinned-logs": "Maximum of {{PINNED_LOGS_LIMIT}} pinned logs reached. Unpin a log to add another.",
|
||||
"no-logs-found": "No logs found.",
|
||||
@ -719,6 +709,7 @@
|
||||
"stop-scan": "Stop scan"
|
||||
},
|
||||
"query-library": {
|
||||
"add-edit-description": "Add/edit description",
|
||||
"cancel": "Cancel",
|
||||
"default-description": "Public",
|
||||
"delete-query": "Delete query",
|
||||
@ -727,10 +718,26 @@
|
||||
"private": "Private",
|
||||
"public": "Public",
|
||||
"query-deleted": "Query deleted",
|
||||
"query-template-add-error": "Error attempting to add this query to the library",
|
||||
"query-template-added": "Query template successfully added to the library",
|
||||
"query-template-error": "Error attempting to add this query to the library",
|
||||
"query-template-edit-error": "Error attempting to edit this query",
|
||||
"query-template-edited": "Query template successfully edited",
|
||||
"save": "Save"
|
||||
},
|
||||
"query-template-modal": {
|
||||
"add-info": "You're about to save this query. Once saved, you can easily access it in the Query Library tab for future use and reference.",
|
||||
"add-title": "Add query to Query Library",
|
||||
"auto-star": "Auto-star this query to add it to your starred list in the Query Library.",
|
||||
"data-source-name": "Data source name",
|
||||
"description": "Description",
|
||||
"edit-info": "You're about to edit this query. Once saved, you can easily access it in the Query Library tab for future use and reference.",
|
||||
"edit-title": "Edit query",
|
||||
"query": "Query",
|
||||
"visibility": "Visibility"
|
||||
},
|
||||
"query-template-modall": {
|
||||
"data-source-type": "Data source type"
|
||||
},
|
||||
"rich-history": {
|
||||
"close-tooltip": "Close query history",
|
||||
"datasource-a-z": "Data source A-Z",
|
||||
|
@ -702,16 +702,6 @@
|
||||
},
|
||||
"explore": {
|
||||
"add-to-dashboard": "Åđđ ŧő đäşĥþőäřđ",
|
||||
"add-to-library-modal": {
|
||||
"auto-star": "Åūŧő-şŧäř ŧĥįş qūęřy ŧő äđđ įŧ ŧő yőūř şŧäřřęđ ľįşŧ įʼn ŧĥę Qūęřy Ŀįþřäřy.",
|
||||
"data-source-name": "Đäŧä şőūřčę ʼnämę",
|
||||
"data-source-type": "Đäŧä şőūřčę ŧypę",
|
||||
"description": "Đęşčřįpŧįőʼn",
|
||||
"info": "Ÿőū'řę äþőūŧ ŧő şävę ŧĥįş qūęřy. Øʼnčę şävęđ, yőū čäʼn ęäşįľy äččęşş įŧ įʼn ŧĥę Qūęřy Ŀįþřäřy ŧäþ ƒőř ƒūŧūřę ūşę äʼnđ řęƒęřęʼnčę.",
|
||||
"query": "Qūęřy",
|
||||
"title": "Åđđ qūęřy ŧő Qūęřy Ŀįþřäřy",
|
||||
"visibility": "Vįşįþįľįŧy"
|
||||
},
|
||||
"logs": {
|
||||
"maximum-pinned-logs": "Mäχįmūm őƒ {{PINNED_LOGS_LIMIT}} pįʼnʼnęđ ľőģş řęäčĥęđ. Ůʼnpįʼn ä ľőģ ŧő äđđ äʼnőŧĥęř.",
|
||||
"no-logs-found": "Ńő ľőģş ƒőūʼnđ.",
|
||||
@ -719,6 +709,7 @@
|
||||
"stop-scan": "Ŝŧőp şčäʼn"
|
||||
},
|
||||
"query-library": {
|
||||
"add-edit-description": "Åđđ/ęđįŧ đęşčřįpŧįőʼn",
|
||||
"cancel": "Cäʼnčęľ",
|
||||
"default-description": "Pūþľįč",
|
||||
"delete-query": "Đęľęŧę qūęřy",
|
||||
@ -727,10 +718,26 @@
|
||||
"private": "Přįväŧę",
|
||||
"public": "Pūþľįč",
|
||||
"query-deleted": "Qūęřy đęľęŧęđ",
|
||||
"query-template-add-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő äđđ ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy",
|
||||
"query-template-added": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy äđđęđ ŧő ŧĥę ľįþřäřy",
|
||||
"query-template-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő äđđ ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy",
|
||||
"query-template-edit-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ęđįŧ ŧĥįş qūęřy",
|
||||
"query-template-edited": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy ęđįŧęđ",
|
||||
"save": "Ŝävę"
|
||||
},
|
||||
"query-template-modal": {
|
||||
"add-info": "Ÿőū'řę äþőūŧ ŧő şävę ŧĥįş qūęřy. Øʼnčę şävęđ, yőū čäʼn ęäşįľy äččęşş įŧ įʼn ŧĥę Qūęřy Ŀįþřäřy ŧäþ ƒőř ƒūŧūřę ūşę äʼnđ řęƒęřęʼnčę.",
|
||||
"add-title": "Åđđ qūęřy ŧő Qūęřy Ŀįþřäřy",
|
||||
"auto-star": "Åūŧő-şŧäř ŧĥįş qūęřy ŧő äđđ įŧ ŧő yőūř şŧäřřęđ ľįşŧ įʼn ŧĥę Qūęřy Ŀįþřäřy.",
|
||||
"data-source-name": "Đäŧä şőūřčę ʼnämę",
|
||||
"description": "Đęşčřįpŧįőʼn",
|
||||
"edit-info": "Ÿőū'řę äþőūŧ ŧő ęđįŧ ŧĥįş qūęřy. Øʼnčę şävęđ, yőū čäʼn ęäşįľy äččęşş įŧ įʼn ŧĥę Qūęřy Ŀįþřäřy ŧäþ ƒőř ƒūŧūřę ūşę äʼnđ řęƒęřęʼnčę.",
|
||||
"edit-title": "Ēđįŧ qūęřy",
|
||||
"query": "Qūęřy",
|
||||
"visibility": "Vįşįþįľįŧy"
|
||||
},
|
||||
"query-template-modall": {
|
||||
"data-source-type": "Đäŧä şőūřčę ŧypę"
|
||||
},
|
||||
"rich-history": {
|
||||
"close-tooltip": "Cľőşę qūęřy ĥįşŧőřy",
|
||||
"datasource-a-z": "Đäŧä şőūřčę Å-Ż",
|
||||
|
Loading…
Reference in New Issue
Block a user