mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Correlations: Add an editor in Explore (#73315)
* Start adding correlations editor mode * Add selector for merge conflict resolution * Enable saving * Build out new corelation helper component * flesh out save with label/description, change color * Have breadcrumb exit correlation editor mode * Add extension property to show/hide, use it for correlations * Bring in feature toggle * Remove unnecessary param * Cleanup * Parse logs json * Work on correlation edit mode bar * Tinker with a top element for the editor mode * Handle various explore state changes with correlations editor mode * WIP - add unsaved changes modal * Have correlation bar always rendered, sometimes hidden * Add various prompt modals * Clear correlations data on mode bar unmount, only use not left pane changes to count as dirty * Move special logic to explore * Remove all shouldShow logic from plugin extensions * remove grafana data changes * WIP - clean up correlations state * Interpolate data before sending to onclick * Override outline button coloring to account for dark background * More cleanup, more color tweaking * Prettier formatting, change state to refer to editor * Fix tests * More state change tweaks * ensure correlation save ability, change correlation editor state vars * fix import * Remove independent selector for editorMode, work close pane into editor exit flow * Add change datasource post action * Clean up based on PR feedback, handle closing left panel with helper better * Remove breadcrumb additions, add section and better ID to cmd palette action * Interpolate query results if it is ran with a helper with vars * Pass the datasource query along with the correlate link to ensure the datasource unique ID requirement passes * Use different onmount function to capture state of panes at time of close instead of time of mount * Fix node graph’s datalink not working * Actually commit the fix to saving * Fix saving correlations with mixed datasource to use the first query correlation UID * Add tracking * Use query datasources in mixed scenario, move exit tracking to click handler * Add correlations to a place where both can be used in the correlations editor * Be more selective on when we await the datasource get * Fix CSS to use objects * Update betterer * Add test around new decorator functionality * Add tests for decorate with correlations * Some reorganization and a few tweaks based on feedback * Move dirty state change to state function and out of component * Change the verbiage around a little * Various suggestions from Gio Co-authored-by: Giordano Ricci <me@giordanoricci.com> * More small Gio-related tweaks * Tie helper data to datasource - clear it out when the datasource changes * Missed another Gio tweak * Fix linter error * Only clear helper data on left pane changes * Add height offset for correlation editor bar so it doesn’t scroll off page --------- Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com> Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
parent
6150d1370c
commit
4b3d63dcdc
@ -3799,9 +3799,6 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "2"]
|
[0, 0, 0, "Styles should be written using objects.", "2"]
|
||||||
],
|
],
|
||||||
"public/app/features/explore/ExplorePage.tsx:5381": [
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
|
||||||
],
|
|
||||||
"public/app/features/explore/ExplorePaneContainer.tsx:5381": [
|
"public/app/features/explore/ExplorePaneContainer.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||||
],
|
],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ExplorePanelsState } from './explore';
|
import { ExploreCorrelationHelperData, ExplorePanelsState } from './explore';
|
||||||
import { InterpolateFunction } from './panel';
|
import { InterpolateFunction } from './panel';
|
||||||
import { DataQuery } from './query';
|
import { DataQuery } from './query';
|
||||||
import { TimeRange } from './time';
|
import { TimeRange } from './time';
|
||||||
@ -19,6 +19,7 @@ export interface DataLinkClickEvent<T = any> {
|
|||||||
export enum DataLinkConfigOrigin {
|
export enum DataLinkConfigOrigin {
|
||||||
Datasource = 'Datasource',
|
Datasource = 'Datasource',
|
||||||
Correlations = 'Correlations',
|
Correlations = 'Correlations',
|
||||||
|
ExploreCorrelationsEditor = 'CorrelationsEditor',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,6 +78,9 @@ export interface InternalDataLink<T extends DataQuery = any> {
|
|||||||
datasourceUid: string;
|
datasourceUid: string;
|
||||||
datasourceName: string; // used as a title if `DataLink.title` is empty
|
datasourceName: string; // used as a title if `DataLink.title` is empty
|
||||||
panelsState?: ExplorePanelsState;
|
panelsState?: ExplorePanelsState;
|
||||||
|
meta?: {
|
||||||
|
correlationData?: ExploreCorrelationHelperData;
|
||||||
|
};
|
||||||
transformations?: DataLinkTransformationConfig[];
|
transformations?: DataLinkTransformationConfig[];
|
||||||
range?: TimeRange;
|
range?: TimeRange;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,15 @@ export interface ExplorePanelsState extends Partial<Record<PreferredVisualisatio
|
|||||||
logs?: ExploreLogsPanelState;
|
logs?: ExploreLogsPanelState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep a list of vars the correlations editor / helper in explore will use
|
||||||
|
*/
|
||||||
|
/** @internal */
|
||||||
|
export interface ExploreCorrelationHelperData {
|
||||||
|
resultField: string;
|
||||||
|
vars: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExploreTracePanelState {
|
export interface ExploreTracePanelState {
|
||||||
spanId?: string;
|
spanId?: string;
|
||||||
}
|
}
|
||||||
@ -46,6 +55,7 @@ export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {
|
|||||||
queries?: T[];
|
queries?: T[];
|
||||||
range?: TimeRange;
|
range?: TimeRange;
|
||||||
panelsState?: ExplorePanelsState;
|
panelsState?: ExplorePanelsState;
|
||||||
|
correlationHelperData?: ExploreCorrelationHelperData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,6 +45,11 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod
|
|||||||
|
|
||||||
const interpolatedQuery = interpolateObject(link.internal?.query, scopedVars, replaceVariables);
|
const interpolatedQuery = interpolateObject(link.internal?.query, scopedVars, replaceVariables);
|
||||||
const interpolatedPanelsState = interpolateObject(link.internal?.panelsState, scopedVars, replaceVariables);
|
const interpolatedPanelsState = interpolateObject(link.internal?.panelsState, scopedVars, replaceVariables);
|
||||||
|
const interpolatedCorrelationData = interpolateObject(
|
||||||
|
link.internal?.meta?.correlationData,
|
||||||
|
scopedVars,
|
||||||
|
replaceVariables
|
||||||
|
);
|
||||||
const title = link.title ? link.title : internalLink.datasourceName;
|
const title = link.title ? link.title : internalLink.datasourceName;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -57,11 +62,15 @@ export function mapInternalLinkToExplore(options: LinkToExploreOptions): LinkMod
|
|||||||
// Explore data links can be displayed not only in DataLinkButton but it can be used by the consumer in
|
// Explore data links can be displayed not only in DataLinkButton but it can be used by the consumer in
|
||||||
// other way, for example MenuItem. We want to provide the URL (for opening in the new tab as well as
|
// other way, for example MenuItem. We want to provide the URL (for opening in the new tab as well as
|
||||||
// the onClick to open the split view).
|
// the onClick to open the split view).
|
||||||
event.preventDefault();
|
if (event.preventDefault) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
onClickFn({
|
onClickFn({
|
||||||
datasourceUid: internalLink.datasourceUid,
|
datasourceUid: internalLink.datasourceUid,
|
||||||
queries: [interpolatedQuery],
|
queries: [interpolatedQuery],
|
||||||
panelsState: interpolatedPanelsState,
|
panelsState: interpolatedPanelsState,
|
||||||
|
correlationHelperData: interpolatedCorrelationData,
|
||||||
range,
|
range,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ interface Props {
|
|||||||
maxSize?: number;
|
maxSize?: number;
|
||||||
primary?: 'first' | 'second';
|
primary?: 'first' | 'second';
|
||||||
onDragFinished?: (size?: number) => void;
|
onDragFinished?: (size?: number) => void;
|
||||||
|
parentStyle?: React.CSSProperties;
|
||||||
paneStyle?: React.CSSProperties;
|
paneStyle?: React.CSSProperties;
|
||||||
secondaryPaneStyle?: React.CSSProperties;
|
secondaryPaneStyle?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
@ -58,6 +59,7 @@ export class SplitPaneWrapper extends PureComponent<React.PropsWithChildren<Prop
|
|||||||
maxSize,
|
maxSize,
|
||||||
minSize,
|
minSize,
|
||||||
primary,
|
primary,
|
||||||
|
parentStyle,
|
||||||
paneStyle,
|
paneStyle,
|
||||||
secondaryPaneStyle,
|
secondaryPaneStyle,
|
||||||
splitVisible = true,
|
splitVisible = true,
|
||||||
@ -95,6 +97,7 @@ export class SplitPaneWrapper extends PureComponent<React.PropsWithChildren<Prop
|
|||||||
resizerClassName={splitOrientation === 'horizontal' ? styles.resizerH : styles.resizerV}
|
resizerClassName={splitOrientation === 'horizontal' ? styles.resizerH : styles.resizerV}
|
||||||
onDragStarted={() => this.onDragStarted()}
|
onDragStarted={() => this.onDragStarted()}
|
||||||
onDragFinished={(size) => this.onDragFinished(size)}
|
onDragFinished={(size) => this.onDragFinished(size)}
|
||||||
|
style={parentStyle}
|
||||||
paneStyle={paneStyle}
|
paneStyle={paneStyle}
|
||||||
pane2Style={secondaryPaneStyle}
|
pane2Style={secondaryPaneStyle}
|
||||||
>
|
>
|
||||||
|
@ -99,7 +99,8 @@ export function buildQueryTransaction(
|
|||||||
queryOptions: QueryOptions,
|
queryOptions: QueryOptions,
|
||||||
range: TimeRange,
|
range: TimeRange,
|
||||||
scanning: boolean,
|
scanning: boolean,
|
||||||
timeZone?: TimeZone
|
timeZone?: TimeZone,
|
||||||
|
scopedVars?: ScopedVars
|
||||||
): QueryTransaction {
|
): QueryTransaction {
|
||||||
const key = queries.reduce((combinedKey, query) => {
|
const key = queries.reduce((combinedKey, query) => {
|
||||||
combinedKey += query.key;
|
combinedKey += query.key;
|
||||||
@ -131,6 +132,7 @@ export function buildQueryTransaction(
|
|||||||
scopedVars: {
|
scopedVars: {
|
||||||
__interval: { text: interval, value: interval },
|
__interval: { text: interval, value: interval },
|
||||||
__interval_ms: { text: intervalMs, value: intervalMs },
|
__interval_ms: { text: intervalMs, value: intervalMs },
|
||||||
|
...scopedVars,
|
||||||
},
|
},
|
||||||
maxDataPoints: queryOptions.maxDataPoints,
|
maxDataPoints: queryOptions.maxDataPoints,
|
||||||
liveStreaming: queryOptions.liveStreaming,
|
liveStreaming: queryOptions.liveStreaming,
|
||||||
|
@ -5,6 +5,7 @@ import { getBackendSrv } from '@grafana/runtime';
|
|||||||
|
|
||||||
import { formatValueName } from '../explore/PrometheusListView/ItemLabels';
|
import { formatValueName } from '../explore/PrometheusListView/ItemLabels';
|
||||||
|
|
||||||
|
import { CreateCorrelationParams, CreateCorrelationResponse } from './types';
|
||||||
import {
|
import {
|
||||||
CorrelationData,
|
CorrelationData,
|
||||||
CorrelationsData,
|
CorrelationsData,
|
||||||
@ -82,3 +83,10 @@ export const getCorrelationsBySourceUIDs = async (sourceUIDs: string[]): Promise
|
|||||||
.then(getData)
|
.then(getData)
|
||||||
.then(toEnrichedCorrelationsData);
|
.then(toEnrichedCorrelationsData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createCorrelation = async (
|
||||||
|
sourceUID: string,
|
||||||
|
correlation: CreateCorrelationParams
|
||||||
|
): Promise<CreateCorrelationResponse> => {
|
||||||
|
return getBackendSrv().post<CreateCorrelationResponse>(`/api/datasources/uid/${sourceUID}/correlations`, correlation);
|
||||||
|
};
|
||||||
|
251
public/app/features/explore/CorrelationEditorModeBar.tsx
Normal file
251
public/app/features/explore/CorrelationEditorModeBar.tsx
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Prompt } from 'react-router-dom';
|
||||||
|
import { useBeforeUnload, useUnmount } from 'react-use';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, colorManipulator } from '@grafana/data';
|
||||||
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
|
import { Button, HorizontalGroup, Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
|
import { CORRELATION_EDITOR_POST_CONFIRM_ACTION, ExploreItemState, useDispatch, useSelector } from 'app/types';
|
||||||
|
|
||||||
|
import { CorrelationUnsavedChangesModal } from './CorrelationUnsavedChangesModal';
|
||||||
|
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 } from './state/selectors';
|
||||||
|
|
||||||
|
export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, ExploreItemState]> }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const correlationDetails = useSelector(selectCorrelationDetails);
|
||||||
|
const [showSavePrompt, setShowSavePrompt] = useState(false);
|
||||||
|
|
||||||
|
// handle refreshing and closing the tab
|
||||||
|
useBeforeUnload(correlationDetails?.dirty || false, 'Save correlation?');
|
||||||
|
|
||||||
|
// handle exiting (staying within explore)
|
||||||
|
useEffect(() => {
|
||||||
|
if (correlationDetails?.isExiting && correlationDetails?.dirty) {
|
||||||
|
setShowSavePrompt(true);
|
||||||
|
} else if (correlationDetails?.isExiting && !correlationDetails?.dirty) {
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationEditorDetails({
|
||||||
|
editorMode: false,
|
||||||
|
dirty: false,
|
||||||
|
isExiting: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [correlationDetails?.dirty, correlationDetails?.isExiting, dispatch]);
|
||||||
|
|
||||||
|
// clear data when unmounted
|
||||||
|
useUnmount(() => {
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationEditorDetails({
|
||||||
|
editorMode: false,
|
||||||
|
isExiting: false,
|
||||||
|
dirty: false,
|
||||||
|
label: undefined,
|
||||||
|
description: undefined,
|
||||||
|
canSave: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
panes.forEach((pane) => {
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationHelperData({
|
||||||
|
exploreId: pane[0],
|
||||||
|
correlationEditorHelperData: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(runQueries({ exploreId: pane[0] }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const closePaneAndReset = (exploreId: string) => {
|
||||||
|
setShowSavePrompt(false);
|
||||||
|
dispatch(splitClose(exploreId));
|
||||||
|
reportInteraction('grafana_explore_split_view_closed');
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationEditorDetails({
|
||||||
|
editorMode: true,
|
||||||
|
isExiting: false,
|
||||||
|
dirty: false,
|
||||||
|
label: undefined,
|
||||||
|
description: undefined,
|
||||||
|
canSave: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
panes.forEach((pane) => {
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationHelperData({
|
||||||
|
exploreId: pane[0],
|
||||||
|
correlationEditorHelperData: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(runQueries({ exploreId: pane[0] }));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeDatasourceAndReset = (exploreId: string, datasourceUid: string) => {
|
||||||
|
setShowSavePrompt(false);
|
||||||
|
dispatch(changeDatasource(exploreId, datasourceUid, { importQueries: true }));
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationEditorDetails({
|
||||||
|
editorMode: true,
|
||||||
|
isExiting: false,
|
||||||
|
dirty: false,
|
||||||
|
label: undefined,
|
||||||
|
description: undefined,
|
||||||
|
canSave: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
panes.forEach((pane) => {
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationHelperData({
|
||||||
|
exploreId: pane[0],
|
||||||
|
correlationEditorHelperData: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveCorrelation = (skipPostConfirmAction: boolean) => {
|
||||||
|
dispatch(saveCurrentCorrelation(correlationDetails?.label, correlationDetails?.description));
|
||||||
|
if (!skipPostConfirmAction && correlationDetails?.postConfirmAction !== undefined) {
|
||||||
|
const { exploreId, action, changeDatasourceUid } = correlationDetails?.postConfirmAction;
|
||||||
|
if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE) {
|
||||||
|
closePaneAndReset(exploreId);
|
||||||
|
} else if (
|
||||||
|
action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE &&
|
||||||
|
changeDatasourceUid !== undefined
|
||||||
|
) {
|
||||||
|
changeDatasourceAndReset(exploreId, changeDatasourceUid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(changeCorrelationEditorDetails({ editorMode: false, dirty: false, isExiting: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Handle navigating outside of Explore */}
|
||||||
|
<Prompt
|
||||||
|
message={(location) => {
|
||||||
|
if (
|
||||||
|
location.pathname !== '/explore' &&
|
||||||
|
(correlationDetails?.editorMode || false) &&
|
||||||
|
(correlationDetails?.dirty || false)
|
||||||
|
) {
|
||||||
|
return 'You have unsaved correlation data. Continue?';
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showSavePrompt && (
|
||||||
|
<CorrelationUnsavedChangesModal
|
||||||
|
onDiscard={() => {
|
||||||
|
if (correlationDetails?.postConfirmAction !== undefined) {
|
||||||
|
const { exploreId, action, changeDatasourceUid } = correlationDetails?.postConfirmAction;
|
||||||
|
if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE) {
|
||||||
|
closePaneAndReset(exploreId);
|
||||||
|
} else if (
|
||||||
|
action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE &&
|
||||||
|
changeDatasourceUid !== undefined
|
||||||
|
) {
|
||||||
|
changeDatasourceAndReset(exploreId, changeDatasourceUid);
|
||||||
|
}
|
||||||
|
} 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,
|
||||||
|
dirty: 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 }));
|
||||||
|
setShowSavePrompt(false);
|
||||||
|
}}
|
||||||
|
onSave={() => {
|
||||||
|
saveCorrelation(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={styles.correlationEditorTop}>
|
||||||
|
<HorizontalGroup spacing="md" justify="flex-end">
|
||||||
|
<Tooltip content="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={() => {
|
||||||
|
saveCorrelation(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
fill="outline"
|
||||||
|
className={styles.buttonColor}
|
||||||
|
icon="times"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(changeCorrelationEditorDetails({ isExiting: true }));
|
||||||
|
reportInteraction('grafana_explore_correlation_editor_exit_pressed');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Exit correlation editor
|
||||||
|
</Button>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</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`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
76
public/app/features/explore/CorrelationHelper.tsx
Normal file
76
public/app/features/explore/CorrelationHelper.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import React, { useState, useEffect, useId } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { ExploreCorrelationHelperData } from '@grafana/data';
|
||||||
|
import { Collapse, Alert, Field, Input } from '@grafana/ui';
|
||||||
|
import { useDispatch, useSelector } from 'app/types';
|
||||||
|
|
||||||
|
import { changeCorrelationEditorDetails } from './state/main';
|
||||||
|
import { selectCorrelationDetails } from './state/selectors';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
correlations: ExploreCorrelationHelperData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormValues {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CorrelationHelper = ({ correlations }: Props) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { register, watch } = useForm<FormValues>();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const correlationDetails = useSelector(selectCorrelationDetails);
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = watch((value) => {
|
||||||
|
let dirty = false;
|
||||||
|
|
||||||
|
if (!correlationDetails?.dirty && (value.label !== '' || value.description !== '')) {
|
||||||
|
dirty = true;
|
||||||
|
} else if (correlationDetails?.dirty && value.label.trim() === '' && value.description.trim() === '') {
|
||||||
|
dirty = false;
|
||||||
|
}
|
||||||
|
dispatch(changeCorrelationEditorDetails({ label: value.label, description: value.description, dirty: dirty }));
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [correlationDetails?.dirty, dispatch, watch]);
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert title="Correlation details" severity="info">
|
||||||
|
The correlation link will appear by the <code>{correlations.resultField}</code> field. You can use the following
|
||||||
|
variables to set up your correlations:
|
||||||
|
<pre>
|
||||||
|
{Object.entries(correlations.vars).map((entry) => {
|
||||||
|
return `\$\{${entry[0]}\} = ${entry[1]}\n`;
|
||||||
|
})}
|
||||||
|
</pre>
|
||||||
|
<Collapse
|
||||||
|
collapsible
|
||||||
|
isOpen={isOpen}
|
||||||
|
onToggle={() => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
label="Label/Description"
|
||||||
|
>
|
||||||
|
<Field label="Label" htmlFor={`${id}-label`}>
|
||||||
|
<Input {...register('label')} id={`${id}-label`} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Description" htmlFor={`${id}-description`}>
|
||||||
|
<Input {...register('description')} id={`${id}-description`} />
|
||||||
|
</Field>
|
||||||
|
</Collapse>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button, Modal } from '@grafana/ui';
|
||||||
|
|
||||||
|
interface UnsavedChangesModalProps {
|
||||||
|
onDiscard: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CorrelationUnsavedChangesModal = ({ onSave, onDiscard, onCancel }: UnsavedChangesModalProps) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={true}
|
||||||
|
title="Unsaved changes to correlation"
|
||||||
|
onDismiss={onCancel}
|
||||||
|
icon="exclamation-triangle"
|
||||||
|
className={css({ width: '500px' })}
|
||||||
|
>
|
||||||
|
<h5>Do you want to save changes to this Correlation?</h5>
|
||||||
|
<Modal.ButtonRow>
|
||||||
|
<Button variant="secondary" onClick={onCancel} fill="outline">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={onDiscard}>
|
||||||
|
Discard correlation
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={onSave}>
|
||||||
|
Save correlation
|
||||||
|
</Button>
|
||||||
|
</Modal.ButtonRow>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -95,6 +95,8 @@ const dummyProps: Props = {
|
|||||||
showLogsSample: false,
|
showLogsSample: false,
|
||||||
logsSample: { enabled: false },
|
logsSample: { enabled: false },
|
||||||
setSupplementaryQueryEnabled: jest.fn(),
|
setSupplementaryQueryEnabled: jest.fn(),
|
||||||
|
correlationEditorDetails: undefined,
|
||||||
|
correlationEditorHelperData: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
|
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
|
||||||
|
@ -35,6 +35,7 @@ import { StoreState } from 'app/types';
|
|||||||
|
|
||||||
import { getTimeZone } from '../profile/state/selectors';
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
|
|
||||||
|
import { CorrelationHelper } from './CorrelationHelper';
|
||||||
import { CustomContainer } from './CustomContainer';
|
import { CustomContainer } from './CustomContainer';
|
||||||
import ExploreQueryInspector from './ExploreQueryInspector';
|
import ExploreQueryInspector from './ExploreQueryInspector';
|
||||||
import { ExploreToolbar } from './ExploreToolbar';
|
import { ExploreToolbar } from './ExploreToolbar';
|
||||||
@ -477,6 +478,8 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
|||||||
showFlameGraph,
|
showFlameGraph,
|
||||||
timeZone,
|
timeZone,
|
||||||
showLogsSample,
|
showLogsSample,
|
||||||
|
correlationEditorDetails,
|
||||||
|
correlationEditorHelperData,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { openDrawer } = this.state;
|
const { openDrawer } = this.state;
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
@ -497,6 +500,13 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
|||||||
queryResponse.customFrames,
|
queryResponse.customFrames,
|
||||||
].every((e) => e.length === 0);
|
].every((e) => e.length === 0);
|
||||||
|
|
||||||
|
let correlationsBox = undefined;
|
||||||
|
const isCorrelationsEditorMode = correlationEditorDetails?.editorMode;
|
||||||
|
const showCorrelationHelper = Boolean(isCorrelationsEditorMode || correlationEditorDetails?.dirty);
|
||||||
|
if (showCorrelationHelper && correlationEditorHelperData !== undefined) {
|
||||||
|
correlationsBox = <CorrelationHelper correlations={correlationEditorHelperData} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} topOfViewRef={this.topOfViewRef} />
|
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} topOfViewRef={this.topOfViewRef} />
|
||||||
@ -508,9 +518,11 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
|||||||
{datasourceInstance ? (
|
{datasourceInstance ? (
|
||||||
<div className={styles.exploreContainer}>
|
<div className={styles.exploreContainer}>
|
||||||
<PanelContainer className={styles.queryContainer}>
|
<PanelContainer className={styles.queryContainer}>
|
||||||
|
{correlationsBox}
|
||||||
<QueryRows exploreId={exploreId} />
|
<QueryRows exploreId={exploreId} />
|
||||||
<SecondaryActions
|
<SecondaryActions
|
||||||
addQueryRowButtonDisabled={isLive}
|
// do not allow people to add queries with potentially different datasources in correlations editor mode
|
||||||
|
addQueryRowButtonDisabled={isLive || (isCorrelationsEditorMode && datasourceInstance.meta.mixed)}
|
||||||
// We cannot show multiple traces at the same time right now so we do not show add query button.
|
// We cannot show multiple traces at the same time right now so we do not show add query button.
|
||||||
//TODO:unification
|
//TODO:unification
|
||||||
addQueryRowButtonHidden={false}
|
addQueryRowButtonHidden={false}
|
||||||
@ -605,6 +617,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
|||||||
showFlameGraph,
|
showFlameGraph,
|
||||||
showRawPrometheus,
|
showRawPrometheus,
|
||||||
supplementaryQueries,
|
supplementaryQueries,
|
||||||
|
correlationEditorHelperData,
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
const loading = selectIsWaitingForData(exploreId)(state);
|
const loading = selectIsWaitingForData(exploreId)(state);
|
||||||
@ -635,6 +648,8 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) {
|
|||||||
loading,
|
loading,
|
||||||
logsSample,
|
logsSample,
|
||||||
showLogsSample,
|
showLogsSample,
|
||||||
|
correlationEditorHelperData,
|
||||||
|
correlationEditorDetails: explore.correlationEditorDetails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { useRegisterActions, useKBar, Action, Priority } from 'kbar';
|
import { useRegisterActions, useKBar, Action, Priority } from 'kbar';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useDispatch, useSelector } from 'app/types';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||||
|
import { AccessControlAction, useDispatch, useSelector } from 'app/types';
|
||||||
|
|
||||||
import { splitOpen, splitClose } from './state/main';
|
import { splitOpen, splitClose, changeCorrelationEditorDetails } from './state/main';
|
||||||
import { runQueries } from './state/query';
|
import { runQueries } from './state/query';
|
||||||
import { isSplit, selectPanes } from './state/selectors';
|
import { isSplit, selectPanes } from './state/selectors';
|
||||||
|
|
||||||
@ -15,6 +18,8 @@ export const ExploreActions = () => {
|
|||||||
const panes = useSelector(selectPanes);
|
const panes = useSelector(selectPanes);
|
||||||
const splitted = useSelector(isSplit);
|
const splitted = useSelector(isSplit);
|
||||||
|
|
||||||
|
const canWriteCorrelations = contextSrv.hasPermission(AccessControlAction.DataSourcesWrite);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keys = Object.keys(panes);
|
const keys = Object.keys(panes);
|
||||||
const exploreSection = {
|
const exploreSection = {
|
||||||
@ -65,6 +70,23 @@ export const ExploreActions = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// command palette doesn't know what pane we're in, only show option if not split and no datasource is mixed
|
||||||
|
const hasMixed = Object.values(panes).some((pane) => {
|
||||||
|
return pane?.datasourceInstance?.uid === MIXED_DATASOURCE_NAME;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.featureToggles.correlations && canWriteCorrelations && !hasMixed) {
|
||||||
|
actionsArr.push({
|
||||||
|
id: 'explore/correlations-editor',
|
||||||
|
name: 'Correlations editor',
|
||||||
|
perform: () => {
|
||||||
|
dispatch(changeCorrelationEditorDetails({ editorMode: true }));
|
||||||
|
dispatch(runQueries({ exploreId: keys[0] }));
|
||||||
|
},
|
||||||
|
section: exploreSection,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
actionsArr.push({
|
actionsArr.push({
|
||||||
id: 'explore/run-query',
|
id: 'explore/run-query',
|
||||||
name: 'Run query',
|
name: 'Run query',
|
||||||
@ -85,7 +107,7 @@ export const ExploreActions = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setActions(actionsArr);
|
setActions(actionsArr);
|
||||||
}, [panes, splitted, query, dispatch]);
|
}, [panes, splitted, query, dispatch, canWriteCorrelations]);
|
||||||
|
|
||||||
useRegisterActions(!query ? [] : actions, [actions, query]);
|
useRegisterActions(!query ? [] : actions, [actions, query]);
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { ErrorBoundaryAlert } from '@grafana/ui';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { ErrorBoundaryAlert, useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
|
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
import { useNavModel } from 'app/core/hooks/useNavModel';
|
import { useNavModel } from 'app/core/hooks/useNavModel';
|
||||||
@ -9,6 +11,7 @@ import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
|||||||
import { useSelector } from 'app/types';
|
import { useSelector } from 'app/types';
|
||||||
import { ExploreQueryParams } from 'app/types/explore';
|
import { ExploreQueryParams } from 'app/types/explore';
|
||||||
|
|
||||||
|
import { CorrelationEditorModeBar } from './CorrelationEditorModeBar';
|
||||||
import { ExploreActions } from './ExploreActions';
|
import { ExploreActions } from './ExploreActions';
|
||||||
import { ExplorePaneContainer } from './ExplorePaneContainer';
|
import { ExplorePaneContainer } from './ExplorePaneContainer';
|
||||||
import { useExplorePageTitle } from './hooks/useExplorePageTitle';
|
import { useExplorePageTitle } from './hooks/useExplorePageTitle';
|
||||||
@ -16,21 +19,13 @@ import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
|
|||||||
import { useSplitSizeUpdater } from './hooks/useSplitSizeUpdater';
|
import { useSplitSizeUpdater } from './hooks/useSplitSizeUpdater';
|
||||||
import { useStateSync } from './hooks/useStateSync';
|
import { useStateSync } from './hooks/useStateSync';
|
||||||
import { useTimeSrvFix } from './hooks/useTimeSrvFix';
|
import { useTimeSrvFix } from './hooks/useTimeSrvFix';
|
||||||
import { isSplit, selectPanesEntries } from './state/selectors';
|
import { isSplit, selectCorrelationDetails, selectPanesEntries } from './state/selectors';
|
||||||
|
|
||||||
const MIN_PANE_WIDTH = 200;
|
const MIN_PANE_WIDTH = 200;
|
||||||
|
|
||||||
const styles = {
|
|
||||||
pageScrollbarWrapper: css`
|
|
||||||
width: 100%;
|
|
||||||
flex-grow: 1;
|
|
||||||
min-height: 0;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
|
export default function ExplorePage(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const theme = useTheme2();
|
||||||
useTimeSrvFix();
|
useTimeSrvFix();
|
||||||
useStateSync(props.queryParams);
|
useStateSync(props.queryParams);
|
||||||
// We want to set the title according to the URL and not to the state because the URL itself may lag
|
// We want to set the title according to the URL and not to the state because the URL itself may lag
|
||||||
@ -45,6 +40,8 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
|||||||
|
|
||||||
const panes = useSelector(selectPanesEntries);
|
const panes = useSelector(selectPanesEntries);
|
||||||
const hasSplit = useSelector(isSplit);
|
const hasSplit = useSelector(isSplit);
|
||||||
|
const correlationDetails = useSelector(selectCorrelationDetails);
|
||||||
|
const showCorrelationEditorBar = config.featureToggles.correlations && (correlationDetails?.editorMode || false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
//This is needed for breadcrumbs and topnav.
|
//This is needed for breadcrumbs and topnav.
|
||||||
@ -55,9 +52,13 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
|||||||
useKeyboardShortcuts();
|
useKeyboardShortcuts();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.pageScrollbarWrapper}>
|
<div
|
||||||
|
className={cx(styles.pageScrollbarWrapper, {
|
||||||
|
[styles.correlationsEditorIndicator]: showCorrelationEditorBar,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<ExploreActions />
|
<ExploreActions />
|
||||||
|
{showCorrelationEditorBar && <CorrelationEditorModeBar panes={panes} />}
|
||||||
<SplitPaneWrapper
|
<SplitPaneWrapper
|
||||||
splitOrientation="vertical"
|
splitOrientation="vertical"
|
||||||
paneSize={widthCalc}
|
paneSize={widthCalc}
|
||||||
@ -65,6 +66,7 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
|||||||
maxSize={MIN_PANE_WIDTH * -1}
|
maxSize={MIN_PANE_WIDTH * -1}
|
||||||
primary="second"
|
primary="second"
|
||||||
splitVisible={hasSplit}
|
splitVisible={hasSplit}
|
||||||
|
parentStyle={showCorrelationEditorBar ? { height: `calc(100% - ${theme.spacing(6)}` } : {}} // button = 4, padding = 1 x 2
|
||||||
paneStyle={{ overflow: 'auto', display: 'flex', flexDirection: 'column' }}
|
paneStyle={{ overflow: 'auto', display: 'flex', flexDirection: 'column' }}
|
||||||
onDragFinished={(size) => size && updateSplitSize(size)}
|
onDragFinished={(size) => size && updateSplitSize(size)}
|
||||||
>
|
>
|
||||||
@ -79,3 +81,21 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
pageScrollbarWrapper: css({
|
||||||
|
width: '100%',
|
||||||
|
flexGrow: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
}),
|
||||||
|
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',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -18,6 +18,7 @@ import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
|||||||
import { t, Trans } from 'app/core/internationalization';
|
import { t, Trans } from 'app/core/internationalization';
|
||||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||||
|
import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types/explore';
|
||||||
import { StoreState, useDispatch, useSelector } from 'app/types/store';
|
import { StoreState, useDispatch, useSelector } from 'app/types/store';
|
||||||
|
|
||||||
import { contextSrv } from '../../core/core';
|
import { contextSrv } from '../../core/core';
|
||||||
@ -29,9 +30,16 @@ import { ExploreTimeControls } from './ExploreTimeControls';
|
|||||||
import { LiveTailButton } from './LiveTailButton';
|
import { LiveTailButton } from './LiveTailButton';
|
||||||
import { ToolbarExtensionPoint } from './extensions/ToolbarExtensionPoint';
|
import { ToolbarExtensionPoint } from './extensions/ToolbarExtensionPoint';
|
||||||
import { changeDatasource } from './state/datasource';
|
import { changeDatasource } from './state/datasource';
|
||||||
import { splitClose, splitOpen, maximizePaneAction, evenPaneResizeAction } from './state/main';
|
import { changeCorrelationHelperData } from './state/explorePane';
|
||||||
|
import {
|
||||||
|
splitClose,
|
||||||
|
splitOpen,
|
||||||
|
maximizePaneAction,
|
||||||
|
evenPaneResizeAction,
|
||||||
|
changeCorrelationEditorDetails,
|
||||||
|
} from './state/main';
|
||||||
import { cancelQueries, runQueries, selectIsWaitingForData } from './state/query';
|
import { cancelQueries, runQueries, selectIsWaitingForData } from './state/query';
|
||||||
import { isSplit, selectPanesEntries } from './state/selectors';
|
import { isLeftPaneSelector, isSplit, selectCorrelationDetails, selectPanesEntries } from './state/selectors';
|
||||||
import { syncTimes, changeRefreshInterval } from './state/time';
|
import { syncTimes, changeRefreshInterval } from './state/time';
|
||||||
import { LiveTailControls } from './useLiveTailControls';
|
import { LiveTailControls } from './useLiveTailControls';
|
||||||
|
|
||||||
@ -71,10 +79,13 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props)
|
|||||||
);
|
);
|
||||||
|
|
||||||
const panes = useSelector(selectPanesEntries);
|
const panes = useSelector(selectPanesEntries);
|
||||||
|
const correlationDetails = useSelector(selectCorrelationDetails);
|
||||||
|
const isCorrelationsEditorMode = correlationDetails?.editorMode || false;
|
||||||
|
const isLeftPane = useSelector(isLeftPaneSelector(exploreId));
|
||||||
|
|
||||||
const shouldRotateSplitIcon = useMemo(
|
const shouldRotateSplitIcon = useMemo(
|
||||||
() => (exploreId === panes[0][0] && isLargerPane) || (exploreId === panes[1]?.[0] && !isLargerPane),
|
() => (isLeftPane && isLargerPane) || (!isLeftPane && !isLargerPane),
|
||||||
[isLargerPane, exploreId, panes]
|
[isLeftPane, isLargerPane]
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshPickerLabel = loading
|
const refreshPickerLabel = loading
|
||||||
@ -87,7 +98,37 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props)
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onChangeDatasource = async (dsSettings: DataSourceInstanceSettings) => {
|
const onChangeDatasource = async (dsSettings: DataSourceInstanceSettings) => {
|
||||||
dispatch(changeDatasource(exploreId, dsSettings.uid, { importQueries: true }));
|
if (!isCorrelationsEditorMode) {
|
||||||
|
dispatch(changeDatasource(exploreId, dsSettings.uid, { importQueries: true }));
|
||||||
|
} else {
|
||||||
|
if (correlationDetails?.dirty) {
|
||||||
|
// prompt will handle datasource change if needed
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationEditorDetails({
|
||||||
|
isExiting: true,
|
||||||
|
postConfirmAction: {
|
||||||
|
exploreId: exploreId,
|
||||||
|
action: CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE,
|
||||||
|
changeDatasourceUid: dsSettings.uid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// if the left pane is changing, clear helper data for right pane
|
||||||
|
if (isLeftPane) {
|
||||||
|
panes.forEach((pane) => {
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationHelperData({
|
||||||
|
exploreId: pane[0],
|
||||||
|
correlationEditorHelperData: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(changeDatasource(exploreId, dsSettings.uid, { importQueries: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRunQuery = (loading = false) => {
|
const onRunQuery = (loading = false) => {
|
||||||
@ -106,8 +147,35 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props)
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCloseSplitView = () => {
|
const onCloseSplitView = () => {
|
||||||
dispatch(splitClose(exploreId));
|
if (isCorrelationsEditorMode) {
|
||||||
reportInteraction('grafana_explore_split_view_closed');
|
if (correlationDetails?.dirty) {
|
||||||
|
// if dirty, prompt
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationEditorDetails({
|
||||||
|
isExiting: true,
|
||||||
|
postConfirmAction: {
|
||||||
|
exploreId: exploreId,
|
||||||
|
action: CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// otherwise, clear helper data and close
|
||||||
|
panes.forEach((pane) => {
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationHelperData({
|
||||||
|
exploreId: pane[0],
|
||||||
|
correlationEditorHelperData: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
dispatch(splitClose(exploreId));
|
||||||
|
reportInteraction('grafana_explore_split_view_closed');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch(splitClose(exploreId));
|
||||||
|
reportInteraction('grafana_explore_split_view_closed');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClickResize = () => {
|
const onClickResize = () => {
|
||||||
@ -129,29 +197,29 @@ export function ExploreToolbar({ exploreId, topOfViewRef, onChangeTime }: Props)
|
|||||||
dispatch(changeRefreshInterval({ exploreId, refreshInterval }));
|
dispatch(changeRefreshInterval({ exploreId, refreshInterval }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const navBarActions = [
|
||||||
|
<DashNavButton
|
||||||
|
key="share"
|
||||||
|
tooltip={t('explore.toolbar.copy-shortened-link', 'Copy shortened link')}
|
||||||
|
icon="share-alt"
|
||||||
|
onClick={onCopyShortLink}
|
||||||
|
aria-label={t('explore.toolbar.copy-shortened-link', 'Copy shortened link')}
|
||||||
|
/>,
|
||||||
|
<div style={{ flex: 1 }} key="spacer0" />,
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={topOfViewRef}>
|
<div ref={topOfViewRef}>
|
||||||
{refreshInterval && <SetInterval func={onRunQuery} interval={refreshInterval} loading={loading} />}
|
{refreshInterval && <SetInterval func={onRunQuery} interval={refreshInterval} loading={loading} />}
|
||||||
<div ref={topOfViewRef}>
|
<div ref={topOfViewRef}>
|
||||||
<AppChromeUpdate
|
<AppChromeUpdate actions={navBarActions} />
|
||||||
actions={[
|
|
||||||
<DashNavButton
|
|
||||||
key="share"
|
|
||||||
tooltip={t('explore.toolbar.copy-shortened-link', 'Copy shortened link')}
|
|
||||||
icon="share-alt"
|
|
||||||
onClick={onCopyShortLink}
|
|
||||||
aria-label={t('explore.toolbar.copy-shortened-link', 'Copy shortened link')}
|
|
||||||
/>,
|
|
||||||
<div style={{ flex: 1 }} key="spacer" />,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<PageToolbar
|
<PageToolbar
|
||||||
aria-label={t('explore.toolbar.aria-label', 'Explore toolbar')}
|
aria-label={t('explore.toolbar.aria-label', 'Explore toolbar')}
|
||||||
leftItems={[
|
leftItems={[
|
||||||
<DataSourcePicker
|
<DataSourcePicker
|
||||||
key={`${exploreId}-ds-picker`}
|
key={`${exploreId}-ds-picker`}
|
||||||
mixed
|
mixed={!isCorrelationsEditorMode}
|
||||||
onChange={onChangeDatasource}
|
onChange={onChangeDatasource}
|
||||||
current={datasourceInstance?.getRef()}
|
current={datasourceInstance?.getRef()}
|
||||||
hideTextValue={showSmallDataSourcePicker}
|
hideTextValue={showSmallDataSourcePicker}
|
||||||
|
@ -59,6 +59,7 @@ function setup(queries: DataQuery[]) {
|
|||||||
correlations: [],
|
correlations: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
correlationEditorDetails: { editorMode: false, dirty: false, isExiting: false },
|
||||||
syncedTimes: false,
|
syncedTimes: false,
|
||||||
richHistoryStorageFull: false,
|
richHistoryStorageFull: false,
|
||||||
richHistoryLimitExceededWarningShown: false,
|
richHistoryLimitExceededWarningShown: false,
|
||||||
|
@ -147,6 +147,7 @@ describe('ToolbarExtensionPoint', () => {
|
|||||||
}),
|
}),
|
||||||
timeZone: 'browser',
|
timeZone: 'browser',
|
||||||
timeRange: { from: 'now-1h', to: 'now' },
|
timeRange: { from: 'now-1h', to: 'now' },
|
||||||
|
shouldShowAddCorrelation: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React, { lazy, ReactElement, Suspense, useMemo, useState } from 'react';
|
import React, { lazy, ReactElement, Suspense, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { type PluginExtensionLink, PluginExtensionPoints, RawTimeRange } from '@grafana/data';
|
import { type PluginExtensionLink, PluginExtensionPoints, RawTimeRange } from '@grafana/data';
|
||||||
import { getPluginLinkExtensions } from '@grafana/runtime';
|
import { getPluginLinkExtensions, config } from '@grafana/runtime';
|
||||||
import { DataQuery, TimeZone } from '@grafana/schema';
|
import { DataQuery, TimeZone } from '@grafana/schema';
|
||||||
import { Dropdown, ToolbarButton } from '@grafana/ui';
|
import { Dropdown, ToolbarButton } from '@grafana/ui';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { AccessControlAction, ExplorePanelData, useSelector } from 'app/types';
|
import { AccessControlAction, ExplorePanelData, useSelector } from 'app/types';
|
||||||
|
|
||||||
import { getExploreItemSelector } from '../state/selectors';
|
import { getExploreItemSelector, isLeftPaneSelector, selectCorrelationDetails } from '../state/selectors';
|
||||||
|
|
||||||
import { ConfirmNavigationModal } from './ConfirmNavigationModal';
|
import { ConfirmNavigationModal } from './ConfirmNavigationModal';
|
||||||
import { ToolbarExtensionPointMenu } from './ToolbarExtensionPointMenu';
|
import { ToolbarExtensionPointMenu } from './ToolbarExtensionPointMenu';
|
||||||
@ -81,11 +81,19 @@ export type PluginExtensionExploreContext = {
|
|||||||
data: ExplorePanelData;
|
data: ExplorePanelData;
|
||||||
timeRange: RawTimeRange;
|
timeRange: RawTimeRange;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
|
shouldShowAddCorrelation: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function useExtensionPointContext(props: Props): PluginExtensionExploreContext {
|
function useExtensionPointContext(props: Props): PluginExtensionExploreContext {
|
||||||
const { exploreId, timeZone } = props;
|
const { exploreId, timeZone } = props;
|
||||||
|
const isCorrelationDetails = useSelector(selectCorrelationDetails);
|
||||||
|
const isCorrelationsEditorMode = isCorrelationDetails?.editorMode || false;
|
||||||
const { queries, queryResponse, range } = useSelector(getExploreItemSelector(exploreId))!;
|
const { queries, queryResponse, range } = useSelector(getExploreItemSelector(exploreId))!;
|
||||||
|
const isLeftPane = useSelector(isLeftPaneSelector(exploreId));
|
||||||
|
|
||||||
|
const datasourceUids = queries.map((query) => query?.datasource?.uid).filter((uid) => uid !== undefined);
|
||||||
|
const numUniqueIds = [...new Set(datasourceUids)].length;
|
||||||
|
const canWriteCorrelations = contextSrv.hasPermission(AccessControlAction.DataSourcesWrite);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@ -94,8 +102,24 @@ function useExtensionPointContext(props: Props): PluginExtensionExploreContext {
|
|||||||
data: queryResponse,
|
data: queryResponse,
|
||||||
timeRange: range.raw,
|
timeRange: range.raw,
|
||||||
timeZone: timeZone,
|
timeZone: timeZone,
|
||||||
|
shouldShowAddCorrelation:
|
||||||
|
config.featureToggles.correlations === true &&
|
||||||
|
canWriteCorrelations &&
|
||||||
|
!isCorrelationsEditorMode &&
|
||||||
|
isLeftPane &&
|
||||||
|
numUniqueIds === 1,
|
||||||
};
|
};
|
||||||
}, [exploreId, queries, queryResponse, range, timeZone]);
|
}, [
|
||||||
|
exploreId,
|
||||||
|
queries,
|
||||||
|
queryResponse,
|
||||||
|
range.raw,
|
||||||
|
timeZone,
|
||||||
|
canWriteCorrelations,
|
||||||
|
isCorrelationsEditorMode,
|
||||||
|
isLeftPane,
|
||||||
|
numUniqueIds,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useExtensionLinks(context: PluginExtensionExploreContext): PluginExtensionLink[] {
|
function useExtensionLinks(context: PluginExtensionExploreContext): PluginExtensionLink[] {
|
||||||
|
@ -23,6 +23,15 @@ describe('getExploreExtensionConfigs', () => {
|
|||||||
onClick: expect.any(Function),
|
onClick: expect.any(Function),
|
||||||
category: 'Dashboards',
|
category: 'Dashboards',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
title: 'Add correlation',
|
||||||
|
description: 'Create a correlation from this query',
|
||||||
|
extensionPointId: PluginExtensionPoints.ExploreToolbarAction,
|
||||||
|
icon: 'link',
|
||||||
|
configure: expect.any(Function),
|
||||||
|
onClick: expect.any(Function),
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,9 +2,12 @@ import React from 'react';
|
|||||||
|
|
||||||
import { PluginExtensionPoints, type PluginExtensionLinkConfig } from '@grafana/data';
|
import { PluginExtensionPoints, type PluginExtensionLinkConfig } from '@grafana/data';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { dispatch } from 'app/store/store';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
import { createExtensionLinkConfig, logWarning } from '../../plugins/extensions/utils';
|
import { createExtensionLinkConfig, logWarning } from '../../plugins/extensions/utils';
|
||||||
|
import { changeCorrelationEditorDetails } from '../state/main';
|
||||||
|
import { runQueries } from '../state/query';
|
||||||
|
|
||||||
import { AddToDashboardForm } from './AddToDashboard/AddToDashboardForm';
|
import { AddToDashboardForm } from './AddToDashboard/AddToDashboardForm';
|
||||||
import { getAddToDashboardTitle } from './AddToDashboard/getAddToDashboardTitle';
|
import { getAddToDashboardTitle } from './AddToDashboard/getAddToDashboardTitle';
|
||||||
@ -38,6 +41,19 @@ export function getExploreExtensionConfigs(): PluginExtensionLinkConfig[] {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
createExtensionLinkConfig<PluginExtensionExploreContext>({
|
||||||
|
title: 'Add correlation',
|
||||||
|
description: 'Create a correlation from this query',
|
||||||
|
extensionPointId: PluginExtensionPoints.ExploreToolbarAction,
|
||||||
|
icon: 'link',
|
||||||
|
configure: (context) => {
|
||||||
|
return context?.shouldShowAddCorrelation ? {} : undefined;
|
||||||
|
},
|
||||||
|
onClick: (_, { context }) => {
|
||||||
|
dispatch(changeCorrelationEditorDetails({ editorMode: true }));
|
||||||
|
dispatch(runQueries({ exploreId: context!.exploreId }));
|
||||||
|
},
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logWarning(`Could not configure extensions for Explore due to: "${error}"`);
|
logWarning(`Could not configure extensions for Explore due to: "${error}"`);
|
||||||
|
101
public/app/features/explore/state/correlations.ts
Normal file
101
public/app/features/explore/state/correlations.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { getDataSourceSrv, reportInteraction } from '@grafana/runtime';
|
||||||
|
import { notifyApp } from 'app/core/actions';
|
||||||
|
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||||
|
import { CreateCorrelationParams } from 'app/features/correlations/types';
|
||||||
|
import { CorrelationData } from 'app/features/correlations/useCorrelations';
|
||||||
|
import { getCorrelationsBySourceUIDs, createCorrelation } from 'app/features/correlations/utils';
|
||||||
|
import { store } from 'app/store/store';
|
||||||
|
import { ThunkResult } from 'app/types';
|
||||||
|
|
||||||
|
import { saveCorrelationsAction } from './explorePane';
|
||||||
|
import { splitClose } from './main';
|
||||||
|
import { runQueries } from './query';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an observable that emits correlations once they are loaded
|
||||||
|
*/
|
||||||
|
export const getCorrelations = (exploreId: string) => {
|
||||||
|
return new Observable<CorrelationData[]>((subscriber) => {
|
||||||
|
const existingCorrelations = store.getState().explore.panes[exploreId]?.correlations;
|
||||||
|
if (existingCorrelations) {
|
||||||
|
subscriber.next(existingCorrelations);
|
||||||
|
subscriber.complete();
|
||||||
|
} else {
|
||||||
|
const unsubscribe = store.subscribe(() => {
|
||||||
|
const correlations = store.getState().explore.panes[exploreId]?.correlations;
|
||||||
|
if (correlations) {
|
||||||
|
unsubscribe();
|
||||||
|
subscriber.next(correlations);
|
||||||
|
subscriber.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function reloadCorrelations(exploreId: string): ThunkResult<Promise<void>> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const pane = getState().explore!.panes[exploreId]!;
|
||||||
|
|
||||||
|
if (pane.datasourceInstance?.uid !== undefined) {
|
||||||
|
// TODO: Tie correlations with query refID for mixed datasource
|
||||||
|
let datasourceUIDs = pane.datasourceInstance.meta.mixed
|
||||||
|
? pane.queries.map((query) => query.datasource?.uid).filter((x): x is string => x !== null)
|
||||||
|
: [pane.datasourceInstance.uid];
|
||||||
|
const correlations = await getCorrelationsBySourceUIDs(datasourceUIDs);
|
||||||
|
dispatch(saveCorrelationsAction({ exploreId, correlations: correlations.correlations || [] }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveCurrentCorrelation(label?: string, description?: string): ThunkResult<Promise<void>> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const keys = Object.keys(getState().explore?.panes);
|
||||||
|
const sourcePane = getState().explore?.panes[keys[0]];
|
||||||
|
const targetPane = getState().explore?.panes[keys[1]];
|
||||||
|
if (!sourcePane || !targetPane) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sourceDatasourceRef = sourcePane.datasourceInstance?.meta.mixed
|
||||||
|
? sourcePane.queries[0].datasource
|
||||||
|
: sourcePane.datasourceInstance?.getRef();
|
||||||
|
const targetDataSourceRef = targetPane.datasourceInstance?.meta.mixed
|
||||||
|
? targetPane.queries[0].datasource
|
||||||
|
: targetPane.datasourceInstance?.getRef();
|
||||||
|
|
||||||
|
const [sourceDatasource, targetDatasource] = await Promise.all([
|
||||||
|
getDataSourceSrv().get(sourceDatasourceRef),
|
||||||
|
getDataSourceSrv().get(targetDataSourceRef),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (sourceDatasource?.uid && targetDatasource?.uid && targetPane.correlationEditorHelperData?.resultField) {
|
||||||
|
const correlation: CreateCorrelationParams = {
|
||||||
|
sourceUID: sourceDatasource.uid,
|
||||||
|
targetUID: targetDatasource.uid,
|
||||||
|
label: label || `${sourceDatasource?.name} to ${targetDatasource.name}`,
|
||||||
|
description,
|
||||||
|
config: {
|
||||||
|
field: targetPane.correlationEditorHelperData.resultField,
|
||||||
|
target: targetPane.queries[0],
|
||||||
|
type: 'query',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await createCorrelation(sourceDatasource.uid, correlation)
|
||||||
|
.then(async () => {
|
||||||
|
dispatch(splitClose(keys[1]));
|
||||||
|
await dispatch(reloadCorrelations(keys[0]));
|
||||||
|
await dispatch(runQueries({ exploreId: keys[0] }));
|
||||||
|
reportInteraction('grafana_explore_correlation_editor_saved', {
|
||||||
|
sourceDatasourceType: sourceDatasource.type,
|
||||||
|
targetDataSourceType: targetDatasource.type,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
dispatch(notifyApp(createErrorNotification('Error creating correlation', err)));
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -8,6 +8,7 @@ import {
|
|||||||
ExplorePanelsState,
|
ExplorePanelsState,
|
||||||
PreferredVisualisationType,
|
PreferredVisualisationType,
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
|
ExploreCorrelationHelperData,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||||
import { getQueryKeys } from 'app/core/utils/explore';
|
import { getQueryKeys } from 'app/core/utils/explore';
|
||||||
@ -76,6 +77,17 @@ export function changePanelState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the state of correlation helper data in the panel
|
||||||
|
*/
|
||||||
|
interface ChangeCorrelationHelperData {
|
||||||
|
exploreId: string;
|
||||||
|
correlationEditorHelperData?: ExploreCorrelationHelperData;
|
||||||
|
}
|
||||||
|
export const changeCorrelationHelperData = createAction<ChangeCorrelationHelperData>(
|
||||||
|
'explore/changeCorrelationHelperData'
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Explore state with state from the URL and the React component.
|
* Initialize Explore state with state from the URL and the React component.
|
||||||
* Call this only on components for with the Explore state has not been initialized.
|
* Call this only on components for with the Explore state has not been initialized.
|
||||||
@ -114,6 +126,7 @@ export interface InitializeExploreOptions {
|
|||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
range: RawTimeRange;
|
range: RawTimeRange;
|
||||||
panelsState?: ExplorePanelsState;
|
panelsState?: ExplorePanelsState;
|
||||||
|
correlationHelperData?: ExploreCorrelationHelperData;
|
||||||
position?: number;
|
position?: number;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -127,7 +140,7 @@ export interface InitializeExploreOptions {
|
|||||||
export const initializeExplore = createAsyncThunk(
|
export const initializeExplore = createAsyncThunk(
|
||||||
'explore/initializeExplore',
|
'explore/initializeExplore',
|
||||||
async (
|
async (
|
||||||
{ exploreId, datasource, queries, range, panelsState }: InitializeExploreOptions,
|
{ exploreId, datasource, queries, range, panelsState, correlationHelperData }: InitializeExploreOptions,
|
||||||
{ dispatch, getState, fulfillWithValue }
|
{ dispatch, getState, fulfillWithValue }
|
||||||
) => {
|
) => {
|
||||||
let instance = undefined;
|
let instance = undefined;
|
||||||
@ -152,6 +165,7 @@ export const initializeExplore = createAsyncThunk(
|
|||||||
if (panelsState !== undefined) {
|
if (panelsState !== undefined) {
|
||||||
dispatch(changePanelsStateAction({ exploreId, panelsState }));
|
dispatch(changePanelsStateAction({ exploreId, panelsState }));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(updateTime({ exploreId }));
|
dispatch(updateTime({ exploreId }));
|
||||||
|
|
||||||
if (instance) {
|
if (instance) {
|
||||||
@ -162,6 +176,16 @@ export const initializeExplore = createAsyncThunk(
|
|||||||
dispatch(runQueries({ exploreId }));
|
dispatch(runQueries({ exploreId }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initialize new pane with helper data
|
||||||
|
if (correlationHelperData !== undefined && getState().explore.correlationEditorDetails?.editorMode) {
|
||||||
|
dispatch(
|
||||||
|
changeCorrelationHelperData({
|
||||||
|
exploreId,
|
||||||
|
correlationEditorHelperData: correlationHelperData,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return fulfillWithValue({ exploreId, state: getState().explore.panes[exploreId]! });
|
return fulfillWithValue({ exploreId, state: getState().explore.panes[exploreId]! });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -207,6 +231,11 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
|||||||
return { ...state, panelsState };
|
return { ...state, panelsState };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changeCorrelationHelperData.match(action)) {
|
||||||
|
const { correlationEditorHelperData } = action.payload;
|
||||||
|
return { ...state, correlationEditorHelperData };
|
||||||
|
}
|
||||||
|
|
||||||
if (saveCorrelationsAction.match(action)) {
|
if (saveCorrelationsAction.match(action)) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -5,7 +5,7 @@ import { SplitOpenOptions, TimeRange } from '@grafana/data';
|
|||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { generateExploreId, GetExploreUrlArguments } from 'app/core/utils/explore';
|
import { generateExploreId, GetExploreUrlArguments } from 'app/core/utils/explore';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { ExploreItemState, ExploreState } from 'app/types/explore';
|
import { CorrelationEditorDetailsUpdate, ExploreItemState, ExploreState } from 'app/types/explore';
|
||||||
|
|
||||||
import { RichHistoryResults } from '../../../core/history/RichHistoryStorage';
|
import { RichHistoryResults } from '../../../core/history/RichHistoryStorage';
|
||||||
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
|
import { RichHistorySearchFilters, RichHistorySettings } from '../../../core/utils/richHistoryTypes';
|
||||||
@ -83,6 +83,7 @@ export const splitOpen = createAsyncThunk(
|
|||||||
queries: withUniqueRefIds(queries),
|
queries: withUniqueRefIds(queries),
|
||||||
range: options?.range || originState?.range.raw || DEFAULT_RANGE,
|
range: options?.range || originState?.range.raw || DEFAULT_RANGE,
|
||||||
panelsState: options?.panelsState || originState?.panelsState,
|
panelsState: options?.panelsState || originState?.panelsState,
|
||||||
|
correlationHelperData: options?.correlationHelperData,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -104,6 +105,13 @@ const createNewSplitOpenPane = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves explore into and out of correlations editor mode
|
||||||
|
*/
|
||||||
|
export const changeCorrelationEditorDetails = createAction<CorrelationEditorDetailsUpdate>(
|
||||||
|
'explore/changeCorrelationEditorDetails'
|
||||||
|
);
|
||||||
|
|
||||||
export interface NavigateToExploreDependencies {
|
export interface NavigateToExploreDependencies {
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
getExploreUrl: (args: GetExploreUrlArguments) => Promise<string | undefined>;
|
getExploreUrl: (args: GetExploreUrlArguments) => Promise<string | undefined>;
|
||||||
@ -140,6 +148,7 @@ const initialExploreItemState = makeExplorePaneState();
|
|||||||
export const initialExploreState: ExploreState = {
|
export const initialExploreState: ExploreState = {
|
||||||
syncedTimes: false,
|
syncedTimes: false,
|
||||||
panes: {},
|
panes: {},
|
||||||
|
correlationEditorDetails: { editorMode: false, dirty: false, isExiting: false },
|
||||||
richHistoryStorageFull: false,
|
richHistoryStorageFull: false,
|
||||||
richHistoryLimitExceededWarningShown: false,
|
richHistoryLimitExceededWarningShown: false,
|
||||||
largerExploreId: undefined,
|
largerExploreId: undefined,
|
||||||
@ -252,6 +261,22 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changeCorrelationEditorDetails.match(action)) {
|
||||||
|
const { editorMode, label, description, canSave, dirty, isExiting, postConfirmAction } = action.payload;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
correlationEditorDetails: {
|
||||||
|
editorMode: Boolean(editorMode ?? state.correlationEditorDetails?.editorMode),
|
||||||
|
canSave: Boolean(canSave ?? state.correlationEditorDetails?.canSave),
|
||||||
|
label: label ?? state.correlationEditorDetails?.label,
|
||||||
|
description: description ?? state.correlationEditorDetails?.description,
|
||||||
|
dirty: Boolean(dirty ?? state.correlationEditorDetails?.dirty),
|
||||||
|
isExiting: Boolean(isExiting ?? state.correlationEditorDetails?.isExiting),
|
||||||
|
postConfirmAction,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const exploreId: string | undefined = action.payload?.exploreId;
|
const exploreId: string | undefined = action.payload?.exploreId;
|
||||||
if (typeof exploreId === 'string') {
|
if (typeof exploreId === 'string') {
|
||||||
return {
|
return {
|
||||||
|
@ -34,11 +34,9 @@ import {
|
|||||||
updateHistory,
|
updateHistory,
|
||||||
} from 'app/core/utils/explore';
|
} from 'app/core/utils/explore';
|
||||||
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
|
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
|
||||||
import { CorrelationData } from 'app/features/correlations/useCorrelations';
|
|
||||||
import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils';
|
import { getCorrelationsBySourceUIDs } from 'app/features/correlations/utils';
|
||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||||
import { store } from 'app/store/store';
|
|
||||||
import {
|
import {
|
||||||
createAsyncThunk,
|
createAsyncThunk,
|
||||||
ExploreItemState,
|
ExploreItemState,
|
||||||
@ -60,8 +58,10 @@ import {
|
|||||||
supplementaryQueryTypes,
|
supplementaryQueryTypes,
|
||||||
} from '../utils/supplementaryQueries';
|
} from '../utils/supplementaryQueries';
|
||||||
|
|
||||||
|
import { getCorrelations } from './correlations';
|
||||||
import { saveCorrelationsAction } from './explorePane';
|
import { saveCorrelationsAction } from './explorePane';
|
||||||
import { addHistoryItem, historyUpdatedAction, loadRichHistory } from './history';
|
import { addHistoryItem, historyUpdatedAction, loadRichHistory } from './history';
|
||||||
|
import { changeCorrelationEditorDetails } from './main';
|
||||||
import { updateTime } from './time';
|
import { updateTime } from './time';
|
||||||
import { createCacheKey, filterLogRowsByIndex, getDatasourceUIDs, getResultsFromCache } from './utils';
|
import { createCacheKey, filterLogRowsByIndex, getDatasourceUIDs, getResultsFromCache } from './utils';
|
||||||
|
|
||||||
@ -320,6 +320,13 @@ export const changeQueries = createAsyncThunk<void, ChangeQueriesPayload>(
|
|||||||
let queriesImported = false;
|
let queriesImported = false;
|
||||||
const oldQueries = getState().explore.panes[exploreId]!.queries;
|
const oldQueries = getState().explore.panes[exploreId]!.queries;
|
||||||
const rootUID = getState().explore.panes[exploreId]!.datasourceInstance?.uid;
|
const rootUID = getState().explore.panes[exploreId]!.datasourceInstance?.uid;
|
||||||
|
const correlationDetails = getState().explore.correlationEditorDetails;
|
||||||
|
const isCorrelationsEditorMode = correlationDetails?.editorMode || false;
|
||||||
|
const isLeftPane = Object.keys(getState().explore.panes)[0] === exploreId;
|
||||||
|
|
||||||
|
if (!isLeftPane && isCorrelationsEditorMode && !correlationDetails?.dirty) {
|
||||||
|
dispatch(changeCorrelationEditorDetails({ dirty: true }));
|
||||||
|
}
|
||||||
|
|
||||||
for (const newQuery of queries) {
|
for (const newQuery of queries) {
|
||||||
for (const oldQuery of oldQueries) {
|
for (const oldQuery of oldQueries) {
|
||||||
@ -500,6 +507,7 @@ export const runQueries = createAsyncThunk<void, RunQueriesOptions>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const exploreItemState = getState().explore.panes[exploreId]!;
|
const exploreItemState = getState().explore.panes[exploreId]!;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
@ -512,7 +520,14 @@ export const runQueries = createAsyncThunk<void, RunQueriesOptions>(
|
|||||||
absoluteRange,
|
absoluteRange,
|
||||||
cache,
|
cache,
|
||||||
supplementaryQueries,
|
supplementaryQueries,
|
||||||
|
correlationEditorHelperData,
|
||||||
} = exploreItemState;
|
} = exploreItemState;
|
||||||
|
const isCorrelationEditorMode = getState().explore.correlationEditorDetails?.editorMode || false;
|
||||||
|
const isLeftPane = Object.keys(getState().explore.panes)[0] === exploreId;
|
||||||
|
const showCorrelationEditorLinks = isCorrelationEditorMode && isLeftPane;
|
||||||
|
const defaultCorrelationEditorDatasource = showCorrelationEditorLinks ? await getDataSourceSrv().get() : undefined;
|
||||||
|
const interpolateCorrelationHelperVars =
|
||||||
|
isCorrelationEditorMode && !isLeftPane && correlationEditorHelperData !== undefined;
|
||||||
let newQuerySource: Observable<ExplorePanelData>;
|
let newQuerySource: Observable<ExplorePanelData>;
|
||||||
let newQuerySubscription: SubscriptionLike;
|
let newQuerySubscription: SubscriptionLike;
|
||||||
|
|
||||||
@ -531,7 +546,16 @@ export const runQueries = createAsyncThunk<void, RunQueriesOptions>(
|
|||||||
if (cachedValue) {
|
if (cachedValue) {
|
||||||
newQuerySource = combineLatest([of(cachedValue), correlations$]).pipe(
|
newQuerySource = combineLatest([of(cachedValue), correlations$]).pipe(
|
||||||
mergeMap(([data, correlations]) =>
|
mergeMap(([data, correlations]) =>
|
||||||
decorateData(data, queryResponse, absoluteRange, refreshInterval, queries, correlations)
|
decorateData(
|
||||||
|
data,
|
||||||
|
queryResponse,
|
||||||
|
absoluteRange,
|
||||||
|
refreshInterval,
|
||||||
|
queries,
|
||||||
|
correlations,
|
||||||
|
showCorrelationEditorLinks,
|
||||||
|
defaultCorrelationEditorDatasource
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -563,8 +587,23 @@ export const runQueries = createAsyncThunk<void, RunQueriesOptions>(
|
|||||||
liveStreaming: live,
|
liveStreaming: live,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let scopedVars: ScopedVars = {};
|
||||||
|
if (interpolateCorrelationHelperVars && correlationEditorHelperData !== undefined) {
|
||||||
|
Object.entries(correlationEditorHelperData?.vars).forEach((variable) => {
|
||||||
|
scopedVars[variable[0]] = { value: variable[1] };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const timeZone = getTimeZone(getState().user);
|
const timeZone = getTimeZone(getState().user);
|
||||||
const transaction = buildQueryTransaction(exploreId, queries, queryOptions, range, scanning, timeZone);
|
const transaction = buildQueryTransaction(
|
||||||
|
exploreId,
|
||||||
|
queries,
|
||||||
|
queryOptions,
|
||||||
|
range,
|
||||||
|
scanning,
|
||||||
|
timeZone,
|
||||||
|
scopedVars
|
||||||
|
);
|
||||||
|
|
||||||
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading }));
|
dispatch(changeLoadingStateAction({ exploreId, loadingState: LoadingState.Loading }));
|
||||||
|
|
||||||
@ -577,7 +616,16 @@ export const runQueries = createAsyncThunk<void, RunQueriesOptions>(
|
|||||||
correlations$,
|
correlations$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
mergeMap(([data, correlations]) =>
|
mergeMap(([data, correlations]) =>
|
||||||
decorateData(data, queryResponse, absoluteRange, refreshInterval, queries, correlations)
|
decorateData(
|
||||||
|
data,
|
||||||
|
queryResponse,
|
||||||
|
absoluteRange,
|
||||||
|
refreshInterval,
|
||||||
|
queries,
|
||||||
|
correlations,
|
||||||
|
showCorrelationEditorLinks,
|
||||||
|
defaultCorrelationEditorDatasource
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1142,27 +1190,6 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
|
|||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an observable that emits correlations once they are loaded
|
|
||||||
*/
|
|
||||||
const getCorrelations = (exploreId: string) => {
|
|
||||||
return new Observable<CorrelationData[]>((subscriber) => {
|
|
||||||
const existingCorrelations = store.getState().explore.panes[exploreId]?.correlations;
|
|
||||||
if (existingCorrelations) {
|
|
||||||
subscriber.next(existingCorrelations);
|
|
||||||
subscriber.complete();
|
|
||||||
} else {
|
|
||||||
const unsubscribe = store.subscribe(() => {
|
|
||||||
const correlations = store.getState().explore.panes[exploreId]?.correlations;
|
|
||||||
if (correlations) {
|
|
||||||
unsubscribe();
|
|
||||||
subscriber.next(correlations);
|
|
||||||
subscriber.complete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const processQueryResponse = (
|
export const processQueryResponse = (
|
||||||
state: ExploreItemState,
|
state: ExploreItemState,
|
||||||
action: PayloadAction<QueryEndedPayload>
|
action: PayloadAction<QueryEndedPayload>
|
||||||
|
@ -3,6 +3,7 @@ import { createSelector } from '@reduxjs/toolkit';
|
|||||||
import { ExploreItemState, StoreState } from 'app/types';
|
import { ExploreItemState, StoreState } from 'app/types';
|
||||||
|
|
||||||
export const selectPanes = (state: Pick<StoreState, 'explore'>) => state.explore.panes;
|
export const selectPanes = (state: Pick<StoreState, 'explore'>) => state.explore.panes;
|
||||||
|
export const selectExploreRoot = (state: Pick<StoreState, 'explore'>) => state.explore;
|
||||||
|
|
||||||
export const selectPanesEntries = createSelector<
|
export const selectPanesEntries = createSelector<
|
||||||
[(state: Pick<StoreState, 'explore'>) => Record<string, ExploreItemState | undefined>],
|
[(state: Pick<StoreState, 'explore'>) => Record<string, ExploreItemState | undefined>],
|
||||||
@ -11,4 +12,11 @@ export const selectPanesEntries = createSelector<
|
|||||||
|
|
||||||
export const isSplit = createSelector(selectPanesEntries, (panes) => panes.length > 1);
|
export const isSplit = createSelector(selectPanesEntries, (panes) => panes.length > 1);
|
||||||
|
|
||||||
|
export const isLeftPaneSelector = (exploreId: string) =>
|
||||||
|
createSelector(selectPanes, (panes) => {
|
||||||
|
return Object.keys(panes)[0] === exploreId;
|
||||||
|
});
|
||||||
|
|
||||||
export const getExploreItemSelector = (exploreId: string) => createSelector(selectPanes, (panes) => panes[exploreId]);
|
export const getExploreItemSelector = (exploreId: string) => createSelector(selectPanes, (panes) => panes[exploreId]);
|
||||||
|
|
||||||
|
export const selectCorrelationDetails = createSelector(selectExploreRoot, (state) => state.correlationEditorDetails);
|
||||||
|
@ -1,10 +1,23 @@
|
|||||||
|
import { flattenDeep } from 'lodash';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import { DataFrame, FieldType, LoadingState, PanelData, getDefaultTimeRange, toDataFrame } from '@grafana/data';
|
import {
|
||||||
|
DataFrame,
|
||||||
|
FieldType,
|
||||||
|
LoadingState,
|
||||||
|
PanelData,
|
||||||
|
getDefaultTimeRange,
|
||||||
|
toDataFrame,
|
||||||
|
DataSourceApi,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { DataSourceJsonData, DataQuery } from '@grafana/schema';
|
||||||
import TableModel from 'app/core/TableModel';
|
import TableModel from 'app/core/TableModel';
|
||||||
|
import { CorrelationData } from 'app/features/correlations/useCorrelations';
|
||||||
import { ExplorePanelData } from 'app/types';
|
import { ExplorePanelData } from 'app/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
decorateWithCorrelations,
|
||||||
decorateWithFrameTypeMetadata,
|
decorateWithFrameTypeMetadata,
|
||||||
decorateWithGraphResult,
|
decorateWithGraphResult,
|
||||||
decorateWithLogsResult,
|
decorateWithLogsResult,
|
||||||
@ -103,6 +116,23 @@ const createExplorePanelData = (args: Partial<ExplorePanelData>): ExplorePanelDa
|
|||||||
return { ...defaults, ...args };
|
return { ...defaults, ...args };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const datasource = {
|
||||||
|
name: 'testDs',
|
||||||
|
type: 'postgres',
|
||||||
|
uid: 'ds1',
|
||||||
|
getRef: () => {
|
||||||
|
return { type: 'postgres', uid: 'ds1' };
|
||||||
|
},
|
||||||
|
} as DataSourceApi<DataQuery, DataSourceJsonData, {}>;
|
||||||
|
|
||||||
|
const datasourceInstance = {
|
||||||
|
name: datasource.name,
|
||||||
|
id: 1,
|
||||||
|
uid: datasource.uid,
|
||||||
|
type: datasource.type,
|
||||||
|
jsonData: {},
|
||||||
|
} as DataSourceInstanceSettings<DataSourceJsonData>;
|
||||||
|
|
||||||
describe('decorateWithGraphLogsTraceTableAndFlameGraph', () => {
|
describe('decorateWithGraphLogsTraceTableAndFlameGraph', () => {
|
||||||
it('should correctly classify the dataFrames', () => {
|
it('should correctly classify the dataFrames', () => {
|
||||||
const { table, logs, timeSeries, emptyTable, flameGraph } = getTestContext();
|
const { table, logs, timeSeries, emptyTable, flameGraph } = getTestContext();
|
||||||
@ -362,3 +392,103 @@ describe('decorateWithCustomFrames', () => {
|
|||||||
expect(decorateWithFrameTypeMetadata(panelData).customFrames).toEqual([customFrame]);
|
expect(decorateWithFrameTypeMetadata(panelData).customFrames).toEqual([customFrame]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('decorateWithCorrelations', () => {
|
||||||
|
it('returns no links if there are no correlations and no editor links', () => {
|
||||||
|
const { table, logs, timeSeries, emptyTable, flameGraph } = getTestContext();
|
||||||
|
const series = [table, logs, timeSeries, emptyTable, flameGraph];
|
||||||
|
const timeRange = getDefaultTimeRange();
|
||||||
|
const panelData: PanelData = {
|
||||||
|
series,
|
||||||
|
state: LoadingState.Done,
|
||||||
|
timeRange,
|
||||||
|
};
|
||||||
|
const postDecoratedPanel = decorateWithCorrelations({
|
||||||
|
showCorrelationEditorLinks: false,
|
||||||
|
queries: [],
|
||||||
|
correlations: [],
|
||||||
|
defaultTargetDatasource: undefined,
|
||||||
|
})(panelData);
|
||||||
|
expect(
|
||||||
|
flattenDeep(postDecoratedPanel.series.map((frame) => frame.fields.map((field) => field.config.links)))
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns one field link per field if there are no correlations, but there are editor links', () => {
|
||||||
|
const { table } = getTestContext();
|
||||||
|
const series = [table];
|
||||||
|
const timeRange = getDefaultTimeRange();
|
||||||
|
const panelData: PanelData = {
|
||||||
|
series,
|
||||||
|
state: LoadingState.Done,
|
||||||
|
timeRange,
|
||||||
|
};
|
||||||
|
|
||||||
|
const postDecoratedPanel = decorateWithCorrelations({
|
||||||
|
showCorrelationEditorLinks: true,
|
||||||
|
queries: [],
|
||||||
|
correlations: [],
|
||||||
|
defaultTargetDatasource: datasource,
|
||||||
|
})(panelData);
|
||||||
|
const flattenedLinks = flattenDeep(
|
||||||
|
postDecoratedPanel.series.map((frame) => frame.fields.map((field) => field.config.links))
|
||||||
|
);
|
||||||
|
expect(flattenedLinks.length).toEqual(table.fields.length);
|
||||||
|
expect(flattenedLinks[0]).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns one field link per field if there are correlations and editor links', () => {
|
||||||
|
const { table } = getTestContext();
|
||||||
|
const series = [table];
|
||||||
|
const timeRange = getDefaultTimeRange();
|
||||||
|
const panelData: PanelData = {
|
||||||
|
series,
|
||||||
|
state: LoadingState.Done,
|
||||||
|
timeRange,
|
||||||
|
};
|
||||||
|
|
||||||
|
const correlations = [{ source: datasourceInstance, target: datasourceInstance }] as CorrelationData[];
|
||||||
|
const postDecoratedPanel = decorateWithCorrelations({
|
||||||
|
showCorrelationEditorLinks: true,
|
||||||
|
queries: [],
|
||||||
|
correlations: correlations,
|
||||||
|
defaultTargetDatasource: datasource,
|
||||||
|
})(panelData);
|
||||||
|
const flattenedLinks = flattenDeep(
|
||||||
|
postDecoratedPanel.series.map((frame) => frame.fields.map((field) => field.config.links))
|
||||||
|
);
|
||||||
|
expect(flattenedLinks.length).toEqual(table.fields.length);
|
||||||
|
expect(flattenedLinks[0]).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns one field link per correlation if there are correlations and we are not showing editor links', () => {
|
||||||
|
const { table } = getTestContext();
|
||||||
|
const series = [table];
|
||||||
|
const timeRange = getDefaultTimeRange();
|
||||||
|
const panelData: PanelData = {
|
||||||
|
series,
|
||||||
|
state: LoadingState.Done,
|
||||||
|
timeRange,
|
||||||
|
};
|
||||||
|
|
||||||
|
const correlations = [
|
||||||
|
{
|
||||||
|
uid: '0',
|
||||||
|
source: datasourceInstance,
|
||||||
|
target: datasourceInstance,
|
||||||
|
provisioned: true,
|
||||||
|
config: { field: panelData.series[0].fields[0].name },
|
||||||
|
},
|
||||||
|
] as CorrelationData[];
|
||||||
|
|
||||||
|
const postDecoratedPanel = decorateWithCorrelations({
|
||||||
|
showCorrelationEditorLinks: false,
|
||||||
|
queries: [{ refId: 'A', datasource: datasource.getRef() }],
|
||||||
|
correlations: correlations,
|
||||||
|
defaultTargetDatasource: undefined,
|
||||||
|
})(panelData);
|
||||||
|
expect(
|
||||||
|
flattenDeep(postDecoratedPanel.series.map((frame) => frame.fields.map((field) => field.config.links))).length
|
||||||
|
).toEqual(correlations.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -10,6 +10,9 @@ import {
|
|||||||
PanelData,
|
PanelData,
|
||||||
standardTransformers,
|
standardTransformers,
|
||||||
preProcessPanelData,
|
preProcessPanelData,
|
||||||
|
DataLinkConfigOrigin,
|
||||||
|
getRawDisplayProcessor,
|
||||||
|
DataSourceApi,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
@ -93,14 +96,45 @@ export const decorateWithFrameTypeMetadata = (data: PanelData): ExplorePanelData
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const decorateWithCorrelations = ({
|
export const decorateWithCorrelations = ({
|
||||||
|
showCorrelationEditorLinks,
|
||||||
queries,
|
queries,
|
||||||
correlations,
|
correlations,
|
||||||
|
defaultTargetDatasource,
|
||||||
}: {
|
}: {
|
||||||
|
showCorrelationEditorLinks: boolean;
|
||||||
queries: DataQuery[] | undefined;
|
queries: DataQuery[] | undefined;
|
||||||
correlations: CorrelationData[] | undefined;
|
correlations: CorrelationData[] | undefined;
|
||||||
|
defaultTargetDatasource?: DataSourceApi;
|
||||||
}) => {
|
}) => {
|
||||||
return (data: PanelData): PanelData => {
|
return (data: PanelData): PanelData => {
|
||||||
if (queries?.length && correlations?.length) {
|
if (showCorrelationEditorLinks && defaultTargetDatasource) {
|
||||||
|
for (const frame of data.series) {
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
field.config.links = []; // hide all previous links, we only want to show fake correlations in this view
|
||||||
|
|
||||||
|
field.display = field.display || getRawDisplayProcessor();
|
||||||
|
|
||||||
|
const availableVars: Record<string, string> = {};
|
||||||
|
frame.fields.map((field) => {
|
||||||
|
availableVars[`${field.name}`] = "${__data.fields.['" + `${field.name}` + `']}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
field.config.links.push({
|
||||||
|
url: '',
|
||||||
|
origin: DataLinkConfigOrigin.ExploreCorrelationsEditor,
|
||||||
|
title: `Correlate with ${field.name}`,
|
||||||
|
internal: {
|
||||||
|
datasourceUid: defaultTargetDatasource.uid,
|
||||||
|
datasourceName: defaultTargetDatasource.name,
|
||||||
|
query: { datasource: { uid: defaultTargetDatasource.uid } },
|
||||||
|
meta: {
|
||||||
|
correlationData: { resultField: field.name, vars: availableVars },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (queries?.length && correlations?.length) {
|
||||||
const queryRefIdToDataSourceUid = mapValues(groupBy(queries, 'refId'), '0.datasource.uid');
|
const queryRefIdToDataSourceUid = mapValues(groupBy(queries, 'refId'), '0.datasource.uid');
|
||||||
attachCorrelationsToDataFrames(data.series, correlations, queryRefIdToDataSourceUid);
|
attachCorrelationsToDataFrames(data.series, correlations, queryRefIdToDataSourceUid);
|
||||||
}
|
}
|
||||||
@ -255,11 +289,20 @@ export function decorateData(
|
|||||||
absoluteRange: AbsoluteTimeRange,
|
absoluteRange: AbsoluteTimeRange,
|
||||||
refreshInterval: string | undefined,
|
refreshInterval: string | undefined,
|
||||||
queries: DataQuery[] | undefined,
|
queries: DataQuery[] | undefined,
|
||||||
correlations: CorrelationData[] | undefined
|
correlations: CorrelationData[] | undefined,
|
||||||
|
showCorrelationEditorLinks: boolean,
|
||||||
|
defaultCorrelationTargetDatasource?: DataSourceApi
|
||||||
): Observable<ExplorePanelData> {
|
): Observable<ExplorePanelData> {
|
||||||
return of(data).pipe(
|
return of(data).pipe(
|
||||||
map((data: PanelData) => preProcessPanelData(data, queryResponse)),
|
map((data: PanelData) => preProcessPanelData(data, queryResponse)),
|
||||||
map(decorateWithCorrelations({ queries, correlations })),
|
map(
|
||||||
|
decorateWithCorrelations({
|
||||||
|
defaultTargetDatasource: defaultCorrelationTargetDatasource,
|
||||||
|
showCorrelationEditorLinks,
|
||||||
|
queries,
|
||||||
|
correlations,
|
||||||
|
})
|
||||||
|
),
|
||||||
map(decorateWithFrameTypeMetadata),
|
map(decorateWithFrameTypeMetadata),
|
||||||
map(decorateWithGraphResult),
|
map(decorateWithGraphResult),
|
||||||
map(decorateWithLogsResult({ absoluteRange, refreshInterval, queries })),
|
map(decorateWithLogsResult({ absoluteRange, refreshInterval, queries })),
|
||||||
|
@ -39,7 +39,9 @@ export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange
|
|||||||
onRunQuery();
|
onRunQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryTypeOptions = getQueryTypeOptions(app === CoreApp.Explore || app === CoreApp.PanelEditor);
|
const queryTypeOptions = getQueryTypeOptions(
|
||||||
|
app === CoreApp.Explore || app === CoreApp.Correlations || app === CoreApp.PanelEditor
|
||||||
|
);
|
||||||
const onQueryTypeChange = getQueryTypeChangeHandler(query, onChange);
|
const onQueryTypeChange = getQueryTypeChangeHandler(query, onChange);
|
||||||
|
|
||||||
const onExemplarChange = (event: SyntheticEvent<HTMLInputElement>) => {
|
const onExemplarChange = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||||
|
@ -16,12 +16,37 @@ import {
|
|||||||
ExplorePanelsState,
|
ExplorePanelsState,
|
||||||
SupplementaryQueryType,
|
SupplementaryQueryType,
|
||||||
UrlQueryMap,
|
UrlQueryMap,
|
||||||
|
ExploreCorrelationHelperData,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes';
|
import { RichHistorySearchFilters, RichHistorySettings } from 'app/core/utils/richHistoryTypes';
|
||||||
|
|
||||||
import { CorrelationData } from '../features/correlations/useCorrelations';
|
import { CorrelationData } from '../features/correlations/useCorrelations';
|
||||||
|
|
||||||
export type ExploreQueryParams = UrlQueryMap;
|
export type ExploreQueryParams = UrlQueryMap;
|
||||||
|
|
||||||
|
export enum CORRELATION_EDITOR_POST_CONFIRM_ACTION {
|
||||||
|
CLOSE_PANE,
|
||||||
|
CHANGE_DATASOURCE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CorrelationEditorDetails {
|
||||||
|
editorMode: boolean;
|
||||||
|
dirty: boolean;
|
||||||
|
isExiting: boolean;
|
||||||
|
postConfirmAction?: {
|
||||||
|
// perform an action after a confirmation modal instead of exiting editor mode
|
||||||
|
exploreId: string;
|
||||||
|
action: CORRELATION_EDITOR_POST_CONFIRM_ACTION;
|
||||||
|
changeDatasourceUid?: string;
|
||||||
|
};
|
||||||
|
canSave?: boolean;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// updates can have any properties
|
||||||
|
export interface CorrelationEditorDetailsUpdate extends Partial<CorrelationEditorDetails> {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global Explore state
|
* Global Explore state
|
||||||
*/
|
*/
|
||||||
@ -49,6 +74,11 @@ export interface ExploreState {
|
|||||||
*/
|
*/
|
||||||
richHistoryLimitExceededWarningShown: boolean;
|
richHistoryLimitExceededWarningShown: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details on a correlation being created from explore
|
||||||
|
*/
|
||||||
|
correlationEditorDetails?: CorrelationEditorDetails;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On a split manual resize, we calculate which pane is larger, or if they are roughly the same size. If undefined, it is not split or they are roughly the same size
|
* On a split manual resize, we calculate which pane is larger, or if they are roughly the same size. If undefined, it is not split or they are roughly the same size
|
||||||
*/
|
*/
|
||||||
@ -192,6 +222,8 @@ export interface ExploreItemState {
|
|||||||
|
|
||||||
panelsState: ExplorePanelsState;
|
panelsState: ExplorePanelsState;
|
||||||
|
|
||||||
|
correlationEditorHelperData?: ExploreCorrelationHelperData;
|
||||||
|
|
||||||
correlations?: CorrelationData[];
|
correlations?: CorrelationData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user