mirror of
https://github.com/grafana/grafana.git
synced 2026-01-15 05:35:41 +00:00
Compare commits
12 Commits
sriram/SQL
...
alexspence
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b9416a360 | ||
|
|
14d91eec01 | ||
|
|
3d6deb4656 | ||
|
|
7f30ecb494 | ||
|
|
7c11ea8ed5 | ||
|
|
bc3a3ba74e | ||
|
|
f251bb1070 | ||
|
|
aca15a9571 | ||
|
|
38c826fd49 | ||
|
|
bf1a8ab7c8 | ||
|
|
eaa5762778 | ||
|
|
b35a8a6c36 |
@@ -1189,4 +1189,9 @@ export interface FeatureToggles {
|
||||
* @default false
|
||||
*/
|
||||
rudderstackUpgrade?: boolean;
|
||||
/**
|
||||
* Enables the new correlations editor in Explore
|
||||
* @default false
|
||||
*/
|
||||
correlationsExploreEditor?: boolean;
|
||||
}
|
||||
|
||||
@@ -1963,6 +1963,14 @@ var (
|
||||
RequiresRestart: false,
|
||||
HideFromDocs: false,
|
||||
},
|
||||
{
|
||||
Name: "correlationsExploreEditor",
|
||||
Description: "Enables the new correlations editor in Explore",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaDataProSquad,
|
||||
FrontendOnly: true,
|
||||
Expression: "false",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -266,3 +266,4 @@ transformationsEmptyPlaceholder,preview,@grafana/datapro,false,false,true
|
||||
ttlPluginInstanceManager,experimental,@grafana/plugins-platform-backend,false,false,true
|
||||
lokiQueryLimitsContext,experimental,@grafana/observability-logs,false,false,true
|
||||
rudderstackUpgrade,experimental,@grafana/grafana-frontend-platform,false,false,true
|
||||
correlationsExploreEditor,experimental,@grafana/datapro,false,false,true
|
||||
|
||||
|
14
pkg/services/featuremgmt/toggles_gen.json
generated
14
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -923,6 +923,20 @@
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "correlationsExploreEditor",
|
||||
"resourceVersion": "1764191352059",
|
||||
"creationTimestamp": "2025-11-26T21:09:12Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the new correlations editor in Explore",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/datapro",
|
||||
"frontend": true,
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "crashDetection",
|
||||
|
||||
@@ -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 |
|
||||
<ContentOutlineItemButton
|
||||
icon={'arrow-from-right'}
|
||||
tooltip={
|
||||
contentOutlineExpanded
|
||||
isExpanded
|
||||
? t('explore.content-outline.tooltip-collapse-outline', 'Collapse outline')
|
||||
: t('explore.content-outline.tooltip-expand-outline', 'Expand outline')
|
||||
}
|
||||
tooltipPlacement={contentOutlineExpanded ? 'right' : 'bottom'}
|
||||
tooltipPlacement={isExpanded ? 'right' : 'bottom'}
|
||||
onClick={toggle}
|
||||
className={cx(styles.toggleContentOutlineButton, {
|
||||
[styles.justifyCenter]: !contentOutlineExpanded && !outlineItemsShouldIndent,
|
||||
[styles.justifyCenter]: !isExpanded && !outlineItemsShouldIndent,
|
||||
})}
|
||||
aria-expanded={contentOutlineExpanded}
|
||||
aria-expanded={isExpanded}
|
||||
/>
|
||||
|
||||
{outlineItems.map((item) => {
|
||||
@@ -177,16 +189,16 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
<Fragment key={item.id}>
|
||||
<ContentOutlineItemButton
|
||||
key={item.id}
|
||||
title={contentOutlineExpanded ? item.title : undefined}
|
||||
contentOutlineExpanded={contentOutlineExpanded}
|
||||
title={isExpanded ? item.title : undefined}
|
||||
contentOutlineExpanded={isExpanded}
|
||||
className={cx(styles.buttonStyles, {
|
||||
[styles.justifyCenter]: !contentOutlineExpanded && !outlineItemsHaveDeleteButton,
|
||||
[styles.sectionHighlighter]: isChildActive(item, activeSectionChildId) && !contentOutlineExpanded,
|
||||
[styles.justifyCenter]: !isExpanded && !outlineItemsHaveDeleteButton,
|
||||
[styles.sectionHighlighter]: isChildActive(item, activeSectionChildId) && !isExpanded,
|
||||
})}
|
||||
indentStyle={cx({
|
||||
[styles.indentRoot]: !isCollapsible(item) && outlineItemsShouldIndent,
|
||||
[styles.sectionHighlighter]:
|
||||
isChildActive(item, activeSectionChildId) && !contentOutlineExpanded && sectionsExpanded[item.id],
|
||||
isChildActive(item, activeSectionChildId) && !isExpanded && sectionsExpanded[item.id],
|
||||
})}
|
||||
icon={item.icon}
|
||||
onClick={() => handleItemClicked(item)}
|
||||
@@ -204,7 +216,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
sectionsExpanded[item.id] &&
|
||||
item.children.map((child, i) => (
|
||||
<div key={child.id} className={styles.itemWrapper}>
|
||||
{contentOutlineExpanded && (
|
||||
{isExpanded && (
|
||||
<div
|
||||
className={cx(styles.itemConnector, {
|
||||
[styles.firstItemConnector]: i === 0,
|
||||
@@ -214,13 +226,12 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
|
||||
)}
|
||||
<ContentOutlineItemButton
|
||||
key={child.id}
|
||||
title={contentOutlineExpanded ? child.title : undefined}
|
||||
contentOutlineExpanded={contentOutlineExpanded}
|
||||
icon={contentOutlineExpanded ? undefined : item.icon}
|
||||
title={isExpanded ? child.title : undefined}
|
||||
contentOutlineExpanded={isExpanded}
|
||||
icon={isExpanded ? undefined : item.icon}
|
||||
className={cx(styles.buttonStyles, {
|
||||
[styles.justifyCenter]: !contentOutlineExpanded && !outlineItemsHaveDeleteButton,
|
||||
[styles.sectionHighlighter]:
|
||||
isChildActive(item, activeSectionChildId) && !contentOutlineExpanded,
|
||||
[styles.justifyCenter]: !isExpanded && !outlineItemsHaveDeleteButton,
|
||||
[styles.sectionHighlighter]: isChildActive(item, activeSectionChildId) && !isExpanded,
|
||||
})}
|
||||
indentStyle={styles.indentChild}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBeforeUnload, useUnmount } from 'react-use';
|
||||
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
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';
|
||||
|
||||
export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, ExploreItemState]> }) => {
|
||||
const dispatch = useDispatch();
|
||||
const correlationDetails = useSelector(selectCorrelationDetails);
|
||||
const isHelperShowing = useSelector(selectIsHelperShowing);
|
||||
const [saveMessage, setSaveMessage] = useState<string | undefined>(undefined); // undefined means do not show
|
||||
const { shouldShowTour, dismissTour } = useCorrelationEditorTour();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Show tour for first-time users */}
|
||||
{shouldShowTour && <CorrelationEditorTour onDismiss={dismissTour} />}
|
||||
|
||||
{saveMessage !== undefined && (
|
||||
<CorrelationUnsavedChangesModal
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
<Alert title="" severity="info" bottomSpacing={0} topSpacing={0}>
|
||||
<Stack width="100%" alignItems="start" justifyContent="space-between">
|
||||
<Stack gap={1} direction="column">
|
||||
<Stack gap={2} alignItems="center" direction="row">
|
||||
<Text variant="h4">
|
||||
<Trans i18nKey="explore.correlation-editor-mode-bar.edit-mode-title">Correlation Editor Mode</Trans>
|
||||
</Text>
|
||||
<Badge
|
||||
color="orange"
|
||||
text={t('explore.correlation-editor-mode-bar.experimental', 'Experimental')}
|
||||
tooltip={t(
|
||||
'explore.correlation-editor-mode-bar.content-correlations-editor-explore-experimental-feature',
|
||||
'Correlations editor in Explore is an experimental feature.'
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack gap={0} direction="column">
|
||||
<Text variant="bodySmall">
|
||||
<Trans i18nKey="explore.correlation-editor-mode-bar.instructions">
|
||||
Step 1: Run a query and click a table cell link or a <Icon name="link" size="sm" />{' '}
|
||||
<strong>Correlate with</strong> button.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text variant="bodySmall">
|
||||
<Trans i18nKey="explore.correlation-editor-mode-bar.instructions-2">
|
||||
Step 2: In the right pane (Correlation), build and test your correlation query.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text variant="bodySmall">
|
||||
<Trans i18nKey="explore.correlation-editor-mode-bar.instructions-3">
|
||||
Step 3: Click Save to create the correlation.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack gap={1} alignItems="center">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!correlationDetails?.canSave}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
saveCorrelationPostAction(true);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="explore.correlation-editor-mode-bar.save">Save</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
icon="times"
|
||||
variant="secondary"
|
||||
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>
|
||||
</Stack>
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,310 @@
|
||||
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: (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text>
|
||||
<Trans i18nKey="explore.correlation-tour.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.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans i18nKey="explore.correlation-tour.welcome-example">
|
||||
For example, you can click a service name in your logs and automatically open a dashboard showing metrics
|
||||
for that service.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('explore.correlation-tour.step1-title', 'Step 1: Run a Query and Click a Link'),
|
||||
content: (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text>
|
||||
<Trans i18nKey="explore.correlation-tour.step1-body">
|
||||
Run a query that returns data. You can then click a link in a table cell, or use the{' '}
|
||||
<Icon name="link" size="sm" /> <strong>Correlate with [field name]</strong> button to start creating a
|
||||
correlation.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<Icon name="info-circle" />
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
<Trans i18nKey="explore.correlation-tour.step1-tip">
|
||||
Tip: Look for these correlation links in table cells or log lines
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('explore.correlation-tour.step2-title', 'Step 2: Build Your Target Query'),
|
||||
content: (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text>
|
||||
<Trans i18nKey="explore.correlation-tour.step2-body">
|
||||
After clicking a correlation link, the <strong>right pane</strong> (target) opens with a query editor. Build
|
||||
and test your query here.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans i18nKey="explore.correlation-tour.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.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('explore.correlation-tour.step3-title', 'Step 3: Save Your Correlation'),
|
||||
content: (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text>
|
||||
<Trans i18nKey="explore.correlation-tour.step3-body">
|
||||
Once your query works correctly, click the <strong>Save</strong> button . Give your correlation a name and
|
||||
optionally add a description.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans i18nKey="explore.correlation-tour.step3-result">
|
||||
After saving, this correlation link will appear for all users in the same field across all queries from your
|
||||
source data source!
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('explore.correlation-tour.ready-title', "You're All Set!"),
|
||||
content: (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text>
|
||||
<Trans i18nKey="explore.correlation-tour.ready-body">
|
||||
You now know the basics of creating correlations. Remember, you can exit the editor at any time by clicking
|
||||
the <strong>Exit correlation editor</strong> button.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Stack direction="column" gap={1}>
|
||||
<Text variant="h6">
|
||||
<Trans i18nKey="explore.correlation-tour.ready-tips-title">Quick Tips:</Trans>
|
||||
</Text>
|
||||
<Stack direction="row" gap={1}>
|
||||
<Icon name="check" />
|
||||
<Text variant="bodySmall">
|
||||
<Trans i18nKey="explore.correlation-tour.ready-tip1">
|
||||
Test your correlation query thoroughly before saving
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack direction="row" gap={1}>
|
||||
<Icon name="check" />
|
||||
<Text variant="bodySmall">
|
||||
<Trans i18nKey="explore.correlation-tour.ready-tip2">
|
||||
Use clear, descriptive names so other users understand the link
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack direction="row" gap={1}>
|
||||
<Icon name="check" />
|
||||
<Text variant="bodySmall">
|
||||
<Trans i18nKey="explore.correlation-tour.ready-tip3">
|
||||
Custom variables let you extract specific parts of field values
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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 (
|
||||
<Modal isOpen={true} title={currentTourStep.title} onDismiss={handleComplete}>
|
||||
<div className={styles.modalContent}>
|
||||
<div className={isTransitioning ? styles.contentTransitioning : styles.contentVisible}>
|
||||
{currentTourStep.content}
|
||||
</div>
|
||||
|
||||
<div className={styles.progressContainer}>
|
||||
<Stack direction="column" gap={1.5}>
|
||||
{/* Progress indicator */}
|
||||
<Stack direction="row" gap={0.5} justifyContent="center" alignItems="center">
|
||||
{tourSteps.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={index === currentStep ? styles.progressDotActive : styles.progressDot}
|
||||
aria-label={
|
||||
index === currentStep
|
||||
? t('explore.correlation-tour.current-step', 'Current step {{step}}', {
|
||||
step: index + 1,
|
||||
})
|
||||
: t('explore.correlation-tour.step-number', 'Step {{step}}', { step: index + 1 })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* Step counter */}
|
||||
<Text variant="bodySmall" color="secondary" textAlignment="center">
|
||||
<Trans
|
||||
i18nKey="explore.correlation-tour.step-counter"
|
||||
values={{ current: currentStep + 1, total: tourSteps.length }}
|
||||
>
|
||||
Step {{ current: currentStep + 1 }} of {{ total: tourSteps.length }}
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Button variant="secondary" fill="outline" onClick={handleComplete}>
|
||||
<Trans i18nKey="explore.correlation-tour.skip">Skip tour</Trans>
|
||||
</Button>
|
||||
<Stack direction="row" gap={1}>
|
||||
{!isFirstStep && (
|
||||
<Button variant="secondary" onClick={handleBack}>
|
||||
<Trans i18nKey="explore.correlation-tour.back">Back</Trans>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="primary" onClick={handleNext}>
|
||||
{isLastStep ? (
|
||||
<Trans i18nKey="explore.correlation-tour.got-it">Got it!</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="explore.correlation-tour.next">Next</Trans>
|
||||
)}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal.ButtonRow>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) => ({
|
||||
modalContent: css({
|
||||
minHeight: '180px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
}),
|
||||
contentVisible: css({
|
||||
minHeight: '180px',
|
||||
opacity: 1,
|
||||
[theme.transitions.handleMotion('no-preference')]: {
|
||||
transform: 'translateY(0)',
|
||||
transition: 'opacity 0.2s ease, transform 0.2s ease',
|
||||
},
|
||||
}),
|
||||
contentTransitioning: css({
|
||||
minHeight: '180px',
|
||||
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',
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -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 (
|
||||
<FormSection title={<Trans i18nKey="explore.correlation-helper.title-variables">Variables (optional)</Trans>}>
|
||||
<Text variant="bodySmall">
|
||||
<Trans i18nKey="explore.correlation-helper.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.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Stack direction="column" gap={1.5}>
|
||||
{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 (
|
||||
<Stack key={name} direction="row" gap={1} alignItems="flex-start" justifyContent="space-between">
|
||||
<Stack direction="row" gap={1} alignItems="center" grow={1}>
|
||||
<code className={styles.variableName}>${`{${name}}`}</code>
|
||||
<Tooltip content={value} placement="auto-start">
|
||||
<span className={styles.variableValue}>{value}</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
|
||||
{isCustomVariable && transformationIdx !== undefined && (
|
||||
<Stack direction="row" gap={0.5}>
|
||||
<IconButton
|
||||
name="edit"
|
||||
size="sm"
|
||||
aria-label={t('explore.correlation-helper.aria-label-edit-transformation', 'Edit transformation')}
|
||||
onClick={() => handlers.onEdit(transformationIdx)}
|
||||
/>
|
||||
<DeleteButton
|
||||
size="sm"
|
||||
aria-label={t(
|
||||
'explore.correlation-helper.aria-label-delete-transformation',
|
||||
'Delete transformation'
|
||||
)}
|
||||
onConfirm={() => handlers.onDelete(transformationIdx)}
|
||||
closeOnConfirm
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
<Button variant="secondary" fill="outline" onClick={handlers.onAdd} className={styles.addButton}>
|
||||
<Trans i18nKey="explore.correlation-helper.add-custom-variable">Add custom variable</Trans>
|
||||
</Button>
|
||||
</FormSection>
|
||||
);
|
||||
};
|
||||
|
||||
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',
|
||||
}),
|
||||
});
|
||||
@@ -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<ComboboxOption<CorrelationType>> = 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 (
|
||||
<FormSection title={<Trans i18nKey="explore.correlation-helper.title-correlation-info">Correlation Info</Trans>}>
|
||||
<Field noMargin label={t('explore.correlation-form-information.label-type', 'Type')} htmlFor={`${id}-type`}>
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Combobox
|
||||
id={`${id}-type`}
|
||||
options={typeOptions}
|
||||
value={value || CorrelationType.ExploreQuery}
|
||||
onChange={(option) => onChange(option?.value || CorrelationType.ExploreQuery)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{selectedType === CorrelationType.Link && (
|
||||
<Field
|
||||
noMargin
|
||||
label={t('explore.correlation-form-information.label-url', 'URL')}
|
||||
description={t(
|
||||
'explore.correlation-form-information.url-description',
|
||||
'Specify the URL that will open when the link is clicked'
|
||||
)}
|
||||
htmlFor={`${id}-url`}
|
||||
>
|
||||
<Input
|
||||
{...register('url')}
|
||||
id={`${id}-url`}
|
||||
placeholder={t('explore.correlation-form-information.url-placeholder', 'https://example.com')}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field noMargin label={t('explore.correlation-form-information.label-name', 'Name')} 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-form-information.label-description', 'Description')}
|
||||
htmlFor={`${id}-description`}
|
||||
>
|
||||
<Input {...register('description')} id={`${id}-description`} />
|
||||
</Field>
|
||||
</FormSection>
|
||||
);
|
||||
};
|
||||
@@ -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<FormValues>({
|
||||
defaultValues: { type: CorrelationType.ExploreQuery },
|
||||
});
|
||||
|
||||
const selectedType = useWatch({ control, name: 'type' });
|
||||
|
||||
const [showTransformationAddModal, setShowTransformationAddModal] = useState(false);
|
||||
const [transformations, setTransformations] = useState<DataLinkTransformationConfig[]>([]);
|
||||
const [transformationIdxToEdit, setTransformationIdxToEdit] = useState<number | undefined>(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<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 (
|
||||
<>
|
||||
<Stack direction="column" gap={1.5}>
|
||||
<div className={styles.infoBox}>
|
||||
<Stack direction="row" gap={1} alignItems="flex-start">
|
||||
<Icon name="info-circle" size="sm" className={styles.infoIcon} />
|
||||
<Text variant="body" color="secondary">
|
||||
{selectedType === CorrelationType.Link ? (
|
||||
<Trans
|
||||
i18nKey="explore.correlation-helper.body-correlation-details-link"
|
||||
values={{ resultField: correlations.resultField }}
|
||||
>
|
||||
When saved, the <code>{'{{resultField}}'}</code> field will have a clickable link that opens the
|
||||
specified URL.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="explore.correlation-helper.body-correlation-details-query"
|
||||
values={{ resultField: correlations.resultField }}
|
||||
>
|
||||
When saved, the <code>{'{{resultField}}'}</code> field will have a clickable link that runs your
|
||||
target query below.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
<CorrelationFormInformation
|
||||
control={control}
|
||||
register={register}
|
||||
getValues={getValues}
|
||||
setValue={setValue}
|
||||
defaultLabel={defaultLabel}
|
||||
selectedType={selectedType}
|
||||
/>
|
||||
{selectedType === CorrelationType.ExploreQuery && (
|
||||
<CorrelationFormCustomVariables
|
||||
correlations={correlations}
|
||||
transformations={transformations}
|
||||
handlers={transformationHandlers}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<div className={styles.divider} />
|
||||
{showTransformationAddModal && selectedType === CorrelationType.ExploreQuery && (
|
||||
<CorrelationTransformationAddModal
|
||||
onCancel={transformationHandlers.onModalCancel}
|
||||
onSave={transformationHandlers.onModalSave}
|
||||
fieldList={correlations.origVars}
|
||||
transformationToEdit={
|
||||
transformationIdxToEdit !== undefined ? transformations[transformationIdxToEdit] : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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),
|
||||
}),
|
||||
});
|
||||
@@ -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 (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Text variant="h5">{title}</Text>
|
||||
<div className={styles.formFieldsWrapper}>
|
||||
<Stack direction="column" gap={1.5}>
|
||||
{children}
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
}),
|
||||
});
|
||||
@@ -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<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 CorrelationTransformationAddModal = ({
|
||||
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-custom-variable', 'Edit custom variable')
|
||||
: t('explore.correlation-transformation-add-modal.title-add-custom-variable', 'Add custom variable')
|
||||
}
|
||||
onDismiss={onCancel}
|
||||
className={css({ width: '700px' })}
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
<Field
|
||||
noMargin
|
||||
label={t('explore.correlation-transformation-add-modal.label-field', 'Field')}
|
||||
description={t(
|
||||
'explore.correlation-transformation-add-modal.description-field',
|
||||
'Select the field from which to extract a value for your variable'
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
{exampleValue && (
|
||||
<>
|
||||
{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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
<Stack gap={1} direction="column">
|
||||
<Text variant="bodySmall">
|
||||
<Trans i18nKey="explore.correlation-transformation-add-modal.example-value">
|
||||
Example value for your variable:
|
||||
</Trans>
|
||||
</Text>
|
||||
<pre>
|
||||
<Highlighter
|
||||
textToHighlight={exampleValue}
|
||||
searchWords={[isExpValid ? (getValues('expression') ?? '') : '']}
|
||||
autoEscape={false}
|
||||
/>
|
||||
</pre>
|
||||
</Stack>
|
||||
|
||||
{Object.entries(transformationVars).length > 0 && (
|
||||
<>
|
||||
<Trans i18nKey="explore.correlation-transformation-add-modal.added-variables">
|
||||
This custom variable 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>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -10,16 +10,16 @@ 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 { 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';
|
||||
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 { CorrelationUnsavedChangesModal } from '../CorrelationUnsavedChangesModal';
|
||||
import { showModalMessage } from '../correlationEditLogic';
|
||||
|
||||
export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, ExploreItemState]> }) => {
|
||||
export const CorrelationEditorModeBarLegacy = ({ panes }: { panes: Array<[string, ExploreItemState]> }) => {
|
||||
const dispatch = useDispatch();
|
||||
const styles = useStyles2(getStyles);
|
||||
const correlationDetails = useSelector(selectCorrelationDetails);
|
||||
@@ -21,13 +21,13 @@ import {
|
||||
} from '@grafana/ui';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import { getTransformationVars } from '../correlations/transformations';
|
||||
import { generateDefaultLabel } from '../correlations/utils';
|
||||
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 { changeCorrelationHelperData } from './state/explorePane';
|
||||
import { changeCorrelationEditorDetails } from './state/main';
|
||||
import { selectCorrelationDetails, selectPanes } from './state/selectors';
|
||||
import { CorrelationTransformationAddModalLegacy } from './CorrelationTransformationAddModalLegacy';
|
||||
|
||||
interface Props {
|
||||
exploreId: string;
|
||||
@@ -39,7 +39,7 @@ interface FormValues {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const CorrelationHelper = ({ exploreId, correlations }: Props) => {
|
||||
export const CorrelationHelperLegacy = ({ exploreId, correlations }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const styles = useStyles2(getStyles);
|
||||
const panes = useSelector(selectPanes);
|
||||
@@ -135,7 +135,7 @@ export const CorrelationHelper = ({ exploreId, correlations }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{showTransformationAddModal && (
|
||||
<CorrelationTransformationAddModal
|
||||
<CorrelationTransformationAddModalLegacy
|
||||
onCancel={() => {
|
||||
setTransformationIdxToEdit(undefined);
|
||||
setShowTransformationAddModal(false);
|
||||
@@ -184,7 +184,7 @@ export const CorrelationHelper = ({ exploreId, correlations }: Props) => {
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Field label={t('explore.correlation-helper.label-label', 'Label')} htmlFor={`${id}-label`}>
|
||||
<Field noMargin label={t('explore.correlation-helper.label-label', 'Label')} htmlFor={`${id}-label`}>
|
||||
<Input
|
||||
{...register('label')}
|
||||
id={`${id}-label`}
|
||||
@@ -195,7 +195,11 @@ export const CorrelationHelper = ({ exploreId, correlations }: Props) => {
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t('explore.correlation-helper.label-description', 'Description')} htmlFor={`${id}-description`}>
|
||||
<Field
|
||||
noMargin
|
||||
label={t('explore.correlation-helper.label-description', 'Description')}
|
||||
htmlFor={`${id}-description`}
|
||||
>
|
||||
<Input {...register('description')} id={`${id}-description`} />
|
||||
</Field>
|
||||
</Collapse>
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
getSupportedTransTypeDetails,
|
||||
getTransformOptions,
|
||||
TransformationFieldDetails,
|
||||
} from '../correlations/Forms/types';
|
||||
import { getTransformationVars } from '../correlations/transformations';
|
||||
} from '../../../correlations/Forms/types';
|
||||
import { getTransformationVars } from '../../../correlations/transformations';
|
||||
|
||||
interface CorrelationTransformationAddModalProps {
|
||||
onCancel: () => void;
|
||||
@@ -35,7 +35,7 @@ const LabelWithTooltip = ({ label, tooltipText }: { label: string; tooltipText:
|
||||
</Stack>
|
||||
);
|
||||
|
||||
export const CorrelationTransformationAddModal = ({
|
||||
export const CorrelationTransformationAddModalLegacy = ({
|
||||
onSave,
|
||||
onCancel,
|
||||
fieldList,
|
||||
@@ -144,7 +144,7 @@ export const CorrelationTransformationAddModal = ({
|
||||
field variables.
|
||||
</Trans>
|
||||
</p>
|
||||
<Field label={t('explore.correlation-transformation-add-modal.label-field', 'Field')}>
|
||||
<Field noMargin label={t('explore.correlation-transformation-add-modal.label-field', 'Field')}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
@@ -175,7 +175,7 @@ export const CorrelationTransformationAddModal = ({
|
||||
autoEscape={false}
|
||||
/>
|
||||
</pre>
|
||||
<Field label={t('explore.correlation-transformation-add-modal.label-type', 'Type')}>
|
||||
<Field noMargin label={t('explore.correlation-transformation-add-modal.label-type', 'Type')}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
@@ -198,6 +198,7 @@ export const CorrelationTransformationAddModal = ({
|
||||
</Field>
|
||||
{formFieldsVis.expressionDetails.show && (
|
||||
<Field
|
||||
noMargin
|
||||
label={
|
||||
formFieldsVis.expressionDetails.helpText ? (
|
||||
<LabelWithTooltip
|
||||
@@ -216,6 +217,7 @@ export const CorrelationTransformationAddModal = ({
|
||||
)}
|
||||
{formFieldsVis.mapValueDetails.show && (
|
||||
<Field
|
||||
noMargin
|
||||
label={
|
||||
formFieldsVis.mapValueDetails.helpText ? (
|
||||
<LabelWithTooltip
|
||||
49
public/app/features/explore/CorrelationEditor/types.ts
Normal file
49
public/app/features/explore/CorrelationEditor/types.ts
Normal file
@@ -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<FormValues>;
|
||||
register: UseFormRegister<FormValues>;
|
||||
getValues: UseFormGetValues<FormValues>;
|
||||
setValue: UseFormSetValue<FormValues>;
|
||||
defaultLabel: string | undefined;
|
||||
selectedType: CorrelationType;
|
||||
}
|
||||
|
||||
export interface CorrelationFormCustomVariablesProps {
|
||||
correlations: ExploreCorrelationHelperData;
|
||||
transformations: DataLinkTransformationConfig[];
|
||||
handlers: TransformationHandlers;
|
||||
}
|
||||
|
||||
export interface FormSectionProps {
|
||||
title: JSX.Element;
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -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,
|
||||
@@ -40,7 +40,8 @@ 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 { CorrelationHelperLegacy } from './CorrelationEditor/Legacy/CorrelationHelperLegacy';
|
||||
import { CustomContainer } from './CustomContainer';
|
||||
import { ExploreToolbar } from './ExploreToolbar';
|
||||
import { FlameGraphExploreContainer } from './FlameGraph/FlameGraphExploreContainer';
|
||||
@@ -596,6 +597,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;
|
||||
@@ -616,7 +618,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 (
|
||||
@@ -625,7 +631,7 @@ export class Explore extends PureComponent<Props, ExploreState> {
|
||||
exploreId={exploreId}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onContentOutlineToogle={this.onContentOutlineToogle}
|
||||
isContentOutlineOpen={contentOutlineVisible}
|
||||
isContentOutlineOpen={contentOutlineVisible && !showCorrelationHelper}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
@@ -636,7 +642,11 @@ export class Explore extends PureComponent<Props, ExploreState> {
|
||||
>
|
||||
<div className={styles.wrapper}>
|
||||
{contentOutlineVisible && !compact && (
|
||||
<ContentOutline scroller={this.scrollElement} panelId={`content-outline-container-${exploreId}`} />
|
||||
<ContentOutline
|
||||
scroller={this.scrollElement}
|
||||
panelId={`content-outline-container-${exploreId}`}
|
||||
defaultCollapsed={showCorrelationHelper}
|
||||
/>
|
||||
)}
|
||||
<ScrollContainer
|
||||
data-testid={selectors.pages.Explore.General.scrollView}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { ErrorBoundaryAlert, LoadingPlaceholder, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
@@ -11,7 +12,8 @@ import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { ExploreQueryParams } from 'app/types/explore';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
import { CorrelationEditorModeBar } from './CorrelationEditorModeBar';
|
||||
import { CorrelationEditorModeBar } from './CorrelationEditor/CorrelationEditorModeBar';
|
||||
import { CorrelationEditorModeBarLegacy } from './CorrelationEditor/Legacy/CorrelationEditorModeBarLegacy';
|
||||
import { ExploreActions } from './ExploreActions';
|
||||
import { ExploreDrawer } from './ExploreDrawer';
|
||||
import { ExplorePaneContainer } from './ExplorePaneContainer';
|
||||
@@ -31,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);
|
||||
@@ -71,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}
|
||||
@@ -108,7 +116,7 @@ function ExplorePageContent(props: GrafanaRouteComponentProps<{}, ExploreQueryPa
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const getStyles = (theme: GrafanaTheme2, correlationsExploreEditor?: boolean) => {
|
||||
return {
|
||||
pageScrollbarWrapper: css({
|
||||
width: '100%',
|
||||
@@ -119,10 +127,10 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
correlationsEditorIndicator: css({
|
||||
borderLeft: `4px solid ${theme.colors.primary.main}`,
|
||||
borderRight: `4px solid ${theme.colors.primary.main}`,
|
||||
borderBottom: `4px solid ${theme.colors.primary.main}`,
|
||||
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}`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
ButtonGroup,
|
||||
useStyles2,
|
||||
Button,
|
||||
Badge,
|
||||
Tooltip,
|
||||
} from '@grafana/ui';
|
||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
@@ -57,6 +59,10 @@ const getStyles = (theme: GrafanaTheme2, splitted: Boolean) => ({
|
||||
marginRight: theme.spacing(0.5),
|
||||
width: splitted && theme.spacing(6),
|
||||
}),
|
||||
badgeWrapper: css({
|
||||
marginTop: theme.spacing(2),
|
||||
marginLeft: theme.spacing(2),
|
||||
}),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
@@ -219,10 +225,46 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
|
||||
<ShortLinkButtonMenu key="share" />,
|
||||
];
|
||||
|
||||
const correlationsExploreEditor = config.featureToggles.correlationsExploreEditor;
|
||||
const showBuilderIndicator = isCorrelationsEditorMode && !isLeftPane;
|
||||
const showSourceIndicator = isCorrelationsEditorMode && isLeftPane && splitted;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{refreshInterval && <SetInterval func={onRunQuery} interval={refreshInterval} loading={loading} />}
|
||||
<AppChromeUpdate actions={navBarActions} />
|
||||
{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')}
|
||||
leftItems={[
|
||||
|
||||
@@ -7122,30 +7122,84 @@
|
||||
},
|
||||
"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 <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"
|
||||
},
|
||||
"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-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)",
|
||||
"tooltip-transformations": "A transformation extracts one or more variables out of a single field.",
|
||||
"transformations": "Transformations"
|
||||
},
|
||||
"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 <strong>Exit correlation editor</strong> 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 <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.",
|
||||
"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 <strong>Save</strong> 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",
|
||||
@@ -7153,7 +7207,9 @@
|
||||
"label-variable-name": "Variable name",
|
||||
"label-variable-name-without-tooltip": "Variable name",
|
||||
"title-add": "Add transformation",
|
||||
"title-edit": "Edit 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",
|
||||
@@ -7600,6 +7656,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)",
|
||||
@@ -7610,12 +7667,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",
|
||||
|
||||
Reference in New Issue
Block a user