Explore: Allow saving to Query Library from query row (#89381)

* wip

* Add save functionality to query row

* add success conditional

* move around translations

* Add translations

* Add key to fix test

* Add key to the right spot

* define specific save button

* WIP - Use RowActionComponents to add action without modifying the core component

* Only add component once on render

* Move logic to main explore page

* Add keyed render actions to prevent redundancy, use this to add keyed action

* Overcome the forces of dayquil to attempt to make actual sense

* Add scoped actions to query action component

* Spaces not allowed in generateName
This commit is contained in:
Kristina 2024-07-30 13:56:44 -05:00 committed by GitHub
parent e7156e7e60
commit 783ff71560
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 157 additions and 59 deletions

View File

@ -3951,10 +3951,6 @@ exports[`better eslint`] = {
"public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx:5381": [ "public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"] [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
], ],
"public/app/features/explore/RichHistory/RichHistoryAddToLibraryForm.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
],
"public/app/features/explore/RichHistory/RichHistoryCard.tsx:5381": [ "public/app/features/explore/RichHistory/RichHistoryCard.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "Styles should be written using objects.", "1"],

View File

@ -1,22 +1,27 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { CoreApp, GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { ErrorBoundaryAlert, useStyles2, useTheme2 } from '@grafana/ui'; import { DataQuery } from '@grafana/schema/dist/esm/index';
import { ErrorBoundaryAlert, Modal, useStyles2, useTheme2 } from '@grafana/ui';
import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction';
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
import { useNavModel } from 'app/core/hooks/useNavModel'; import { useNavModel } from 'app/core/hooks/useNavModel';
import { Trans } from 'app/core/internationalization'; import { Trans, t } from 'app/core/internationalization';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { useSelector } from 'app/types'; import { useSelector } from 'app/types';
import { ExploreQueryParams } from 'app/types/explore'; import { ExploreQueryParams } from 'app/types/explore';
import { RowActionComponents } from '../query/components/QueryActionComponent';
import { CorrelationEditorModeBar } from './CorrelationEditorModeBar'; import { CorrelationEditorModeBar } from './CorrelationEditorModeBar';
import { ExploreActions } from './ExploreActions'; import { ExploreActions } from './ExploreActions';
import { ExploreDrawer } from './ExploreDrawer'; import { ExploreDrawer } from './ExploreDrawer';
import { ExplorePaneContainer } from './ExplorePaneContainer'; import { ExplorePaneContainer } from './ExplorePaneContainer';
import { QueriesDrawerContextProvider, useQueriesDrawerContext } from './QueriesDrawer/QueriesDrawerContext'; import { QueriesDrawerContextProvider, useQueriesDrawerContext } from './QueriesDrawer/QueriesDrawerContext';
import { AddToLibraryForm } from './QueryLibrary/AddToLibraryForm';
import RichHistoryContainer from './RichHistory/RichHistoryContainer'; import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import { useExplorePageTitle } from './hooks/useExplorePageTitle'; import { useExplorePageTitle } from './hooks/useExplorePageTitle';
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
@ -26,6 +31,7 @@ import { useTimeSrvFix } from './hooks/useTimeSrvFix';
import { isSplit, selectCorrelationDetails, selectPanesEntries } from './state/selectors'; import { isSplit, selectCorrelationDetails, selectPanesEntries } from './state/selectors';
const MIN_PANE_WIDTH = 200; const MIN_PANE_WIDTH = 200;
const QUERY_LIBRARY_ACTION_KEY = 'queryLibraryAction';
export default function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { export default function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
return ( return (
@ -55,6 +61,7 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
const correlationDetails = useSelector(selectCorrelationDetails); const correlationDetails = useSelector(selectCorrelationDetails);
const { drawerOpened, setDrawerOpened, queryLibraryAvailable } = useQueriesDrawerContext(); const { drawerOpened, setDrawerOpened, queryLibraryAvailable } = useQueriesDrawerContext();
const showCorrelationEditorBar = config.featureToggles.correlations && (correlationDetails?.editorMode || false); const showCorrelationEditorBar = config.featureToggles.correlations && (correlationDetails?.editorMode || false);
const [queryToAdd, setQueryToAdd] = useState<DataQuery | undefined>();
useEffect(() => { useEffect(() => {
//This is needed for breadcrumbs and topnav. //This is needed for breadcrumbs and topnav.
@ -64,6 +71,25 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
}); });
}, [chrome, navModel]); }, [chrome, navModel]);
useEffect(() => {
const hasQueryLibrary = config.featureToggles.queryLibrary || false;
if (hasQueryLibrary) {
RowActionComponents.addKeyedExtraRenderAction(QUERY_LIBRARY_ACTION_KEY, {
scope: CoreApp.Explore,
queryActionComponent: (props) => (
<QueryOperationAction
key={props.key}
title={t('query-operation.header.save-to-query-library', 'Save to query library')}
icon="save"
onClick={() => {
setQueryToAdd(props.query);
}}
/>
),
});
}
}, []);
useKeyboardShortcuts(); useKeyboardShortcuts();
return ( return (
@ -105,6 +131,23 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
/> />
</ExploreDrawer> </ExploreDrawer>
)} )}
<Modal
title={t('explore.add-to-library-modal.title', 'Add query to Query Library')}
isOpen={queryToAdd !== undefined}
onDismiss={() => setQueryToAdd(undefined)}
>
<AddToLibraryForm
onCancel={() => {
setQueryToAdd(undefined);
}}
onSave={(isSuccess) => {
if (isSuccess) {
setQueryToAdd(undefined);
}
}}
query={queryToAdd!}
/>
</Modal>
</div> </div>
); );
} }

