diff --git a/.betterer.results b/.betterer.results index 634983e4e11..8f55ae02fe6 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3951,10 +3951,6 @@ exports[`better eslint`] = { "public/app/features/explore/QueryLibrary/QueryTemplatesTable/QueryDescriptionCell.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], - "public/app/features/explore/RichHistory/RichHistoryAddToLibraryForm.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] - ], "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.", "1"], diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index 050c0eaf16f..2372df87670 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -1,22 +1,27 @@ 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 { 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 { useGrafana } from 'app/core/context/GrafanaContext'; 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 { useSelector } from 'app/types'; import { ExploreQueryParams } from 'app/types/explore'; +import { RowActionComponents } from '../query/components/QueryActionComponent'; + import { CorrelationEditorModeBar } from './CorrelationEditorModeBar'; import { ExploreActions } from './ExploreActions'; import { ExploreDrawer } from './ExploreDrawer'; import { ExplorePaneContainer } from './ExplorePaneContainer'; import { QueriesDrawerContextProvider, useQueriesDrawerContext } from './QueriesDrawer/QueriesDrawerContext'; +import { AddToLibraryForm } from './QueryLibrary/AddToLibraryForm'; import RichHistoryContainer from './RichHistory/RichHistoryContainer'; import { useExplorePageTitle } from './hooks/useExplorePageTitle'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; @@ -26,6 +31,7 @@ import { useTimeSrvFix } from './hooks/useTimeSrvFix'; import { isSplit, selectCorrelationDetails, selectPanesEntries } from './state/selectors'; const MIN_PANE_WIDTH = 200; +const QUERY_LIBRARY_ACTION_KEY = 'queryLibraryAction'; export default function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { return ( @@ -55,6 +61,7 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa const correlationDetails = useSelector(selectCorrelationDetails); const { drawerOpened, setDrawerOpened, queryLibraryAvailable } = useQueriesDrawerContext(); const showCorrelationEditorBar = config.featureToggles.correlations && (correlationDetails?.editorMode || false); + const [queryToAdd, setQueryToAdd] = useState(); useEffect(() => { //This is needed for breadcrumbs and topnav. @@ -64,6 +71,25 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa }); }, [chrome, navModel]); + useEffect(() => { + const hasQueryLibrary = config.featureToggles.queryLibrary || false; + if (hasQueryLibrary) { + RowActionComponents.addKeyedExtraRenderAction(QUERY_LIBRARY_ACTION_KEY, { + scope: CoreApp.Explore, + queryActionComponent: (props) => ( + { + setQueryToAdd(props.query); + }} + /> + ), + }); + } + }, []); + useKeyboardShortcuts(); return ( @@ -105,6 +131,23 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa /> )} + setQueryToAdd(undefined)} + > + { + setQueryToAdd(undefined); + }} + onSave={(isSuccess) => { + if (isSuccess) { + setQueryToAdd(undefined); + } + }} + query={queryToAdd!} + /> + ); } diff --git a/public/app/features/explore/RichHistory/RichHistoryAddToLibraryForm.tsx b/public/app/features/explore/QueryLibrary/AddToLibraryForm.tsx similarity index 55% rename from public/app/features/explore/RichHistory/RichHistoryAddToLibraryForm.tsx rename to public/app/features/explore/QueryLibrary/AddToLibraryForm.tsx index b787773fb42..551f63edde2 100644 --- a/public/app/features/explore/RichHistory/RichHistoryAddToLibraryForm.tsx +++ b/public/app/features/explore/QueryLibrary/AddToLibraryForm.tsx @@ -1,19 +1,22 @@ import { useMemo } from 'react'; 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 { Button, InlineSwitch, Modal, RadioButtonGroup, TextArea } from '@grafana/ui'; import { Field } from '@grafana/ui/'; 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'; type Props = { onCancel: () => void; - onSave: (details: QueryDetails) => void; + onSave: (isSuccess: boolean) => void; query: DataQuery; }; @@ -22,8 +25,8 @@ export type QueryDetails = { }; const VisibilityOptions = [ - { value: 'Public', label: 'Public' }, - { value: 'Private', label: 'Private' }, + { value: 'Public', label: t('explore.query-library.public', 'Public') }, + { value: 'Private', label: t('explore.query-library.private', 'Private') }, ]; 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.` ); -export const RichHistoryAddToLibraryForm = ({ onCancel, onSave, query }: Props) => { +export const AddToLibraryForm = ({ onCancel, onSave, query }: Props) => { const { register, handleSubmit } = useForm(); + const [addQueryTemplate] = useAddQueryTemplateMutation(); + + const handleAddQueryTemplate = async (addQueryTemplateCommand: AddQueryTemplateCommand) => { + return addQueryTemplate(addQueryTemplateCommand) + .unwrap() + .then(() => { + getAppEvents().publish({ + type: AppEvents.alertSuccess.name, + payload: [ + t('explore.query-library.query-template-added', 'Query template successfully added to the library'), + ], + }); + return true; + }) + .catch(() => { + getAppEvents().publish({ + type: AppEvents.alertError.name, + payload: [ + t('explore.query-library.query-template-error', 'Error attempting to add this query to the library'), + ], + }); + return false; + }); + }; + const datasource = useDatasource(query.datasource); const displayText = useMemo(() => { return datasource?.getQueryDisplayText?.(query) || getQueryDisplayText(query); }, [datasource, query]); - const onSubmit = (data: QueryDetails) => { - onSave(data); + const onSubmit = async (data: QueryDetails) => { + const timestamp = dateTime().toISOString(); + const temporaryDefaultTitle = + data.description || t('explore.query-library.default-description', 'Public', { timestamp: timestamp }); + handleAddQueryTemplate({ title: temporaryDefaultTitle, targets: [query] }).then((isSuccess) => { + onSave(isSuccess); + }); }; return ( @@ -72,10 +105,10 @@ export const RichHistoryAddToLibraryForm = ({ onCancel, onSave, query }: Props) /> diff --git a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx index a9cc225455a..8bfcde6e6e8 100644 --- a/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryAddToLibrary.tsx @@ -1,14 +1,11 @@ import { t } from 'i18next'; import { useState } from 'react'; -import { AppEvents, dateTime } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Button, Modal } from '@grafana/ui'; -import { isQueryLibraryEnabled, useAddQueryTemplateMutation } from 'app/features/query-library'; -import { AddQueryTemplateCommand } from 'app/features/query-library/types'; +import { isQueryLibraryEnabled } from 'app/features/query-library'; -import { QueryDetails, RichHistoryAddToLibraryForm } from './RichHistoryAddToLibraryForm'; +import { AddToLibraryForm } from '../QueryLibrary/AddToLibraryForm'; type Props = { query: DataQuery; @@ -16,29 +13,11 @@ type Props = { export const RichHistoryAddToLibrary = ({ query }: Props) => { const [isOpen, setIsOpen] = useState(false); - const [addQueryTemplate, { isSuccess }] = useAddQueryTemplateMutation(); - - const handleAddQueryTemplate = async (addQueryTemplateCommand: AddQueryTemplateCommand) => { - const result = await addQueryTemplate(addQueryTemplateCommand); - if (!result.error) { - getAppEvents().publish({ - type: AppEvents.alertSuccess.name, - payload: [ - t('explore.rich-history-card.query-template-added', 'Query template successfully added to the library'), - ], - }); - } - }; + const [hasBeenSaved, setHasBeenSaved] = useState(false); const buttonLabel = t('explore.rich-history-card.add-to-library', 'Add to library'); - const submit = (data: QueryDetails) => { - const timestamp = dateTime().toISOString(); - const temporaryDefaultTitle = data.description || `Imported from Explore - ${timestamp}`; - handleAddQueryTemplate({ title: temporaryDefaultTitle, targets: [query] }); - }; - - return isQueryLibraryEnabled() && !isSuccess ? ( + return isQueryLibraryEnabled() && !hasBeenSaved ? ( <>