diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 3ceffdec347..64ac099efba 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -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(response, responseType); const fetchResponse: FetchResponse = { diff --git a/public/app/core/utils/fetch.test.ts b/public/app/core/utils/fetch.test.ts index 556c3b40d0b..ffb1058ff14 100644 --- a/public/app/core/utils/fetch.test.ts +++ b/public/app/core/utils/fetch.test.ts @@ -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); }); }); diff --git a/public/app/core/utils/fetch.ts b/public/app/core/utils/fetch.ts index 49253f74a31..252d0e6aca4 100644 --- a/public/app/core/utils/fetch.ts +++ b/public/app/core/utils/fetch.ts @@ -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; } diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index 2372df87670..a62a291e162 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -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 )} setQueryToAdd(undefined)} > - { setQueryToAdd(undefined); }} @@ -145,7 +145,7 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa setQueryToAdd(undefined); } }} - query={queryToAdd!} + queryToAdd={queryToAdd!} /> diff --git a/public/app/features/explore/QueryLibrary/AddToLibraryForm.tsx b/public/app/features/explore/QueryLibrary/AddToLibraryForm.tsx deleted file mode 100644 index 551f63edde2..00000000000 --- a/public/app/features/explore/QueryLibrary/AddToLibraryForm.tsx +++ /dev/null @@ -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(); - - 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 ( -
-

{info}

- - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx new file mode 100644 index 00000000000..ed654fa378d --- /dev/null +++ b/public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx @@ -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({ + 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 ( +
+

{getInstuctions(templateData === undefined)}

+ {queryText && + queryText.map((queryString, i) => ( + + + + ))} + {queryToAdd && ( + <> + + + + + + + + )} + + + + + + + + + + + + + ); +}; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx index 8e78f64c295..e3ae8d14ce6 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx @@ -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) { } }} /> + { + setEditFormOpen(true); + }} + /> setDrawerOpened(false)} /> + setEditFormOpen(false)} + > + setEditFormOpen(false)} + templateData={queryTemplate} + onSave={() => { + setEditFormOpen(false); + }} + /> + ); } diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx index 376e7457dd8..c95398b0956 100644 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx +++ b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx @@ -25,7 +25,7 @@ const columns: Array> = [ id: 'actions', header: '', cell: ({ row: { original } }) => ( - + ), }, ]; diff --git a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx index 8bfcde6e6e8..611558c0b19 100644 --- a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx @@ -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} setIsOpen(false)} > - setIsOpen(() => false)} - query={query} + queryToAdd={query} onSave={(isSuccess) => { if (isSuccess) { setIsOpen(false); diff --git a/public/app/features/query-library/api/factory.ts b/public/app/features/query-library/api/factory.ts index 09cc17f76f6..d67b8666aac 100644 --- a/public/app/features/query-library/api/factory.ts +++ b/public/app/features/query-library/api/factory.ts @@ -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({ + query: (editQueryTemplateCommand) => ({ + url: `${editQueryTemplateCommand.uid}`, + method: 'PATCH', + headers: { + 'Content-Type': 'application/merge-patch+json', + }, + data: { spec: editQueryTemplateCommand.partialSpec }, + }), + invalidatesTags: ['QueryTemplatesList'], + }), }), reducerPath: 'queryLibrary', }); diff --git a/public/app/features/query-library/api/mappers.ts b/public/app/features/query-library/api/mappers.ts index 3df53b95795..6c0af6c8539 100644 --- a/public/app/features/query-library/api/mappers.ts +++ b/public/app/features/query-library/api/mappers.ts @@ -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, diff --git a/public/app/features/query-library/api/query.ts b/public/app/features/query-library/api/query.ts index 2d08c1736ae..eb730daf594 100644 --- a/public/app/features/query-library/api/query.ts +++ b/public/app/features/query-library/api/query.ts @@ -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 { url?: string; + headers?: { [key: string]: string }; } /** @@ -42,6 +43,7 @@ export const baseQuery: BaseQueryFn; + export type DataQuerySpecResponse = { apiVersion: string; - items: DataQuerySpec[]; + items: DataQueryFullSpec[]; }; export const CREATED_BY_KEY = 'grafana.app/createdBy'; diff --git a/public/app/features/query-library/index.ts b/public/app/features/query-library/index.ts index 513b7ca5bb8..8c5bd02a098 100644 --- a/public/app/features/query-library/index.ts +++ b/public/app/features/query-library/index.ts @@ -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; diff --git a/public/app/features/query-library/types.ts b/public/app/features/query-library/types.ts index 66a92b085a2..91e050b2774 100644 --- a/public/app/features/query-library/types.ts +++ b/public/app/features/query-library/types.ts @@ -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; }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 685a46c6580..d0d99aaffcb 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 01c8396aca1..3f31878d512 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -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": "Đäŧä şőūřčę Å-Ż",