diff --git a/.betterer.results b/.betterer.results index c9ce73693f2..e33c3ac4abd 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4701,27 +4701,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] ], - "public/app/features/explore/QueryLibrary/QueryLibraryExpmInfo.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"] - ], - "public/app/features/explore/QueryLibrary/QueryTemplateForm.tsx:5381": [ - [0, 0, 0, "\'@grafana/ui/src/components/Input/Input\' import is restricted from being used by a pattern. Import from the public export instead.", "0"] - ], - "public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "2"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "3"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "4"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "5"] - ], - "public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] - ], "public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index 04c8fb80615..227eaeedeb2 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -1,5 +1,5 @@ import { Action, KBarProvider } from 'kbar'; -import { Component, ComponentType, Fragment } from 'react'; +import { Component, ComponentType, Fragment, ReactNode } from 'react'; import CacheProvider from 'react-inlinesvg/provider'; import { Provider } from 'react-redux'; import { Route, Routes } from 'react-router-dom-v5-compat'; @@ -37,6 +37,11 @@ interface AppWrapperState { /** Used by enterprise */ let bodyRenderHooks: ComponentType[] = []; let pageBanners: ComponentType[] = []; +const enterpriseProviders: Array> = []; + +export function addEnterpriseProviders(provider: ComponentType<{ children: ReactNode }>) { + enterpriseProviders.push(provider); +} export function addBodyRenderHook(fn: ComponentType) { bodyRenderHooks.push(fn); @@ -100,6 +105,7 @@ export class AppWrapper extends Component { routes: ready && this.renderRoutes(), pageBanners, bodyRenderHooks, + providers: enterpriseProviders, }; const MaybeTimeRangeProvider = config.featureToggles.timeRangeProvider ? TimeRangeProvider : Fragment; 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 cef20ef129f..66f83b97bd5 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx @@ -309,7 +309,7 @@ export class PanelDataQueriesTab extends SceneObjectBase) { const { datasource, dsSettings } = model.useState(); const { data, queries } = model.queryRunner.useState(); - const { openDrawer: openQueryLibraryDrawer } = useQueryLibraryContext(); + const { openDrawer: openQueryLibraryDrawer, queryLibraryEnabled } = useQueryLibraryContext(); if (!datasource || !dsSettings || !data) { return null; @@ -355,7 +355,7 @@ export function PanelDataQueriesTabRendered({ model }: SceneComponentProps Add query - {config.featureToggles.queryLibrary && ( + {queryLibraryEnabled && ( - - - - ); -}; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.test.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.test.tsx deleted file mode 100644 index 15ec78719e5..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { render, waitFor, screen } from '@testing-library/react'; - -import { AnnoKeyCreatedBy } from '../../apiserver/types'; -import { ListQueryTemplateApiResponse } from '../../query-library/api/endpoints.gen'; - -import { QueryTemplatesList } from './QueryTemplatesList'; -import { QueryActionButtonProps } from './types'; - -let data: ListQueryTemplateApiResponse = { - items: [], -}; - -jest.mock('app/features/query-library', () => { - const actual = jest.requireActual('app/features/query-library'); - return { - ...actual, - useDeleteQueryTemplateMutation: () => [() => {}], - useListQueryTemplateQuery: () => { - return { - data: data, - isLoading: false, - error: null, - }; - }, - }; -}); - -jest.mock('./utils/dataFetching', () => { - return { - __esModule: true, - useLoadQueryMetadata: () => { - return { - loading: false, - value: [ - { - index: '0', - uid: '0', - datasourceName: 'prometheus', - datasourceRef: { type: 'prometheus', uid: 'Prometheus0' }, - datasourceType: 'prometheus', - createdAtTimestamp: 0, - query: { refId: 'A' }, - queryText: 'http_requests_total{job="test"}', - description: 'template0', - user: { - uid: 'viewer:JohnDoe', - displayName: 'John Doe', - avatarUrl: '', - }, - error: undefined, - }, - ], - }; - }, - useLoadUsers: () => { - return { - value: { - display: [ - { - avatarUrl: '', - displayName: 'john doe', - identity: { - name: 'JohnDoe', - type: 'viewer', - }, - }, - ], - }, - loading: false, - error: null, - }; - }, - }; -}); - -describe('QueryTemplatesList', () => { - it('renders empty state', async () => { - data = {}; - render(); - await waitFor(() => { - expect(screen.getByText(/You haven't saved any queries to your library yet/)).toBeInTheDocument(); - }); - }); - - it('renders query', async () => { - data.items = testItems; - render(); - await waitFor(() => { - // We don't really show query template title for some reason so creator name - expect(screen.getByText(/John Doe/)).toBeInTheDocument(); - }); - }); - - it('renders actionButton for query', async () => { - data.items = testItems; - let passedProps: QueryActionButtonProps; - - const queryActionButton = (props: QueryActionButtonProps) => { - passedProps = props; - return ; - }; - - render(); - await waitFor(() => { - // We don't really show query template title for some reason so creator name - expect(screen.getByText(/John Doe/)).toBeInTheDocument(); - expect(screen.getByText(/TEST_ACTION_BUTTON/)).toBeInTheDocument(); - // We didn't put much else into the query object but should be enough to check the prop - expect(passedProps.queries).toMatchObject([{ refId: 'A' }]); - }); - }); -}); - -const testItems = [ - { - metadata: { - name: 'TEST_QUERY', - creationTimestamp: '2025-01-01T11:11:11.00Z', - annotations: { - [AnnoKeyCreatedBy]: 'viewer:JohnDoe', - }, - }, - spec: { - title: 'Test Query title', - targets: [ - { - variables: {}, - properties: { - refId: 'A', - datasource: { - uid: 'Prometheus', - type: 'prometheus', - }, - }, - }, - ], - }, - }, -]; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx deleted file mode 100644 index 946e3de9eea..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { css } from '@emotion/css'; -import { uniqBy } from 'lodash'; -import { useEffect, useMemo, useState } from 'react'; - -import { AppEvents, GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; -import { EmptyState, FilterInput, InlineLabel, MultiSelect, Spinner, useStyles2, Stack, Badge } from '@grafana/ui'; -import { t, Trans } from 'app/core/internationalization'; -import { useListQueryTemplateQuery } from 'app/features/query-library'; -import { QueryTemplate } from 'app/features/query-library/types'; - -import { convertDataQueryResponseToQueryTemplates } from '../../query-library/api/mappers'; - -import { QueryLibraryProps } from './QueryLibrary'; -import { queryLibraryTrackFilterDatasource } from './QueryLibraryAnalyticsEvents'; -import { QueryLibraryExpmInfo } from './QueryLibraryExpmInfo'; -import QueryTemplatesTable from './QueryTemplatesTable'; -import { useLoadQueryMetadata, useLoadUsers } from './utils/dataFetching'; -import { searchQueryLibrary } from './utils/search'; - -interface QueryTemplatesListProps extends QueryLibraryProps {} - -export function QueryTemplatesList(props: QueryTemplatesListProps) { - const { data: rawData, isLoading, error } = useListQueryTemplateQuery({}); - const data = useMemo(() => (rawData ? convertDataQueryResponseToQueryTemplates(rawData) : undefined), [rawData]); - const [isModalOpen, setIsModalOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - const [datasourceFilters, setDatasourceFilters] = useState>>( - props.activeDatasources?.map((ds) => ({ value: ds, label: ds })) || [] - ); - const [userFilters, setUserFilters] = useState>>([]); - const styles = useStyles2(getStyles); - - const loadUsersResult = useLoadUsersWithError(data); - const userNames = loadUsersResult.data ? loadUsersResult.data.display.map((user) => user.displayName) : []; - - const loadQueryMetadataResult = useLoadQueryMetadataWithError(data, loadUsersResult.data); - - // Filtering right now is done just on the frontend until there is better backend support for this. - const filteredRows = useMemo( - () => - searchQueryLibrary( - loadQueryMetadataResult.value || [], - searchQuery, - datasourceFilters.map((f) => f.value || ''), - userFilters.map((f) => f.value || '') - ), - [loadQueryMetadataResult.value, searchQuery, datasourceFilters, userFilters] - ); - - const datasourceNames = useMemo(() => { - return uniqBy(loadQueryMetadataResult.value, 'datasourceName').map((row) => row.datasourceName); - }, [loadQueryMetadataResult.value]); - - if (error instanceof Error) { - return ( - - {error.message} - - ); - } - - if (isLoading || loadUsersResult.isLoading || loadQueryMetadataResult.loading) { - return ; - } - - if (!data || data.length === 0) { - return ( - -

