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:
Kristina 2024-08-14 10:11:53 -05:00 committed by GitHub
parent da6b02a2b0
commit 7cc925d319
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 299 additions and 176 deletions

View File

@ -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> = {

View File

@ -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);
});
});

View File

@ -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;
}

View File

@ -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>

View File

@ -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>
);
};

View 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>
);
};

View File

@ -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>
);
}

View File

@ -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} />
),
},
];

View File

@ -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);

View File

@ -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',
});

View File

@ -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,

View File

@ -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) {

View File

@ -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';

View File

@ -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;

View File

@ -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;
};

View File

@ -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",

View File

@ -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": "Đäŧä şőūřčę Å-Ż",