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 ? (
<>