Explore: Add a query template to query library from query history (#88047)
* Add basic button for adding a query template * Add hook to create a template * Handle notifications * Add tags to invalidate cache * Generate translations * Updates types * Add tests * Simplify code * Add a better default title
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
import { t } from 'i18next';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { AppEvents, dateTime } from '@grafana/data';
|
||||||
|
import { getAppEvents } from '@grafana/runtime';
|
||||||
|
import { DataQuery } from '@grafana/schema';
|
||||||
|
import { Button } from '@grafana/ui';
|
||||||
|
import { isQueryLibraryEnabled, useAddQueryTemplateMutation } from 'app/features/query-library';
|
||||||
|
import { AddQueryTemplateCommand } from 'app/features/query-library/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
query: DataQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RichHistoryAddToLibrary = ({ query }: Props) => {
|
||||||
|
const [addQueryTemplate, { isSuccess }] = useAddQueryTemplateMutation();
|
||||||
|
|
||||||
|
const handleAddQueryTemplate = async (addQueryTemplateCommand: AddQueryTemplateCommand) => {
|
||||||
|
const result = await addQueryTemplate(addQueryTemplateCommand);
|
||||||
|
if (!result.error) {
|
||||||
|
getAppEvents().publish({
|
||||||
|
type: AppEvents.alertSuccess.name,
|
||||||
|
payload: [
|
||||||
|
t('explore.rich-history-card.query-template-added', 'Query template successfully added to the library'),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonLabel = t('explore.rich-history-card.add-to-library', 'Add to library');
|
||||||
|
|
||||||
|
return isQueryLibraryEnabled() && !isSuccess ? (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
aria-label={buttonLabel}
|
||||||
|
onClick={() => {
|
||||||
|
const timestamp = dateTime().toISOString();
|
||||||
|
const temporaryDefaultTitle = `Imported from Explore - ${timestamp}`;
|
||||||
|
handleAddQueryTemplate({ title: temporaryDefaultTitle, targets: [query] });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{buttonLabel}
|
||||||
|
</Button>
|
||||||
|
) : undefined;
|
||||||
|
};
|
||||||
@@ -21,6 +21,8 @@ import { RichHistoryQuery } from 'app/types/explore';
|
|||||||
|
|
||||||
import ExploreRunQueryButton from '../ExploreRunQueryButton';
|
import ExploreRunQueryButton from '../ExploreRunQueryButton';
|
||||||
|
|
||||||
|
import { RichHistoryAddToLibrary } from './RichHistoryAddToLibrary';
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
changeDatasource,
|
changeDatasource,
|
||||||
deleteHistoryItem,
|
deleteHistoryItem,
|
||||||
@@ -342,6 +344,7 @@ export function RichHistoryCard(props: Props) {
|
|||||||
)}
|
)}
|
||||||
{activeUpdateComment && updateComment}
|
{activeUpdateComment && updateComment}
|
||||||
</div>
|
</div>
|
||||||
|
{!activeUpdateComment && <RichHistoryAddToLibrary query={queryHistoryItem?.queries[0]} />}
|
||||||
{!activeUpdateComment && (
|
{!activeUpdateComment && (
|
||||||
<div className={styles.runButton}>
|
<div className={styles.runButton}>
|
||||||
<ExploreRunQueryButton queries={queryHistoryItem.queries} rootDatasourceUid={cardRootDatasource?.uid} />
|
<ExploreRunQueryButton queries={queryHistoryItem.queries} rootDatasourceUid={cardRootDatasource?.uid} />
|
||||||
|
|||||||
@@ -33,6 +33,19 @@ export const assertQueryLibraryTemplateExists = async (datasource: string, descr
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const assertAddToQueryLibraryButtonExists = async (value = true) => {
|
||||||
|
await waitFor(() => {
|
||||||
|
// ensures buttons for the card have been loaded to avoid false positives
|
||||||
|
expect(withinQueryHistory().getByRole('button', { name: /run query/i })).toBeInTheDocument();
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
expect(withinQueryHistory().queryByRole('button', { name: /add to library/i })).toBeInTheDocument();
|
||||||
|
} else {
|
||||||
|
expect(withinQueryHistory().queryByRole('button', { name: /add to library/i })).not.toBeInTheDocument();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const assertQueryHistoryIsEmpty = async () => {
|
export const assertQueryHistoryIsEmpty = async () => {
|
||||||
const selector = withinQueryHistory();
|
const selector = withinQueryHistory();
|
||||||
const queryTexts = selector.queryAllByLabelText('Query text');
|
const queryTexts = selector.queryAllByLabelText('Query text');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { screen, within } from '@testing-library/react';
|
import { screen, waitFor, within } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
@@ -36,6 +36,23 @@ export const openQueryLibrary = async () => {
|
|||||||
const explore = withinExplore('left');
|
const explore = withinExplore('left');
|
||||||
const button = explore.getByRole('button', { name: 'Query library' });
|
const button = explore.getByRole('button', { name: 'Query library' });
|
||||||
await userEvent.click(button);
|
await userEvent.click(button);
|
||||||
|
await waitFor(async () => {
|
||||||
|
screen.getByRole('tab', {
|
||||||
|
name: /tab query library/i,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const switchToQueryHistory = async () => {
|
||||||
|
const tab = screen.getByRole('tab', {
|
||||||
|
name: /tab query history/i,
|
||||||
|
});
|
||||||
|
await userEvent.click(tab);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addQueryHistoryToQueryLibrary = async () => {
|
||||||
|
const button = withinQueryHistory().getByRole('button', { name: /add to library/i });
|
||||||
|
await userEvent.click(button);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const closeQueryHistory = async () => {
|
export const closeQueryHistory = async () => {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ type SetupOptions = {
|
|||||||
queryHistory?: { queryHistory: Array<Partial<RichHistoryRemoteStorageDTO>>; totalCount: number };
|
queryHistory?: { queryHistory: Array<Partial<RichHistoryRemoteStorageDTO>>; totalCount: number };
|
||||||
urlParams?: ExploreQueryParams;
|
urlParams?: ExploreQueryParams;
|
||||||
prevUsedDatasource?: { orgId: number; datasource: string };
|
prevUsedDatasource?: { orgId: number; datasource: string };
|
||||||
|
failAddToLibrary?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TearDownOptions = {
|
type TearDownOptions = {
|
||||||
|
|||||||
@@ -3,15 +3,30 @@ import { Props } from 'react-virtualized-auto-sizer';
|
|||||||
|
|
||||||
import { EventBusSrv } from '@grafana/data';
|
import { EventBusSrv } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { DataQuery } from '@grafana/schema/dist/esm/veneer/common.types';
|
||||||
|
|
||||||
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
||||||
|
|
||||||
import { assertQueryLibraryTemplateExists } from './helper/assert';
|
import {
|
||||||
import { openQueryLibrary } from './helper/interactions';
|
assertAddToQueryLibraryButtonExists,
|
||||||
|
assertQueryHistory,
|
||||||
|
assertQueryLibraryTemplateExists,
|
||||||
|
} from './helper/assert';
|
||||||
|
import {
|
||||||
|
addQueryHistoryToQueryLibrary,
|
||||||
|
openQueryHistory,
|
||||||
|
openQueryLibrary,
|
||||||
|
switchToQueryHistory,
|
||||||
|
} from './helper/interactions';
|
||||||
import { setupExplore, waitForExplore } from './helper/setup';
|
import { setupExplore, waitForExplore } from './helper/setup';
|
||||||
|
|
||||||
const reportInteractionMock = jest.fn();
|
const reportInteractionMock = jest.fn();
|
||||||
const testEventBus = new EventBusSrv();
|
const testEventBus = new EventBusSrv();
|
||||||
|
testEventBus.publish = jest.fn();
|
||||||
|
|
||||||
|
interface MockQuery extends DataQuery {
|
||||||
|
expr: string;
|
||||||
|
}
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
@@ -55,22 +70,76 @@ jest.mock('react-virtualized-auto-sizer', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setupQueryLibrary() {
|
||||||
|
const mockQuery: MockQuery = { refId: 'TEST', expr: 'TEST' };
|
||||||
|
setupExplore({
|
||||||
|
queryHistory: {
|
||||||
|
queryHistory: [{ datasourceUid: 'loki', queries: [mockQuery] }],
|
||||||
|
totalCount: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousQueryLibraryEnabled: boolean | undefined;
|
||||||
|
let previousQueryHistoryEnabled: boolean;
|
||||||
|
|
||||||
describe('QueryLibrary', () => {
|
describe('QueryLibrary', () => {
|
||||||
silenceConsoleOutput();
|
silenceConsoleOutput();
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
previousQueryLibraryEnabled = config.featureToggles.queryLibrary;
|
||||||
|
previousQueryHistoryEnabled = config.queryHistoryEnabled;
|
||||||
|
|
||||||
config.featureToggles.queryLibrary = true;
|
config.featureToggles.queryLibrary = true;
|
||||||
|
config.queryHistoryEnabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
config.featureToggles.queryLibrary = false;
|
config.featureToggles.queryLibrary = previousQueryLibraryEnabled;
|
||||||
|
config.queryHistoryEnabled = previousQueryHistoryEnabled;
|
||||||
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Load query templates', async () => {
|
it('Load query templates', async () => {
|
||||||
setupExplore();
|
setupQueryLibrary();
|
||||||
await waitForExplore();
|
await waitForExplore();
|
||||||
await openQueryLibrary();
|
await openQueryLibrary();
|
||||||
await assertQueryLibraryTemplateExists('loki', 'Loki Query Template');
|
await assertQueryLibraryTemplateExists('loki', 'Loki Query Template');
|
||||||
await assertQueryLibraryTemplateExists('elastic', 'Elastic Query Template');
|
await assertQueryLibraryTemplateExists('elastic', 'Elastic Query Template');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Shows add to query library button only when the toggle is enabled', async () => {
|
||||||
|
setupQueryLibrary();
|
||||||
|
await waitForExplore();
|
||||||
|
await openQueryLibrary();
|
||||||
|
await switchToQueryHistory();
|
||||||
|
await assertQueryHistory(['{"expr":"TEST"}']);
|
||||||
|
await assertAddToQueryLibraryButtonExists(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Does not show the query library button when the toggle is disabled', async () => {
|
||||||
|
config.featureToggles.queryLibrary = false;
|
||||||
|
setupQueryLibrary();
|
||||||
|
await waitForExplore();
|
||||||
|
await openQueryHistory();
|
||||||
|
await assertQueryHistory(['{"expr":"TEST"}']);
|
||||||
|
await assertAddToQueryLibraryButtonExists(false);
|
||||||
|
config.featureToggles.queryLibrary = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shows a notification when a template is added and hides the add button', async () => {
|
||||||
|
setupQueryLibrary();
|
||||||
|
await waitForExplore();
|
||||||
|
await openQueryLibrary();
|
||||||
|
await switchToQueryHistory();
|
||||||
|
await assertQueryHistory(['{"expr":"TEST"}']);
|
||||||
|
await addQueryHistoryToQueryLibrary();
|
||||||
|
expect(testEventBus.publish).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'alert-success',
|
||||||
|
payload: ['Query template successfully added to the library'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await assertAddToQueryLibraryButtonExists(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||||
|
|
||||||
import { QueryTemplate } from '../types';
|
import { AddQueryTemplateCommand, QueryTemplate } from '../types';
|
||||||
|
|
||||||
import { convertDataQueryResponseToQueryTemplates } from './mappers';
|
import { convertAddQueryTemplateCommandToDataQuerySpec, convertDataQueryResponseToQueryTemplates } from './mappers';
|
||||||
import { baseQuery } from './query';
|
import { baseQuery } from './query';
|
||||||
|
|
||||||
export const queryLibraryApi = createApi({
|
export const queryLibraryApi = createApi({
|
||||||
baseQuery,
|
baseQuery,
|
||||||
|
tagTypes: ['QueryTemplatesList'],
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
allQueryTemplates: builder.query<QueryTemplate[], void>({
|
allQueryTemplates: builder.query<QueryTemplate[], void>({
|
||||||
query: () => undefined,
|
query: () => ({}),
|
||||||
transformResponse: convertDataQueryResponseToQueryTemplates,
|
transformResponse: convertDataQueryResponseToQueryTemplates,
|
||||||
|
providesTags: ['QueryTemplatesList'],
|
||||||
|
}),
|
||||||
|
addQueryTemplate: builder.mutation<QueryTemplate, AddQueryTemplateCommand>({
|
||||||
|
query: (addQueryTemplateCommand) => ({
|
||||||
|
method: 'POST',
|
||||||
|
data: convertAddQueryTemplateCommandToDataQuerySpec(addQueryTemplateCommand),
|
||||||
|
}),
|
||||||
|
invalidatesTags: ['QueryTemplatesList'],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
reducerPath: 'queryLibrary',
|
reducerPath: 'queryLibrary',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { QueryTemplate } from '../types';
|
import { AddQueryTemplateCommand, QueryTemplate } from '../types';
|
||||||
|
|
||||||
import { DataQuerySpecResponse, DataQueryTarget } from './types';
|
import { API_VERSION, QueryTemplateKinds } from './query';
|
||||||
|
import { DataQuerySpec, DataQuerySpecResponse, DataQueryTarget } from './types';
|
||||||
|
|
||||||
export const convertDataQueryResponseToQueryTemplates = (result: DataQuerySpecResponse): QueryTemplate[] => {
|
export const convertDataQueryResponseToQueryTemplates = (result: DataQuerySpecResponse): QueryTemplate[] => {
|
||||||
if (!result.items) {
|
if (!result.items) {
|
||||||
@@ -15,3 +16,24 @@ export const convertDataQueryResponseToQueryTemplates = (result: DataQuerySpecRe
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const convertAddQueryTemplateCommandToDataQuerySpec = (
|
||||||
|
addQueryTemplateCommand: AddQueryTemplateCommand
|
||||||
|
): DataQuerySpec => {
|
||||||
|
const { title, targets } = addQueryTemplateCommand;
|
||||||
|
return {
|
||||||
|
apiVersion: API_VERSION,
|
||||||
|
kind: QueryTemplateKinds.QueryTemplate,
|
||||||
|
metadata: {
|
||||||
|
generateName: 'A' + title,
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
title: title,
|
||||||
|
vars: [], // TODO: Detect variables in #86838
|
||||||
|
targets: targets.map((dataQuery) => ({
|
||||||
|
variables: {},
|
||||||
|
properties: dataQuery,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,25 +1,41 @@
|
|||||||
import { BaseQueryFn } from '@reduxjs/toolkit/query/react';
|
import { BaseQueryFn } from '@reduxjs/toolkit/query/react';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import { getBackendSrv, isFetchError } from '@grafana/runtime/src/services/backendSrv';
|
import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime/src/services/backendSrv';
|
||||||
|
|
||||||
import { DataQuerySpecResponse } from './types';
|
import { DataQuerySpecResponse } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export const API_VERSION = 'peakq.grafana.app/v0alpha1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export enum QueryTemplateKinds {
|
||||||
|
QueryTemplate = 'QueryTemplate',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query Library is an experimental feature. API (including the URL path) will likely change.
|
* Query Library is an experimental feature. API (including the URL path) will likely change.
|
||||||
*
|
*
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export const BASE_URL = '/apis/peakq.grafana.app/v0alpha1/namespaces/default/querytemplates/';
|
export const BASE_URL = `/apis/${API_VERSION}/namespaces/default/querytemplates/`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: similar code is duplicated in many places. To be unified in #86960
|
* TODO: similar code is duplicated in many places. To be unified in #86960
|
||||||
*/
|
*/
|
||||||
export const baseQuery: BaseQueryFn<void, DataQuerySpecResponse, Error> = async () => {
|
export const baseQuery: BaseQueryFn<Pick<BackendSrvRequest, 'data' | 'method'>, DataQuerySpecResponse, Error> = async (
|
||||||
|
requestOptions
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const responseObservable = getBackendSrv().fetch<DataQuerySpecResponse>({
|
const responseObservable = getBackendSrv().fetch<DataQuerySpecResponse>({
|
||||||
url: BASE_URL,
|
url: BASE_URL,
|
||||||
showErrorAlert: true,
|
showErrorAlert: true,
|
||||||
|
method: requestOptions.method || 'GET',
|
||||||
|
data: requestOptions.data,
|
||||||
});
|
});
|
||||||
return await lastValueFrom(responseObservable);
|
return await lastValueFrom(responseObservable);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -7,10 +7,16 @@
|
|||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
import { queryLibraryApi } from './api/factory';
|
import { queryLibraryApi } from './api/factory';
|
||||||
import { mockData } from './api/mocks';
|
import { mockData } from './api/mocks';
|
||||||
|
|
||||||
export const { useAllQueryTemplatesQuery } = queryLibraryApi;
|
export const { useAllQueryTemplatesQuery, useAddQueryTemplateMutation } = queryLibraryApi;
|
||||||
|
|
||||||
|
export function isQueryLibraryEnabled() {
|
||||||
|
return config.featureToggles.queryLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
export const QueryLibraryMocks = {
|
export const QueryLibraryMocks = {
|
||||||
data: mockData,
|
data: mockData,
|
||||||
|
|||||||
@@ -6,3 +6,8 @@ export type QueryTemplate = {
|
|||||||
targets: DataQuery[];
|
targets: DataQuery[];
|
||||||
createdAtTimestamp: number;
|
createdAtTimestamp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AddQueryTemplateCommand = {
|
||||||
|
title: string;
|
||||||
|
targets: DataQuery[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -479,6 +479,7 @@
|
|||||||
"rich-history-card": {
|
"rich-history-card": {
|
||||||
"add-comment-form": "Add comment form",
|
"add-comment-form": "Add comment form",
|
||||||
"add-comment-tooltip": "Add comment",
|
"add-comment-tooltip": "Add comment",
|
||||||
|
"add-to-library": "Add to library",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"confirm-delete": "Delete",
|
"confirm-delete": "Delete",
|
||||||
"copy-query-tooltip": "Copy query to clipboard",
|
"copy-query-tooltip": "Copy query to clipboard",
|
||||||
@@ -493,6 +494,7 @@
|
|||||||
"edit-comment-tooltip": "Edit comment",
|
"edit-comment-tooltip": "Edit comment",
|
||||||
"optional-description": "An optional description of what the query does.",
|
"optional-description": "An optional description of what the query does.",
|
||||||
"query-comment-label": "Query comment",
|
"query-comment-label": "Query comment",
|
||||||
|
"query-template-added": "Query template successfully added to the library",
|
||||||
"query-text-label": "Query text",
|
"query-text-label": "Query text",
|
||||||
"save-comment": "Save comment",
|
"save-comment": "Save comment",
|
||||||
"star-query-tooltip": "Star query",
|
"star-query-tooltip": "Star query",
|
||||||
|
|||||||
@@ -479,6 +479,7 @@
|
|||||||
"rich-history-card": {
|
"rich-history-card": {
|
||||||
"add-comment-form": "Åđđ čőmmęʼnŧ ƒőřm",
|
"add-comment-form": "Åđđ čőmmęʼnŧ ƒőřm",
|
||||||
"add-comment-tooltip": "Åđđ čőmmęʼnŧ",
|
"add-comment-tooltip": "Åđđ čőmmęʼnŧ",
|
||||||
|
"add-to-library": "Åđđ ŧő ľįþřäřy",
|
||||||
"cancel": "Cäʼnčęľ",
|
"cancel": "Cäʼnčęľ",
|
||||||
"confirm-delete": "Đęľęŧę",
|
"confirm-delete": "Đęľęŧę",
|
||||||
"copy-query-tooltip": "Cőpy qūęřy ŧő čľįpþőäřđ",
|
"copy-query-tooltip": "Cőpy qūęřy ŧő čľįpþőäřđ",
|
||||||
@@ -493,6 +494,7 @@
|
|||||||
"edit-comment-tooltip": "Ēđįŧ čőmmęʼnŧ",
|
"edit-comment-tooltip": "Ēđįŧ čőmmęʼnŧ",
|
||||||
"optional-description": "Åʼn őpŧįőʼnäľ đęşčřįpŧįőʼn őƒ ŵĥäŧ ŧĥę qūęřy đőęş.",
|
"optional-description": "Åʼn őpŧįőʼnäľ đęşčřįpŧįőʼn őƒ ŵĥäŧ ŧĥę qūęřy đőęş.",
|
||||||
"query-comment-label": "Qūęřy čőmmęʼnŧ",
|
"query-comment-label": "Qūęřy čőmmęʼnŧ",
|
||||||
|
"query-template-added": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy äđđęđ ŧő ŧĥę ľįþřäřy",
|
||||||
"query-text-label": "Qūęřy ŧęχŧ",
|
"query-text-label": "Qūęřy ŧęχŧ",
|
||||||
"save-comment": "Ŝävę čőmmęʼnŧ",
|
"save-comment": "Ŝävę čőmmęʼnŧ",
|
||||||
"star-query-tooltip": "Ŝŧäř qūęřy",
|
"star-query-tooltip": "Ŝŧäř qūęřy",
|
||||||
|
|||||||
Reference in New Issue
Block a user