- { - "You haven't saved any queries to your library yet. Start adding them from Explore or your Query History tab." - } -

-
- ); - } - - return ( - <> - setIsModalOpen(false)} /> - - setSearchQuery(query)} - escapeRegex={false} - /> - - Datasource name(s): - - { - setDatasourceFilters(items); - actionMeta.action === 'select-option' && queryLibraryTrackFilterDatasource(); - }} - value={datasourceFilters} - options={datasourceNames.map((r) => { - return { value: r, label: r }; - })} - placeholder={'Filter queries for data sources(s)'} - aria-label={'Filter queries for data sources(s)'} - /> - - User name(s): - - { - setUserFilters(items); - actionMeta.action === 'select-option' && queryLibraryTrackFilterDatasource(); - }} - value={userFilters} - options={userNames.map((r) => { - return { value: r, label: r }; - })} - placeholder={'Filter queries for user name(s)'} - aria-label={'Filter queries for user name(s)'} - /> - setIsModalOpen(true)} - /> - - - - ); -} - -/** - * Wrap useLoadUsers with error handling. - * @param data - */ -function useLoadUsersWithError(data: QueryTemplate[] | undefined) { - const userUIDs = useMemo(() => data?.map((qt) => qt.user?.uid).filter((uid) => uid !== undefined), [data]); - const loadUsersResult = useLoadUsers(userUIDs); - useEffect(() => { - if (loadUsersResult.error) { - getAppEvents().publish({ - type: AppEvents.alertError.name, - payload: [ - t('query-library.user-info-get-error', 'Error attempting to get user info from the library: {{error}}', { - error: JSON.stringify(loadUsersResult.error), - }), - ], - }); - } - }, [loadUsersResult.error]); - return loadUsersResult; -} - -/** - * Wrap useLoadQueryMetadata with error handling. - * @param queryTemplates - * @param userDataList - */ -function useLoadQueryMetadataWithError( - queryTemplates: QueryTemplate[] | undefined, - userDataList: ReturnType['data'] -) { - const result = useLoadQueryMetadata(queryTemplates, userDataList); - - // useLoadQueryMetadata returns errors in the values so we filter and group them and later alert only one time for - // all the errors. This way we show data that is loaded even if some rows errored out. - // TODO: maybe we could show the rows with incomplete data to see exactly which ones errored out. I assume this - // can happen for example when data source for saved query was deleted. Would be nice if user would still be able - // to delete such row or decide what to do. - const [values, errors] = useMemo(() => { - let errors: Error[] = []; - let values = []; - if (!result.loading) { - for (const value of result.value!) { - if (value.error) { - errors.push(value.error); - } else { - values.push(value); - } - } - } - return [values, errors]; - }, [result]); - - useEffect(() => { - if (errors.length) { - getAppEvents().publish({ - type: AppEvents.alertError.name, - payload: [ - t('query-library.query-template-get-error', 'Error attempting to load query template metadata: {{error}}', { - error: JSON.stringify(errors), - }), - ], - }); - } - }, [errors]); - - return { - loading: result.loading, - value: values, - }; -} - -const getStyles = (theme: GrafanaTheme2) => ({ - searchInput: css({ - maxWidth: theme.spacing(55), - }), - multiSelect: css({ - maxWidth: theme.spacing(65), - }), - label: css({ - marginLeft: theme.spacing(1), - border: `1px solid ${theme.colors.secondary.border}`, - }), -}); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx deleted file mode 100644 index 575f10bd063..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/ActionsCell.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState } from 'react'; - -import { getAppEvents } from '@grafana/runtime'; -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'; -import { useDeleteQueryTemplateMutation } from 'app/features/query-library'; -import { dispatch } from 'app/store/store'; -import { ShowConfirmModalEvent } from 'app/types/events'; - -import { - queryLibaryTrackDeleteQuery, - queryLibraryTrackAddOrEditDescription, - queryLibraryTrackRunQuery, -} from '../QueryLibraryAnalyticsEvents'; -import { QueryTemplateForm } from '../QueryTemplateForm'; -import { QueryActionButton } from '../types'; - -import { useQueryLibraryListStyles } from './styles'; -import { QueryTemplateRow } from './types'; - -interface ActionsCellProps { - queryUid?: string; - queryTemplate: QueryTemplateRow; - rootDatasourceUid?: string; - QueryActionButton?: QueryActionButton; -} - -function ActionsCell({ queryTemplate, rootDatasourceUid, queryUid, QueryActionButton }: ActionsCellProps) { - const [deleteQueryTemplate] = useDeleteQueryTemplateMutation(); - const [editFormOpen, setEditFormOpen] = useState(false); - const styles = useQueryLibraryListStyles(); - - const onDeleteQuery = (queryUid: string) => { - const performDelete = (queryUid: string) => { - deleteQueryTemplate({ - name: queryUid, - deleteOptions: {}, - }); - dispatch(notifyApp(createSuccessNotification(t('explore.query-library.query-deleted', 'Query deleted')))); - queryLibaryTrackDeleteQuery(); - }; - - getAppEvents().publish( - new ShowConfirmModalEvent({ - title: t('explore.query-library.delete-query-title', 'Delete query'), - text: t( - 'explore.query-library.delete-query-text', - "You're about to remove this query from the query library. This action cannot be undone. Do you want to continue?" - ), - yesText: t('query-library.delete-query-button', 'Delete query'), - icon: 'trash-alt', - onConfirm: () => performDelete(queryUid), - }) - ); - }; - - return ( -
- { - if (queryUid) { - onDeleteQuery(queryUid); - } - }} - /> - { - setEditFormOpen(true); - queryLibraryTrackAddOrEditDescription(); - }} - /> - {QueryActionButton && ( - { - queryLibraryTrackRunQuery(queryTemplate.datasourceType || ''); - }} - /> - )} - setEditFormOpen(false)} - > - setEditFormOpen(false)} - templateData={queryTemplate} - onSave={() => { - setEditFormOpen(false); - }} - /> - -
- ); -} - -export default ActionsCell; diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/AddedByCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/AddedByCell.tsx deleted file mode 100644 index 35793ab7fbd..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/AddedByCell.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Avatar } from '@grafana/ui'; -import { User } from 'app/features/query-library/types'; - -import { useQueryLibraryListStyles } from './styles'; - -type AddedByCellProps = { - user?: User; -}; -export function AddedByCell(props: AddedByCellProps) { - const styles = useQueryLibraryListStyles(); - - return ( -
- - - - {props.user?.displayName || 'Unknown'} -
- ); -} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DatasourceTypeCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DatasourceTypeCell.tsx deleted file mode 100644 index bdc361b7532..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DatasourceTypeCell.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { CellProps } from 'react-table'; - -import { useDatasource } from '../utils/useDatasource'; - -import { useQueryLibraryListStyles } from './styles'; -import { QueryTemplateRow } from './types'; - -export function DatasourceTypeCell(props: CellProps) { - const datasourceApi = useDatasource(props.row.original.datasourceRef); - const styles = useQueryLibraryListStyles(); - - return

{datasourceApi?.meta.name}

; -} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DateAddedCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DateAddedCell.tsx deleted file mode 100644 index 29bf6a96b07..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/DateAddedCell.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { CellProps } from 'react-table'; - -import { dateTime } from '@grafana/data'; - -import { useQueryLibraryListStyles } from './styles'; -import { QueryTemplateRow } from './types'; - -export function DateAddedCell(props: CellProps) { - const styles = useQueryLibraryListStyles(); - const formattedTime = dateTime(props.row.original.createdAtTimestamp).format('YYYY-MM-DD HH:mm:ss'); - - return

{formattedTime}

; -} diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx deleted file mode 100644 index f19e95f14c8..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { css, cx } from '@emotion/css'; -import { CellProps } from 'react-table'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Spinner, Tooltip, useStyles2 } from '@grafana/ui'; - -import { useDatasource } from '../utils/useDatasource'; - -import { useQueryLibraryListStyles } from './styles'; -import { QueryTemplateRow } from './types'; - -export function QueryDescriptionCell(props: CellProps) { - const datasourceApi = useDatasource(props.row.original.datasourceRef); - const queryLibraryListStyles = useQueryLibraryListStyles(); - const styles = useStyles2(getStyles); - - if (!datasourceApi) { - return ; - } - - if (!props.row.original.query) { - return
No queries
; - } - const queryDisplayText = props.row.original.queryText; - const description = props.row.original.description; - const dsName = props.row.original.datasourceName; - - return ( -
-

- {datasourceApi?.meta.info.description} - {dsName} -

- -

- {queryDisplayText} -

-
-

{description}

-
- ); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - container: css({ - maxWidth: theme.spacing(60), - }), - queryDisplayText: css({ - backgroundColor: theme.colors.background.canvas, - }), -}); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx deleted file mode 100644 index bd7b94e39ef..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { css } from '@emotion/css'; -import { SortByFn } from 'react-table'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Column, InteractiveTable, useStyles2 } from '@grafana/ui'; - -import { QueryActionButton } from '../types'; - -import ActionsCell from './ActionsCell'; -import { AddedByCell } from './AddedByCell'; -import { DatasourceTypeCell } from './DatasourceTypeCell'; -import { DateAddedCell } from './DateAddedCell'; -import { QueryDescriptionCell } from './QueryDescriptionCell'; -import { QueryTemplateRow } from './types'; - -const timestampSort: SortByFn = (rowA, rowB, _, desc) => { - const timeA = rowA.original.createdAtTimestamp || 0; - const timeB = rowB.original.createdAtTimestamp || 0; - return desc ? timeA - timeB : timeB - timeA; -}; - -function createColumns(queryActionButton?: QueryActionButton): Array> { - return [ - { id: 'description', header: 'Data source and query', cell: QueryDescriptionCell }, - { id: 'addedBy', header: 'Added by', cell: ({ row: { original } }) => }, - { id: 'datasourceType', header: 'Datasource type', cell: DatasourceTypeCell, sortType: 'string' }, - { id: 'createdAtTimestamp', header: 'Date added', cell: DateAddedCell, sortType: timestampSort }, - { - id: 'actions', - header: '', - cell: ({ row: { original } }) => ( - - ), - }, - ]; -} - -type Props = { - queryTemplateRows: QueryTemplateRow[]; - queryActionButton?: QueryActionButton; -}; - -export default function QueryTemplatesTable({ queryTemplateRows, queryActionButton }: Props) { - const styles = useStyles2(getStyles); - const columns = createColumns(queryActionButton); - - return ( - row.index} - pageSize={20} - className={styles.table} - /> - ); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - table: css({ - 'tbody tr': { - position: 'relative', - backgroundColor: theme.colors.background.secondary, - borderCollapse: 'collapse', - borderBottom: 'unset', - overflow: 'hidden', // Ensure the row doesn't overflow and cause additonal scrollbars - }, - /* Adds the pseudo-element for the lines between table rows */ - 'tbody tr::after': { - content: '""', - position: 'absolute', - inset: 'auto 0 0 0', - height: theme.spacing(0.5), - backgroundColor: theme.colors.background.primary, - }, - }), -}); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx deleted file mode 100644 index 715cd5d235b..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/styles.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { css } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data/'; -import { useStyles2 } from '@grafana/ui/'; - -export const useQueryLibraryListStyles = () => { - return useStyles2(getStyles); -}; - -const getStyles = (theme: GrafanaTheme2) => ({ - logo: css({ - marginRight: theme.spacing(2), - width: '16px', - }), - header: css({ - margin: 0, - fontSize: theme.typography.h5.fontSize, - color: theme.colors.text.secondary, - }), - mainText: css({ - margin: 0, - fontSize: theme.typography.body.fontSize, - textOverflow: 'ellipsis', - }), - otherText: css({ - margin: 0, - fontSize: theme.typography.body.fontSize, - color: theme.colors.text.secondary, - textOverflow: 'ellipsis', - }), - singleLine: css({ - display: '-webkit-box', - WebkitBoxOrient: 'vertical', - WebkitLineClamp: 1, - overflow: 'hidden', - }), - cell: css({ - display: 'flex', - alignItems: 'center', - '&:last-child': { - justifyContent: 'end', - }, - }), - actionButton: css({ - padding: theme.spacing(1), - }), -}); diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts b/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts deleted file mode 100644 index 51f5e490953..00000000000 --- a/public/app/features/explore/QueryLibrary/QueryTemplatesTable/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DataQuery, DataSourceRef } from '@grafana/schema'; -import { User } from 'app/features/query-library/types'; - -export type QueryTemplateRow = { - index: string; - datasourceName?: string; - description?: string; - query?: DataQuery; - queryText?: string; - datasourceRef?: DataSourceRef | null; - datasourceType?: string; - createdAtTimestamp?: number; - user?: User; - uid?: string; -}; diff --git a/public/app/features/explore/QueryLibrary/SaveQueryButton.tsx b/public/app/features/explore/QueryLibrary/SaveQueryButton.tsx deleted file mode 100644 index 07c75179ed6..00000000000 --- a/public/app/features/explore/QueryLibrary/SaveQueryButton.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useLocalStorage } from 'react-use'; - -import { DataQuery } from '@grafana/schema'; -import { Badge } from '@grafana/ui'; - -import { QueryOperationAction } from '../../../core/components/QueryOperationRow/QueryOperationAction'; -import { t } from '../../../core/internationalization'; - -import { QUERY_LIBRARY_LOCAL_STORAGE_KEYS } from './QueryLibrary'; -import { useQueryLibraryContext } from './QueryLibraryContext'; - -interface Props { - query: DataQuery; -} - -export function SaveQueryButton({ query }: Props) { - const { openAddQueryModal } = useQueryLibraryContext(); - - const [showQueryLibraryBadgeButton, setShowQueryLibraryBadgeButton] = useLocalStorage( - QUERY_LIBRARY_LOCAL_STORAGE_KEYS.explore.newButton, - true - ); - - return showQueryLibraryBadgeButton ? ( - { - openAddQueryModal(query); - setShowQueryLibraryBadgeButton(false); - }} - style={{ cursor: 'pointer' }} - /> - ) : ( - { - openAddQueryModal(query); - }} - /> - ); -} diff --git a/public/app/features/explore/QueryLibrary/utils/dataFetching.ts b/public/app/features/explore/QueryLibrary/utils/dataFetching.ts deleted file mode 100644 index e874d6a331e..00000000000 --- a/public/app/features/explore/QueryLibrary/utils/dataFetching.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query'; -import { compact, uniq } from 'lodash'; -import { useAsync } from 'react-use'; -import { AsyncState } from 'react-use/lib/useAsync'; - -import { getDataSourceSrv } from '@grafana/runtime'; -import { DataQuery, DataSourceRef } from '@grafana/schema'; - -import { createQueryText } from '../../../../core/utils/richHistory'; -import { useGetDisplayMappingQuery } from '../../../iam'; -import { getDatasourceSrv } from '../../../plugins/datasource_srv'; -import { QueryTemplate } from '../../../query-library/types'; - -export function useLoadUsers(userUIDs: string[] | undefined) { - const userQtList = uniq(compact(userUIDs)); - return useGetDisplayMappingQuery( - userUIDs - ? { - key: userQtList, - } - : skipToken - ); -} - -// Explicitly type the result so TS knows to discriminate between the error result and good result by the error prop -// value. -type MetadataValue = - | { - index: string; - uid: string; - datasourceName: string; - datasourceRef: DataSourceRef | undefined | null; - datasourceType: string; - createdAtTimestamp: number; - query: DataQuery; - queryText: string; - description: string; - user: { - uid: string; - displayName: string; - avatarUrl: string; - }; - error: undefined; - } - | { - index: string; - error: Error; - }; - -/** - * Map metadata to query templates we get from the DB. - * @param queryTemplates - * @param userDataList - */ -export function useLoadQueryMetadata( - queryTemplates: QueryTemplate[] | undefined, - userDataList: ReturnType['data'] -): AsyncState { - return useAsync(async () => { - if (!(queryTemplates && userDataList)) { - return []; - } - - const rowsPromises = queryTemplates.map( - async (queryTemplate: QueryTemplate, index: number): Promise => { - try { - const datasourceRef = queryTemplate.targets[0]?.datasource; - const datasourceApi = await getDataSourceSrv().get(datasourceRef); - const datasourceType = getDatasourceSrv().getInstanceSettings(datasourceRef)?.meta.name || ''; - const query = queryTemplate.targets[0]; - const queryText = createQueryText(query, datasourceApi); - const datasourceName = datasourceApi?.name || ''; - const extendedUserData = userDataList.display.find( - (user) => `${user?.identity.type}:${user?.identity.name}` === queryTemplate.user?.uid - ); - - return { - index: index.toString(), - uid: queryTemplate.uid, - datasourceName, - datasourceRef, - datasourceType, - createdAtTimestamp: queryTemplate?.createdAtTimestamp || 0, - query, - queryText, - description: queryTemplate.title, - user: { - uid: queryTemplate.user?.uid || '', - displayName: extendedUserData?.displayName || '', - avatarUrl: extendedUserData?.avatarURL || '', - }, - error: undefined, - }; - } catch (error) { - // Instead of throwing we collect the errors in the result so upstream code can decide what to do. - return { - index: index.toString(), - error: error instanceof Error ? error : new Error('unknown error ' + JSON.stringify(error)), - }; - } - } - ); - - return Promise.all(rowsPromises); - }, [queryTemplates, userDataList]); -} diff --git a/public/app/features/explore/QueryLibrary/utils/search.ts b/public/app/features/explore/QueryLibrary/utils/search.ts deleted file mode 100644 index 2c55be7e9d0..00000000000 --- a/public/app/features/explore/QueryLibrary/utils/search.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { QueryTemplateRow } from '../QueryTemplatesTable/types'; - -export const searchQueryLibrary = ( - queryLibrary: QueryTemplateRow[], - query: string, - dsFilters: string[], - userNameFilters: string[] -) => { - const result = queryLibrary.filter((item) => { - const matchesDsFilter = - dsFilters.length === 0 || dsFilters.some((f) => item.datasourceName?.toLowerCase().includes(f.toLowerCase())); - const matchesUserNameFilter = - userNameFilters.length === 0 || userNameFilters.includes(item.user?.displayName || ''); - return ( - (item.datasourceName?.toLowerCase().includes(query.toLowerCase()) || - item.datasourceType?.toLowerCase().includes(query.toLowerCase()) || - item.description?.toLowerCase().includes(query.toLowerCase()) || - item.queryText?.toLowerCase().includes(query.toLowerCase())) && - matchesDsFilter && - matchesUserNameFilter - ); - }); - return result; -}; diff --git a/public/app/features/explore/QueryLibrary/utils/useDatasource.tsx b/public/app/features/explore/QueryLibrary/utils/useDatasource.tsx deleted file mode 100644 index 1ea4fd29b76..00000000000 --- a/public/app/features/explore/QueryLibrary/utils/useDatasource.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useAsync } from 'react-use'; - -import { getDataSourceSrv } from '@grafana/runtime'; -import { DataSourceRef } from '@grafana/schema'; - -export function useDatasource(dataSourceRef?: DataSourceRef | null) { - const { value } = useAsync(async () => await getDataSourceSrv().get(dataSourceRef), [dataSourceRef]); - return value; -} diff --git a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx index e6aa0e25143..8e7a057a453 100644 --- a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx @@ -1,57 +1,32 @@ import { useState } from 'react'; import { DataQuery } from '@grafana/schema'; -import { Button, Modal } from '@grafana/ui'; +import { Button } from '@grafana/ui'; import { t } from 'app/core/internationalization'; -import { isQueryLibraryEnabled, useListQueryTemplateQuery } from 'app/features/query-library'; -import { - queryLibraryTrackAddFromQueryHistory, - queryLibraryTrackAddFromQueryHistoryAddModalShown, -} from '../QueryLibrary/QueryLibraryAnalyticsEvents'; -import { QueryTemplateForm } from '../QueryLibrary/QueryTemplateForm'; +import { useQueryLibraryContext } from '../QueryLibrary/QueryLibraryContext'; type Props = { query: DataQuery; }; export const RichHistoryAddToLibrary = ({ query }: Props) => { - const { refetch } = useListQueryTemplateQuery({}); - const [isOpen, setIsOpen] = useState(false); const [hasBeenSaved, setHasBeenSaved] = useState(false); + const { openAddQueryModal, queryLibraryEnabled } = useQueryLibraryContext(); const buttonLabel = t('explore.rich-history-card.add-to-library', 'Add to library'); - return isQueryLibraryEnabled() && !hasBeenSaved ? ( + return queryLibraryEnabled && !hasBeenSaved ? ( <> - setIsOpen(false)} - > - setIsOpen(() => false)} - queryToAdd={query} - onSave={(isSuccess) => { - if (isSuccess) { - setIsOpen(false); - setHasBeenSaved(true); - refetch(); - queryLibraryTrackAddFromQueryHistory(query.datasource?.type || ''); - } - }} - /> - ) : undefined; }; diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index c57cf52c435..78aa044cab3 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -4,6 +4,7 @@ import { createMemoryHistory } from 'history'; import { KBarProvider } from 'kbar'; import { fromPairs } from 'lodash'; import { stringify } from 'querystring'; +import { ComponentType, ReactNode } from 'react'; import { Provider } from 'react-redux'; // eslint-disable-next-line no-restricted-imports import { Route, Router } from 'react-router-dom'; @@ -47,7 +48,6 @@ import { ExploreQueryParams } from '../../../../types'; import { initialUserState } from '../../../profile/state/reducers'; import ExplorePage from '../../ExplorePage'; import { QueriesDrawerContextProvider } from '../../QueriesDrawer/QueriesDrawerContext'; -import { QueryLibraryContextProvider } from '../../QueryLibrary/QueryLibraryContext'; type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; @@ -60,6 +60,7 @@ type SetupOptions = { failAddToLibrary?: boolean; // Use AppChrome wrapper around ExplorePage - needed to test query library/history withAppChrome?: boolean; + provider?: ComponentType<{ children: ReactNode }>; }; type TearDownOptions = { @@ -179,12 +180,18 @@ export function setupExplore(options?: SetupOptions): { const contextMock = getGrafanaContextMock({ location }); + const FinalProvider = + options?.provider || + (({ children }) => { + return children; + }); + const { unmount, container } = render( - - + + {options?.withAppChrome ? ( @@ -204,8 +211,8 @@ export function setupExplore(options?: SetupOptions): { render={(props) => } /> )} - - + + diff --git a/public/app/features/explore/spec/queryLibrary.test.tsx b/public/app/features/explore/spec/queryLibrary.test.tsx deleted file mode 100644 index 8de9796fdf0..00000000000 --- a/public/app/features/explore/spec/queryLibrary.test.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { Props } from 'react-virtualized-auto-sizer'; - -import { EventBusSrv } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { DataQuery } from '@grafana/schema/dist/esm/veneer/common.types'; - -import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput'; - -import { - assertAddToQueryLibraryButtonExists, - assertQueryHistory, - assertQueryLibraryTemplateExists, -} from './helper/assert'; -import { - addQueryHistoryToQueryLibrary, - openQueryHistory, - openQueryLibrary, - submitAddToQueryLibrary, -} from './helper/interactions'; -import { setupExplore, waitForExplore } from './helper/setup'; - -const reportInteractionMock = jest.fn(); -const testEventBus = new EventBusSrv(); -testEventBus.publish = jest.fn(); - -interface MockQuery extends DataQuery { - expr: string; -} - -jest.mock('../QueryLibrary/utils/dataFetching', () => { - return { - __esModule: true, - ...jest.requireActual('../QueryLibrary/utils/dataFetching'), - useLoadUsers: () => { - return { - data: { - display: [ - { - avatarUrl: '', - displayName: 'john doe', - identity: { - name: 'JohnDoe', - type: 'viewer', - }, - }, - ], - }, - isLoading: false, - error: null, - }; - }, - }; -}); - -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - reportInteraction: (...args: object[]) => { - reportInteractionMock(...args); - }, - getAppEvents: () => testEventBus, - usePluginLinks: jest.fn().mockReturnValue({ links: [] }), -})); - -jest.mock('app/core/core', () => ({ - contextSrv: { - hasPermission: () => true, - isSignedIn: true, - getValidIntervals: (defaultIntervals: string[]) => defaultIntervals, - user: { - isSignedIn: true, - }, - }, -})); - -jest.mock('app/core/services/PreferencesService', () => ({ - PreferencesService: function () { - return { - patch: jest.fn(), - load: jest.fn().mockResolvedValue({ - queryHistory: { - homeTab: 'query', - }, - }), - }; - }, -})); - -jest.mock('../hooks/useExplorePageTitle', () => ({ - useExplorePageTitle: jest.fn(), -})); - -jest.mock('react-virtualized-auto-sizer', () => { - return { - __esModule: true, - default(props: Props) { - return
{props.children({ height: 1, scaledHeight: 1, scaledWidth: 1000, width: 1000 })}
; - }, - }; -}); - -function setupQueryLibrary() { - const mockQuery: MockQuery = { refId: 'TEST', expr: 'TEST' }; - setupExplore({ - queryHistory: { - queryHistory: [{ datasourceUid: 'loki', queries: [mockQuery] }], - totalCount: 1, - }, - withAppChrome: true, - }); -} - -let previousQueryLibraryEnabled: boolean | undefined; -let previousQueryHistoryEnabled: boolean; - -describe('QueryLibrary', () => { - silenceConsoleOutput(); - - beforeAll(() => { - previousQueryLibraryEnabled = config.featureToggles.queryLibrary; - previousQueryHistoryEnabled = config.queryHistoryEnabled; - - config.featureToggles.queryLibrary = true; - config.queryHistoryEnabled = true; - }); - - afterAll(() => { - config.featureToggles.queryLibrary = previousQueryLibraryEnabled; - config.queryHistoryEnabled = previousQueryHistoryEnabled; - jest.restoreAllMocks(); - }); - - it('Load query templates', async () => { - setupQueryLibrary(); - await waitForExplore(); - await openQueryLibrary(); - await assertQueryLibraryTemplateExists('loki', 'Loki Query Template'); - }); - - it('Shows add to query library button only when the toggle is enabled', async () => { - setupQueryLibrary(); - await waitForExplore(); - await openQueryHistory(); - 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 openQueryHistory(); - await assertQueryHistory(['{"expr":"TEST"}']); - await addQueryHistoryToQueryLibrary(); - await submitAddToQueryLibrary({ description: 'Test' }); - expect(testEventBus.publish).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'alert-success', - payload: ['Query successfully saved to the library'], - }) - ); - await assertAddToQueryLibraryButtonExists(false); - }); -}); diff --git a/public/app/features/query-library/index.ts b/public/app/features/query-library/index.ts index 246ca735681..7f0c6f1c325 100644 --- a/public/app/features/query-library/index.ts +++ b/public/app/features/query-library/index.ts @@ -7,8 +7,6 @@ * @alpha */ -import { config } from '@grafana/runtime'; - import { QUERY_LIBRARY_GET_LIMIT } from './api/api'; import { generatedQueryLibraryApi } from './api/endpoints.gen'; import { mockData } from './api/mocks'; @@ -46,10 +44,6 @@ export const { }, }); -export function isQueryLibraryEnabled() { - return config.featureToggles.queryLibrary; -} - export const QueryLibraryMocks = { data: mockData.all, }; diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index a3dc05930d7..36a3d5818ee 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -8,7 +8,6 @@ import { PureComponent, ReactNode } from 'react'; // Utils & Services import { CoreApp, - DataQuery, DataSourceApi, DataSourceInstanceSettings, DataSourcePluginContextProvider, @@ -24,7 +23,8 @@ import { toLegacyResponseData, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { AngularComponent, config, getAngularLoader, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { AngularComponent, getAngularLoader, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { Badge, ErrorBoundaryAlert } from '@grafana/ui'; import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp'; import { @@ -40,7 +40,7 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { SaveQueryButton as SaveQueryToQueryLibraryButton } from '../../explore/QueryLibrary/SaveQueryButton'; +import { useQueryLibraryContext } from '../../explore/QueryLibrary/QueryLibraryContext'; import { QueryActionComponent, RowActionComponents } from './QueryActionComponent'; import { QueryEditorRowHeader } from './QueryEditorRowHeader'; @@ -489,7 +489,7 @@ export class QueryEditorRow extends PureComponent )} {this.renderExtraActions()} - {config.featureToggles.queryLibrary && } + > }) { + return props.providers.reduce((tree, Provider): ReactNode => { + return {tree}; + }, props.children); +} type RouterWrapperProps = { routes?: JSX.Element | false; bodyRenderHooks: ComponentType[]; pageBanners: ComponentType[]; + providers: Array>; }; export function RouterWrapper(props: RouterWrapperProps) { return ( @@ -31,7 +37,7 @@ export function RouterWrapper(props: RouterWrapperProps) { - + @@ -48,7 +54,7 @@ export function RouterWrapper(props: RouterWrapperProps) { - + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 66492eac6b1..f4bbcfbcf7c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1346,36 +1346,6 @@ "scan-for-older-logs": "Scan for older logs", "stop-scan": "Stop scan" }, - "query-library": { - "add-edit-description": "Add/edit description", - "cancel": "Cancel", - "default-description": "Public", - "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-title": "Delete query", - "private": "Private", - "public": "Public", - "query-deleted": "Query deleted", - "query-template-add-error": "Error attempting to save this query to the library", - "query-template-added": "Query successfully saved 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", @@ -2993,14 +2963,6 @@ "role-label": "Role" } }, - "query-library": { - "datasource-names": "Datasource name(s):", - "delete-query-button": "Delete query", - "query-template-get-error": "Error attempting to load query template metadata: {{error}}", - "search": "Search by data source, query content or description", - "user-info-get-error": "Error attempting to get user info from the library: {{error}}", - "user-names": "User name(s):" - }, "query-operation": { "header": { "collapse-row": "Collapse query row", @@ -3010,8 +2972,6 @@ "expand-row": "Expand query row", "hide-response": "Hide response", "remove-query": "Remove query", - "save-to-query-library": "Save to query library", - "save-to-query-library-new": "New: Save to query library", "show-response": "Show response", "toggle-edit-mode": "Toggle text edit mode" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 193643a5111..af213f5ae27 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1346,36 +1346,6 @@ "scan-for-older-logs": "Ŝčäʼn ƒőř őľđęř ľőģş", "stop-scan": "Ŝŧőp şčäʼn" }, - "query-library": { - "add-edit-description": "Åđđ/ęđįŧ đęşčřįpŧįőʼn", - "cancel": "Cäʼnčęľ", - "default-description": "Pūþľįč", - "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-title": "Đęľęŧę qūęřy", - "private": "Přįväŧę", - "public": "Pūþľįč", - "query-deleted": "Qūęřy đęľęŧęđ", - "query-template-add-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő şävę ŧĥįş qūęřy ŧő ŧĥę ľįþřäřy", - "query-template-added": "Qūęřy şūččęşşƒūľľy şävęđ ŧő ŧĥę ľįþřäř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": "Đäŧä şőūřčę Å-Ż", @@ -2993,14 +2963,6 @@ "role-label": "Ŗőľę" } }, - "query-library": { - "datasource-names": "Đäŧäşőūřčę ʼnämę(ş):", - "delete-query-button": "Đęľęŧę qūęřy", - "query-template-get-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ľőäđ qūęřy ŧęmpľäŧę męŧäđäŧä: {{error}}", - "search": "Ŝęäřčĥ þy đäŧä şőūřčę, qūęřy čőʼnŧęʼnŧ őř đęşčřįpŧįőʼn", - "user-info-get-error": "Ēřřőř äŧŧęmpŧįʼnģ ŧő ģęŧ ūşęř įʼnƒő ƒřőm ŧĥę ľįþřäřy: {{error}}", - "user-names": "Ůşęř ʼnämę(ş):" - }, "query-operation": { "header": { "collapse-row": "Cőľľäpşę qūęřy řőŵ", @@ -3010,8 +2972,6 @@ "expand-row": "Ēχpäʼnđ qūęřy řőŵ", "hide-response": "Ħįđę řęşpőʼnşę", "remove-query": "Ŗęmővę qūęřy", - "save-to-query-library": "Ŝävę ŧő qūęřy ľįþřäřy", - "save-to-query-library-new": "Ńęŵ: Ŝävę ŧő qūęřy ľįþřäřy", "show-response": "Ŝĥőŵ řęşpőʼnşę", "toggle-edit-mode": "Ŧőģģľę ŧęχŧ ęđįŧ mőđę" },