View File

@ -1,19 +1,22 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { DataSourcePicker } from '@grafana/runtime'; import { AppEvents, dateTime } from '@grafana/data';
import { DataSourcePicker, getAppEvents } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema'; import { DataQuery } from '@grafana/schema';
import { Button, InlineSwitch, Modal, RadioButtonGroup, TextArea } from '@grafana/ui'; import { Button, InlineSwitch, Modal, RadioButtonGroup, TextArea } from '@grafana/ui';
import { Field } from '@grafana/ui/'; import { Field } from '@grafana/ui/';
import { Input } from '@grafana/ui/src/components/Input/Input'; import { Input } from '@grafana/ui/src/components/Input/Input';
import { t } from 'app/core/internationalization'; 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 { getQueryDisplayText } from '../../../core/utils/richHistory';
import { useDatasource } from '../QueryLibrary/utils/useDatasource'; import { useDatasource } from '../QueryLibrary/utils/useDatasource';
type Props = { type Props = {
onCancel: () => void; onCancel: () => void;
onSave: (details: QueryDetails) => void; onSave: (isSuccess: boolean) => void;
query: DataQuery; query: DataQuery;
}; };
@ -22,8 +25,8 @@ export type QueryDetails = {
}; };
const VisibilityOptions = [ const VisibilityOptions = [
{ value: 'Public', label: 'Public' }, { value: 'Public', label: t('explore.query-library.public', 'Public') },
{ value: 'Private', label: 'Private' }, { value: 'Private', label: t('explore.query-library.private', 'Private') },
]; ];
const info = t( const info = t(
@ -31,17 +34,47 @@ const info = t(
`You're about to save this query. Once saved, you can easily access it in the Query Library tab for future use and reference.` `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 RichHistoryAddToLibraryForm = ({ onCancel, onSave, query }: Props) => { export const AddToLibraryForm = ({ onCancel, onSave, query }: Props) => {
const { register, handleSubmit } = useForm<QueryDetails>(); 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 datasource = useDatasource(query.datasource);
const displayText = useMemo(() => { const displayText = useMemo(() => {
return datasource?.getQueryDisplayText?.(query) || getQueryDisplayText(query); return datasource?.getQueryDisplayText?.(query) || getQueryDisplayText(query);
}, [datasource, query]); }, [datasource, query]);
const onSubmit = (data: QueryDetails) => { const onSubmit = async (data: QueryDetails) => {
onSave(data); 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 ( return (
@ -72,10 +105,10 @@ export const RichHistoryAddToLibraryForm = ({ onCancel, onSave, query }: Props)
/> />
<Modal.ButtonRow> <Modal.ButtonRow>
<Button variant="secondary" onClick={() => onCancel()} fill="outline"> <Button variant="secondary" onClick={() => onCancel()} fill="outline">
Cancel <Trans i18nKey="explore.query-library.cancel">Cancel</Trans>
</Button> </Button>
<Button variant="primary" type="submit"> <Button variant="primary" type="submit">
Save <Trans i18nKey="explore.query-library.save">Save</Trans>
</Button> </Button>
</Modal.ButtonRow> </Modal.ButtonRow>
</form> </form>

View File

@ -1,14 +1,11 @@
import { t } from 'i18next'; import { t } from 'i18next';
import { useState } from 'react'; import { useState } from 'react';
import { AppEvents, dateTime } from '@grafana/data';
import { getAppEvents } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema'; import { DataQuery } from '@grafana/schema';
import { Button, Modal } from '@grafana/ui'; import { Button, Modal } from '@grafana/ui';
import { isQueryLibraryEnabled, useAddQueryTemplateMutation } from 'app/features/query-library'; import { isQueryLibraryEnabled } from 'app/features/query-library';
import { AddQueryTemplateCommand } from 'app/features/query-library/types';
import { QueryDetails, RichHistoryAddToLibraryForm } from './RichHistoryAddToLibraryForm'; import { AddToLibraryForm } from '../QueryLibrary/AddToLibraryForm';
type Props = { type Props = {
query: DataQuery; query: DataQuery;
@ -16,29 +13,11 @@ type Props = {
export const RichHistoryAddToLibrary = ({ query }: Props) => { export const RichHistoryAddToLibrary = ({ query }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [addQueryTemplate, { isSuccess }] = useAddQueryTemplateMutation(); const [hasBeenSaved, setHasBeenSaved] = useState(false);
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'); const buttonLabel = t('explore.rich-history-card.add-to-library', 'Add to library');
const submit = (data: QueryDetails) => { return isQueryLibraryEnabled() && !hasBeenSaved ? (
const timestamp = dateTime().toISOString();
const temporaryDefaultTitle = data.description || `Imported from Explore - ${timestamp}`;
handleAddQueryTemplate({ title: temporaryDefaultTitle, targets: [query] });
};
return isQueryLibraryEnabled() && !isSuccess ? (
<> <>
<Button variant="secondary" aria-label={buttonLabel} onClick={() => setIsOpen(true)}> <Button variant="secondary" aria-label={buttonLabel} onClick={() => setIsOpen(true)}>
{buttonLabel} {buttonLabel}
@ -48,12 +27,14 @@ export const RichHistoryAddToLibrary = ({ query }: Props) => {
isOpen={isOpen} isOpen={isOpen}
onDismiss={() => setIsOpen(false)} onDismiss={() => setIsOpen(false)}
> >
<RichHistoryAddToLibraryForm <AddToLibraryForm
onCancel={() => setIsOpen(() => false)} onCancel={() => setIsOpen(() => false)}
query={query} query={query}
onSave={(data) => { onSave={(isSuccess) => {
submit(data); if (isSuccess) {
setIsOpen(false); setIsOpen(false);
setHasBeenSaved(true);
}
}} }}
/> />
</Modal> </Modal>

View File

@ -59,7 +59,7 @@ export const submitAddToQueryLibrary = async ({ description }: { description: st
const input = within(screen.getByRole('dialog')).getByLabelText('Description'); const input = within(screen.getByRole('dialog')).getByLabelText('Description');
await userEvent.type(input, description); await userEvent.type(input, description);
const saveButton = screen.getByRole('button', { const saveButton = screen.getByRole('button', {
name: /save/i, name: /^save$/i,
}); });
await userEvent.click(saveButton); await userEvent.click(saveButton);
}; };

View File

@ -48,7 +48,7 @@ export const convertAddQueryTemplateCommandToDataQuerySpec = (
apiVersion: API_VERSION, apiVersion: API_VERSION,
kind: QueryTemplateKinds.QueryTemplate, kind: QueryTemplateKinds.QueryTemplate,
metadata: { metadata: {
generateName: 'A' + title, generateName: 'A' + title.replaceAll(' ', '-'),
}, },
spec: { spec: {
title: title, title: title,

View File

@ -1,4 +1,4 @@
import { DataQuery, DataSourceInstanceSettings, TimeRange } from '@grafana/data'; import { CoreApp, DataQuery, DataSourceInstanceSettings, TimeRange } from '@grafana/data';
interface ActionComponentProps { interface ActionComponentProps {
query?: DataQuery; query?: DataQuery;
@ -10,18 +10,41 @@ interface ActionComponentProps {
key: string | number; key: string | number;
} }
type QueryActionComponent = (props: ActionComponentProps) => JSX.Element | null; export type QueryActionComponent = (props: ActionComponentProps) => JSX.Element | null;
type ScopedQueryActionComponent = {
scope: CoreApp;
queryActionComponent: QueryActionComponent;
};
class QueryActionComponents { class QueryActionComponents {
extraRenderActions: QueryActionComponent[] = []; extraRenderActions: QueryActionComponent[] = [];
/* additional actions added in core grafana are likely to be needed in only one kind of app,
and the add function may be ran multiple times by the component so it is keyed to ensure uniqueness
*/
keyedScopedExtraRenderActions: Map<string, ScopedQueryActionComponent> = new Map();
addExtraRenderAction(extra: QueryActionComponent) { addExtraRenderAction(extra: QueryActionComponent) {
this.extraRenderActions = this.extraRenderActions.concat(extra); this.extraRenderActions = this.extraRenderActions.concat(extra);
} }
// for adding actions that will need to be unique, even if the add function is ran multiple times
addKeyedExtraRenderAction(key: string, extra: ScopedQueryActionComponent) {
this.keyedScopedExtraRenderActions.set(key, extra);
}
// only returns actions that are not scoped to a specific CoreApp
getAllExtraRenderAction(): QueryActionComponent[] { getAllExtraRenderAction(): QueryActionComponent[] {
return this.extraRenderActions; return this.extraRenderActions;
} }
getScopedExtraRenderAction(scope: CoreApp): QueryActionComponent[] {
const scopedActions = Array.from(this.keyedScopedExtraRenderActions, (value) => value[1]).filter(
(sra) => sra.scope === scope
);
return scopedActions.map((sa) => sa.queryActionComponent);
}
} }
/** /**

View File

@ -40,7 +40,7 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { RowActionComponents } from './QueryActionComponent'; import { QueryActionComponent, RowActionComponents } from './QueryActionComponent';
import { QueryEditorRowHeader } from './QueryEditorRowHeader'; import { QueryEditorRowHeader } from './QueryEditorRowHeader';
import { QueryErrorAlert } from './QueryErrorAlert'; import { QueryErrorAlert } from './QueryErrorAlert';
@ -425,9 +425,17 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
}; };
renderExtraActions = () => { renderExtraActions = () => {
const { query, queries, data, onAddQuery, dataSource } = this.props; const { query, queries, data, onAddQuery, dataSource, app } = this.props;
const extraActions = RowActionComponents.getAllExtraRenderAction() const unscopedActions = RowActionComponents.getAllExtraRenderAction();
let scopedActions: QueryActionComponent[] = [];
if (app !== undefined) {
scopedActions = RowActionComponents.getScopedExtraRenderAction(app);
}
const extraActions = [...unscopedActions, ...scopedActions]
.map((action, index) => .map((action, index) =>
action({ action({
query, query,

View File

@ -660,10 +660,17 @@
"stop-scan": "Stop scan" "stop-scan": "Stop scan"
}, },
"query-library": { "query-library": {
"cancel": "Cancel",
"default-description": "Public",
"delete-query": "Delete query", "delete-query": "Delete query",
"delete-query-text": "You're about to remove this query from the query library. This action cannot be undone. Do you want to continue?", "delete-query-text": "You're about to remove this query from the query library. This action cannot be undone. Do you want to continue?",
"delete-query-title": "Delete query", "delete-query-title": "Delete query",
"query-deleted": "Query deleted" "private": "Private",
"public": "Public",
"query-deleted": "Query deleted",
"query-template-added": "Query template successfully added to the library",
"query-template-error": "Error attempting to add this query to the library",
"save": "Save"
}, },
"rich-history": { "rich-history": {
"close-tooltip": "Close query history", "close-tooltip": "Close query history",
@ -694,7 +701,6 @@
"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",
@ -1787,6 +1793,7 @@
"expand-row": "Expand query row", "expand-row": "Expand query row",
"hide-response": "Hide response", "hide-response": "Hide response",
"remove-query": "Remove query", "remove-query": "Remove query",
"save-to-query-library": "Save to query library",
"show-response": "Show response", "show-response": "Show response",
"toggle-edit-mode": "Toggle text edit mode" "toggle-edit-mode": "Toggle text edit mode"
}, },

View File

@ -660,10 +660,17 @@
"stop-scan": "Ŝŧőp şčäʼn" "stop-scan": "Ŝŧőp şčäʼn"
}, },
"query-library": { "query-library": {
"cancel": "Cäʼnčęľ",
"default-description": "Pūþľįč",
"delete-query": "Đęľęŧę qūęřy", "delete-query": "Đęľęŧę qūęřy",
"delete-query-text": "Ÿőū'řę äþőūŧ ŧő řęmővę ŧĥįş qūęřy ƒřőm ŧĥę qūęřy ľįþřäřy. Ŧĥįş äčŧįőʼn čäʼnʼnőŧ þę ūʼnđőʼnę. Đő yőū ŵäʼnŧ ŧő čőʼnŧįʼnūę?", "delete-query-text": "Ÿőū'řę äþőūŧ ŧő řęmővę ŧĥįş qūęřy ƒřőm ŧĥę qūęřy ľįþřäřy. Ŧĥįş äčŧįőʼn čäʼnʼnőŧ þę ūʼnđőʼnę. Đő yőū ŵäʼnŧ ŧő čőʼnŧįʼnūę?",
"delete-query-title": "Đęľęŧę qūęřy", "delete-query-title": "Đęľęŧę qūęřy",
"query-deleted": "Qūęřy đęľęŧęđ" "private": "Přįväŧę",
"public": "Pūþľįč",
"query-deleted": "Qūęřy đęľęŧęđ",
"query-template-added": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy äđđęđ ŧő ŧĥę ľįþřäřy",
"query-template-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő äđđ ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy",
"save": "Ŝävę"
}, },
"rich-history": { "rich-history": {
"close-tooltip": "Cľőşę qūęřy ĥįşŧőřy", "close-tooltip": "Cľőşę qūęřy ĥįşŧőřy",
@ -694,7 +701,6 @@
"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",
@ -1787,6 +1793,7 @@
"expand-row": "Ēχpäʼnđ qūęřy řőŵ", "expand-row": "Ēχpäʼnđ qūęřy řőŵ",
"hide-response": "Ħįđę řęşpőʼnşę", "hide-response": "Ħįđę řęşpőʼnşę",
"remove-query": "Ŗęmővę qūęřy", "remove-query": "Ŗęmővę qūęřy",
"save-to-query-library": "Ŝävę ŧő qūęřy ľįþřäřy",
"show-response": "Ŝĥőŵ řęşpőʼnşę", "show-response": "Ŝĥőŵ řęşpőʼnşę",
"toggle-edit-mode": "Ŧőģģľę ŧęχŧ ęđįŧ mőđę" "toggle-edit-mode": "Ŧőģģľę ŧęχŧ ęđįŧ mőđę"
}, },