mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
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:
parent
e7156e7e60
commit
783ff71560
@ -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 <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": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
|
@ -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<DataQuery | undefined>();
|
||||
|
||||
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) => (
|
||||
<QueryOperationAction
|
||||
key={props.key}
|
||||
title={t('query-operation.header.save-to-query-library', 'Save to query library')}
|
||||
icon="save"
|
||||
onClick={() => {
|
||||
setQueryToAdd(props.query);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useKeyboardShortcuts();
|
||||
|
||||
return (
|
||||
@ -105,6 +131,23 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -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<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 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)
|
||||
/>
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" onClick={() => onCancel()} fill="outline">
|
||||
Cancel
|
||||
<Trans i18nKey="explore.query-library.cancel">Cancel</Trans>
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
Save
|
||||
<Trans i18nKey="explore.query-library.save">Save</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
</form>
|
@ -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 ? (
|
||||
<>
|
||||
<Button variant="secondary" aria-label={buttonLabel} onClick={() => setIsOpen(true)}>
|
||||
{buttonLabel}
|
||||
@ -48,12 +27,14 @@ export const RichHistoryAddToLibrary = ({ query }: Props) => {
|
||||
isOpen={isOpen}
|
||||
onDismiss={() => setIsOpen(false)}
|
||||
>
|
||||
<RichHistoryAddToLibraryForm
|
||||
<AddToLibraryForm
|
||||
onCancel={() => setIsOpen(() => false)}
|
||||
query={query}
|
||||
onSave={(data) => {
|
||||
submit(data);
|
||||
setIsOpen(false);
|
||||
onSave={(isSuccess) => {
|
||||
if (isSuccess) {
|
||||
setIsOpen(false);
|
||||
setHasBeenSaved(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
@ -59,7 +59,7 @@ export const submitAddToQueryLibrary = async ({ description }: { description: st
|
||||
const input = within(screen.getByRole('dialog')).getByLabelText('Description');
|
||||
await userEvent.type(input, description);
|
||||
const saveButton = screen.getByRole('button', {
|
||||
name: /save/i,
|
||||
name: /^save$/i,
|
||||
});
|
||||
await userEvent.click(saveButton);
|
||||
};
|
||||
|
@ -48,7 +48,7 @@ export const convertAddQueryTemplateCommandToDataQuerySpec = (
|
||||
apiVersion: API_VERSION,
|
||||
kind: QueryTemplateKinds.QueryTemplate,
|
||||
metadata: {
|
||||
generateName: 'A' + title,
|
||||
generateName: 'A' + title.replaceAll(' ', '-'),
|
||||
},
|
||||
spec: {
|
||||
title: title,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataQuery, DataSourceInstanceSettings, TimeRange } from '@grafana/data';
|
||||
import { CoreApp, DataQuery, DataSourceInstanceSettings, TimeRange } from '@grafana/data';
|
||||
|
||||
interface ActionComponentProps {
|
||||
query?: DataQuery;
|
||||
@ -10,18 +10,41 @@ interface ActionComponentProps {
|
||||
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 {
|
||||
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) {
|
||||
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[] {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 { RowActionComponents } from './QueryActionComponent';
|
||||
import { QueryActionComponent, RowActionComponents } from './QueryActionComponent';
|
||||
import { QueryEditorRowHeader } from './QueryEditorRowHeader';
|
||||
import { QueryErrorAlert } from './QueryErrorAlert';
|
||||
|
||||
@ -425,9 +425,17 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
||||
};
|
||||
|
||||
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) =>
|
||||
action({
|
||||
query,
|
||||
|
@ -660,10 +660,17 @@
|
||||
"stop-scan": "Stop scan"
|
||||
},
|
||||
"query-library": {
|
||||
"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",
|
||||
"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": {
|
||||
"close-tooltip": "Close query history",
|
||||
@ -694,7 +701,6 @@
|
||||
"edit-comment-tooltip": "Edit comment",
|
||||
"optional-description": "An optional description of what the query does.",
|
||||
"query-comment-label": "Query comment",
|
||||
"query-template-added": "Query template successfully added to the library",
|
||||
"query-text-label": "Query text",
|
||||
"save-comment": "Save comment",
|
||||
"star-query-tooltip": "Star query",
|
||||
@ -1787,6 +1793,7 @@
|
||||
"expand-row": "Expand query row",
|
||||
"hide-response": "Hide response",
|
||||
"remove-query": "Remove query",
|
||||
"save-to-query-library": "Save to query library",
|
||||
"show-response": "Show response",
|
||||
"toggle-edit-mode": "Toggle text edit mode"
|
||||
},
|
||||
|
@ -660,10 +660,17 @@
|
||||
"stop-scan": "Ŝŧőp şčäʼn"
|
||||
},
|
||||
"query-library": {
|
||||
"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",
|
||||
"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": {
|
||||
"close-tooltip": "Cľőşę qūęřy ĥįşŧőřy",
|
||||
@ -694,7 +701,6 @@
|
||||
"edit-comment-tooltip": "Ēđįŧ čőmmęʼnŧ",
|
||||
"optional-description": "Åʼn őpŧįőʼnäľ đęşčřįpŧįőʼn őƒ ŵĥäŧ ŧĥę qūęřy đőęş.",
|
||||
"query-comment-label": "Qūęřy čőmmęʼnŧ",
|
||||
"query-template-added": "Qūęřy ŧęmpľäŧę şūččęşşƒūľľy äđđęđ ŧő ŧĥę ľįþřäřy",
|
||||
"query-text-label": "Qūęřy ŧęχŧ",
|
||||
"save-comment": "Ŝävę čőmmęʼnŧ",
|
||||
"star-query-tooltip": "Ŝŧäř qūęřy",
|
||||
@ -1787,6 +1793,7 @@
|
||||
"expand-row": "Ēχpäʼnđ qūęřy řőŵ",
|
||||
"hide-response": "Ħįđę řęşpőʼnşę",
|
||||
"remove-query": "Ŗęmővę qūęřy",
|
||||
"save-to-query-library": "Ŝävę ŧő qūęřy ľįþřäřy",
|
||||
"show-response": "Ŝĥőŵ řęşpőʼnşę",
|
||||
"toggle-edit-mode": "Ŧőģģľę ŧęχŧ ęđįŧ mőđę"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user