Alerting: Make Loki & Prometheus instant vector by default (#66797)

Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
This commit is contained in:
Gilles De Mey 2023-04-27 16:38:22 +02:00 committed by GitHub
parent e9971dd153
commit b94fceddad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 435 additions and 360 deletions

View File

@ -24,6 +24,7 @@ import {
import alertDef, { EvalFunction } from '../state/alertDef'; import alertDef, { EvalFunction } from '../state/alertDef';
import { ExpressionResult } from './components/expressions/Expression'; import { ExpressionResult } from './components/expressions/Expression';
import { getThresholdsForQueries, ThresholdDefinition } from './components/rule-editor/util';
import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization'; import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization';
interface GrafanaRuleViewerProps { interface GrafanaRuleViewerProps {
@ -46,6 +47,8 @@ export function GrafanaRuleQueryViewer({
const expressions = queries.filter((q) => isExpressionQuery(q.model)); const expressions = queries.filter((q) => isExpressionQuery(q.model));
const styles = useStyles2(getExpressionViewerStyles); const styles = useStyles2(getExpressionViewerStyles);
const thresholds = getThresholdsForQueries(queries);
return ( return (
<Stack gap={2} direction="column"> <Stack gap={2} direction="column">
<div className={styles.maxWidthContainer}> <div className={styles.maxWidthContainer}>
@ -62,6 +65,7 @@ export function GrafanaRuleQueryViewer({
relativeTimeRange={relativeTimeRange} relativeTimeRange={relativeTimeRange}
evalTimeRange={evalTimeRanges[refId]} evalTimeRange={evalTimeRanges[refId]}
dataSource={dataSource} dataSource={dataSource}
thresholds={thresholds[refId]}
queryData={evalDataByQuery[refId]} queryData={evalDataByQuery[refId]}
onEvalTimeRangeChange={(timeRange) => onTimeRangeChange(refId, timeRange)} onEvalTimeRangeChange={(timeRange) => onTimeRangeChange(refId, timeRange)}
/> />
@ -97,6 +101,7 @@ interface QueryPreviewProps extends Pick<AlertQuery, 'refId' | 'relativeTimeRang
isAlertCondition: boolean; isAlertCondition: boolean;
dataSource?: DataSourceInstanceSettings; dataSource?: DataSourceInstanceSettings;
queryData?: PanelData; queryData?: PanelData;
thresholds?: ThresholdDefinition;
evalTimeRange?: RelativeTimeRange; evalTimeRange?: RelativeTimeRange;
onEvalTimeRangeChange: (timeRange: RelativeTimeRange) => void; onEvalTimeRangeChange: (timeRange: RelativeTimeRange) => void;
} }
@ -104,6 +109,7 @@ interface QueryPreviewProps extends Pick<AlertQuery, 'refId' | 'relativeTimeRang
export function QueryPreview({ export function QueryPreview({
refId, refId,
relativeTimeRange, relativeTimeRange,
thresholds,
model, model,
dataSource, dataSource,
queryData, queryData,
@ -127,9 +133,10 @@ export function QueryPreview({
{dataSource && ( {dataSource && (
<RuleViewerVisualization <RuleViewerVisualization
refId={refId} refId={refId}
datasourceUid={dataSource.uid} dsSettings={dataSource}
model={model} model={model}
data={queryData} data={queryData}
thresholds={thresholds}
relativeTimeRange={evalTimeRange} relativeTimeRange={evalTimeRange}
onTimeRangeChange={onEvalTimeRangeChange} onTimeRangeChange={onEvalTimeRangeChange}
className={styles.visualization} className={styles.visualization}
@ -144,7 +151,7 @@ const getQueryPreviewStyles = (theme: GrafanaTheme2) => ({
margin: ${theme.spacing(1)}; margin: ${theme.spacing(1)};
`, `,
contentBox: css` contentBox: css`
flex: 1 0 100%; // RuleViewerVisualization uses AutoSizer which doesn't expand the box flex: 1 0 100%;
`, `,
visualization: css` visualization: css`
padding: ${theme.spacing(1)}; padding: ${theme.spacing(1)};

View File

@ -1,46 +0,0 @@
import React, { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { RadioButtonGroup } from '@grafana/ui';
import { STAT, TABLE, TIMESERIES } from '../utils/constants';
export type SupportedPanelPlugins = 'timeseries' | 'table' | 'stat';
type Props = {
value: SupportedPanelPlugins;
onChange: (value: SupportedPanelPlugins) => void;
size?: 'sm' | 'md';
};
export function PanelPluginsButtonGroup(props: Props): JSX.Element | null {
const { value, onChange, size = 'md' } = props;
const panels = useMemo(() => getSupportedPanels(), []);
return <RadioButtonGroup options={panels} value={value} onChange={onChange} size={size} />;
}
function getSupportedPanels(): Array<SelectableValue<SupportedPanelPlugins>> {
return Object.values(config.panels).reduce((panels: Array<SelectableValue<SupportedPanelPlugins>>, panel) => {
if (isSupportedPanelPlugin(panel.id)) {
panels.push({
value: panel.id,
label: panel.name,
imgUrl: panel.info.logos.small,
});
}
return panels;
}, []);
}
function isSupportedPanelPlugin(id: string): id is SupportedPanelPlugins {
switch (id) {
case TIMESERIES:
case TABLE:
case STAT:
return true;
default:
return false;
}
}

View File

@ -14,6 +14,20 @@ describe('TestResult', () => {
}).not.toThrow(); }).not.toThrow();
}); });
it('should show labels and values', () => {
const series: DataFrame[] = [
toDataFrame({ fields: [{ name: 'temp', values: [0.1234], labels: { label1: 'value1', label2: 'value2' } }] }),
toDataFrame({ fields: [{ name: 'temp', values: [0.5678], labels: { label1: 'value3', label2: 'value4' } }] }),
];
render(<ExpressionResult series={series} />);
expect(screen.getByTitle('{label1=value1, label2=value2}')).toBeInTheDocument();
expect(screen.getByText('0.1234')).toBeInTheDocument();
expect(screen.getByTitle('{label1=value3, label2=value4}')).toBeInTheDocument();
expect(screen.getByText('0.5678')).toBeInTheDocument();
});
it('should not paginate with less than PAGE_SIZE', () => { it('should not paginate with less than PAGE_SIZE', () => {
const series: DataFrame[] = [ const series: DataFrame[] = [
toDataFrame({ toDataFrame({
@ -58,7 +72,11 @@ function makeSeries(n: number) {
fields: [ fields: [
{ {
name: 'temp', name: 'temp',
values: [1], values: [0.1234],
labels: {
label1: 'value1',
label2: 'value2',
},
}, },
], ],
}) })

View File

@ -19,7 +19,7 @@ import { Spacer } from '../Spacer';
import { AlertStateTag } from '../rules/AlertStateTag'; import { AlertStateTag } from '../rules/AlertStateTag';
import { AlertConditionIndicator } from './AlertConditionIndicator'; import { AlertConditionIndicator } from './AlertConditionIndicator';
import { formatLabels, getSeriesName, getSeriesValue, isEmptySeries } from './util'; import { formatLabels, getSeriesLabels, getSeriesName, getSeriesValue, isEmptySeries } from './util';
interface ExpressionProps { interface ExpressionProps {
isAlertCondition?: boolean; isAlertCondition?: boolean;
@ -302,16 +302,37 @@ const FrameRow: FC<FrameProps> = ({ frame, index, isAlertCondition }) => {
const name = getSeriesName(frame) || 'Series ' + index; const name = getSeriesName(frame) || 'Series ' + index;
const value = getSeriesValue(frame); const value = getSeriesValue(frame);
const labelsRecord = getSeriesLabels(frame);
const labels = Object.entries(labelsRecord);
const hasLabels = labels.length > 0;
const showFiring = isAlertCondition && value !== 0; const showFiring = isAlertCondition && value !== 0;
const showNormal = isAlertCondition && value === 0; const showNormal = isAlertCondition && value === 0;
const title = `${hasLabels ? '' : name}${hasLabels ? `{${formatLabels(labelsRecord)}}` : ''}`;
return ( return (
<div className={styles.expression.resultsRow}> <div className={styles.expression.resultsRow}>
<Stack direction="row" gap={1} alignItems="center"> <Stack direction="row" gap={1} alignItems="center">
<span className={cx(styles.mutedText, styles.expression.resultLabel)} title={name}> <div className={styles.expression.resultLabel} title={title}>
{name} <span>{hasLabels ? '' : name}</span>
{hasLabels && (
<>
<span>{'{'}</span>
{labels.map(([key, value], index) => (
<span key={uniqueId()}>
<span className={styles.expression.labelKey}>{key}</span>
<span>=</span>
<span>&quot;</span>
<span className={styles.expression.labelValue}>{value}</span>
<span>&quot;</span>
{index < labels.length - 1 && <span>, </span>}
</span> </span>
))}
<span>{'}'}</span>
</>
)}
</div>
<div className={styles.expression.resultValue}>{value}</div> <div className={styles.expression.resultValue}>{value}</div>
{showFiring && <AlertStateTag state={PromAlertingRuleState.Firing} size="sm" />} {showFiring && <AlertStateTag state={PromAlertingRuleState.Firing} size="sm" />}
{showNormal && <AlertStateTag state={PromAlertingRuleState.Inactive} size="sm" />} {showNormal && <AlertStateTag state={PromAlertingRuleState.Inactive} size="sm" />}
@ -397,6 +418,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
color: ${theme.colors.primary.text}; color: ${theme.colors.primary.text};
`, `,
results: css` results: css`
display: flex;
flex-direction: column;
flex-wrap: nowrap;
border-top: solid 1px ${theme.colors.border.medium}; border-top: solid 1px ${theme.colors.border.medium};
`, `,
noResults: css` noResults: css`
@ -415,12 +440,21 @@ const getStyles = (theme: GrafanaTheme2) => ({
background-color: ${theme.colors.background.canvas}; background-color: ${theme.colors.background.canvas};
} }
`, `,
labelKey: css`
color: ${theme.isDark ? '#73bf69' : '#56a64b'};
`,
labelValue: css`
color: ${theme.isDark ? '#ce9178' : '#a31515'};
`,
resultValue: css` resultValue: css`
color: ${theme.colors.text.maxContrast};
text-align: right; text-align: right;
`, `,
resultLabel: css` resultLabel: css`
flex: 1; flex: 1;
overflow-x: auto;
display: inline-block;
white-space: nowrap;
`, `,
noData: css` noData: css`
display: flex; display: flex;

View File

@ -1,6 +1,6 @@
import { DataFrame, FieldType, toDataFrame } from '@grafana/data'; import { DataFrame, FieldType, toDataFrame } from '@grafana/data';
import { getSeriesName, formatLabels, getSeriesValue, isEmptySeries } from './util'; import { getSeriesName, formatLabels, getSeriesValue, isEmptySeries, getSeriesLabels } from './util';
const EMPTY_FRAME: DataFrame = toDataFrame([]); const EMPTY_FRAME: DataFrame = toDataFrame([]);
const NAMED_FRAME: DataFrame = { const NAMED_FRAME: DataFrame = {
@ -17,7 +17,7 @@ const DATA_FRAME_LARGE_DECIMAL: DataFrame = toDataFrame({
}); });
const DATA_FRAME_WITH_LABELS: DataFrame = toDataFrame({ const DATA_FRAME_WITH_LABELS: DataFrame = toDataFrame({
fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3], labels: { foo: 'bar' } }], fields: [{ name: 'value', type: FieldType.number, values: [1, 2, 3], labels: { __name__: 'my-series', foo: 'bar' } }],
}); });
describe('formatLabels', () => { describe('formatLabels', () => {
@ -51,16 +51,16 @@ describe('getSeriesName', () => {
}); });
it('should work with empty data frame', () => { it('should work with empty data frame', () => {
expect(getSeriesName(EMPTY_FRAME)).toBe(''); expect(getSeriesName(EMPTY_FRAME)).toBe(undefined);
}); });
it('should work with labeled frame', () => { it('should work with __name__ labeled frame', () => {
const name = getSeriesName(DATA_FRAME_WITH_LABELS); const name = getSeriesName(DATA_FRAME_WITH_LABELS);
expect(name).toBe('foo=bar'); expect(name).toBe('my-series');
}); });
it('should work with NoData frames', () => { it('should work with NoData frames', () => {
expect(getSeriesName(EMPTY_FRAME)).toBe(''); expect(getSeriesName(EMPTY_FRAME)).toBe(undefined);
}); });
it('should give preference to displayNameFromDS', () => { it('should give preference to displayNameFromDS', () => {
@ -97,3 +97,13 @@ describe('getSeriesValue', () => {
expect(getSeriesValue(DATA_FRAME_LARGE_DECIMAL)).toBe(1.23457); expect(getSeriesValue(DATA_FRAME_LARGE_DECIMAL)).toBe(1.23457);
}); });
}); });
describe('getSeriesLabels', () => {
it('should work for dataframe with labels', () => {
expect(getSeriesLabels(DATA_FRAME_WITH_LABELS)).toStrictEqual({ __name__: 'my-series', foo: 'bar' });
});
it('should work for dataframe with no labels', () => {
expect(getSeriesLabels(EMPTY_FRAME)).toStrictEqual({});
});
});

View File

@ -9,11 +9,11 @@ import { DataFrame, Labels, roundDecimals } from '@grafana/data';
* see https://github.com/Microsoft/TypeScript/issues/13778 * see https://github.com/Microsoft/TypeScript/issues/13778
*/ */
const getSeriesName = (frame: DataFrame): string => { const getSeriesName = (frame: DataFrame): string | undefined => {
const firstField = frame.fields[0]; const firstField = frame.fields[0];
const displayNameFromDS = firstField?.config?.displayNameFromDS; const displayNameFromDS = firstField?.config?.displayNameFromDS;
return displayNameFromDS ?? frame.name ?? formatLabels(firstField?.labels ?? {}); return displayNameFromDS ?? frame.name ?? firstField?.labels?.__name__;
}; };
const getSeriesValue = (frame: DataFrame) => { const getSeriesValue = (frame: DataFrame) => {
@ -26,6 +26,11 @@ const getSeriesValue = (frame: DataFrame) => {
return value; return value;
}; };
const getSeriesLabels = (frame: DataFrame): Record<string, string> => {
const firstField = frame.fields[0];
return firstField?.labels ?? {};
};
const formatLabels = (labels: Labels): string => { const formatLabels = (labels: Labels): string => {
return Object.entries(labels) return Object.entries(labels)
.map(([key, value]) => key + '=' + value) .map(([key, value]) => key + '=' + value)
@ -38,4 +43,4 @@ const isEmptySeries = (series: DataFrame[]): boolean => {
return isEmpty; return isEmpty;
}; };
export { getSeriesName, getSeriesValue, formatLabels, isEmptySeries }; export { getSeriesName, getSeriesValue, getSeriesLabels, formatLabels, isEmptySeries };

View File

@ -5,6 +5,7 @@ import React, { ChangeEvent, useState } from 'react';
import { import {
CoreApp, CoreApp,
DataQuery, DataQuery,
DataSourceApi,
DataSourceInstanceSettings, DataSourceInstanceSettings,
getDefaultRelativeTimeRange, getDefaultRelativeTimeRange,
GrafanaTheme2, GrafanaTheme2,
@ -23,12 +24,9 @@ import {
Tooltip, Tooltip,
useStyles2, useStyles2,
} from '@grafana/ui'; } from '@grafana/ui';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow'; import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow';
import { AlertQuery } from 'app/types/unified-alerting-dto'; import { AlertQuery } from 'app/types/unified-alerting-dto';
import { TABLE, TIMESERIES } from '../../utils/constants';
import { SupportedPanelPlugins } from '../PanelPluginsButtonGroup';
import { AlertConditionIndicator } from '../expressions/AlertConditionIndicator'; import { AlertConditionIndicator } from '../expressions/AlertConditionIndicator';
import { VizWrapper } from './VizWrapper'; import { VizWrapper } from './VizWrapper';
@ -81,8 +79,8 @@ export const QueryWrapper = ({
onChangeQueryOptions, onChangeQueryOptions,
}: Props) => { }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const isExpression = isExpressionQuery(query.model); const [dsInstance, setDsInstance] = useState<DataSourceApi>();
const [pluginId, changePluginId] = useState<SupportedPanelPlugins>(isExpression ? TABLE : TIMESERIES); const defaults = dsInstance?.getDefaultQuery ? dsInstance.getDefaultQuery(CoreApp.UnifiedAlerting) : {};
function SelectingDataSourceTooltip() { function SelectingDataSourceTooltip() {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@ -117,9 +115,6 @@ export const QueryWrapper = ({
maxDataPoints: queryOptions.maxDataPoints, maxDataPoints: queryOptions.maxDataPoints,
}; };
if (isExpressionQuery(query.model)) {
return null;
} else {
return ( return (
<Stack direction="row" alignItems="baseline" gap={1}> <Stack direction="row" alignItems="baseline" gap={1}>
<SelectingDataSourceTooltip /> <SelectingDataSourceTooltip />
@ -143,19 +138,23 @@ export const QueryWrapper = ({
</Stack> </Stack>
); );
} }
}
return ( return (
<Stack direction="column" gap={0.5}>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<QueryEditorRow<DataQuery> <QueryEditorRow<DataQuery>
alerting alerting
dataSource={dsSettings} dataSource={dsSettings}
onChangeDataSource={!isExpression ? (settings) => onChangeDataSource(settings, index) : undefined} onDataSourceLoaded={setDsInstance}
onChangeDataSource={(settings) => onChangeDataSource(settings, index)}
id={query.refId} id={query.refId}
index={index} index={index}
key={query.refId} key={query.refId}
data={data} data={data}
query={cloneDeep(query.model)} query={{
...defaults,
...cloneDeep(query.model),
}}
onChange={(query) => onChangeQuery(query, index)} onChange={(query) => onChangeQuery(query, index)}
onRemoveQuery={onRemoveQuery} onRemoveQuery={onRemoveQuery}
onAddQuery={() => onDuplicateQuery(cloneDeep(query))} onAddQuery={() => onDuplicateQuery(cloneDeep(query))}
@ -163,21 +162,18 @@ export const QueryWrapper = ({
queries={queries} queries={queries}
renderHeaderExtras={() => <HeaderExtras query={query} index={index} error={error} />} renderHeaderExtras={() => <HeaderExtras query={query} index={index} error={error} />}
app={CoreApp.UnifiedAlerting} app={CoreApp.UnifiedAlerting}
visualization={ hideDisableQuery={true}
data.state !== LoadingState.NotStarted ? ( />
</div>
{data.state !== LoadingState.NotStarted && (
<VizWrapper <VizWrapper
data={data} data={data}
changePanel={changePluginId}
currentPanel={pluginId}
thresholds={thresholds} thresholds={thresholds}
thresholdsType={thresholdsType} thresholdsType={thresholdsType}
onThresholdsChange={onChangeThreshold ? (thresholds) => onChangeThreshold(thresholds, index) : undefined} onThresholdsChange={onChangeThreshold ? (thresholds) => onChangeThreshold(thresholds, index) : undefined}
/> />
) : null )}
} </Stack>
hideDisableQuery={true}
/>
</div>
); );
}; };

View File

@ -7,12 +7,9 @@ import { getDataSourceSrv } from '@grafana/runtime';
import { DataQuery, LoadingState } from '@grafana/schema'; import { DataQuery, LoadingState } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { isExpressionQuery } from 'app/features/expressions/guards';
import { AlertQuery } from 'app/types/unified-alerting-dto'; import { AlertQuery } from 'app/types/unified-alerting-dto';
import { TABLE, TIMESERIES } from '../../utils/constants';
import { isPromOrLokiQuery } from '../../utils/rule-form'; import { isPromOrLokiQuery } from '../../utils/rule-form';
import { SupportedPanelPlugins } from '../PanelPluginsButtonGroup';
import { VizWrapper } from './VizWrapper'; import { VizWrapper } from './VizWrapper';
@ -39,10 +36,6 @@ export const RecordingRuleEditor: FC<RecordingRuleEditorProps> = ({
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const isExpression = isExpressionQuery(queries[0]?.model);
const [pluginId, changePluginId] = useState<SupportedPanelPlugins>(isExpression ? TABLE : TIMESERIES);
useEffect(() => { useEffect(() => {
setData(panelData?.[queries[0]?.refId]); setData(panelData?.[queries[0]?.refId]);
}, [panelData, queries]); }, [panelData, queries]);
@ -108,7 +101,7 @@ export const RecordingRuleEditor: FC<RecordingRuleEditorProps> = ({
{data && ( {data && (
<div className={styles.vizWrapper}> <div className={styles.vizWrapper}>
<VizWrapper data={data} currentPanel={pluginId} changePanel={changePluginId} /> <VizWrapper data={data} />
</div> </div>
)} )}
</> </>

View File

@ -1,136 +1,90 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { useEffect, useMemo, useState } from 'react'; import React from 'react';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { FieldConfigSource, GrafanaTheme2, PanelData, ThresholdsConfig } from '@grafana/data'; import { GrafanaTheme2, isTimeSeriesFrames, PanelData, ThresholdsConfig } from '@grafana/data';
import { PanelRenderer } from '@grafana/runtime'; import { GraphTresholdsStyleMode, LoadingState } from '@grafana/schema';
import { GraphFieldConfig, GraphTresholdsStyleMode } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui';
import { PanelContext, PanelContextProvider, useStyles2 } from '@grafana/ui';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { PanelOptions } from 'app/plugins/panel/table/panelcfg.gen'; import { GraphContainer } from 'app/features/explore/Graph/GraphContainer';
import { useVizHeight } from '../../hooks/useVizHeight'; import { ExpressionResult } from '../expressions/Expression';
import { SupportedPanelPlugins, PanelPluginsButtonGroup } from '../PanelPluginsButtonGroup';
import { getStatusMessage } from './util';
interface Props { interface Props {
data: PanelData; data: PanelData;
currentPanel: SupportedPanelPlugins;
changePanel: (panel: SupportedPanelPlugins) => void;
thresholds?: ThresholdsConfig; thresholds?: ThresholdsConfig;
thresholdsType?: GraphTresholdsStyleMode; thresholdsType?: GraphTresholdsStyleMode;
onThresholdsChange?: (thresholds: ThresholdsConfig) => void; onThresholdsChange?: (thresholds: ThresholdsConfig) => void;
} }
type PanelFieldConfig = FieldConfigSource<GraphFieldConfig>; /** The VizWrapper is just a simple component that renders either a table or a graph based on the type of data we receive from "PanelData" */
export const VizWrapper = ({ data, thresholds, thresholdsType }: Props) => {
export const VizWrapper = ({ const styles = useStyles2(getStyles);
data, const isTimeSeriesData = isTimeSeriesFrames(data.series);
currentPanel, const statusMessage = getStatusMessage(data);
changePanel, const thresholdsStyle = thresholdsType ? { mode: thresholdsType } : undefined;
thresholds, const timeRange = {
thresholdsType = GraphTresholdsStyleMode.Line, from: data.timeRange.from.valueOf(),
}: Props) => { to: data.timeRange.to.valueOf(),
const [options, setOptions] = useState<PanelOptions>({ };
frameIndex: 0,
showHeader: true,
});
const vizHeight = useVizHeight(data, currentPanel, options.frameIndex);
const styles = useStyles2(getStyles(vizHeight));
const [fieldConfig, setFieldConfig] = useState<PanelFieldConfig>(defaultFieldConfig(data, thresholds));
useEffect(() => {
setFieldConfig((fieldConfig) => ({
...fieldConfig,
defaults: {
...fieldConfig.defaults,
thresholds: thresholds,
unit: defaultUnit(data),
custom: {
...fieldConfig.defaults.custom,
thresholdsStyle: {
mode: thresholdsType,
},
},
},
}));
}, [thresholds, setFieldConfig, data, thresholdsType]);
const context: PanelContext = useMemo(
() => ({
eventBus: appEvents,
canEditThresholds: false,
showThresholds: true,
}),
[]
);
if (!options || !data) {
return null;
}
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.buttonGroup}> <AutoSizer disableHeight>
<PanelPluginsButtonGroup onChange={changePanel} value={currentPanel} /> {({ width }) => (
</div> <div style={{ width }}>
<AutoSizer> {isTimeSeriesData ? (
{({ width }) => { <GraphContainer
if (width === 0) { loading={data.state === LoadingState.Loading}
return null; statusMessage={statusMessage}
} data={data.series}
return ( eventBus={appEvents}
<div style={{ height: `${vizHeight}px`, width: `${width}px` }}> height={300}
<PanelContextProvider value={context}>
<PanelRenderer
height={vizHeight}
width={width} width={width}
data={data} absoluteRange={timeRange}
pluginId={currentPanel} timeZone="browser"
title="title" onChangeTime={() => {}}
onOptionsChange={setOptions} splitOpenFn={() => {}}
options={options} loadingState={data.state}
fieldConfig={fieldConfig} thresholdsConfig={thresholds}
thresholdsStyle={thresholdsStyle}
/> />
</PanelContextProvider> ) : (
<div className={styles.instantVectorResultWrapper}>
<header className={styles.title}>Table</header>
<ExpressionResult series={data.series} />
</div> </div>
); )}
}} </div>
)}
</AutoSizer> </AutoSizer>
</div> </div>
); );
}; };
const getStyles = (visHeight: number) => (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css` wrapper: css`
padding: 0 ${theme.spacing(2)}; width: 100%;
height: ${visHeight + theme.spacing.gridSize * 4}px; position: relative;
`, `,
buttonGroup: css` instantVectorResultWrapper: css`
border: solid 1px ${theme.colors.border.medium};
border-radius: ${theme.shape.borderRadius()};
padding: 0;
display: flex; display: flex;
justify-content: flex-end; flex-direction: column;
flex-wrap: nowrap;
`, `,
title: css({
label: 'panel-title',
padding: theme.spacing(),
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
fontSize: theme.typography.h6.fontSize,
fontWeight: theme.typography.h6.fontWeight,
}),
}); });
function defaultUnit(data: PanelData): string | undefined {
return data.series[0]?.fields.find((field) => field.type === 'number')?.config.unit;
}
function defaultFieldConfig(data: PanelData, thresholds?: ThresholdsConfig): PanelFieldConfig {
if (!thresholds) {
return { defaults: {}, overrides: [] };
}
return {
defaults: {
thresholds: thresholds,
unit: defaultUnit(data),
custom: {
thresholdsStyle: {
mode: GraphTresholdsStyleMode.Line,
},
},
},
overrides: [],
};
}

View File

@ -285,6 +285,56 @@ describe('getThresholdsForQueries', () => {
expect(thresholdsClassic).toMatchSnapshot(); expect(thresholdsClassic).toMatchSnapshot();
}); });
it('should not throw if no refId exists', () => {
const dataQuery: AlertQuery = {
refId: 'A',
datasourceUid: 'abc123',
queryType: '',
relativeTimeRange: {
from: 600,
to: 0,
},
model: {
refId: 'A',
},
};
const classicCondition = {
refId: 'B',
datasourceUid: '__expr__',
queryType: '',
model: {
refId: 'B',
type: 'classic_conditions',
datasource: ExpressionDatasourceRef,
conditions: [
{
type: 'query',
evaluator: {
params: [0],
type: 'gt',
},
operator: {
type: 'and',
},
query: {
params: [''],
},
reducer: {
params: [],
type: 'last',
},
},
],
},
};
expect(() => {
const thresholds = getThresholdsForQueries([dataQuery, classicCondition]);
expect(thresholds).toStrictEqual({});
}).not.toThrowError();
});
it('should work for within_range', () => { it('should work for within_range', () => {
const queries = createThresholdExample('within_range'); const queries = createThresholdExample('within_range');
const thresholds = getThresholdsForQueries(queries); const thresholds = getThresholdsForQueries(queries);

View File

@ -1,7 +1,7 @@
import { ValidateResult } from 'react-hook-form'; import { ValidateResult } from 'react-hook-form';
import { DataFrame, ThresholdsConfig, ThresholdsMode, isTimeSeriesFrames } from '@grafana/data'; import { DataFrame, ThresholdsConfig, ThresholdsMode, isTimeSeriesFrames, PanelData } from '@grafana/data';
import { GraphTresholdsStyleMode } from '@grafana/schema'; import { GraphTresholdsStyleMode, LoadingState } from '@grafana/schema';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { EvalFunction } from 'app/features/alerting/state/alertDef'; import { EvalFunction } from 'app/features/alerting/state/alertDef';
import { isExpressionQuery } from 'app/features/expressions/guards'; import { isExpressionQuery } from 'app/features/expressions/guards';
@ -114,13 +114,12 @@ export function warningFromSeries(series: DataFrame[]): Error | undefined {
return warning ? new Error(warning) : undefined; return warning ? new Error(warning) : undefined;
} }
export type ThresholdDefinitions = Record< export type ThresholdDefinition = {
string,
{
config: ThresholdsConfig; config: ThresholdsConfig;
mode: GraphTresholdsStyleMode; mode: GraphTresholdsStyleMode;
} };
>;
export type ThresholdDefinitions = Record<string, ThresholdDefinition>;
/** /**
* This function will retrieve threshold definitions for the given array of data and expression queries. * This function will retrieve threshold definitions for the given array of data and expression queries.
@ -147,11 +146,17 @@ export function getThresholdsForQueries(queries: AlertQuery[]) {
// the time series panel does not support both. // the time series panel does not support both.
const hasRangeThreshold = query.model.conditions.some(isRangeCondition); const hasRangeThreshold = query.model.conditions.some(isRangeCondition);
query.model.conditions.forEach((condition, index) => { query.model.conditions.forEach((condition) => {
const threshold = condition.evaluator.params; const threshold = condition.evaluator.params;
// "classic_conditions" use `condition.query.params[]` and "threshold" uses `query.model.expression` // "classic_conditions" use `condition.query.params[]` and "threshold" uses `query.model.expression`
const refId = condition.query.params[0] ?? query.model.expression; const refId = condition.query.params[0] ?? query.model.expression;
// if an expression hasn't been linked to a data query yet, it won't have a refId
if (!refId) {
return;
}
const isRangeThreshold = isRangeCondition(condition); const isRangeThreshold = isRangeCondition(condition);
try { try {
@ -261,6 +266,9 @@ export function getThresholdsForQueries(queries: AlertQuery[]) {
// now also sort the threshold values, if we don't then they will look weird in the time series panel // now also sort the threshold values, if we don't then they will look weird in the time series panel
// TODO this doesn't work for negative values for now, those need to be sorted inverse // TODO this doesn't work for negative values for now, those need to be sorted inverse
thresholds[refId].config.steps.sort((a, b) => a.value - b.value); thresholds[refId].config.steps.sort((a, b) => a.value - b.value);
// also make sure we remove any "undefined" values from our steps in case the threshold config is incomplete
thresholds[refId].config.steps = thresholds[refId].config.steps.filter((step) => step.value !== undefined);
} }
return thresholds; return thresholds;
@ -271,3 +279,17 @@ function isRangeCondition(condition: ClassicCondition) {
condition.evaluator.type === EvalFunction.IsWithinRange || condition.evaluator.type === EvalFunction.IsOutsideRange condition.evaluator.type === EvalFunction.IsWithinRange || condition.evaluator.type === EvalFunction.IsOutsideRange
); );
} }
export function getStatusMessage(data: PanelData): string | undefined {
const genericErrorMessage = 'Failed to fetch data';
if (data.state !== LoadingState.Error) {
return;
}
const errors = data.errors;
if (errors?.length) {
return errors.map((error) => error.message ?? genericErrorMessage).join(', ');
}
return data.error?.message ?? genericErrorMessage;
}

View File

@ -1,9 +1,9 @@
import { css, cx } from '@emotion/css'; import { css } from '@emotion/css';
import React, { useCallback, useState } from 'react'; import React, { useCallback } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { import {
DataSourceInstanceSettings, DataSourceInstanceSettings,
DataSourceJsonData,
DateTime, DateTime,
dateTime, dateTime,
GrafanaTheme2, GrafanaTheme2,
@ -11,20 +11,20 @@ import {
RelativeTimeRange, RelativeTimeRange,
urlUtil, urlUtil,
} from '@grafana/data'; } from '@grafana/data';
import { config, getDataSourceSrv, PanelRenderer } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Alert, CodeEditor, DateTimePicker, LinkButton, useStyles2, useTheme2 } from '@grafana/ui'; import { DateTimePicker, LinkButton, useStyles2 } from '@grafana/ui';
import { isExpressionQuery } from 'app/features/expressions/guards'; import { isExpressionQuery } from 'app/features/expressions/guards';
import { PanelOptions } from 'app/plugins/panel/table/panelcfg.gen';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto';
import { TABLE, TIMESERIES } from '../../utils/constants';
import { Authorize } from '../Authorize'; import { Authorize } from '../Authorize';
import { PanelPluginsButtonGroup, SupportedPanelPlugins } from '../PanelPluginsButtonGroup'; import { VizWrapper } from '../rule-editor/VizWrapper';
import { ThresholdDefinition } from '../rule-editor/util';
interface RuleViewerVisualizationProps interface RuleViewerVisualizationProps extends Pick<AlertQuery, 'refId' | 'model' | 'relativeTimeRange'> {
extends Pick<AlertQuery, 'refId' | 'datasourceUid' | 'model' | 'relativeTimeRange'> { dsSettings: DataSourceInstanceSettings<DataSourceJsonData>;
data?: PanelData; data?: PanelData;
thresholds?: ThresholdDefinition;
onTimeRangeChange: (range: RelativeTimeRange) => void; onTimeRangeChange: (range: RelativeTimeRange) => void;
className?: string; className?: string;
} }
@ -33,22 +33,15 @@ const headerHeight = 4;
export function RuleViewerVisualization({ export function RuleViewerVisualization({
data, data,
refId,
model, model,
datasourceUid, thresholds,
dsSettings,
relativeTimeRange, relativeTimeRange,
onTimeRangeChange, onTimeRangeChange,
className, className,
}: RuleViewerVisualizationProps): JSX.Element | null { }: RuleViewerVisualizationProps): JSX.Element | null {
const theme = useTheme2();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const defaultPanel = isExpressionQuery(model) ? TABLE : TIMESERIES; const isExpression = isExpressionQuery(model);
const [panel, setPanel] = useState<SupportedPanelPlugins>(defaultPanel);
const dsSettings = getDataSourceSrv().getInstanceSettings(datasourceUid);
const [options, setOptions] = useState<PanelOptions>({
frameIndex: 0,
showHeader: true,
});
const onTimeChange = useCallback( const onTimeChange = useCallback(
(newDateTime: DateTime) => { (newDateTime: DateTime) => {
@ -70,43 +63,15 @@ export function RuleViewerVisualization({
return null; return null;
} }
if (!dsSettings) {
return ( return (
<div className={cx(styles.content, className)}> <div className={className}>
<Alert title="Could not find datasource for query" />
<CodeEditor
width="100%"
height="250px"
language="json"
showLineNumbers={false}
showMiniMap={false}
value={JSON.stringify(model, null, '\t')}
readOnly={true}
/>
</div>
);
}
return (
<div className={cx(styles.content, className)}>
<AutoSizer>
{({ width, height }) => {
return (
<div style={{ width, height }}>
<div className={styles.header}> <div className={styles.header}>
<div className={styles.actions}> <div className={styles.actions}>
{!isExpressionQuery(model) && relativeTimeRange ? ( {!isExpression && relativeTimeRange ? (
<DateTimePicker <DateTimePicker date={setDateTime(relativeTimeRange.to)} onChange={onTimeChange} maxDate={new Date()} />
date={setDateTime(relativeTimeRange.to)}
onChange={onTimeChange}
maxDate={new Date()}
/>
) : null} ) : null}
<PanelPluginsButtonGroup onChange={setPanel} value={panel} size="md" />
<Authorize actions={[AccessControlAction.DataSourcesExplore]}> <Authorize actions={[AccessControlAction.DataSourcesExplore]}>
{!isExpressionQuery(model) && ( {!isExpression && (
<>
<div className={styles.spacing} />
<LinkButton <LinkButton
size="md" size="md"
variant="secondary" variant="secondary"
@ -116,24 +81,11 @@ export function RuleViewerVisualization({
> >
View in Explore View in Explore
</LinkButton> </LinkButton>
</>
)} )}
</Authorize> </Authorize>
</div> </div>
</div> </div>
<PanelRenderer <VizWrapper data={data} thresholds={thresholds?.config} thresholdsType={thresholds?.mode} />
height={height - theme.spacing.gridSize * headerHeight}
width={width}
data={data}
pluginId={panel}
title=""
onOptionsChange={setOptions}
options={options}
/>
</div>
);
}}
</AutoSizer>
</div> </div>
); );
} }
@ -142,7 +94,7 @@ function createExploreLink(settings: DataSourceInstanceSettings, model: AlertDat
const { name } = settings; const { name } = settings;
const { refId, ...rest } = model; const { refId, ...rest } = model;
/** /*
In my testing I've found some alerts that don't have a data source embedded inside the model. In my testing I've found some alerts that don't have a data source embedded inside the model.
At this moment in time it is unclear to me why some alert definitions not have a data source embedded in the model. At this moment in time it is unclear to me why some alert definitions not have a data source embedded in the model.
@ -161,16 +113,13 @@ function createExploreLink(settings: DataSourceInstanceSettings, model: AlertDat
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
content: css`
width: 100%;
height: 250px;
`,
header: css` header: css`
height: ${theme.spacing(headerHeight)}; height: ${theme.spacing(headerHeight)};
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
white-space: nowrap; white-space: nowrap;
margin-bottom: ${theme.spacing(2)};
`, `,
refId: css` refId: css`
font-weight: ${theme.typography.fontWeightMedium}; font-weight: ${theme.typography.fontWeightMedium};
@ -186,9 +135,6 @@ const getStyles = (theme: GrafanaTheme2) => {
display: flex; display: flex;
align-items: center; align-items: center;
`, `,
spacing: css`
padding: ${theme.spacing(0, 1, 0, 0)};
`,
errorMessage: css` errorMessage: css`
white-space: pre-wrap; white-space: pre-wrap;
`, `,

View File

@ -15,11 +15,18 @@ import {
LoadingState, LoadingState,
SplitOpen, SplitOpen,
TimeZone, TimeZone,
ThresholdsConfig,
DashboardCursorSync, DashboardCursorSync,
EventBus, EventBus,
} from '@grafana/data'; } from '@grafana/data';
import { PanelRenderer } from '@grafana/runtime'; import { PanelRenderer } from '@grafana/runtime';
import { GraphDrawStyle, LegendDisplayMode, TooltipDisplayMode, SortOrder } from '@grafana/schema'; import {
GraphDrawStyle,
LegendDisplayMode,
TooltipDisplayMode,
SortOrder,
GraphThresholdsStyleConfig,
} from '@grafana/schema';
import { import {
Button, Button,
Icon, Icon,
@ -29,13 +36,14 @@ import {
useStyles2, useStyles2,
useTheme2, useTheme2,
} from '@grafana/ui'; } from '@grafana/ui';
import { GraphFieldConfig } from 'app/plugins/panel/graph/types';
import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config'; import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
import { PanelOptions as TimeSeriesOptions } from 'app/plugins/panel/timeseries/panelcfg.gen'; import { PanelOptions as TimeSeriesOptions } from 'app/plugins/panel/timeseries/panelcfg.gen';
import { ExploreGraphStyle } from 'app/types'; import { ExploreGraphStyle } from 'app/types';
import { seriesVisibilityConfigFactory } from '../../dashboard/dashgrid/SeriesVisibilityConfigFactory'; import { seriesVisibilityConfigFactory } from '../../dashboard/dashgrid/SeriesVisibilityConfigFactory';
import { applyGraphStyle } from './exploreGraphStyleUtils'; import { applyGraphStyle, applyThresholdsConfig } from './exploreGraphStyleUtils';
import { useStructureRev } from './useStructureRev'; import { useStructureRev } from './useStructureRev';
const MAX_NUMBER_OF_TIME_SERIES = 20; const MAX_NUMBER_OF_TIME_SERIES = 20;
@ -55,6 +63,8 @@ interface Props {
graphStyle: ExploreGraphStyle; graphStyle: ExploreGraphStyle;
anchorToZero?: boolean; anchorToZero?: boolean;
yAxisMaximum?: number; yAxisMaximum?: number;
thresholdsConfig?: ThresholdsConfig;
thresholdsStyle?: GraphThresholdsStyleConfig;
eventBus: EventBus; eventBus: EventBus;
} }
@ -73,6 +83,8 @@ export function ExploreGraph({
tooltipDisplayMode = TooltipDisplayMode.Single, tooltipDisplayMode = TooltipDisplayMode.Single,
anchorToZero = false, anchorToZero = false,
yAxisMaximum, yAxisMaximum,
thresholdsConfig,
thresholdsStyle,
eventBus, eventBus,
}: Props) { }: Props) {
const theme = useTheme2(); const theme = useTheme2();
@ -93,7 +105,7 @@ export function ExploreGraph({
[] []
); );
const [fieldConfig, setFieldConfig] = useState<FieldConfigSource>({ const [fieldConfig, setFieldConfig] = useState<FieldConfigSource<GraphFieldConfig>>({
defaults: { defaults: {
min: anchorToZero ? 0 : undefined, min: anchorToZero ? 0 : undefined,
max: yAxisMaximum || undefined, max: yAxisMaximum || undefined,
@ -109,10 +121,10 @@ export function ExploreGraph({
overrides: [], overrides: [],
}); });
const styledFieldConfig = useMemo( const styledFieldConfig = useMemo(() => {
() => applyGraphStyle(fieldConfig, graphStyle, yAxisMaximum), const withGraphStyle = applyGraphStyle(fieldConfig, graphStyle, yAxisMaximum);
[fieldConfig, graphStyle, yAxisMaximum] return applyThresholdsConfig(withGraphStyle, thresholdsStyle, thresholdsConfig);
); }, [fieldConfig, graphStyle, yAxisMaximum, thresholdsConfig, thresholdsStyle]);
const dataWithConfig = useMemo(() => { const dataWithConfig = useMemo(() => {
return applyFieldOverrides({ return applyFieldOverrides({

View File

@ -1,7 +1,15 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { DataFrame, EventBus, AbsoluteTimeRange, TimeZone, SplitOpen, LoadingState } from '@grafana/data'; import {
import { PanelChrome } from '@grafana/ui'; DataFrame,
EventBus,
AbsoluteTimeRange,
TimeZone,
SplitOpen,
LoadingState,
ThresholdsConfig,
} from '@grafana/data';
import { GraphThresholdsStyleConfig, PanelChrome, PanelChromeProps } from '@grafana/ui';
import { ExploreGraphStyle } from 'app/types'; import { ExploreGraphStyle } from 'app/types';
import { storeGraphStyle } from '../state/utils'; import { storeGraphStyle } from '../state/utils';
@ -10,18 +18,18 @@ import { ExploreGraph } from './ExploreGraph';
import { ExploreGraphLabel } from './ExploreGraphLabel'; import { ExploreGraphLabel } from './ExploreGraphLabel';
import { loadGraphStyle } from './utils'; import { loadGraphStyle } from './utils';
interface Props { interface Props extends Pick<PanelChromeProps, 'width' | 'height' | 'statusMessage'> {
loading: boolean; loading: boolean;
data: DataFrame[]; data: DataFrame[];
annotations?: DataFrame[]; annotations?: DataFrame[];
eventBus: EventBus; eventBus: EventBus;
height: number;
width: number;
absoluteRange: AbsoluteTimeRange; absoluteRange: AbsoluteTimeRange;
timeZone: TimeZone; timeZone: TimeZone;
onChangeTime: (absoluteRange: AbsoluteTimeRange) => void; onChangeTime: (absoluteRange: AbsoluteTimeRange) => void;
splitOpenFn: SplitOpen; splitOpenFn: SplitOpen;
loadingState: LoadingState; loadingState: LoadingState;
thresholdsConfig?: ThresholdsConfig;
thresholdsStyle?: GraphThresholdsStyleConfig;
} }
export const GraphContainer = ({ export const GraphContainer = ({
@ -34,7 +42,10 @@ export const GraphContainer = ({
annotations, annotations,
onChangeTime, onChangeTime,
splitOpenFn, splitOpenFn,
thresholdsConfig,
thresholdsStyle,
loadingState, loadingState,
statusMessage,
}: Props) => { }: Props) => {
const [graphStyle, setGraphStyle] = useState(loadGraphStyle); const [graphStyle, setGraphStyle] = useState(loadGraphStyle);
@ -49,6 +60,7 @@ export const GraphContainer = ({
width={width} width={width}
height={height} height={height}
loadingState={loadingState} loadingState={loadingState}
statusMessage={statusMessage}
actions={<ExploreGraphLabel graphStyle={graphStyle} onChangeGraphStyle={onGraphStyleChange} />} actions={<ExploreGraphLabel graphStyle={graphStyle} onChangeGraphStyle={onGraphStyleChange} />}
> >
{(innerWidth, innerHeight) => ( {(innerWidth, innerHeight) => (
@ -63,6 +75,8 @@ export const GraphContainer = ({
annotations={annotations} annotations={annotations}
splitOpenFn={splitOpenFn} splitOpenFn={splitOpenFn}
loadingState={loadingState} loadingState={loadingState}
thresholdsConfig={thresholdsConfig}
thresholdsStyle={thresholdsStyle}
eventBus={eventBus} eventBus={eventBus}
/> />
)} )}

View File

@ -1,7 +1,7 @@
import produce from 'immer'; import produce from 'immer';
import { FieldConfigSource } from '@grafana/data'; import { FieldConfigSource, ThresholdsConfig } from '@grafana/data';
import { GraphDrawStyle, GraphFieldConfig, StackingMode } from '@grafana/schema'; import { GraphDrawStyle, GraphFieldConfig, GraphThresholdsStyleConfig, StackingMode } from '@grafana/schema';
import { ExploreGraphStyle } from 'app/types'; import { ExploreGraphStyle } from 'app/types';
export type FieldConfig = FieldConfigSource<GraphFieldConfig>; export type FieldConfig = FieldConfigSource<GraphFieldConfig>;
@ -57,3 +57,15 @@ export function applyGraphStyle(config: FieldConfig, style: ExploreGraphStyle, m
} }
}); });
} }
export function applyThresholdsConfig(
config: FieldConfig,
thresholdsStyle?: GraphThresholdsStyleConfig,
thresholdsConfig?: ThresholdsConfig
): FieldConfig {
return produce(config, (draft) => {
draft.defaults.thresholds = thresholdsConfig;
draft.defaults.custom = draft.defaults.custom ?? {};
draft.defaults.custom.thresholdsStyle = thresholdsStyle;
});
}

View File

@ -46,6 +46,7 @@ interface Props<TQuery extends DataQuery> {
index: number; index: number;
dataSource: DataSourceInstanceSettings; dataSource: DataSourceInstanceSettings;
onChangeDataSource?: (dsSettings: DataSourceInstanceSettings) => void; onChangeDataSource?: (dsSettings: DataSourceInstanceSettings) => void;
onDataSourceLoaded?: (instance: DataSourceApi) => void;
renderHeaderExtras?: () => ReactNode; renderHeaderExtras?: () => ReactNode;
onAddQuery: (query: TQuery) => void; onAddQuery: (query: TQuery) => void;
onRemoveQuery: (query: TQuery) => void; onRemoveQuery: (query: TQuery) => void;
@ -162,6 +163,10 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
datasource = await this.dataSourceSrv.get(); datasource = await this.dataSourceSrv.get();
} }
if (typeof this.props.onDataSourceLoaded === 'function') {
this.props.onDataSourceLoaded(datasource);
}
this.setState({ this.setState({
datasource: datasource as unknown as DataSourceApi<TQuery>, datasource: datasource as unknown as DataSourceApi<TQuery>,
queriedDataSourceIdentifier: interpolatedUID, queriedDataSourceIdentifier: interpolatedUID,

View File

@ -835,6 +835,22 @@ export class LokiDatasource
getQueryHints(query: LokiQuery, result: DataFrame[]): QueryHint[] { getQueryHints(query: LokiQuery, result: DataFrame[]): QueryHint[] {
return getQueryHints(query.expr, result); return getQueryHints(query.expr, result);
} }
getDefaultQuery(app: CoreApp): LokiQuery {
const defaults = { refId: 'A', expr: '' };
if (app === CoreApp.UnifiedAlerting) {
return {
...defaults,
queryType: LokiQueryType.Instant,
};
}
return {
...defaults,
queryType: LokiQueryType.Range,
};
}
} }
// NOTE: these two functions are very similar to the escapeLabelValueIn* functions // NOTE: these two functions are very similar to the escapeLabelValueIn* functions

View File

@ -1263,6 +1263,33 @@ export class PrometheusDatasource
getCacheDurationInMinutes(): number { getCacheDurationInMinutes(): number {
return getClientCacheDurationInMinutes(this.cacheLevel); return getClientCacheDurationInMinutes(this.cacheLevel);
} }
getDefaultQuery(app: CoreApp): PromQuery {
const defaults = {
refId: 'A',
expr: '',
range: true,
instant: false,
};
if (app === CoreApp.UnifiedAlerting) {
return {
...defaults,
instant: true,
range: false,
};
}
if (app === CoreApp.Explore) {
return {
...defaults,
instant: true,
range: true,
};
}
return defaults;
}
} }
/** /**