From b9034f413ef0b4b981f97da1c3f5f107cd4c7c16 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 14 Feb 2025 14:44:47 +0000 Subject: [PATCH] Query library: Refactor to use `onSelectQuery` callback (#100360) * starting to refactor query library to use callback * replace QueryActionButton with onSelectQuery * hook up properly in explore * fix unit tests * i18n * extract types * fix refId in explore * fix unit tests * handle changing datasource to mixed * enrich queries with datasource * move out into separate function * filter out expression datasources --- .../PanelDataPane/PanelDataQueriesTab.tsx | 68 +++++----- public/app/features/explore/Explore.test.tsx | 5 + public/app/features/explore/Explore.tsx | 28 +++- .../app/features/explore/ExploreToolbar.tsx | 32 ++--- .../QueriesDrawer/QueriesDrawerDropdown.tsx | 120 ------------------ .../QueryLibrary/QueryLibraryContext.tsx | 8 +- .../features/explore/QueryLibrary/mocks.tsx | 25 ++++ .../features/explore/QueryLibrary/types.ts | 10 +- .../explore/SecondaryActions.test.tsx | 28 +++- .../app/features/explore/SecondaryActions.tsx | 65 ++++++++-- .../explore/spec/helper/interactions.ts | 2 +- public/locales/en-US/grafana.json | 5 +- public/locales/pseudo-LOCALE/grafana.json | 5 +- 13 files changed, 188 insertions(+), 213 deletions(-) delete mode 100644 public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx create mode 100644 public/app/features/explore/QueryLibrary/mocks.tsx diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx index 66f83b97bd5..f669ebeeb98 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -13,7 +13,7 @@ import { } from '@grafana/scenes'; import { DataQuery } from '@grafana/schema'; import { Button, Stack, Tab } from '@grafana/ui'; -import { t, Trans } from 'app/core/internationalization'; +import { Trans } from 'app/core/internationalization'; import { addQuery } from 'app/core/utils/query'; import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard'; import { storeLastUsedDataSourceInLocalStorage } from 'app/features/datasources/components/picker/utils'; @@ -25,8 +25,10 @@ import { updateQueries } from 'app/features/query/state/updateQueries'; import { isSharedDashboardQuery } from 'app/plugins/datasource/dashboard/runSharedRequest'; import { QueryGroupOptions } from 'app/types'; +import { MIXED_DATASOURCE_NAME } from '../../../../plugins/datasource/mixed/MixedDataSource'; import { useQueryLibraryContext } from '../../../explore/QueryLibrary/QueryLibraryContext'; -import { QueryActionButtonProps } from '../../../explore/QueryLibrary/types'; +import { ExpressionDatasourceUID } from '../../../expressions/types'; +import { getDatasourceSrv } from '../../../plugins/datasource_srv'; import { PanelTimeRange } from '../../scene/PanelTimeRange'; import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../../utils/utils'; import { getUpdatedHoverHeader } from '../getPanelFrameOptions'; @@ -315,13 +317,35 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps { - for (const query of queries) { - model.onQueriesChange(addQuery(model.getQueries(), query)); + const onSelectQueryFromLibrary = async (query: DataQuery) => { + // ensure all queries explicitly define a datasource + const enrichedQueries = queries.map((q) => + q.datasource + ? q + : { + ...q, + datasource: datasource.getRef(), + } + ); + const newQueries = addQuery(enrichedQueries, query); + model.onQueriesChange(newQueries); + if (query.datasource?.uid) { + const uniqueDatasources = new Set( + newQueries.map((q) => q.datasource?.uid).filter((uid) => uid !== ExpressionDatasourceUID) + ); + const isMixed = uniqueDatasources.size > 1; + const newDatasourceRef = { + uid: isMixed ? MIXED_DATASOURCE_NAME : query.datasource.uid, + }; + const shouldChangeDatasource = datasource.uid !== newDatasourceRef.uid; + if (shouldChangeDatasource) { + const newDatasource = getDatasourceSrv().getInstanceSettings(newDatasourceRef); + if (newDatasource) { + await model.onChangeDataSource(newDatasource); + } + } } - }); + }; return (
@@ -358,9 +382,9 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps { - openQueryLibraryDrawer(getDatasourceNames(datasource, queries), addQueryActionButton); - }} + onClick={() => + openQueryLibraryDrawer(getDatasourceNames(datasource, queries), onSelectQueryFromLibrary) + } variant="secondary" data-testid={selectors.components.QueryTab.addQuery} > @@ -385,28 +409,6 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps void) { - return function AddQueryFromLibraryButton(props: QueryActionButtonProps) { - const label = t('dashboards.query-library.add-query-button', 'Add query'); - return ( - - ); - }; -} - function getDatasourceNames(datasource: DataSourceApi, queries: DataQuery[]): string[] { if (datasource.uid === '-- Mixed --') { // If datasource is mixed, the datasource UID is on the query. Here we map the UIDs to datasource names. diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index 92e5204d742..9fd5ba17ab8 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -103,6 +103,11 @@ const dummyProps: Props = { setSupplementaryQueryEnabled: jest.fn(), correlationEditorDetails: undefined, correlationEditorHelperData: undefined, + exploreActiveDS: { + exploreToDS: [], + dsToExplore: [], + }, + changeDatasource: jest.fn(), }; jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index de38f8019fa..28ad370648e 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -8,6 +8,7 @@ import { AbsoluteTimeRange, DataFrame, EventBus, + getNextRefId, GrafanaTheme2, hasToggleableQueryFiltersSupport, LoadingState, @@ -54,6 +55,7 @@ import { ResponseErrorContainer } from './ResponseErrorContainer'; import { SecondaryActions } from './SecondaryActions'; import TableContainer from './Table/TableContainer'; import { TraceViewContainer } from './TraceView/TraceViewContainer'; +import { changeDatasource } from './state/datasource'; import { changeSize } from './state/explorePane'; import { splitOpen } from './state/main'; import { @@ -65,7 +67,7 @@ import { setQueries, setSupplementaryQueryEnabled, } from './state/query'; -import { isSplit } from './state/selectors'; +import { isSplit, selectExploreDSMaps } from './state/selectors'; import { updateTimeRange } from './state/time'; const getStyles = (theme: GrafanaTheme2) => { @@ -605,6 +607,28 @@ export class Explore extends PureComponent { queryInspectorButtonActive={showQueryInspector} onClickAddQueryRowButton={this.onClickAddQueryRowButton} onClickQueryInspectorButton={() => setShowQueryInspector(!showQueryInspector)} + onSelectQueryFromLibrary={async (query) => { + const { changeDatasource, queries, setQueries } = this.props; + const newQueries = [ + ...queries, + { + ...query, + refId: getNextRefId(queries), + }, + ]; + setQueries(exploreId, newQueries); + if (query.datasource?.uid) { + const uniqueDatasources = new Set(newQueries.map((q) => q.datasource?.uid)); + const isMixed = uniqueDatasources.size > 1; + const newDatasourceRef = { + uid: isMixed ? MIXED_DATASOURCE_NAME : query.datasource.uid, + }; + const shouldChangeDatasource = datasourceInstance.uid !== newDatasourceRef.uid; + if (shouldChangeDatasource) { + await changeDatasource({ exploreId, datasource: newDatasourceRef }); + } + } + }} /> @@ -716,10 +740,12 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { showLogsSample, correlationEditorHelperData, correlationEditorDetails: explore.correlationEditorDetails, + exploreActiveDS: selectExploreDSMaps(state), }; } const mapDispatchToProps = { + changeDatasource, changeSize, modifyQueries, scanStart, diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 9dd6dcd5770..d35dab34118 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -28,8 +28,6 @@ import { getFiscalYearStartMonth, getTimeZone } from '../profile/state/selectors import { ExploreTimeControls } from './ExploreTimeControls'; import { LiveTailButton } from './LiveTailButton'; import { useQueriesDrawerContext } from './QueriesDrawer/QueriesDrawerContext'; -import { QueriesDrawerDropdown } from './QueriesDrawer/QueriesDrawerDropdown'; -import { useQueryLibraryContext } from './QueryLibrary/QueryLibraryContext'; import { ShortLinkButtonMenu } from './ShortLinkButtonMenu'; import { ToolbarExtensionPoint } from './extensions/ToolbarExtensionPoint'; import { changeDatasource } from './state/datasource'; @@ -93,7 +91,6 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle const isCorrelationsEditorMode = correlationDetails?.editorMode || false; const isLeftPane = useSelector(isLeftPaneSelector(exploreId)); const { drawerOpened, setDrawerOpened } = useQueriesDrawerContext(); - const { queryLibraryEnabled } = useQueryLibraryContext(); const shouldRotateSplitIcon = useMemo( () => (isLeftPane && isLargerPane) || (!isLeftPane && !isLargerPane), @@ -206,23 +203,18 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle dispatch(changeRefreshInterval({ exploreId, refreshInterval })); }; - const navBarActions = []; - - if (queryLibraryEnabled) { - navBarActions.unshift(); - } else { - navBarActions.unshift( - setDrawerOpened(!drawerOpened)} - data-testid={Components.QueryTab.queryHistoryButton} - icon="history" - > - Query history - - ); - } + const navBarActions = [ + setDrawerOpened(!drawerOpened)} + data-testid={Components.QueryTab.queryHistoryButton} + icon="history" + > + Query history + , + , + ]; return (
diff --git a/public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx b/public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx deleted file mode 100644 index d800aaf564a..00000000000 --- a/public/app/features/explore/QueriesDrawer/QueriesDrawerDropdown.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { css } from '@emotion/css'; -import { ComponentProps, useState } from 'react'; - -import { Button, ButtonGroup, Dropdown, Menu, ToolbarButton } from '@grafana/ui'; -import { useStyles2 } from '@grafana/ui/'; -import { t } from 'app/core/internationalization'; - -import { createDatasourcesList } from '../../../core/utils/richHistory'; -import { useSelector } from '../../../types'; -import ExploreRunQueryButton from '../ExploreRunQueryButton'; -import { useQueryLibraryContext } from '../QueryLibrary/QueryLibraryContext'; -import { QueryActionButton } from '../QueryLibrary/types'; -import { selectExploreDSMaps } from '../state/selectors'; - -import { useQueriesDrawerContext } from './QueriesDrawerContext'; -import { i18n } from './utils'; - -// This makes TS happy as ExploreRunQueryButton has optional onClick prop while QueryActionButton doesn't -// in addition to map the rootDatasourceUid prop. -function ExploreRunQueryButtonWrapper(props: ComponentProps) { - return ; -} - -type Props = { - variant: 'compact' | 'full'; -}; - -/** - * Dropdown button that can either open a Query History drawer or a Query Library drawer. - * @param variant - * @constructor - */ -export function QueriesDrawerDropdown({ variant }: Props) { - const { drawerOpened, setDrawerOpened } = useQueriesDrawerContext(); - - const { - openDrawer: openQueryLibraryDrawer, - closeDrawer: closeQueryLibraryDrawer, - isDrawerOpen: isQueryLibraryDrawerOpen, - queryLibraryEnabled, - } = useQueryLibraryContext(); - - const [queryOption, setQueryOption] = useState<'library' | 'history'>('library'); - - const exploreActiveDS = useSelector(selectExploreDSMaps); - - const styles = useStyles2(getStyles); - - // In case query library is not enabled we show only simple button for query history in the parent. - if (!queryLibraryEnabled) { - return undefined; - } - - function toggleRichHistory() { - setQueryOption('history'); - setDrawerOpened(!drawerOpened); - } - - function toggleQueryLibrary() { - setQueryOption('library'); - if (isQueryLibraryDrawerOpen) { - closeQueryLibraryDrawer(); - } else { - // Prefill the query library filter with the dataSource. - // Get current dataSource that is open. As this is only used in Explore we get it from Explore state. - const listOfDatasources = createDatasourcesList(); - const activeDatasources = exploreActiveDS.dsToExplore - .map((eDs) => { - return listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name; - }) - .filter((name): name is string => !!name); - - openQueryLibraryDrawer(activeDatasources, ExploreRunQueryButtonWrapper); - } - } - - const menu = ( - - toggleQueryLibrary()} /> - toggleRichHistory()} /> - - ); - - const buttonLabel = queryOption === 'library' ? i18n.queryLibrary : i18n.queryHistory; - const toggle = queryOption === 'library' ? toggleQueryLibrary : toggleRichHistory; - - return ( - - toggle()} - aria-label={buttonLabel} - > - {variant === 'full' ? buttonLabel : undefined} - - - {/* Show either a drops down button so that user can select QL or QH, or show a close button if one of them is - already open.*/} - {drawerOpened || isQueryLibraryDrawerOpen ? ( - - ) : ( - - - - )} - - ); -} - -const getStyles = () => ({ - toggle: css({ width: '36px' }), - // tweaking icon position so it's nicely aligned when dropdown turns into a close button - close: css({ width: '36px', '> svg': { position: 'relative', left: 2 } }), -}); diff --git a/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx index 0e9fc414375..d5b0a6d77b3 100644 --- a/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx +++ b/public/app/features/explore/QueryLibrary/QueryLibraryContext.tsx @@ -2,7 +2,7 @@ import { createContext, ReactNode, useContext } from 'react'; import { DataQuery } from '@grafana/schema'; -import { QueryActionButton } from './types'; +import { OnSelectQueryType } from './types'; /** * Context with state and action to interact with Query Library. The Query Library feature consists of a drawer @@ -20,11 +20,7 @@ export type QueryLibraryContextType = { * @param options.context Used for tracking. Should identify the context this is called from, like 'explore' or * 'dashboard'. */ - openDrawer: ( - datasourceFilters: string[], - queryActionButton: QueryActionButton, - options?: { context?: string } - ) => void; + openDrawer: (datasourceFilters: string[], onSelectQuery: OnSelectQueryType, options?: { context?: string }) => void; closeDrawer: () => void; isDrawerOpen: boolean; diff --git a/public/app/features/explore/QueryLibrary/mocks.tsx b/public/app/features/explore/QueryLibrary/mocks.tsx new file mode 100644 index 00000000000..c59911e7236 --- /dev/null +++ b/public/app/features/explore/QueryLibrary/mocks.tsx @@ -0,0 +1,25 @@ +import { PropsWithChildren } from 'react'; + +import { QueryLibraryContext } from './QueryLibraryContext'; + +type Props = { + queryLibraryAvailable?: boolean; +}; + +export function QueryLibraryContextProviderMock(props: PropsWithChildren) { + return ( + + {props.children} + + ); +} diff --git a/public/app/features/explore/QueryLibrary/types.ts b/public/app/features/explore/QueryLibrary/types.ts index b36c7280fe8..50db96feeb0 100644 --- a/public/app/features/explore/QueryLibrary/types.ts +++ b/public/app/features/explore/QueryLibrary/types.ts @@ -1,11 +1,3 @@ -import { ComponentType } from 'react'; - import { DataQuery } from '@grafana/schema'; -export type QueryActionButtonProps = { - queries: DataQuery[]; - datasourceUid?: string; - onClick: () => void; -}; - -export type QueryActionButton = ComponentType; +export type OnSelectQueryType = (query: DataQuery) => void; diff --git a/public/app/features/explore/SecondaryActions.test.tsx b/public/app/features/explore/SecondaryActions.test.tsx index 076d9883f10..64a1fd2a172 100644 --- a/public/app/features/explore/SecondaryActions.test.tsx +++ b/public/app/features/explore/SecondaryActions.test.tsx @@ -1,13 +1,34 @@ -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { noop } from 'lodash'; +import { render } from '../../../test/test-utils'; + import { QueriesDrawerContextProviderMock } from './QueriesDrawer/mocks'; +import { QueryLibraryContextProviderMock } from './QueryLibrary/mocks'; import { SecondaryActions } from './SecondaryActions'; +jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { + return { + getDataSourceSrv: () => ({ + get: () => Promise.resolve({}), + getList: () => [], + getInstanceSettings: () => {}, + }), + }; +}); + describe('SecondaryActions', () => { it('should render component with two buttons', () => { - render(); + render( + + + + ); expect(screen.getByRole('button', { name: /Add query/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Query inspector/i })).toBeInTheDocument(); @@ -21,6 +42,7 @@ describe('SecondaryActions', () => { richHistoryRowButtonHidden={true} onClickAddQueryRowButton={noop} onClickQueryInspectorButton={noop} + onSelectQueryFromLibrary={noop} /> ); @@ -35,6 +57,7 @@ describe('SecondaryActions', () => { addQueryRowButtonDisabled={true} onClickAddQueryRowButton={noop} onClickQueryInspectorButton={noop} + onSelectQueryFromLibrary={noop} /> ); @@ -54,6 +77,7 @@ describe('SecondaryActions', () => { ); diff --git a/public/app/features/explore/SecondaryActions.tsx b/public/app/features/explore/SecondaryActions.tsx index dfa98f92b77..e50bb642d86 100644 --- a/public/app/features/explore/SecondaryActions.tsx +++ b/public/app/features/explore/SecondaryActions.tsx @@ -3,6 +3,14 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { ToolbarButton, useTheme2 } from '@grafana/ui'; import { t, Trans } from 'app/core/internationalization'; +import { useSelector } from 'app/types'; + +import { createDatasourcesList } from '../../core/utils/richHistory'; +import { MIXED_DATASOURCE_NAME } from '../../plugins/datasource/mixed/MixedDataSource'; + +import { useQueryLibraryContext } from './QueryLibrary/QueryLibraryContext'; +import { type OnSelectQueryType } from './QueryLibrary/types'; +import { selectExploreDSMaps } from './state/selectors'; type Props = { addQueryRowButtonDisabled?: boolean; @@ -12,6 +20,7 @@ type Props = { onClickAddQueryRowButton: () => void; onClickQueryInspectorButton: () => void; + onSelectQueryFromLibrary: OnSelectQueryType; }; const getStyles = (theme: GrafanaTheme2) => { @@ -25,27 +34,57 @@ const getStyles = (theme: GrafanaTheme2) => { }; }; -export function SecondaryActions(props: Props) { +export function SecondaryActions({ + addQueryRowButtonDisabled, + addQueryRowButtonHidden, + onClickAddQueryRowButton, + onClickQueryInspectorButton, + onSelectQueryFromLibrary, + queryInspectorButtonActive, +}: Props) { const theme = useTheme2(); const styles = getStyles(theme); + const exploreActiveDS = useSelector(selectExploreDSMaps); + // Prefill the query library filter with the dataSource. + // Get current dataSource that is open. As this is only used in Explore we get it from Explore state. + const listOfDatasources = createDatasourcesList(); + const activeDatasources = exploreActiveDS.dsToExplore + .map((eDs) => { + return listOfDatasources.find((ds) => ds.uid === eDs.datasource?.uid)?.name; + }) + .filter((name): name is string => !!name && name !== MIXED_DATASOURCE_NAME); + + const { queryLibraryEnabled, openDrawer: openQueryLibraryDrawer } = useQueryLibraryContext(); return (
- {!props.addQueryRowButtonHidden && ( - - Add query - + {!addQueryRowButtonHidden && ( + <> + + Add query + + {queryLibraryEnabled && ( + openQueryLibraryDrawer(activeDatasources, onSelectQueryFromLibrary)} + icon="plus" + > + Add query from library + + )} + )} Query inspector diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts index 41b3f41ae73..73e24da2b1c 100644 --- a/public/app/features/explore/spec/helper/interactions.ts +++ b/public/app/features/explore/spec/helper/interactions.ts @@ -40,7 +40,7 @@ export const openQueryHistory = async () => { }; export const openQueryLibrary = async () => { - const button = screen.getByRole('button', { name: 'Query library' }); + const button = screen.getByRole('button', { name: 'Add query from library' }); await userEvent.click(button); await waitFor(async () => { screen.getByRole('tab', { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 58494d7d319..f0a74858ebe 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1287,9 +1287,6 @@ "panel-queries": { "add-query-from-library": "Add query from library" }, - "query-library": { - "add-query-button": "Add query" - }, "settings": { "variables": { "dependencies": { @@ -1371,7 +1368,6 @@ "close-tooltip": "Close query history", "datasource-a-z": "Data source A-Z", "datasource-z-a": "Data source Z-A", - "library-history-dropdown": "Open query library or query history", "newest-first": "Newest first", "oldest-first": "Oldest first", "query-history": "Query history", @@ -1475,6 +1471,7 @@ "switch-datasource-button": "Switch data source and run query" }, "secondary-actions": { + "add-from-query-library": "Add query from library", "query-add-button": "Add query", "query-add-button-aria-label": "Add query", "query-history-button": "Query history", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 9338082cc96..3002080e1b3 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1287,9 +1287,6 @@ "panel-queries": { "add-query-from-library": "Åđđ qūęřy ƒřőm ľįþřäřy" }, - "query-library": { - "add-query-button": "Åđđ qūęřy" - }, "settings": { "variables": { "dependencies": { @@ -1371,7 +1368,6 @@ "close-tooltip": "Cľőşę qūęřy ĥįşŧőřy", "datasource-a-z": "Đäŧä şőūřčę Å-Ż", "datasource-z-a": "Đäŧä şőūřčę Ż-Å", - "library-history-dropdown": "Øpęʼn qūęřy ľįþřäřy őř qūęřy ĥįşŧőřy", "newest-first": "Ńęŵęşŧ ƒįřşŧ", "oldest-first": "Øľđęşŧ ƒįřşŧ", "query-history": "Qūęřy ĥįşŧőřy", @@ -1475,6 +1471,7 @@ "switch-datasource-button": "Ŝŵįŧčĥ đäŧä şőūřčę äʼnđ řūʼn qūęřy" }, "secondary-actions": { + "add-from-query-library": "Åđđ qūęřy ƒřőm ľįþřäřy", "query-add-button": "Åđđ qūęřy", "query-add-button-aria-label": "Åđđ qūęřy", "query-history-button": "Qūęřy ĥįşŧőřy",