From bf1a8ab7c8c017eadc1bd214875b6dcabab3923a Mon Sep 17 00:00:00 2001 From: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:21:32 -0800 Subject: [PATCH] chore: lots of fixes and enhancements --- .../explore/ContentOutline/ContentOutline.tsx | 47 ++- .../CorrelationEditorModeBar.tsx | 26 +- .../CorrelationEditorTour.tsx | 302 ++++++++++++++ .../CorrelationFormCustomVariables.tsx | 104 +++++ .../CorrelationFormInformation.tsx | 92 +++++ .../CorrelationHelper/CorrelationHelper.tsx | 220 ++++++++++ .../CorrelationHelper/FormSection.tsx | 30 ++ .../CorrelationTransformationAddModal.tsx | 279 +++++++++++++ .../CorrelationUnsavedChangesModal.tsx | 0 .../correlationEditLogic.test.ts | 0 .../correlationEditLogic.ts | 0 .../explore/CorrelationEditor/types.ts | 49 +++ .../features/explore/CorrelationHelper.tsx | 386 ------------------ .../CorrelationTransformationAddModal.tsx | 260 ------------ public/app/features/explore/Explore.tsx | 10 +- public/app/features/explore/ExplorePage.tsx | 2 +- .../app/features/explore/ExploreToolbar.tsx | 2 +- public/locales/en-US/grafana.json | 76 +++- 18 files changed, 1192 insertions(+), 693 deletions(-) rename public/app/features/explore/{ => CorrelationEditor}/CorrelationEditorModeBar.tsx (91%) create mode 100644 public/app/features/explore/CorrelationEditor/CorrelationEditorTour.tsx create mode 100644 public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationFormCustomVariables.tsx create mode 100644 public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationFormInformation.tsx create mode 100644 public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationHelper.tsx create mode 100644 public/app/features/explore/CorrelationEditor/CorrelationHelper/FormSection.tsx create mode 100644 public/app/features/explore/CorrelationEditor/CorrelationTransformationAddModal.tsx rename public/app/features/explore/{ => CorrelationEditor}/CorrelationUnsavedChangesModal.tsx (100%) rename public/app/features/explore/{ => CorrelationEditor}/correlationEditLogic.test.ts (100%) rename public/app/features/explore/{ => CorrelationEditor}/correlationEditLogic.ts (100%) create mode 100644 public/app/features/explore/CorrelationEditor/types.ts delete mode 100644 public/app/features/explore/CorrelationHelper.tsx delete mode 100644 public/app/features/explore/CorrelationTransformationAddModal.tsx diff --git a/public/app/features/explore/ContentOutline/ContentOutline.tsx b/public/app/features/explore/ContentOutline/ContentOutline.tsx index 3ee75dcda44..2d19f9a725f 100644 --- a/public/app/features/explore/ContentOutline/ContentOutline.tsx +++ b/public/app/features/explore/ContentOutline/ContentOutline.tsx @@ -40,11 +40,20 @@ export const CONTENT_OUTLINE_LOCAL_STORAGE_KEYS = { expanded: 'grafana.explore.contentOutline.expanded', }; -export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement | undefined; panelId: string }) { +export function ContentOutline({ + scroller, + panelId, + defaultCollapsed = false, +}: { + scroller: HTMLElement | undefined; + panelId: string; + defaultCollapsed?: boolean; +}) { const [contentOutlineExpanded, toggleContentOutlineExpanded] = useToggle( store.getBool(CONTENT_OUTLINE_LOCAL_STORAGE_KEYS.expanded, true) ); - const styles = useStyles2(getStyles, contentOutlineExpanded); + const isExpanded = contentOutlineExpanded && !defaultCollapsed; + const styles = useStyles2(getStyles, isExpanded); const scrollerRef = useRef(scroller || null); const { y: verticalScroll } = useScroll(scrollerRef); const { outlineItems } = useContentOutlineContext() ?? { outlineItems: [] }; @@ -102,6 +111,9 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement | }; const toggle = () => { + if (defaultCollapsed && !contentOutlineExpanded) { + return; + } store.set(CONTENT_OUTLINE_LOCAL_STORAGE_KEYS.expanded, !contentOutlineExpanded); toggleContentOutlineExpanded(); reportInteraction('explore_toolbar_contentoutline_clicked', { @@ -160,16 +172,16 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement | {outlineItems.map((item) => { @@ -177,16 +189,16 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement | handleItemClicked(item)} @@ -204,7 +216,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement | sectionsExpanded[item.id] && item.children.map((child, i) => (
- {contentOutlineExpanded && ( + {isExpanded && (
{ diff --git a/public/app/features/explore/CorrelationEditorModeBar.tsx b/public/app/features/explore/CorrelationEditor/CorrelationEditorModeBar.tsx similarity index 91% rename from public/app/features/explore/CorrelationEditorModeBar.tsx rename to public/app/features/explore/CorrelationEditor/CorrelationEditorModeBar.tsx index 1d52028c036..77abb97f05b 100644 --- a/public/app/features/explore/CorrelationEditorModeBar.tsx +++ b/public/app/features/explore/CorrelationEditor/CorrelationEditorModeBar.tsx @@ -3,25 +3,28 @@ import { useBeforeUnload, useUnmount } from 'react-use'; import { Trans, t } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; -import { Alert, Badge, Button, Stack, Text } from '@grafana/ui'; +import { Alert, Badge, Button, Icon, Stack, Text } 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 { CorrelationEditorTour, useCorrelationEditorTour } from './CorrelationEditorTour'; import { CorrelationUnsavedChangesModal } from './CorrelationUnsavedChangesModal'; import { showModalMessage } from './correlationEditLogic'; -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'; export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, ExploreItemState]> }) => { const dispatch = useDispatch(); const correlationDetails = useSelector(selectCorrelationDetails); const isHelperShowing = useSelector(selectIsHelperShowing); const [saveMessage, setSaveMessage] = useState(undefined); // undefined means do not show + const { shouldShowTour, dismissTour } = useCorrelationEditorTour(); // handle refreshing and closing the tab useBeforeUnload(correlationDetails?.correlationDirty || false, 'Save correlation?'); @@ -189,6 +192,9 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl }} /> + {/* Show tour for first-time users */} + {shouldShowTour && } + {saveMessage !== undefined && ( { @@ -246,13 +252,13 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl - Step 1: In the left pane, run a query and click a table cell link or a "🔗 Correlate with" - button. + Step 1: Run a query and click a table cell link or a {' '} + Correlate with button. - Step 2: In the right pane (Target Query Builder), build and test your correlation query. + Step 2: In the right pane (Correlation), build and test your correlation query. diff --git a/public/app/features/explore/CorrelationEditor/CorrelationEditorTour.tsx b/public/app/features/explore/CorrelationEditor/CorrelationEditorTour.tsx new file mode 100644 index 00000000000..16a4c6e6298 --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/CorrelationEditorTour.tsx @@ -0,0 +1,302 @@ +import { css } from '@emotion/css'; +import { useState, useEffect } from 'react'; + +import { GrafanaTheme2, store } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { Button, Icon, Modal, Stack, Text, useStyles2 } from '@grafana/ui'; + +const TOUR_STORAGE_KEY = 'grafana.explore.correlationEditor.tourCompleted'; + +interface TourStep { + title: string; + content: JSX.Element; +} + +const getTourSteps = (): TourStep[] => [ + { + title: t('explore.correlation-tour.welcome-title', 'Welcome to the Correlation Editor'), + content: ( + + + + The Correlation Editor helps you create clickable links between different data sources in Grafana. This + makes it easy to jump from one view to another with context preserved. + + + + + For example, you can click a service name in your logs and automatically open a dashboard showing metrics + for that service. + + + + ), + }, + { + title: t('explore.correlation-tour.step1-title', 'Step 1: Run a Query and Click a Link'), + content: ( + + + + 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. + + + + + + + Tip: Look for these correlation links in table cells or log lines + + + + + ), + }, + { + title: t('explore.correlation-tour.step2-title', 'Step 2: Build Your Target Query'), + content: ( + + + + After clicking a correlation link, the right pane (target) opens with a query editor. Build + and test your query here. + + + + + Available variables are shown in the "Variables" section below. You can also create custom + variables by extracting parts of fields using regular expressions or logfmt. + + + + ), + }, + { + title: t('explore.correlation-tour.step3-title', 'Step 3: Save Your Correlation'), + content: ( + + + + Once your query works correctly, click the Save button . Give your correlation a name and + optionally add a description. + + + + + After saving, this correlation link will appear for all users in the same field across all queries from your + source data source! + + + + ), + }, + { + title: t('explore.correlation-tour.ready-title', "You're All Set!"), + content: ( + + + + You now know the basics of creating correlations. Remember, you can exit the editor at any time by clicking + the Exit correlation editor button. + + + + + Quick Tips: + + + + + + Test your correlation query thoroughly before saving + + + + + + + + Use clear, descriptive names so other users understand the link + + + + + + + + Custom variables let you extract specific parts of field values + + + + + + ), + }, +]; + +interface CorrelationEditorTourProps { + onDismiss: () => void; +} + +export const CorrelationEditorTour = ({ onDismiss }: CorrelationEditorTourProps) => { + const [currentStep, setCurrentStep] = useState(0); + const [isTransitioning, setIsTransitioning] = useState(false); + + const styles = useStyles2(getStyles); + const tourSteps = getTourSteps(); + const isLastStep = currentStep === tourSteps.length - 1; + const isFirstStep = currentStep === 0; + + const handleNext = () => { + if (isLastStep) { + handleComplete(); + } else { + setIsTransitioning(true); + setTimeout(() => { + setCurrentStep(currentStep + 1); + setIsTransitioning(false); + }, 150); + } + }; + + const handleBack = () => { + setIsTransitioning(true); + setTimeout(() => { + setCurrentStep(currentStep - 1); + setIsTransitioning(false); + }, 150); + }; + + const handleComplete = () => { + store.set(TOUR_STORAGE_KEY, true); + onDismiss(); + }; + + const currentTourStep = tourSteps[currentStep]; + + return ( + + +
+ {currentTourStep.content} +
+ +
+ + {/* Progress indicator */} + + {tourSteps.map((_, index) => ( +
+ ))} + + + {/* Step counter */} + + + Step {{ current: currentStep + 1 }} of {{ total: tourSteps.length }} + + + +
+
+ + + + + {!isFirstStep && ( + + )} + + + + + ); +}; + +/** + * Hook to check if the tour should be shown for first-time users + */ +export const useCorrelationEditorTour = () => { + const [shouldShowTour, setShouldShowTour] = useState(false); + + useEffect(() => { + const hasCompletedTour = store.getBool(TOUR_STORAGE_KEY, false); + if (!hasCompletedTour) { + // Small delay to let the UI settle before showing the tour + const timer = setTimeout(() => { + setShouldShowTour(true); + }, 500); + return () => clearTimeout(timer); + } + return undefined; + }, []); + + const dismissTour = () => { + setShouldShowTour(false); + }; + + return { shouldShowTour, dismissTour }; +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + contentVisible: css({ + opacity: 1, + [theme.transitions.handleMotion('no-preference')]: { + transform: 'translateY(0)', + transition: 'opacity 0.2s ease, transform 0.2s ease', + }, + }), + contentTransitioning: css({ + opacity: 0, + [theme.transitions.handleMotion('no-preference')]: { + transform: 'translateY(-8px)', + transition: 'opacity 0.15s ease, transform 0.15s ease', + }, + }), + progressContainer: css({ + marginTop: theme.spacing(2), + }), + progressDot: css({ + width: '8px', + height: '8px', + borderRadius: theme.shape.radius.circle, + backgroundColor: theme.colors.border.medium, + [theme.transitions.handleMotion('no-preference')]: { + transition: 'all 0.2s ease', + }, + }), + progressDotActive: css({ + width: '10px', + height: '10px', + borderRadius: theme.shape.radius.circle, + backgroundColor: theme.colors.primary.main, + [theme.transitions.handleMotion('no-preference')]: { + transition: 'all 0.2s ease', + }, + }), +}); diff --git a/public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationFormCustomVariables.tsx b/public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationFormCustomVariables.tsx new file mode 100644 index 00000000000..e32944f0984 --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationFormCustomVariables.tsx @@ -0,0 +1,104 @@ +import { css } from '@emotion/css'; +import { useMemo } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { Button, DeleteButton, IconButton, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui'; + +import { CorrelationFormCustomVariablesProps } from '../types'; + +import { FormSection } from './FormSection'; + +export const CorrelationFormCustomVariables = ({ + correlations, + transformations, + handlers, +}: CorrelationFormCustomVariablesProps) => { + const styles = useStyles2(getStyles); + + const transformationMap = useMemo( + () => new Map(transformations.map((t, idx) => [t.mapValue, idx])), + [transformations] + ); + + return ( + Variables (optional)}> + + + Use these variables in your target query. When a correlation link is clicked, each variable is filled in with + its value from that row. + + + + {Object.entries(correlations.vars).map(([name, value]) => { + // Check if this is a custom variable (not in origVars) + const isCustomVariable = !(name in correlations.origVars); + const transformationIdx = transformationMap.get(name); + + return ( + + + ${`{${name}}`} + + {value} + + + + {isCustomVariable && transformationIdx !== undefined && ( + + handlers.onEdit(transformationIdx)} + /> + handlers.onDelete(transformationIdx)} + closeOnConfirm + /> + + )} + + ); + })} + + + + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + variableName: css({ + backgroundColor: theme.colors.background.secondary, + padding: theme.spacing(0.5, 1), + borderRadius: theme.shape.radius.default, + fontFamily: theme.typography.fontFamilyMonospace, + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.primary.text, + fontWeight: theme.typography.fontWeightMedium, + whiteSpace: 'nowrap', + }), + variableValue: css({ + backgroundColor: theme.colors.background.secondary, + padding: theme.spacing(0.5, 1), + borderRadius: theme.shape.radius.default, + fontFamily: theme.typography.fontFamilyMonospace, + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.text.primary, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + maxWidth: '400px', + }), + addButton: css({ + alignSelf: 'flex-start', + }), +}); diff --git a/public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationFormInformation.tsx b/public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationFormInformation.tsx new file mode 100644 index 00000000000..b85c80622d9 --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationFormInformation.tsx @@ -0,0 +1,92 @@ +import { useId, useMemo } from 'react'; +import { Controller } from 'react-hook-form'; + +import { Trans, t } from '@grafana/i18n'; +import { Combobox, ComboboxOption, Field, Input } from '@grafana/ui'; + +import { CorrelationType, CorrelationFormInformationProps } from '../types'; + +import { FormSection } from './FormSection'; + +export const CorrelationFormInformation = ({ + control, + register, + getValues, + setValue, + defaultLabel, + selectedType, +}: CorrelationFormInformationProps) => { + const id = useId(); + + const typeOptions: Array> = useMemo( + () => [ + { + label: t('explore.correlation-form-information.type-options.label.explore-query', 'Explore Query'), + value: CorrelationType.ExploreQuery, + }, + { + label: t('explore.correlation-form-information.type-options.label.link', 'Link'), + value: CorrelationType.Link, + }, + ], + [] + ); + + return ( + Correlation Info}> + + { + return ( + onChange(option?.value || CorrelationType.ExploreQuery)} + /> + ); + }} + /> + + + {selectedType === CorrelationType.Link && ( + + + + )} + + + { + if (getValues('label') === '' && defaultLabel !== undefined) { + setValue('label', defaultLabel); + } + }} + /> + + + + + + ); +}; diff --git a/public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationHelper.tsx b/public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationHelper.tsx new file mode 100644 index 00000000000..d780c15dd6d --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/CorrelationHelper/CorrelationHelper.tsx @@ -0,0 +1,220 @@ +import { css } from '@emotion/css'; +import { useEffect, useState } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; +import { useAsync } from 'react-use'; + +import { DataLinkTransformationConfig, GrafanaTheme2 } from '@grafana/data'; +import { Trans } from '@grafana/i18n'; +import { Icon, Stack, Text, 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 { CorrelationTransformationAddModal } from '../CorrelationTransformationAddModal'; +import { CorrelationHelperProps, CorrelationType, FormValues, TransformationHandlers } from '../types'; + +import { CorrelationFormCustomVariables } from './CorrelationFormCustomVariables'; +import { CorrelationFormInformation } from './CorrelationFormInformation'; + +export const CorrelationHelper = ({ exploreId, correlations }: CorrelationHelperProps) => { + 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 { control, register, watch, getValues, setValue } = useForm({ + defaultValues: { type: CorrelationType.ExploreQuery }, + }); + + const selectedType = useWatch({ control, name: 'type' }); + + const [showTransformationAddModal, setShowTransformationAddModal] = useState(false); + const [transformations, setTransformations] = useState([]); + const [transformationIdxToEdit, setTransformationIdxToEdit] = useState(undefined); + const correlationDetails = useSelector(selectCorrelationDetails); + + const transformationHandlers: TransformationHandlers = { + onEdit: (index: number) => { + setTransformationIdxToEdit(index); + setShowTransformationAddModal(true); + }, + onDelete: (index: number) => { + setTransformations((prev) => prev.filter((_, idx) => idx !== index)); + }, + onAdd: () => { + setShowTransformationAddModal(true); + }, + onModalCancel: () => { + setTransformationIdxToEdit(undefined); + setShowTransformationAddModal(false); + }, + onModalSave: (transformation: DataLinkTransformationConfig) => { + if (transformationIdxToEdit !== undefined) { + const editTransformations = [...transformations]; + editTransformations[transformationIdxToEdit] = transformation; + setTransformations(editTransformations); + setTransformationIdxToEdit(undefined); + } else { + setTransformations([...transformations, transformation]); + } + setShowTransformationAddModal(false); + }, + }; + + // 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 ( + <> + +
+ + + + {selectedType === CorrelationType.Link ? ( + + When saved, the {'{{resultField}}'} field will have a clickable link that opens the + specified URL. + + ) : ( + + When saved, the {'{{resultField}}'} field will have a clickable link that runs your + target query below. + + )} + + +
+ + {selectedType === CorrelationType.ExploreQuery && ( + + )} +
+ +
+ {showTransformationAddModal && selectedType === CorrelationType.ExploreQuery && ( + + )} + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + infoBox: css({ + padding: theme.spacing(1.5), + backgroundColor: theme.colors.background.secondary, + borderRadius: theme.shape.radius.default, + border: `1px solid ${theme.colors.border.weak}`, + }), + infoIcon: css({ + color: theme.colors.info.text, + marginTop: theme.spacing(0.25), + }), + divider: css({ + height: '1px', + backgroundColor: theme.colors.border.weak, + margin: theme.spacing(2, 0), + }), +}); diff --git a/public/app/features/explore/CorrelationEditor/CorrelationHelper/FormSection.tsx b/public/app/features/explore/CorrelationEditor/CorrelationHelper/FormSection.tsx new file mode 100644 index 00000000000..554398e88f7 --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/CorrelationHelper/FormSection.tsx @@ -0,0 +1,30 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Stack, Text, useStyles2 } from '@grafana/ui'; + +import { FormSectionProps } from '../types'; + +export const FormSection = ({ title, children }: FormSectionProps) => { + const styles = useStyles2(getStyles); + + return ( + + {title} +
+ + {children} + +
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + formFieldsWrapper: css({ + padding: theme.spacing(2), + backgroundColor: theme.colors.background.secondary, + border: `1px solid ${theme.colors.border.weak}`, + borderRadius: theme.shape.radius.default, + }), +}); diff --git a/public/app/features/explore/CorrelationEditor/CorrelationTransformationAddModal.tsx b/public/app/features/explore/CorrelationEditor/CorrelationTransformationAddModal.tsx new file mode 100644 index 00000000000..190853f005a --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/CorrelationTransformationAddModal.tsx @@ -0,0 +1,279 @@ +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, Text } 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 CorrelationTransformationAddModal = ({ + 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 ( + + + + ( + { + 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} + /> + + {exampleValue && ( + <> + {formFieldsVis.mapValueDetails.show && ( + + ) : ( + t( + 'explore.correlation-transformation-add-modal.label-variable-name-without-tooltip', + 'Variable name' + ) + ) + } + htmlFor={`${id}-mapValue`} + > + + + )} + {formFieldsVis.expressionDetails.show && ( + + ) : ( + t('explore.correlation-transformation-add-modal.label-expression-without-tooltip', 'Expression') + ) + } + htmlFor={`${id}-expression`} + required={formFieldsVis.expressionDetails.required} + > + + + )} + + + + Example value for your variable: + + +
+                
+              
+
+ + {Object.entries(transformationVars).length > 0 && ( + <> + + This custom variable will add the following variables: + +
+                  {Object.entries(transformationVars).map((entry) => {
+                    return `\$\{${entry[0]}\} = ${entry[1]?.value}\n`;
+                  })}
+                
+ + )} + + )} + + + + +
+
+ ); +}; diff --git a/public/app/features/explore/CorrelationUnsavedChangesModal.tsx b/public/app/features/explore/CorrelationEditor/CorrelationUnsavedChangesModal.tsx similarity index 100% rename from public/app/features/explore/CorrelationUnsavedChangesModal.tsx rename to public/app/features/explore/CorrelationEditor/CorrelationUnsavedChangesModal.tsx diff --git a/public/app/features/explore/correlationEditLogic.test.ts b/public/app/features/explore/CorrelationEditor/correlationEditLogic.test.ts similarity index 100% rename from public/app/features/explore/correlationEditLogic.test.ts rename to public/app/features/explore/CorrelationEditor/correlationEditLogic.test.ts diff --git a/public/app/features/explore/correlationEditLogic.ts b/public/app/features/explore/CorrelationEditor/correlationEditLogic.ts similarity index 100% rename from public/app/features/explore/correlationEditLogic.ts rename to public/app/features/explore/CorrelationEditor/correlationEditLogic.ts diff --git a/public/app/features/explore/CorrelationEditor/types.ts b/public/app/features/explore/CorrelationEditor/types.ts new file mode 100644 index 00000000000..78c991fed39 --- /dev/null +++ b/public/app/features/explore/CorrelationEditor/types.ts @@ -0,0 +1,49 @@ +import { ReactNode } from 'react'; +import { Control, UseFormGetValues, UseFormRegister, UseFormSetValue } from 'react-hook-form'; + +import { DataLinkTransformationConfig, ExploreCorrelationHelperData } from '@grafana/data'; + +export enum CorrelationType { + ExploreQuery = 'Explore Query', + Link = 'Link', +} + +export interface CorrelationHelperProps { + exploreId: string; + correlations: ExploreCorrelationHelperData; +} + +export interface FormValues { + type: CorrelationType; + label: string; + description: string; + url?: string; +} + +export interface TransformationHandlers { + onEdit: (index: number) => void; + onDelete: (index: number) => void; + onAdd: () => void; + onModalCancel: () => void; + onModalSave: (transformation: DataLinkTransformationConfig) => void; +} + +export interface CorrelationFormInformationProps { + control: Control; + register: UseFormRegister; + getValues: UseFormGetValues; + setValue: UseFormSetValue; + defaultLabel: string | undefined; + selectedType: CorrelationType; +} + +export interface CorrelationFormCustomVariablesProps { + correlations: ExploreCorrelationHelperData; + transformations: DataLinkTransformationConfig[]; + handlers: TransformationHandlers; +} + +export interface FormSectionProps { + title: JSX.Element; + children: ReactNode; +} diff --git a/public/app/features/explore/CorrelationHelper.tsx b/public/app/features/explore/CorrelationHelper.tsx deleted file mode 100644 index 4b54a9ab4f0..00000000000 --- a/public/app/features/explore/CorrelationHelper.tsx +++ /dev/null @@ -1,386 +0,0 @@ -import { css } from '@emotion/css'; -import { useEffect, useId, useState } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { useAsync } from 'react-use'; - -import { DataLinkTransformationConfig, ExploreCorrelationHelperData, GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { Trans, t } from '@grafana/i18n'; -import { - Alert, - Button, - Card, - Collapse, - DeleteButton, - Field, - Icon, - IconButton, - Input, - Select, - Stack, - Tooltip, - useStyles2, -} from '@grafana/ui'; -import { useDispatch, useSelector } from 'app/types/store'; - -import { getTransformationVars } from '../correlations/transformations'; -import { generateDefaultLabel } from '../correlations/utils'; - -import { CorrelationTransformationAddModal } from './CorrelationTransformationAddModal'; -import { changeCorrelationHelperData } from './state/explorePane'; -import { changeCorrelationEditorDetails } from './state/main'; -import { selectCorrelationDetails, selectPanes } from './state/selectors'; - -interface Props { - exploreId: string; - correlations: ExploreCorrelationHelperData; -} - -interface FormValues { - type: string; - label: string; - description: string; -} - -export const CorrelationHelper = ({ 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 { control, register, watch, getValues, setValue } = useForm({ - defaultValues: { - type: 'Link', - }, - }); - 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 - } - /> - )} - -
-
- {/* */} -

- When saved, the {correlations.resultField} field will have a clickable link that runs your target - query below. -

- {/*
*/} -
-
-
- -

General

-
- - { - const typeOptions: Array> = [ - { label: 'Link', value: 'Link' }, - { label: 'Explore Query', value: 'Explore Query' }, - ]; - return ( - { - if (getValues('label') === '' && defaultLabel !== undefined) { - setValue('label', defaultLabel); - } - }} - /> - - - - - - {/* Transformations */} - Variables (optional) - - - - -

Use these variables in your target query. When a correlation link is clicked, each variable is filled in with its value from that row.

-
- - {Object.entries(correlations.vars).map(([name, value]) => ( -
-
- ${`{${name}}`} -
-
- - {value} - -
-
- ))} -
- - - {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', - }), - alertWrapper: css({ - '& > div': { - minWidth: 0, - maxWidth: '100%', - overflow: 'hidden', - }, - }), - alertContent: css({ - minWidth: 0, - maxWidth: '100%', - overflow: 'hidden', - width: '100%', - }), - variableList: css({ - marginTop: theme.spacing(1.5), - marginBottom: theme.spacing(2), - display: 'table', - width: '100%', - tableLayout: 'auto', - borderCollapse: 'collapse', - }), - variableRow: css({ - display: 'table-row', - }), - variableNameCell: css({ - display: 'table-cell', - width: '1%', - paddingBottom: theme.spacing(0.5), - paddingRight: theme.spacing(1), - verticalAlign: 'top', - whiteSpace: 'nowrap', - }), - variableName: css({ - display: 'inline-block', - backgroundColor: theme.colors.background.secondary, - padding: theme.spacing(0.5, 1), - borderRadius: theme.shape.radius.default, - fontFamily: theme.typography.fontFamilyMonospace, - fontSize: theme.typography.bodySmall.fontSize, - color: theme.colors.primary.text, - fontWeight: theme.typography.fontWeightMedium, - whiteSpace: 'nowrap', - }), - variableValueCell: css({ - display: 'table-cell', - width: '100%', - maxWidth: 0, - paddingBottom: theme.spacing(0.5), - verticalAlign: 'top', - }), - variableValue: css({ - display: 'inline-block', - backgroundColor: theme.colors.background.secondary, - padding: theme.spacing(0.5, 1), - borderRadius: theme.shape.radius.default, - fontFamily: theme.typography.fontFamilyMonospace, - fontSize: theme.typography.bodySmall.fontSize, - color: theme.colors.text.primary, - maxWidth: '100%', - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - verticalAlign: 'top', - }), - }; -}; diff --git a/public/app/features/explore/CorrelationTransformationAddModal.tsx b/public/app/features/explore/CorrelationTransformationAddModal.tsx deleted file mode 100644 index 7f1ca843d81..00000000000 --- a/public/app/features/explore/CorrelationTransformationAddModal.tsx +++ /dev/null @@ -1,260 +0,0 @@ -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 CorrelationTransformationAddModal = ({ - 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/Explore.tsx b/public/app/features/explore/Explore.tsx index f8bd275930b..e671caf33d0 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -40,7 +40,7 @@ import { getTimeZone } from '../profile/state/selectors'; import { CONTENT_OUTLINE_LOCAL_STORAGE_KEYS, ContentOutline } from './ContentOutline/ContentOutline'; import { ContentOutlineContextProvider } from './ContentOutline/ContentOutlineContext'; import { ContentOutlineItem } from './ContentOutline/ContentOutlineItem'; -import { CorrelationHelper } from './CorrelationHelper'; +import { CorrelationHelper } from './CorrelationEditor/CorrelationHelper/CorrelationHelper'; import { CustomContainer } from './CustomContainer'; import { ExploreToolbar } from './ExploreToolbar'; import { FlameGraphExploreContainer } from './FlameGraph/FlameGraphExploreContainer'; @@ -626,7 +626,7 @@ export class Explore extends PureComponent { exploreId={exploreId} onChangeTime={this.onChangeTime} onContentOutlineToogle={this.onContentOutlineToogle} - isContentOutlineOpen={contentOutlineVisible} + isContentOutlineOpen={contentOutlineVisible && !showCorrelationHelper} />
{ >
{contentOutlineVisible && !compact && ( - + )} - +
)} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index e15e5c749e5..70307a1c6bb 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -7010,38 +7010,82 @@ }, "correlation-editor-mode-bar": { "content-correlations-editor-explore-experimental-feature": "Correlations editor in Explore is an experimental feature.", + "edit-mode-title": "Correlation Editor Mode", "exit-correlation-editor": "Exit correlation editor", + "experimental": "Experimental", + "instructions": "Step 1: Run a query and click a table cell link or a \"🔗 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" }, + "correlation-form-information": { + "label-description": "Description", + "label-name": "Name", + "label-type": "Type", + "label-url": "URL", + "type-options": { + "label": { + "explore-query": "Explore Query", + "link": "Link" + } + }, + "url-description": "Specify the URL that will open when the link is clicked", + "url-placeholder": "https://example.com" + }, "correlation-helper": { - "add-transformation": "Add transformation", + "add-custom-variable": "Add custom variable", "aria-label-delete-transformation": "Delete transformation", "aria-label-edit-transformation": "Edit transformation", - "body-correlation-details": "When saved, the <1>{{resultField}} field will have a clickable link that runs your target query below. Use the below variables in your query. When clicked, they're replaced with values from that row.", - "expression": "Expression: <1>{{expression}}", - "label-description": "Description", - "label-description-header": "Correlation Label / Description", - "label-label": "Label", - "title-correlation-details": "Correlation details", - "tooltip-transformations": "A transformation extracts one or more variables out of a single field.", - "transformations": "Transformations" + "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.", + "title-correlation-info": "Correlation Info", + "title-variables": "Variables (optional)" + }, + "correlation-tour": { + "back": "Back", + "current-step": "Current step {{step}}", + "got-it": "Got it!", + "next": "Next", + "ready-body": "You now know the basics of creating correlations. Remember, you can exit the editor at any time by clicking the Exit correlation editor button.", + "ready-tip1": "Test your correlation query thoroughly before saving", + "ready-tip2": "Use clear, descriptive names so other users understand the link", + "ready-tip3": "Custom variables let you extract specific parts of field values", + "ready-tips-title": "Quick Tips:", + "ready-title": "You're All Set!", + "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-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.", + "step2-title": "Step 2: Build Your Target Query", + "step2-variables": "Available variables are shown in the \"Variables\" section below. You can also create custom variables by extracting parts of fields using regular expressions or logfmt.", + "step3-body": "Once your query works correctly, click the Save button . Give your correlation a name and optionally add a description.", + "step3-result": "After saving, this correlation link will appear for all users in the same field across all queries from your source data source!", + "step3-title": "Step 3: Save Your Correlation", + "welcome-body": "The Correlation Editor helps you create clickable links between different data sources in Grafana. This makes it easy to jump from one view to another with context preserved.", + "welcome-example": "For example, you can click a service name in your logs and automatically open a dashboard showing metrics for that service.", + "welcome-title": "Welcome to the Correlation Editor" }, "correlation-transformation-add-modal": { "add-transformation": "Add transformation to correlation", - "added-variables": "This transformation will add the following variables:", + "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", + "example-value": "Example value for your variable:", "label-expression": "Expression", "label-expression-without-tooltip": "Expression", "label-field": "Field", "label-type": "Type", "label-variable-name": "Variable name", "label-variable-name-without-tooltip": "Variable name", - "title-add": "Add transformation", - "title-edit": "Edit transformation" + "title-add": "Add custom variable", + "title-edit": "Edit custom variable" }, "correlation-unsaved-changes-modal": { "cancel": "Cancel", @@ -7487,6 +7531,7 @@ "add-to-extensions": "Add", "add-to-queryless-extensions": "Go queryless", "aria-label": "Explore toolbar", + "build-correlation-query": "Build and test your correlation target query here", "copy-link": "Copy URL", "copy-link-abs-time": "Copy absolute URL", "copy-links-absolute-category": "Time-sync URL links (share with time range intact)", @@ -7497,12 +7542,15 @@ "copy-shortened-link-menu": "Open copy link options", "refresh-picker-cancel": "Cancel", "refresh-picker-run": "Run query", + "source-correlation-query": "Run a query and click a correlation link to start building your correlation", + "source-query": "Source query", "split-close": "Close", "split-close-tooltip": "Close split pane", "split-narrow": "Narrow pane", "split-title": "Split", "split-tooltip": "Split the pane", - "split-widen": "Widen pane" + "split-widen": "Widen pane", + "target-query-builder": "Correlation" }, "trace-page-header": { "aria-label-share-dropdown": "Open share trace options menu",