mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Make Loki & Prometheus instant vector by default (#66797)
Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
This commit is contained in:
parent
e9971dd153
commit
b94fceddad
@ -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)};
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -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>
|
||||||
</span>
|
{hasLabels && (
|
||||||
|
<>
|
||||||
|
<span>{'{'}</span>
|
||||||
|
{labels.map(([key, value], index) => (
|
||||||
|
<span key={uniqueId()}>
|
||||||
|
<span className={styles.expression.labelKey}>{key}</span>
|
||||||
|
<span>=</span>
|
||||||
|
<span>"</span>
|
||||||
|
<span className={styles.expression.labelValue}>{value}</span>
|
||||||
|
<span>"</span>
|
||||||
|
{index < labels.length - 1 && <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;
|
||||||
|
@ -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({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -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 };
|
||||||
|
@ -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,67 +115,65 @@ export const QueryWrapper = ({
|
|||||||
maxDataPoints: queryOptions.maxDataPoints,
|
maxDataPoints: queryOptions.maxDataPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isExpressionQuery(query.model)) {
|
return (
|
||||||
return null;
|
<Stack direction="row" alignItems="baseline" gap={1}>
|
||||||
} else {
|
<SelectingDataSourceTooltip />
|
||||||
return (
|
{onChangeTimeRange && (
|
||||||
<Stack direction="row" alignItems="baseline" gap={1}>
|
<RelativeTimeRangePicker
|
||||||
<SelectingDataSourceTooltip />
|
timeRange={query.relativeTimeRange ?? getDefaultRelativeTimeRange()}
|
||||||
{onChangeTimeRange && (
|
onChange={(range) => onChangeTimeRange(range, index)}
|
||||||
<RelativeTimeRangePicker
|
|
||||||
timeRange={query.relativeTimeRange ?? getDefaultRelativeTimeRange()}
|
|
||||||
onChange={(range) => onChangeTimeRange(range, index)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className={styles.queryOptions}>
|
|
||||||
<MaxDataPointsOption
|
|
||||||
options={alertQueryOptions}
|
|
||||||
onChange={(options) => onChangeQueryOptions(options, index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<AlertConditionIndicator
|
|
||||||
onSetCondition={() => onSetCondition(query.refId)}
|
|
||||||
enabled={condition === query.refId}
|
|
||||||
error={error}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
)}
|
||||||
);
|
<div className={styles.queryOptions}>
|
||||||
}
|
<MaxDataPointsOption
|
||||||
|
options={alertQueryOptions}
|
||||||
|
onChange={(options) => onChangeQueryOptions(options, index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AlertConditionIndicator
|
||||||
|
onSetCondition={() => onSetCondition(query.refId)}
|
||||||
|
enabled={condition === query.refId}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<Stack direction="column" gap={0.5}>
|
||||||
<QueryEditorRow<DataQuery>
|
<div className={styles.wrapper}>
|
||||||
alerting
|
<QueryEditorRow<DataQuery>
|
||||||
dataSource={dsSettings}
|
alerting
|
||||||
onChangeDataSource={!isExpression ? (settings) => onChangeDataSource(settings, index) : undefined}
|
dataSource={dsSettings}
|
||||||
id={query.refId}
|
onDataSourceLoaded={setDsInstance}
|
||||||
index={index}
|
onChangeDataSource={(settings) => onChangeDataSource(settings, index)}
|
||||||
key={query.refId}
|
id={query.refId}
|
||||||
data={data}
|
index={index}
|
||||||
query={cloneDeep(query.model)}
|
key={query.refId}
|
||||||
onChange={(query) => onChangeQuery(query, index)}
|
data={data}
|
||||||
onRemoveQuery={onRemoveQuery}
|
query={{
|
||||||
onAddQuery={() => onDuplicateQuery(cloneDeep(query))}
|
...defaults,
|
||||||
onRunQuery={onRunQueries}
|
...cloneDeep(query.model),
|
||||||
queries={queries}
|
}}
|
||||||
renderHeaderExtras={() => <HeaderExtras query={query} index={index} error={error} />}
|
onChange={(query) => onChangeQuery(query, index)}
|
||||||
app={CoreApp.UnifiedAlerting}
|
onRemoveQuery={onRemoveQuery}
|
||||||
visualization={
|
onAddQuery={() => onDuplicateQuery(cloneDeep(query))}
|
||||||
data.state !== LoadingState.NotStarted ? (
|
onRunQuery={onRunQueries}
|
||||||
<VizWrapper
|
queries={queries}
|
||||||
data={data}
|
renderHeaderExtras={() => <HeaderExtras query={query} index={index} error={error} />}
|
||||||
changePanel={changePluginId}
|
app={CoreApp.UnifiedAlerting}
|
||||||
currentPanel={pluginId}
|
hideDisableQuery={true}
|
||||||
thresholds={thresholds}
|
/>
|
||||||
thresholdsType={thresholdsType}
|
</div>
|
||||||
onThresholdsChange={onChangeThreshold ? (thresholds) => onChangeThreshold(thresholds, index) : undefined}
|
{data.state !== LoadingState.NotStarted && (
|
||||||
/>
|
<VizWrapper
|
||||||
) : null
|
data={data}
|
||||||
}
|
thresholds={thresholds}
|
||||||
hideDisableQuery={true}
|
thresholdsType={thresholdsType}
|
||||||
/>
|
onThresholdsChange={onChangeThreshold ? (thresholds) => onChangeThreshold(thresholds, index) : undefined}
|
||||||
</div>
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -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}>
|
width={width}
|
||||||
<PanelRenderer
|
absoluteRange={timeRange}
|
||||||
height={vizHeight}
|
timeZone="browser"
|
||||||
width={width}
|
onChangeTime={() => {}}
|
||||||
data={data}
|
splitOpenFn={() => {}}
|
||||||
pluginId={currentPanel}
|
loadingState={data.state}
|
||||||
title="title"
|
thresholdsConfig={thresholds}
|
||||||
onOptionsChange={setOptions}
|
thresholdsStyle={thresholdsStyle}
|
||||||
options={options}
|
/>
|
||||||
fieldConfig={fieldConfig}
|
) : (
|
||||||
/>
|
<div className={styles.instantVectorResultWrapper}>
|
||||||
</PanelContextProvider>
|
<header className={styles.title}>Table</header>
|
||||||
</div>
|
<ExpressionResult series={data.series} />
|
||||||
);
|
</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: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
{
|
mode: GraphTresholdsStyleMode;
|
||||||
config: ThresholdsConfig;
|
};
|
||||||
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;
|
||||||
|
}
|
||||||
|
@ -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,70 +63,29 @@ export function RuleViewerVisualization({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dsSettings) {
|
|
||||||
return (
|
|
||||||
<div className={cx(styles.content, 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 (
|
return (
|
||||||
<div className={cx(styles.content, className)}>
|
<div className={className}>
|
||||||
<AutoSizer>
|
<div className={styles.header}>
|
||||||
{({ width, height }) => {
|
<div className={styles.actions}>
|
||||||
return (
|
{!isExpression && relativeTimeRange ? (
|
||||||
<div style={{ width, height }}>
|
<DateTimePicker date={setDateTime(relativeTimeRange.to)} onChange={onTimeChange} maxDate={new Date()} />
|
||||||
<div className={styles.header}>
|
) : null}
|
||||||
<div className={styles.actions}>
|
<Authorize actions={[AccessControlAction.DataSourcesExplore]}>
|
||||||
{!isExpressionQuery(model) && relativeTimeRange ? (
|
{!isExpression && (
|
||||||
<DateTimePicker
|
<LinkButton
|
||||||
date={setDateTime(relativeTimeRange.to)}
|
size="md"
|
||||||
onChange={onTimeChange}
|
variant="secondary"
|
||||||
maxDate={new Date()}
|
icon="compass"
|
||||||
/>
|
target="_blank"
|
||||||
) : null}
|
href={createExploreLink(dsSettings, model)}
|
||||||
<PanelPluginsButtonGroup onChange={setPanel} value={panel} size="md" />
|
>
|
||||||
<Authorize actions={[AccessControlAction.DataSourcesExplore]}>
|
View in Explore
|
||||||
{!isExpressionQuery(model) && (
|
</LinkButton>
|
||||||
<>
|
)}
|
||||||
<div className={styles.spacing} />
|
</Authorize>
|
||||||
<LinkButton
|
</div>
|
||||||
size="md"
|
</div>
|
||||||
variant="secondary"
|
<VizWrapper data={data} thresholds={thresholds?.config} thresholdsType={thresholds?.mode} />
|
||||||
icon="compass"
|
|
||||||
target="_blank"
|
|
||||||
href={createExploreLink(dsSettings, model)}
|
|
||||||
>
|
|
||||||
View in Explore
|
|
||||||
</LinkButton>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Authorize>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<PanelRenderer
|
|
||||||
height={height - theme.spacing.gridSize * headerHeight}
|
|
||||||
width={width}
|
|
||||||
data={data}
|
|
||||||
pluginId={panel}
|
|
||||||
title=""
|
|
||||||
onOptionsChange={setOptions}
|
|
||||||
options={options}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -142,12 +94,12 @@ 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.
|
||||||
Ideally we'd resolve the datasource name to the proper datasource Ref "{ type: string, uid: string }" and pass that in to the model.
|
Ideally we'd resolve the datasource name to the proper datasource Ref "{ type: string, uid: string }" and pass that in to the model.
|
||||||
|
|
||||||
I don't think that should happen here, the fact that the datasource ref is sometimes missing here is a symptom of another cause. (Gilles)
|
I don't think that should happen here, the fact that the datasource ref is sometimes missing here is a symptom of another cause. (Gilles)
|
||||||
*/
|
*/
|
||||||
return urlUtil.renderUrl(`${config.appSubUrl}/explore`, {
|
return urlUtil.renderUrl(`${config.appSubUrl}/explore`, {
|
||||||
@ -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;
|
||||||
`,
|
`,
|
||||||
|
@ -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({
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user