feat: incorporate feature flag

This commit is contained in:
Alex Spencer
2025-11-26 13:05:57 -08:00
parent 38c826fd49
commit aca15a9571
14 changed files with 1012 additions and 33 deletions
+5
View File
@@ -1250,4 +1250,9 @@ export interface FeatureToggles {
* @default false
*/
kubernetesAnnotations?: boolean;
/**
* Enables the new correlations editor in Explore
* @default false
*/
correlationsExploreEditor?: boolean;
}
+8
View File
@@ -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",
},
}
)
+1
View File
@@ -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
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
278 panelTimeSettings experimental @grafana/dashboards-squad false false false
279 dashboardTemplates experimental @grafana/sharing-squad false false false
280 kubernetesAnnotations experimental @grafana/grafana-backend-services-squad false false false
281 correlationsExploreEditor experimental @grafana/datapro false false true
+4
View File
@@ -1121,4 +1121,8 @@ const (
// FlagKubernetesAnnotations
// Enables app platform API for annotations
FlagKubernetesAnnotations = "kubernetesAnnotations"
// FlagCorrelationsExploreEditor
// Enables the new correlations editor in Explore
FlagCorrelationsExploreEditor = "correlationsExploreEditor"
)
+14
View File
@@ -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",
@@ -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' })}
@@ -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<string | undefined>(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 */}
<Prompt
message={(location) => {
if (
location.pathname !== '/explore' &&
correlationDetails?.editorMode &&
correlationDetails?.correlationDirty
) {
return 'You have unsaved correlation data. Continue?';
} else {
return true;
}
}}
/>
{saveMessage !== undefined && (
<CorrelationUnsavedChangesModalLegacy
onDiscard={() => {
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}
/>
)}
<div className={styles.correlationEditorTop}>
<Stack gap={2} justifyContent="flex-end" alignItems="center">
<Tooltip
content={t(
'explore.correlation-editor-mode-bar.content-correlations-editor-explore-experimental-feature',
'Correlations editor in Explore is an experimental feature.'
)}
>
<Icon className={styles.iconColor} name="info-circle" size="xl" />
</Tooltip>
<Button
variant="secondary"
disabled={!correlationDetails?.canSave}
fill="outline"
className={correlationDetails?.canSave ? styles.buttonColor : styles.disabledButtonColor}
onClick={() => {
saveCorrelationPostAction(true);
}}
>
<Trans i18nKey="explore.correlation-editor-mode-bar.save">Save</Trans>
</Button>
<Button
variant="secondary"
fill="outline"
className={styles.buttonColor}
icon="times"
onClick={() => {
dispatch(changeCorrelationEditorDetails({ isExiting: true }));
reportInteraction('grafana_explore_correlation_editor_exit_pressed');
}}
>
<Trans i18nKey="explore.correlation-editor-mode-bar.exit-correlation-editor">Exit correlation editor</Trans>
</Button>
</Stack>
</div>
</>
);
};
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`,
}),
};
};
@@ -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<FormValues>();
const [isLabelDescOpen, setIsLabelDescOpen] = useState(false);
const [isTransformOpen, setIsTransformOpen] = useState(false);
const [showTransformationAddModal, setShowTransformationAddModal] = useState(false);
const [transformations, setTransformations] = useState<DataLinkTransformationConfig[]>([]);
const [transformationIdxToEdit, setTransformationIdxToEdit] = useState<number | undefined>(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<string, string> = {};
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 && (
<CorrelationTransformationAddModalLegacy
onCancel={() => {
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
}
/>
)}
<Alert title={t('explore.correlation-helper.title-correlation-details', 'Correlation details')} severity="info">
<Trans
i18nKey="explore.correlation-helper.body-correlation-details"
values={{ resultField: correlations.resultField }}
>
The correlation link will appear by the <code>{'{{resultField}}'}</code> field. You can use the following
variables to set up your correlations:
</Trans>
<pre>
{Object.entries(correlations.vars).map((entry) => {
return `\$\{${entry[0]}\} = ${entry[1]}\n`;
})}
</pre>
<Collapse
isOpen={isLabelDescOpen}
onToggle={() => {
setIsLabelDescOpen(!isLabelDescOpen);
}}
label={
<Stack gap={1} direction="row" wrap="wrap" alignItems="center">
<Trans i18nKey="explore.correlation-helper.label-description-header">Label / Description</Trans>
{!isLabelDescOpen && !loadingLabel && (
<span className={styles.labelCollapseDetails}>{`Label: ${getValues('label') || defaultLabel}`}</span>
)}
</Stack>
}
>
<Field noMargin label={t('explore.correlation-helper.label-label', 'Label')} htmlFor={`${id}-label`}>
<Input
{...register('label')}
id={`${id}-label`}
onBlur={() => {
if (getValues('label') === '' && defaultLabel !== undefined) {
setValue('label', defaultLabel);
}
}}
/>
</Field>
<Field
noMargin
label={t('explore.correlation-helper.label-description', 'Description')}
htmlFor={`${id}-description`}
>
<Input {...register('description')} id={`${id}-description`} />
</Field>
</Collapse>
<Collapse
isOpen={isTransformOpen}
onToggle={() => {
setIsTransformOpen(!isTransformOpen);
}}
label={
<Stack gap={1} direction="row" wrap="wrap" alignItems="center">
<Trans i18nKey="explore.correlation-helper.transformations">Transformations</Trans>
<Tooltip
content={t(
'explore.correlation-helper.tooltip-transformations',
'A transformation extracts one or more variables out of a single field.'
)}
>
<Icon name="info-circle" size="sm" />
</Tooltip>
</Stack>
}
>
<Button
variant="secondary"
fill="outline"
onClick={() => {
setShowTransformationAddModal(true);
}}
className={styles.transformationAction}
>
<Trans i18nKey="explore.correlation-helper.add-transformation">Add transformation</Trans>
</Button>
{transformations.map((transformation, i) => {
const { type, field, expression, mapValue } = transformation;
const detailsString = [
(mapValue ?? '').length > 0 ? `Variable name: ${mapValue}` : undefined,
(expression ?? '').length > 0 ? (
<Trans i18nKey="explore.correlation-helper.expression" values={{ expression }}>
Expression: <code>{'{{expression}}'}</code>
</Trans>
) : undefined,
].filter((val) => val);
return (
<Card noMargin key={`trans-${i}`}>
<Card.Heading>
{field}: {type}
</Card.Heading>
{detailsString.length > 0 && (
<Card.Meta className={styles.transformationMeta}>{detailsString}</Card.Meta>
)}
<Card.SecondaryActions>
<IconButton
key="edit"
name="edit"
aria-label={t('explore.correlation-helper.aria-label-edit-transformation', 'Edit transformation')}
onClick={() => {
setTransformationIdxToEdit(i);
setShowTransformationAddModal(true);
}}
/>
<DeleteButton
aria-label={t(
'explore.correlation-helper.aria-label-delete-transformation',
'Delete transformation'
)}
onConfirm={() => setTransformations(transformations.filter((_, idx) => i !== idx))}
closeOnConfirm
/>
</Card.SecondaryActions>
</Card>
);
})}
</Collapse>
</Alert>
</>
);
};
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',
}),
};
};
@@ -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<string, string>;
transformationToEdit?: DataLinkTransformationConfig;
}
interface ShowFormFields {
expressionDetails: TransformationFieldDetails;
mapValueDetails: TransformationFieldDetails;
}
const LabelWithTooltip = ({ label, tooltipText }: { label: string; tooltipText: string }) => (
<Stack gap={1} direction="row" wrap="wrap" alignItems="flex-start">
<Label>{label}</Label>
<Tooltip content={tooltipText}>
<Icon name="info-circle" size="sm" />
</Tooltip>
</Stack>
);
export const CorrelationTransformationAddModalLegacy = ({
onSave,
onCancel,
fieldList,
transformationToEdit,
}: CorrelationTransformationAddModalProps) => {
const [exampleValue, setExampleValue] = useState<string | undefined>(undefined);
const [transformationVars, setTransformationVars] = useState<ScopedVars>({});
const [formFieldsVis, setFormFieldsVis] = useState<ShowFormFields>({
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<DataLinkTransformationConfig>({
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 (
<Modal
isOpen={true}
title={
transformationToEdit
? t('explore.correlation-transformation-add-modal.title-edit', 'Edit transformation')
: t('explore.correlation-transformation-add-modal.title-add', 'Add transformation')
}
onDismiss={onCancel}
className={css({ width: '700px' })}
>
<p>
<Trans i18nKey="explore.correlation-transformation-add-modal.body">
A transformation extracts variables out of a single field. These variables will be available along with your
field variables.
</Trans>
</p>
<Field noMargin label={t('explore.correlation-transformation-add-modal.label-field', 'Field')}>
<Controller
control={control}
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
onChange={(value) => {
if (value.value) {
onChange(value.value);
setExampleValue(fieldList[value.value]);
}
}}
options={Object.entries(fieldList).map((entry) => {
return { label: entry[0], value: entry[0] };
})}
aria-label={t('explore.correlation-transformation-add-modal.aria-label-field', 'Field')}
/>
)}
name={`field` as const}
/>
</Field>
{exampleValue && (
<>
<pre>
<Highlighter
textToHighlight={exampleValue}
searchWords={[isExpValid ? (getValues('expression') ?? '') : '']}
autoEscape={false}
/>
</pre>
<Field noMargin label={t('explore.correlation-transformation-add-modal.label-type', 'Type')}>
<Controller
control={control}
render={({ field: { onChange, ref, ...field } }) => (
<Select
{...field}
onChange={(value) => {
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}
/>
</Field>
{formFieldsVis.expressionDetails.show && (
<Field
noMargin
label={
formFieldsVis.expressionDetails.helpText ? (
<LabelWithTooltip
label={t('explore.correlation-transformation-add-modal.label-expression', 'Expression')}
tooltipText={formFieldsVis.expressionDetails.helpText}
/>
) : (
t('explore.correlation-transformation-add-modal.label-expression-without-tooltip', 'Expression')
)
}
htmlFor={`${id}-expression`}
required={formFieldsVis.expressionDetails.required}
>
<Input {...register('expression')} id={`${id}-expression`} />
</Field>
)}
{formFieldsVis.mapValueDetails.show && (
<Field
noMargin
label={
formFieldsVis.mapValueDetails.helpText ? (
<LabelWithTooltip
label={t('explore.correlation-transformation-add-modal.label-variable-name', 'Variable name')}
tooltipText={formFieldsVis.mapValueDetails.helpText}
/>
) : (
t('explore.correlation-transformation-add-modal.label-variable-name-without-tooltip', 'Variable name')
)
}
htmlFor={`${id}-mapValue`}
>
<Input {...register('mapValue')} id={`${id}-mapValue`} />
</Field>
)}
{Object.entries(transformationVars).length > 0 && (
<>
<Trans i18nKey="explore.correlation-transformation-add-modal.added-variables">
This transformation will add the following variables:
</Trans>
<pre>
{Object.entries(transformationVars).map((entry) => {
return `\$\{${entry[0]}\} = ${entry[1]?.value}\n`;
})}
</pre>
</>
)}
</>
)}
<Modal.ButtonRow>
<Button variant="secondary" onClick={onCancel} fill="outline">
<Trans i18nKey="explore.correlation-transformation-add-modal.cancel">Cancel</Trans>
</Button>
<Button variant="primary" onClick={() => onSave(getValues())} disabled={!validToSave}>
{transformationToEdit
? t('explore.correlation-transformation-add-modal.edit-transformation', 'Edit transformation')
: t('explore.correlation-transformation-add-modal.add-transformation', 'Add transformation to correlation')}
</Button>
</Modal.ButtonRow>
</Modal>
);
};
@@ -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 (
<Modal
isOpen={true}
title={t(
'explore.correlation-unsaved-changes-modal.title-unsaved-changes-to-correlation',
'Unsaved changes to correlation'
)}
onDismiss={onCancel}
icon="exclamation-triangle"
className={css({ width: '600px' })}
>
<h5>{message}</h5>
<Modal.ButtonRow>
<Button variant="secondary" onClick={onCancel} fill="outline">
<Trans i18nKey="explore.correlation-unsaved-changes-modal.cancel">Cancel</Trans>
</Button>
<Button variant="destructive" onClick={onDiscard}>
<Trans i18nKey="explore.correlation-unsaved-changes-modal.continue-without-saving">
Continue without saving
</Trans>
</Button>
<Button variant="primary" onClick={onSave}>
<Trans i18nKey="explore.correlation-unsaved-changes-modal.save-correlation">Save correlation</Trans>
</Button>
</Modal.ButtonRow>
</Modal>
);
};
+8 -2
View File
@@ -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<Props, ExploreState> {
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<Props, ExploreState> {
const isCorrelationsEditorMode = correlationEditorDetails?.editorMode;
const showCorrelationHelper = Boolean(isCorrelationsEditorMode || correlationEditorDetails?.correlationDirty);
if (showCorrelationHelper && correlationEditorHelperData !== undefined) {
correlationsBox = <CorrelationHelper exploreId={exploreId} correlations={correlationEditorHelperData} />;
correlationsBox = correlationsExploreEditor ? (
<CorrelationHelper exploreId={exploreId} correlations={correlationEditorHelperData} />
) : (
<CorrelationHelperLegacy exploreId={exploreId} correlations={correlationEditorHelperData} />
);
}
return (
+13 -3
View File
@@ -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
<Trans i18nKey="nav.explore.title" />
</h1>
<ExploreActions />
{showCorrelationEditorBar && <CorrelationEditorModeBar panes={panes} />}
{showCorrelationEditorBar &&
(correlationsExploreEditor ? (
<CorrelationEditorModeBar panes={panes} />
) : (
<CorrelationEditorModeBarLegacy panes={panes} />
))}
<SplitPaneWrapper
splitOrientation="vertical"
paneSize={widthCalc}
@@ -109,7 +116,7 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
);
}
const getStyles = (theme: GrafanaTheme2) => {
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}`,
}),
};
};
+33 -21
View File
@@ -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
<ShortLinkButtonMenu key="share" />,
];
const correlationsExploreEditor = config.featureToggles.correlationsExploreEditor;
const showBuilderIndicator = isCorrelationsEditorMode && !isLeftPane;
const showSourceIndicator = isCorrelationsEditorMode && isLeftPane && splitted;
@@ -232,26 +233,37 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
<div>
{refreshInterval && <SetInterval func={onRunQuery} interval={refreshInterval} loading={loading} />}
<AppChromeUpdate actions={navBarActions} />
{showSourceIndicator && (
<div className={styles.badgeWrapper}>
<Tooltip
content={t(
'explore.toolbar.source-correlation-query',
'Run a query and click a correlation link to start building your correlation'
)}
>
<Badge color="blue" icon="arrow-from-right" text={t('explore.toolbar.source-query', 'Source query')} />
</Tooltip>
</div>
)}
{showBuilderIndicator && (
<div className={styles.badgeWrapper}>
<Tooltip
content={t('explore.toolbar.build-correlation-query', 'Build and test your correlation target query here')}
>
<Badge color="orange" icon="crosshair" text={t('explore.toolbar.target-query-builder', 'Correlation')} />
</Tooltip>
</div>
{correlationsExploreEditor && (
<>
{showSourceIndicator && (
<div className={styles.badgeWrapper}>
<Tooltip
content={t(
'explore.toolbar.source-correlation-query',
'Run a query and click a correlation link to start building your correlation'
)}
>
<Badge color="blue" icon="arrow-from-right" text={t('explore.toolbar.source-query', 'Source query')} />
</Tooltip>
</div>
)}
{showBuilderIndicator && (
<div className={styles.badgeWrapper}>
<Tooltip
content={t(
'explore.toolbar.build-correlation-query',
'Build and test your correlation target query here'
)}
>
<Badge
color="orange"
icon="crosshair"
text={t('explore.toolbar.target-query-builder', 'Correlation')}
/>
</Tooltip>
</div>
)}
</>
)}
<PageToolbar
aria-label={t('explore.toolbar.aria-label', 'Explore toolbar')}
+17 -5
View File
@@ -7013,7 +7013,7 @@
"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": "Step 1: Run a query and click a table cell link or a <1></1> <strong>Correlate with</strong> 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}}</1> field. You can use the following variables to set up your correlations:",
"body-correlation-details-link": "When saved, the <1>{{resultField}}</1> field will have a clickable link that opens the specified URL.",
"body-correlation-details-query": "When saved, the <1>{{resultField}}</1> 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}}</1>",
"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></2> <strong>Correlate with [field name]</strong> 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 <strong>right pane</strong> (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",