From aca15a9571570386a8e9fa3b462cec6f52e32bf8 Mon Sep 17 00:00:00 2001 From: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:05:57 -0800 Subject: [PATCH] feat: incorporate feature flag --- .../src/types/featureToggles.gen.ts | 5 + pkg/services/featuremgmt/registry.go | 8 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 14 + .../CorrelationTransformationAddModal.tsx | 4 +- .../Legacy/CorrelationEditorModeBarLegacy.tsx | 304 ++++++++++++++++++ .../Legacy/CorrelationHelperLegacy.tsx | 295 +++++++++++++++++ ...orrelationTransformationAddModalLegacy.tsx | 262 +++++++++++++++ .../CorrelationUnsavedChangesModalLegacy.tsx | 46 +++ public/app/features/explore/Explore.tsx | 10 +- public/app/features/explore/ExplorePage.tsx | 16 +- .../app/features/explore/ExploreToolbar.tsx | 54 ++-- public/locales/en-US/grafana.json | 22 +- 14 files changed, 1012 insertions(+), 33 deletions(-) create mode 100644 public/app/features/explore/CorrelationEditor/Legacy/CorrelationEditorModeBarLegacy.tsx create mode 100644 public/app/features/explore/CorrelationEditor/Legacy/CorrelationHelperLegacy.tsx create mode 100644 public/app/features/explore/CorrelationEditor/Legacy/CorrelationTransformationAddModalLegacy.tsx create mode 100644 public/app/features/explore/CorrelationEditor/Legacy/CorrelationUnsavedChangesModalLegacy.tsx diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 8a50becd224..cac2869c099 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1250,4 +1250,9 @@ export interface FeatureToggles { * @default false */ kubernetesAnnotations?: boolean; + /** + * Enables the new correlations editor in Explore + * @default false + */ + correlationsExploreEditor?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 27a978a6c27..2cc400847cf 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -2165,6 +2165,14 @@ var ( Owner: grafanaBackendServicesSquad, Expression: "false", }, + { + Name: "correlationsExploreEditor", + Description: "Enables the new correlations editor in Explore", + Stage: FeatureStageExperimental, + Owner: grafanaDataProSquad, + FrontendOnly: true, + Expression: "false", + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 71f681c0171..3fd3220a827 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -278,3 +278,4 @@ onlyStoreActionSets,GA,@grafana/identity-access-team,false,false,false panelTimeSettings,experimental,@grafana/dashboards-squad,false,false,false dashboardTemplates,experimental,@grafana/sharing-squad,false,false,false kubernetesAnnotations,experimental,@grafana/grafana-backend-services-squad,false,false,false +correlationsExploreEditor,experimental,@grafana/datapro,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 443fb196228..1f9c486ca33 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -1121,4 +1121,8 @@ const ( // FlagKubernetesAnnotations // Enables app platform API for annotations FlagKubernetesAnnotations = "kubernetesAnnotations" + + // FlagCorrelationsExploreEditor + // Enables the new correlations editor in Explore + FlagCorrelationsExploreEditor = "correlationsExploreEditor" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 48155d54721..cdd3e3c6126 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1012,6 +1012,20 @@ "expression": "true" } }, + { + "metadata": { + "name": "correlationsExploreEditor", + "resourceVersion": "1764186796607", + "creationTimestamp": "2025-11-26T19:53:16Z" + }, + "spec": { + "description": "Enables the new correlations editor in Explore", + "stage": "experimental", + "codeowner": "@grafana/datapro", + "frontend": true, + "expression": "false" + } + }, { "metadata": { "name": "crashDetection", diff --git a/public/app/features/explore/CorrelationEditor/CorrelationTransformationAddModal.tsx b/public/app/features/explore/CorrelationEditor/CorrelationTransformationAddModal.tsx index 190853f005a..0885d6fadd6 100644 --- a/public/app/features/explore/CorrelationEditor/CorrelationTransformationAddModal.tsx +++ b/public/app/features/explore/CorrelationEditor/CorrelationTransformationAddModal.tsx @@ -132,8 +132,8 @@ export const CorrelationTransformationAddModal = ({ isOpen={true} title={ transformationToEdit - ? t('explore.correlation-transformation-add-modal.title-edit', 'Edit custom variable') - : t('explore.correlation-transformation-add-modal.title-add', 'Add custom variable') + ? t('explore.correlation-transformation-add-modal.title-edit-custom-variable', 'Edit custom variable') + : t('explore.correlation-transformation-add-modal.title-add-custom-variable', 'Add custom variable') } onDismiss={onCancel} className={css({ width: '700px' })} diff --git a/public/app/features/explore/CorrelationEditor/Legacy/CorrelationEditorModeBarLegacy.tsx b/public/app/features/explore/CorrelationEditor/Legacy/CorrelationEditorModeBarLegacy.tsx new file mode 100644 index 00000000000..00149bd39a8 --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/Legacy/CorrelationEditorModeBarLegacy.tsx @@ -0,0 +1,304 @@ +import { css } from '@emotion/css'; +import { useEffect, useState } from 'react'; +import { useBeforeUnload, useUnmount } from 'react-use'; + +import { GrafanaTheme2, colorManipulator } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { reportInteraction } from '@grafana/runtime'; +import { Button, Icon, Stack, Tooltip, useStyles2 } from '@grafana/ui'; +import { Prompt } from 'app/core/components/FormPrompt/Prompt'; +import { CORRELATION_EDITOR_POST_CONFIRM_ACTION, ExploreItemState } from 'app/types/explore'; +import { useDispatch, useSelector } from 'app/types/store'; + +import { saveCurrentCorrelation } from '../../state/correlations'; +import { changeDatasource } from '../../state/datasource'; +import { changeCorrelationHelperData } from '../../state/explorePane'; +import { changeCorrelationEditorDetails, splitClose } from '../../state/main'; +import { runQueries } from '../../state/query'; +import { selectCorrelationDetails, selectIsHelperShowing } from '../../state/selectors'; +import { showModalMessage } from '../correlationEditLogic'; + +import { CorrelationUnsavedChangesModalLegacy } from './CorrelationUnsavedChangesModalLegacy'; + +export const CorrelationEditorModeBarLegacy = ({ panes }: { panes: Array<[string, ExploreItemState]> }) => { + const dispatch = useDispatch(); + const styles = useStyles2(getStyles); + const correlationDetails = useSelector(selectCorrelationDetails); + const isHelperShowing = useSelector(selectIsHelperShowing); + const [saveMessage, setSaveMessage] = useState(undefined); // undefined means do not show + + // handle refreshing and closing the tab + useBeforeUnload(correlationDetails?.correlationDirty || false, 'Save correlation?'); + useBeforeUnload( + (!correlationDetails?.correlationDirty && correlationDetails?.queryEditorDirty) || false, + 'The query editor was changed. Save correlation before continuing?' + ); + + // decide if we are displaying prompt, perform action if not + useEffect(() => { + if (correlationDetails?.isExiting) { + const { correlationDirty, queryEditorDirty } = correlationDetails; + let isActionLeft = undefined; + let action = undefined; + if (correlationDetails.postConfirmAction) { + isActionLeft = correlationDetails.postConfirmAction.isActionLeft; + action = correlationDetails.postConfirmAction.action; + } else { + // closing the editor only + action = CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR; + isActionLeft = false; + } + + const modalMessage = showModalMessage(action, isActionLeft, correlationDirty, queryEditorDirty); + if (modalMessage !== undefined) { + setSaveMessage(modalMessage); + } else { + // if no prompt, perform action + if ( + action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE && + correlationDetails.postConfirmAction + ) { + const { exploreId, changeDatasourceUid } = correlationDetails?.postConfirmAction; + if (exploreId && changeDatasourceUid) { + dispatch( + changeDatasource({ exploreId, datasource: changeDatasourceUid, options: { importQueries: true } }) + ); + dispatch( + changeCorrelationEditorDetails({ + isExiting: false, + }) + ); + } + } else if ( + action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE && + correlationDetails.postConfirmAction + ) { + const { exploreId } = correlationDetails?.postConfirmAction; + if (exploreId !== undefined) { + dispatch(splitClose(exploreId)); + dispatch( + changeCorrelationEditorDetails({ + isExiting: false, + }) + ); + } + } else if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR) { + dispatch( + changeCorrelationEditorDetails({ + editorMode: false, + }) + ); + } + } + } + }, [correlationDetails, dispatch, isHelperShowing]); + + // clear data when unmounted + useUnmount(() => { + dispatch( + changeCorrelationEditorDetails({ + editorMode: false, + isExiting: false, + correlationDirty: false, + label: undefined, + description: undefined, + canSave: false, + }) + ); + + panes.forEach((pane) => { + dispatch( + changeCorrelationHelperData({ + exploreId: pane[0], + correlationEditorHelperData: undefined, + }) + ); + dispatch(runQueries({ exploreId: pane[0] })); + }); + }); + + const resetEditor = () => { + dispatch( + changeCorrelationEditorDetails({ + editorMode: true, + isExiting: false, + correlationDirty: false, + label: undefined, + description: undefined, + canSave: false, + }) + ); + + panes.forEach((pane) => { + dispatch( + changeCorrelationHelperData({ + exploreId: pane[0], + correlationEditorHelperData: undefined, + }) + ); + dispatch(runQueries({ exploreId: pane[0] })); + }); + }; + + const closePane = (exploreId: string) => { + setSaveMessage(undefined); + dispatch(splitClose(exploreId)); + reportInteraction('grafana_explore_split_view_closed'); + }; + + const changeDatasourcePostAction = (exploreId: string, datasourceUid: string) => { + setSaveMessage(undefined); + dispatch(changeDatasource({ exploreId, datasource: datasourceUid, options: { importQueries: true } })); + }; + + const saveCorrelationPostAction = (skipPostConfirmAction: boolean) => { + dispatch( + saveCurrentCorrelation( + correlationDetails?.label, + correlationDetails?.description, + correlationDetails?.transformations + ) + ); + if (!skipPostConfirmAction && correlationDetails?.postConfirmAction !== undefined) { + const { exploreId, action, changeDatasourceUid } = correlationDetails?.postConfirmAction; + if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE) { + closePane(exploreId); + resetEditor(); + } else if ( + action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE && + changeDatasourceUid !== undefined + ) { + changeDatasource({ exploreId, datasource: changeDatasourceUid }); + resetEditor(); + } + } else { + dispatch(changeCorrelationEditorDetails({ editorMode: false, correlationDirty: false, isExiting: false })); + } + }; + + return ( + <> + {/* Handle navigating outside Explore */} + { + if ( + location.pathname !== '/explore' && + correlationDetails?.editorMode && + correlationDetails?.correlationDirty + ) { + return 'You have unsaved correlation data. Continue?'; + } else { + return true; + } + }} + /> + + {saveMessage !== undefined && ( + { + if (correlationDetails?.postConfirmAction !== undefined) { + const { exploreId, action, changeDatasourceUid } = correlationDetails?.postConfirmAction; + if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE) { + closePane(exploreId); + } else if ( + action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE && + changeDatasourceUid !== undefined + ) { + changeDatasourcePostAction(exploreId, changeDatasourceUid); + } + dispatch(changeCorrelationEditorDetails({ isExiting: false })); + } else { + // exit correlations mode + // if we are discarding the in progress correlation, reset everything + // this modal only shows if the editorMode is false, so we just need to update the dirty state + dispatch( + changeCorrelationEditorDetails({ + editorMode: false, + correlationDirty: false, + isExiting: false, + }) + ); + } + }} + onCancel={() => { + // if we are cancelling the exit, set the editor mode back to true and hide the prompt + dispatch(changeCorrelationEditorDetails({ isExiting: false })); + setSaveMessage(undefined); + }} + onSave={() => { + saveCorrelationPostAction(false); + }} + message={saveMessage} + /> + )} +
+ + + + + + + +
+ + ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + const contrastColor = theme.colors.getContrastText(theme.colors.primary.main); + const lighterBackgroundColor = colorManipulator.lighten(theme.colors.primary.main, 0.1); + const darkerBackgroundColor = colorManipulator.darken(theme.colors.primary.main, 0.2); + + const disabledColor = colorManipulator.darken(contrastColor, 0.2); + + return { + correlationEditorTop: css({ + backgroundColor: theme.colors.primary.main, + marginTop: '3px', + padding: theme.spacing(1), + }), + iconColor: css({ + color: contrastColor, + }), + buttonColor: css({ + color: contrastColor, + borderColor: contrastColor, + '&:hover': { + color: contrastColor, + borderColor: contrastColor, + backgroundColor: lighterBackgroundColor, + }, + }), + // important needed to override disabled state styling + disabledButtonColor: css({ + color: `${disabledColor} !important`, + backgroundColor: `${darkerBackgroundColor} !important`, + }), + }; +}; diff --git a/public/app/features/explore/CorrelationEditor/Legacy/CorrelationHelperLegacy.tsx b/public/app/features/explore/CorrelationEditor/Legacy/CorrelationHelperLegacy.tsx new file mode 100644 index 00000000000..a513b3af639 --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/Legacy/CorrelationHelperLegacy.tsx @@ -0,0 +1,295 @@ +import { css } from '@emotion/css'; +import { useEffect, useId, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useAsync } from 'react-use'; + +import { DataLinkTransformationConfig, ExploreCorrelationHelperData, GrafanaTheme2 } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { + Alert, + Button, + Card, + Collapse, + DeleteButton, + Field, + Icon, + IconButton, + Input, + Stack, + Tooltip, + useStyles2, +} from '@grafana/ui'; +import { useDispatch, useSelector } from 'app/types/store'; + +import { getTransformationVars } from '../../../correlations/transformations'; +import { generateDefaultLabel } from '../../../correlations/utils'; +import { changeCorrelationHelperData } from '../../state/explorePane'; +import { changeCorrelationEditorDetails } from '../../state/main'; +import { selectCorrelationDetails, selectPanes } from '../../state/selectors'; + +import { CorrelationTransformationAddModalLegacy } from './CorrelationTransformationAddModalLegacy'; + +interface Props { + exploreId: string; + correlations: ExploreCorrelationHelperData; +} + +interface FormValues { + label: string; + description: string; +} + +export const CorrelationHelperLegacy = ({ exploreId, correlations }: Props) => { + const dispatch = useDispatch(); + const styles = useStyles2(getStyles); + const panes = useSelector(selectPanes); + const panesVals = Object.values(panes); + const { value: defaultLabel, loading: loadingLabel } = useAsync( + async () => await generateDefaultLabel(panesVals[0]!, panesVals[1]!), + [ + panesVals[0]?.datasourceInstance, + panesVals[0]?.queries[0].datasource, + panesVals[1]?.datasourceInstance, + panesVals[1]?.queries[0].datasource, + ] + ); + + const { register, watch, getValues, setValue } = useForm(); + const [isLabelDescOpen, setIsLabelDescOpen] = useState(false); + const [isTransformOpen, setIsTransformOpen] = useState(false); + const [showTransformationAddModal, setShowTransformationAddModal] = useState(false); + const [transformations, setTransformations] = useState([]); + const [transformationIdxToEdit, setTransformationIdxToEdit] = useState(undefined); + const correlationDetails = useSelector(selectCorrelationDetails); + const id = useId(); + + // only fire once on mount to allow save button to enable / disable when unmounted + useEffect(() => { + dispatch(changeCorrelationEditorDetails({ canSave: true })); + return () => { + dispatch(changeCorrelationEditorDetails({ canSave: false })); + }; + }, [dispatch]); + + useEffect(() => { + if ( + !loadingLabel && + defaultLabel !== undefined && + !correlationDetails?.correlationDirty && + getValues('label') !== '' + ) { + setValue('label', defaultLabel); + } + }, [correlationDetails?.correlationDirty, defaultLabel, getValues, loadingLabel, setValue]); + + useEffect(() => { + const subscription = watch((value) => { + let dirty = correlationDetails?.correlationDirty || false; + let description = value.description || ''; + if (!dirty && (value.label !== defaultLabel || description !== '')) { + dirty = true; + } else if (dirty && value.label === defaultLabel && description.trim() === '') { + dirty = false; + } + dispatch( + changeCorrelationEditorDetails({ label: value.label, description: value.description, correlationDirty: dirty }) + ); + }); + return () => subscription.unsubscribe(); + }, [correlationDetails?.correlationDirty, defaultLabel, dispatch, watch]); + + useEffect(() => { + const dirty = + !correlationDetails?.correlationDirty && transformations.length > 0 ? true : correlationDetails?.correlationDirty; + dispatch(changeCorrelationEditorDetails({ transformations: transformations, correlationDirty: dirty })); + let transVarRecords: Record = {}; + transformations.forEach((transformation) => { + const transformationVars = getTransformationVars( + { + type: transformation.type, + expression: transformation.expression, + mapValue: transformation.mapValue, + }, + correlations.vars[transformation.field!], + transformation.field! + ); + + Object.keys(transformationVars).forEach((key) => { + transVarRecords[key] = transformationVars[key]?.value; + }); + }); + + dispatch( + changeCorrelationHelperData({ + exploreId: exploreId, + correlationEditorHelperData: { + resultField: correlations.resultField, + origVars: correlations.origVars, + vars: { ...correlations.origVars, ...transVarRecords }, + }, + }) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dispatch, transformations]); + + return ( + <> + {showTransformationAddModal && ( + { + setTransformationIdxToEdit(undefined); + setShowTransformationAddModal(false); + }} + onSave={(transformation: DataLinkTransformationConfig) => { + if (transformationIdxToEdit !== undefined) { + const editTransformations = [...transformations]; + editTransformations[transformationIdxToEdit] = transformation; + setTransformations(editTransformations); + setTransformationIdxToEdit(undefined); + } else { + setTransformations([...transformations, transformation]); + } + setShowTransformationAddModal(false); + }} + fieldList={correlations.origVars} + transformationToEdit={ + transformationIdxToEdit !== undefined ? transformations[transformationIdxToEdit] : undefined + } + /> + )} + + + The correlation link will appear by the {'{{resultField}}'} field. You can use the following + variables to set up your correlations: + +
+          {Object.entries(correlations.vars).map((entry) => {
+            return `\$\{${entry[0]}\} = ${entry[1]}\n`;
+          })}
+        
+ { + setIsLabelDescOpen(!isLabelDescOpen); + }} + label={ + + Label / Description + {!isLabelDescOpen && !loadingLabel && ( + {`Label: ${getValues('label') || defaultLabel}`} + )} + + } + > + + { + if (getValues('label') === '' && defaultLabel !== undefined) { + setValue('label', defaultLabel); + } + }} + /> + + + + + + { + setIsTransformOpen(!isTransformOpen); + }} + label={ + + Transformations + + + + + } + > + + {transformations.map((transformation, i) => { + const { type, field, expression, mapValue } = transformation; + const detailsString = [ + (mapValue ?? '').length > 0 ? `Variable name: ${mapValue}` : undefined, + (expression ?? '').length > 0 ? ( + + Expression: {'{{expression}}'} + + ) : undefined, + ].filter((val) => val); + return ( + + + {field}: {type} + + {detailsString.length > 0 && ( + {detailsString} + )} + + { + setTransformationIdxToEdit(i); + setShowTransformationAddModal(true); + }} + /> + setTransformations(transformations.filter((_, idx) => i !== idx))} + closeOnConfirm + /> + + + ); + })} + +
+ + ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + labelCollapseDetails: css({ + marginLeft: theme.spacing(2), + ...theme.typography['bodySmall'], + fontStyle: 'italic', + }), + transformationAction: css({ + marginBottom: theme.spacing(2), + }), + transformationMeta: css({ + alignItems: 'baseline', + }), + }; +}; diff --git a/public/app/features/explore/CorrelationEditor/Legacy/CorrelationTransformationAddModalLegacy.tsx b/public/app/features/explore/CorrelationEditor/Legacy/CorrelationTransformationAddModalLegacy.tsx new file mode 100644 index 00000000000..627d567bcde --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/Legacy/CorrelationTransformationAddModalLegacy.tsx @@ -0,0 +1,262 @@ +import { css } from '@emotion/css'; +import { useId, useState, useMemo, useEffect } from 'react'; +import Highlighter from 'react-highlight-words'; +import { useForm, Controller } from 'react-hook-form'; + +import { DataLinkTransformationConfig, ScopedVars } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { Button, Field, Icon, Input, Label, Modal, Select, Tooltip, Stack } from '@grafana/ui'; + +import { + getSupportedTransTypeDetails, + getTransformOptions, + TransformationFieldDetails, +} from '../../../correlations/Forms/types'; +import { getTransformationVars } from '../../../correlations/transformations'; + +interface CorrelationTransformationAddModalProps { + onCancel: () => void; + onSave: (transformation: DataLinkTransformationConfig) => void; + fieldList: Record; + transformationToEdit?: DataLinkTransformationConfig; +} + +interface ShowFormFields { + expressionDetails: TransformationFieldDetails; + mapValueDetails: TransformationFieldDetails; +} + +const LabelWithTooltip = ({ label, tooltipText }: { label: string; tooltipText: string }) => ( + + + + + + +); + +export const CorrelationTransformationAddModalLegacy = ({ + onSave, + onCancel, + fieldList, + transformationToEdit, +}: CorrelationTransformationAddModalProps) => { + const [exampleValue, setExampleValue] = useState(undefined); + const [transformationVars, setTransformationVars] = useState({}); + const [formFieldsVis, setFormFieldsVis] = useState({ + mapValueDetails: { show: false }, + expressionDetails: { show: false }, + }); + const [isExpValid, setIsExpValid] = useState(false); // keep the highlighter from erroring on bad expressions + const [validToSave, setValidToSave] = useState(false); + const { getValues, control, register, watch } = useForm({ + defaultValues: useMemo(() => { + if (transformationToEdit) { + const exampleVal = fieldList[transformationToEdit?.field!]; + setExampleValue(exampleVal); + if (transformationToEdit?.expression) { + setIsExpValid(true); + } + const transformationTypeDetails = getSupportedTransTypeDetails(transformationToEdit?.type!); + setFormFieldsVis({ + mapValueDetails: transformationTypeDetails.mapValueDetails, + expressionDetails: transformationTypeDetails.expressionDetails, + }); + + const transformationVars = getTransformationVars( + { + type: transformationToEdit?.type!, + expression: transformationToEdit?.expression, + mapValue: transformationToEdit?.mapValue, + }, + exampleVal || '', + transformationToEdit?.field! + ); + setTransformationVars({ ...transformationVars }); + setValidToSave(true); + return { + type: transformationToEdit?.type, + field: transformationToEdit?.field, + mapValue: transformationToEdit?.mapValue, + expression: transformationToEdit?.expression, + }; + } else { + return undefined; + } + }, [fieldList, transformationToEdit]), + }); + const id = useId(); + + useEffect(() => { + const subscription = watch((formValues) => { + const expression = formValues.expression; + let isExpressionValid = false; + if (expression !== undefined) { + isExpressionValid = true; + try { + new RegExp(expression); + } catch (e) { + isExpressionValid = false; + } + } else { + isExpressionValid = !formFieldsVis.expressionDetails.show; + } + setIsExpValid(isExpressionValid); + let transKeys = []; + if (formValues.type) { + const transformationVars = getTransformationVars( + { + type: formValues.type, + expression: isExpressionValid ? expression : '', + mapValue: formValues.mapValue, + }, + fieldList[formValues.field!] || '', + formValues.field! + ); + + transKeys = Object.keys(transformationVars); + setTransformationVars(transKeys.length > 0 ? { ...transformationVars } : {}); + } + + if (transKeys.length === 0 || !isExpressionValid) { + setValidToSave(false); + } else { + setValidToSave(true); + } + }); + return () => subscription.unsubscribe(); + }, [fieldList, formFieldsVis.expressionDetails.show, watch]); + + return ( + +

+ + A transformation extracts variables out of a single field. These variables will be available along with your + field variables. + +

+ + ( + { + onChange(value.value); + const transformationTypeDetails = getSupportedTransTypeDetails(value.value!); + setFormFieldsVis({ + mapValueDetails: transformationTypeDetails.mapValueDetails, + expressionDetails: transformationTypeDetails.expressionDetails, + }); + }} + options={getTransformOptions()} + aria-label={t('explore.correlation-transformation-add-modal.aria-label-type', 'Type')} + /> + )} + name={`type` as const} + /> + + {formFieldsVis.expressionDetails.show && ( + + ) : ( + t('explore.correlation-transformation-add-modal.label-expression-without-tooltip', 'Expression') + ) + } + htmlFor={`${id}-expression`} + required={formFieldsVis.expressionDetails.required} + > + + + )} + {formFieldsVis.mapValueDetails.show && ( + + ) : ( + t('explore.correlation-transformation-add-modal.label-variable-name-without-tooltip', 'Variable name') + ) + } + htmlFor={`${id}-mapValue`} + > + + + )} + {Object.entries(transformationVars).length > 0 && ( + <> + + This transformation will add the following variables: + +
+                {Object.entries(transformationVars).map((entry) => {
+                  return `\$\{${entry[0]}\} = ${entry[1]?.value}\n`;
+                })}
+              
+ + )} + + )} + + + + +
+ ); +}; diff --git a/public/app/features/explore/CorrelationEditor/Legacy/CorrelationUnsavedChangesModalLegacy.tsx b/public/app/features/explore/CorrelationEditor/Legacy/CorrelationUnsavedChangesModalLegacy.tsx new file mode 100644 index 00000000000..b1b4257d005 --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/Legacy/CorrelationUnsavedChangesModalLegacy.tsx @@ -0,0 +1,46 @@ +import { css } from '@emotion/css'; + +import { Trans, t } from '@grafana/i18n'; +import { Button, Modal } from '@grafana/ui'; + +interface UnsavedChangesModalProps { + message: string; + onDiscard: () => void; + onCancel: () => void; + onSave: () => void; +} + +export const CorrelationUnsavedChangesModalLegacy = ({ + onSave, + onDiscard, + onCancel, + message, +}: UnsavedChangesModalProps) => { + return ( + +
{message}
+ + + + + +
+ ); +}; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index e671caf33d0..f653acc317d 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -20,7 +20,7 @@ import { } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; -import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { AdHocFilterItem, @@ -41,6 +41,7 @@ import { CONTENT_OUTLINE_LOCAL_STORAGE_KEYS, ContentOutline } from './ContentOut import { ContentOutlineContextProvider } from './ContentOutline/ContentOutlineContext'; import { ContentOutlineItem } from './ContentOutline/ContentOutlineItem'; import { CorrelationHelper } from './CorrelationEditor/CorrelationHelper/CorrelationHelper'; +import { CorrelationHelperLegacy } from './CorrelationEditor/Legacy/CorrelationHelperLegacy'; import { CustomContainer } from './CustomContainer'; import { ExploreToolbar } from './ExploreToolbar'; import { FlameGraphExploreContainer } from './FlameGraph/FlameGraphExploreContainer'; @@ -597,6 +598,7 @@ export class Explore extends PureComponent { queryLibraryRef, } = this.props; const { contentOutlineVisible } = this.state; + const correlationsExploreEditor = config.featureToggles.correlationsExploreEditor; const styles = getStyles(theme); const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted; const richHistoryRowButtonHidden = !supportedFeatures().queryHistoryAvailable; @@ -617,7 +619,11 @@ export class Explore extends PureComponent { const isCorrelationsEditorMode = correlationEditorDetails?.editorMode; const showCorrelationHelper = Boolean(isCorrelationsEditorMode || correlationEditorDetails?.correlationDirty); if (showCorrelationHelper && correlationEditorHelperData !== undefined) { - correlationsBox = ; + correlationsBox = correlationsExploreEditor ? ( + + ) : ( + + ); } return ( diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index 364750c8b2d..3480499cf07 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -13,6 +13,7 @@ import { ExploreQueryParams } from 'app/types/explore'; import { useSelector } from 'app/types/store'; import { CorrelationEditorModeBar } from './CorrelationEditor/CorrelationEditorModeBar'; +import { CorrelationEditorModeBarLegacy } from './CorrelationEditor/Legacy/CorrelationEditorModeBarLegacy'; import { ExploreActions } from './ExploreActions'; import { ExploreDrawer } from './ExploreDrawer'; import { ExplorePaneContainer } from './ExplorePaneContainer'; @@ -32,7 +33,8 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor } function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) { - const styles = useStyles2(getStyles); + const correlationsExploreEditor = config.featureToggles.correlationsExploreEditor; + const styles = useStyles2(getStyles, correlationsExploreEditor); const theme = useTheme2(); useTimeSrvFix(); useStateSync(props.queryParams); @@ -72,7 +74,12 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa - {showCorrelationEditorBar && } + {showCorrelationEditorBar && + (correlationsExploreEditor ? ( + + ) : ( + + ))} { +const getStyles = (theme: GrafanaTheme2, correlationsExploreEditor?: boolean) => { return { pageScrollbarWrapper: css({ width: '100%', @@ -121,6 +128,9 @@ const getStyles = (theme: GrafanaTheme2) => { }), correlationsEditorIndicator: css({ overflow: 'scroll', + borderLeft: correlationsExploreEditor ? 'none' : `4px solid ${theme.colors.primary.main}`, + borderRight: correlationsExploreEditor ? 'none' : `4px solid ${theme.colors.primary.main}`, + borderBottom: correlationsExploreEditor ? 'none' : `4px solid ${theme.colors.primary.main}`, }), }; }; diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index a229e5183e4..802a604e232 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -6,7 +6,7 @@ import { shallowEqual } from 'react-redux'; import { DataSourceInstanceSettings, RawTimeRange, GrafanaTheme2 } from '@grafana/data'; import { Components } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { defaultIntervals, PageToolbar, @@ -225,6 +225,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle , ]; + const correlationsExploreEditor = config.featureToggles.correlationsExploreEditor; const showBuilderIndicator = isCorrelationsEditorMode && !isLeftPane; const showSourceIndicator = isCorrelationsEditorMode && isLeftPane && splitted; @@ -232,26 +233,37 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
{refreshInterval && } - {showSourceIndicator && ( -
- - - -
- )} - {showBuilderIndicator && ( -
- - - -
+ {correlationsExploreEditor && ( + <> + {showSourceIndicator && ( +
+ + + +
+ )} + {showBuilderIndicator && ( +
+ + + +
+ )} + )} Correlate with button.", "instructions-2": "Step 2: In the right pane (Correlation), build and test your correlation query.", "instructions-3": "Step 3: Click Save to create the correlation.", "save": "Save" @@ -7034,13 +7034,22 @@ }, "correlation-helper": { "add-custom-variable": "Add custom variable", + "add-transformation": "Add transformation", "aria-label-delete-transformation": "Delete transformation", "aria-label-edit-transformation": "Edit transformation", + "body-correlation-details": "The correlation link will appear by the <1>{{resultField}} field. You can use the following variables to set up your correlations:", "body-correlation-details-link": "When saved, the <1>{{resultField}} field will have a clickable link that opens the specified URL.", "body-correlation-details-query": "When saved, the <1>{{resultField}} field will have a clickable link that runs your target query below.", "body-variables": "Use these variables in your target query. When a correlation link is clicked, each variable is filled in with its value from that row.", + "expression": "Expression: <1>{{expression}}", + "label-description": "Description", + "label-description-header": "Label / Description", + "label-label": "Label", + "title-correlation-details": "Correlation details", "title-correlation-info": "Correlation Info", - "title-variables": "Variables (optional)" + "title-variables": "Variables (optional)", + "tooltip-transformations": "A transformation extracts one or more variables out of a single field.", + "transformations": "Transformations" }, "correlation-tour": { "back": "Back", @@ -7056,7 +7065,7 @@ "skip": "Skip tour", "step-counter": "Step {{current}} of {{total}}", "step-number": "Step {{step}}", - "step1-body": "Run a query that returns data. You can then click a link in a table cell, or use the \"🔗 Correlate with [field name]\" button to start creating a correlation.", + "step1-body": "Run a query that returns data. You can then click a link in a table cell, or use the <2> Correlate with [field name] button to start creating a correlation.", "step1-tip": "Tip: Look for these correlation links in table cells or log lines", "step1-title": "Step 1: Run a Query and Click a Link", "step2-body": "After clicking a correlation link, the right pane (target) opens with a query editor. Build and test your query here.", @@ -7074,6 +7083,7 @@ "added-variables": "This custom variable will add the following variables:", "aria-label-field": "Field", "aria-label-type": "Type", + "body": "A transformation extracts variables out of a single field. These variables will be available along with your field variables.", "cancel": "Cancel", "description-field": "Select the field from which to extract a value for your variable", "edit-transformation": "Edit transformation", @@ -7084,8 +7094,10 @@ "label-type": "Type", "label-variable-name": "Variable name", "label-variable-name-without-tooltip": "Variable name", - "title-add": "Add custom variable", - "title-edit": "Edit custom variable" + "title-add": "Add transformation", + "title-add-custom-variable": "Add custom variable", + "title-edit": "Edit transformation", + "title-edit-custom-variable": "Edit custom variable" }, "correlation-unsaved-changes-modal": { "cancel": "Cancel",