mirror of
https://github.com/grafana/grafana.git
synced 2025-09-20 02:18:56 +08:00
GrafanaDS: Add support for annotation time regions (#65462)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
@ -4204,9 +4204,10 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "7"],
|
[0, 0, 0, "Do not use any type assertions.", "7"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "8"],
|
[0, 0, 0, "Do not use any type assertions.", "8"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
[0, 0, 0, "Do not use any type assertions.", "9"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "11"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "12"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/datasource/graphite/components/FunctionEditor.tsx:5381": [
|
"public/app/plugins/datasource/graphite/components/FunctionEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
|
@ -27,6 +27,8 @@ export interface Props {
|
|||||||
includeInternal?: boolean | InternalTimeZones[];
|
includeInternal?: boolean | InternalTimeZones[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
inputId?: string;
|
inputId?: string;
|
||||||
|
menuShouldPortal?: boolean;
|
||||||
|
openMenuOnFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimeZonePicker = (props: Props) => {
|
export const TimeZonePicker = (props: Props) => {
|
||||||
@ -39,6 +41,8 @@ export const TimeZonePicker = (props: Props) => {
|
|||||||
includeInternal = false,
|
includeInternal = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
inputId,
|
inputId,
|
||||||
|
menuShouldPortal = false,
|
||||||
|
openMenuOnFocus = true,
|
||||||
} = props;
|
} = props;
|
||||||
const groupedTimeZones = useTimeZones(includeInternal);
|
const groupedTimeZones = useTimeZones(includeInternal);
|
||||||
const selected = useSelectedTimeZone(groupedTimeZones, value);
|
const selected = useSelectedTimeZone(groupedTimeZones, value);
|
||||||
@ -61,8 +65,8 @@ export const TimeZonePicker = (props: Props) => {
|
|||||||
value={selected}
|
value={selected}
|
||||||
placeholder={t('time-picker.zone.select-search-input', 'Type to search (country, city, abbreviation)')}
|
placeholder={t('time-picker.zone.select-search-input', 'Type to search (country, city, abbreviation)')}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
menuShouldPortal={false}
|
menuShouldPortal={menuShouldPortal}
|
||||||
openMenuOnFocus={true}
|
openMenuOnFocus={openMenuOnFocus}
|
||||||
width={width}
|
width={width}
|
||||||
filterOption={filterBySearchIndex}
|
filterOption={filterBySearchIndex}
|
||||||
options={groupedTimeZones}
|
options={groupedTimeZones}
|
||||||
|
@ -1,40 +1,160 @@
|
|||||||
import { dateTime, TimeRange } from '@grafana/data';
|
import { dateTime, TimeRange } from '@grafana/data';
|
||||||
|
import { TimeRegionConfig } from 'app/core/utils/timeRegions';
|
||||||
|
|
||||||
import { calculateTimesWithin, TimeRegionConfig } from './timeRegions';
|
import { calculateTimesWithin } from './timeRegions';
|
||||||
|
|
||||||
|
// note: calculateTimesWithin always returns time ranges in UTC
|
||||||
describe('timeRegions', () => {
|
describe('timeRegions', () => {
|
||||||
describe('day of week', () => {
|
describe('day of week', () => {
|
||||||
it('4 sundays in january 2021', () => {
|
it('returns regions with 4 Mondays in March 2023', () => {
|
||||||
const cfg: TimeRegionConfig = {
|
const cfg: TimeRegionConfig = {
|
||||||
fromDayOfWeek: 1,
|
fromDayOfWeek: 1,
|
||||||
from: '12:00',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tr: TimeRange = {
|
const tr: TimeRange = {
|
||||||
from: dateTime('2021-01-00', 'YYYY-MM-dd'),
|
from: dateTime('2023-03-01'),
|
||||||
to: dateTime('2021-02-00', 'YYYY-MM-dd'),
|
to: dateTime('2023-03-31'),
|
||||||
raw: {
|
raw: {
|
||||||
to: '',
|
to: '',
|
||||||
from: '',
|
from: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const regions = calculateTimesWithin(cfg, tr);
|
const regions = calculateTimesWithin(cfg, tr);
|
||||||
expect(regions).toMatchInlineSnapshot(`
|
expect(regions).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"from": 1609779600000,
|
"from": 1678060800000,
|
||||||
"to": 1609779600000,
|
"to": 1678147199000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": 1610384400000,
|
"from": 1678665600000,
|
||||||
"to": 1610384400000,
|
"to": 1678751999000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": 1610989200000,
|
"from": 1679270400000,
|
||||||
"to": 1610989200000,
|
"to": 1679356799000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": 1611594000000,
|
"from": 1679875200000,
|
||||||
"to": 1611594000000,
|
"to": 1679961599000,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('day and time of week', () => {
|
||||||
|
it('returns regions with 4 Mondays at 20:00 in March 2023', () => {
|
||||||
|
const cfg: TimeRegionConfig = {
|
||||||
|
fromDayOfWeek: 1,
|
||||||
|
from: '20:00',
|
||||||
|
};
|
||||||
|
|
||||||
|
const tr: TimeRange = {
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-31'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const regions = calculateTimesWithin(cfg, tr);
|
||||||
|
expect(regions).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"from": 1678132800000,
|
||||||
|
"to": 1678132800000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": 1678737600000,
|
||||||
|
"to": 1678737600000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": 1679342400000,
|
||||||
|
"to": 1679342400000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": 1679947200000,
|
||||||
|
"to": 1679947200000,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('day of week range', () => {
|
||||||
|
it('returns regions with days range', () => {
|
||||||
|
const cfg: TimeRegionConfig = {
|
||||||
|
fromDayOfWeek: 1,
|
||||||
|
toDayOfWeek: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tr: TimeRange = {
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-31'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const regions = calculateTimesWithin(cfg, tr);
|
||||||
|
expect(regions).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"from": 1678060800000,
|
||||||
|
"to": 1678319999000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": 1678665600000,
|
||||||
|
"to": 1678924799000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": 1679270400000,
|
||||||
|
"to": 1679529599000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": 1679875200000,
|
||||||
|
"to": 1680134399000,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
it('returns regions with days/times range', () => {
|
||||||
|
const cfg: TimeRegionConfig = {
|
||||||
|
fromDayOfWeek: 1,
|
||||||
|
from: '20:00',
|
||||||
|
toDayOfWeek: 2,
|
||||||
|
to: '10:00',
|
||||||
|
};
|
||||||
|
|
||||||
|
const tr: TimeRange = {
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-31'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const regions = calculateTimesWithin(cfg, tr);
|
||||||
|
expect(regions).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"from": 1678132800000,
|
||||||
|
"to": 1678183200000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": 1678737600000,
|
||||||
|
"to": 1678788000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": 1679342400000,
|
||||||
|
"to": 1679392800000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": 1679947200000,
|
||||||
|
"to": 1679997600000,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
@ -6,6 +6,8 @@ export interface TimeRegionConfig {
|
|||||||
|
|
||||||
to?: string;
|
to?: string;
|
||||||
toDayOfWeek?: number; // 1-7
|
toDayOfWeek?: number; // 1-7
|
||||||
|
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedTime {
|
interface ParsedTime {
|
||||||
@ -32,8 +34,8 @@ export function calculateTimesWithin(cfg: TimeRegionConfig, tRange: TimeRange):
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hRange = {
|
const hRange = {
|
||||||
from: parseTimeRange(timeRegion.from),
|
from: parseTimeOfDay(timeRegion.from),
|
||||||
to: parseTimeRange(timeRegion.to),
|
to: parseTimeOfDay(timeRegion.to),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!timeRegion.fromDayOfWeek && timeRegion.toDayOfWeek) {
|
if (!timeRegion.fromDayOfWeek && timeRegion.toDayOfWeek) {
|
||||||
@ -78,10 +80,11 @@ export function calculateTimesWithin(cfg: TimeRegionConfig, tRange: TimeRange):
|
|||||||
|
|
||||||
const regions: AbsoluteTimeRange[] = [];
|
const regions: AbsoluteTimeRange[] = [];
|
||||||
|
|
||||||
const fromStart = dateTime(tRange.from);
|
const fromStart = dateTime(tRange.from).utc();
|
||||||
fromStart.set('hour', 0);
|
fromStart.set('hour', 0);
|
||||||
fromStart.set('minute', 0);
|
fromStart.set('minute', 0);
|
||||||
fromStart.set('second', 0);
|
fromStart.set('second', 0);
|
||||||
|
fromStart.set('millisecond', 0);
|
||||||
fromStart.add(hRange.from.h, 'hours');
|
fromStart.add(hRange.from.h, 'hours');
|
||||||
fromStart.add(hRange.from.m, 'minutes');
|
fromStart.add(hRange.from.m, 'minutes');
|
||||||
fromStart.add(hRange.from.s, 'seconds');
|
fromStart.add(hRange.from.s, 'seconds');
|
||||||
@ -95,7 +98,7 @@ export function calculateTimesWithin(cfg: TimeRegionConfig, tRange: TimeRange):
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromEnd = dateTime(fromStart);
|
const fromEnd = dateTime(fromStart).utc();
|
||||||
|
|
||||||
if (fromEnd.hour) {
|
if (fromEnd.hour) {
|
||||||
if (hRange.from.h <= hRange.to.h) {
|
if (hRange.from.h <= hRange.to.h) {
|
||||||
@ -134,35 +137,36 @@ export function calculateTimesWithin(cfg: TimeRegionConfig, tRange: TimeRange):
|
|||||||
return regions;
|
return regions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTimeRange(str?: string): ParsedTime {
|
export function parseTimeOfDay(str?: string): ParsedTime {
|
||||||
const result: ParsedTime = {};
|
const result: ParsedTime = {};
|
||||||
if (!str?.length) {
|
if (!str?.length) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeRegex = /^([\d]+):?(\d{2})?/;
|
const match = str.split(':');
|
||||||
const match = timeRegex.exec(str);
|
if (!match?.length) {
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.h = Math.min(23, Math.max(0, Number(match[0])));
|
||||||
if (match.length > 1) {
|
if (match.length > 1) {
|
||||||
result.h = Number(match[1]);
|
result.m = Math.min(60, Math.max(0, Number(match[1])));
|
||||||
result.m = 0;
|
if (match.length > 2) {
|
||||||
|
result.s = Math.min(60, Math.max(0, Number(match[2])));
|
||||||
if (match.length > 2 && match[2] !== undefined) {
|
|
||||||
result.m = Number(match[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.h > 23) {
|
|
||||||
result.h = 23;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.m > 59) {
|
|
||||||
result.m = 59;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTimeOfDayString(t?: ParsedTime): string {
|
||||||
|
if (!t || (t.h == null && t.m == null && t.s == null)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let str = String(t.h ?? 0).padStart(2, '0') + ':' + String(t.m ?? 0).padStart(2, '0');
|
||||||
|
if (t.s != null) {
|
||||||
|
str += String(t.s ?? 0).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Field, FieldSet, Select, Switch } from '@grafana/ui';
|
import { Field, FieldSet, Select, Switch, useStyles2 } from '@grafana/ui';
|
||||||
import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
|
import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
|
||||||
|
import { TimeRegionConfig } from 'app/core/utils/timeRegions';
|
||||||
import { getAnnotationTags } from 'app/features/annotations/api';
|
import { getAnnotationTags } from 'app/features/annotations/api';
|
||||||
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
|
|
||||||
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery } from '../types';
|
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery, GrafanaQueryType } from '../types';
|
||||||
|
|
||||||
|
import { TimeRegionEditor } from './TimeRegionEditor';
|
||||||
|
|
||||||
const matchTooltipContent = 'Enabling this returns annotations that match any of the tags specified below';
|
const matchTooltipContent = 'Enabling this returns annotations that match any of the tags specified below';
|
||||||
|
|
||||||
@ -14,7 +18,7 @@ const tagsTooltipContent = (
|
|||||||
<div>Specify a list of tags to match. To specify a key and value tag use `key:value` syntax.</div>
|
<div>Specify a list of tags to match. To specify a key and value tag use `key:value` syntax.</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const annotationTypes = [
|
const annotationTypes: Array<SelectableValue<GrafanaAnnotationType>> = [
|
||||||
{
|
{
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
value: GrafanaAnnotationType.Dashboard,
|
value: GrafanaAnnotationType.Dashboard,
|
||||||
@ -27,6 +31,19 @@ const annotationTypes = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const queryTypes: Array<SelectableValue<GrafanaQueryType>> = [
|
||||||
|
{
|
||||||
|
label: 'Annotations & Alerts',
|
||||||
|
value: GrafanaQueryType.Annotations,
|
||||||
|
description: 'Show annotations or alerts managed by grafana',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Time regions',
|
||||||
|
value: GrafanaQueryType.TimeRegions,
|
||||||
|
description: 'Configure a repeating time region',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const limitOptions = [10, 50, 100, 200, 300, 500, 1000, 2000].map((limit) => ({
|
const limitOptions = [10, 50, 100, 200, 300, 500, 1000, 2000].map((limit) => ({
|
||||||
label: String(limit),
|
label: String(limit),
|
||||||
value: limit,
|
value: limit,
|
||||||
@ -39,8 +56,10 @@ interface Props {
|
|||||||
|
|
||||||
export default function AnnotationQueryEditor({ query, onChange }: Props) {
|
export default function AnnotationQueryEditor({ query, onChange }: Props) {
|
||||||
const annotationQuery = query as GrafanaAnnotationQuery;
|
const annotationQuery = query as GrafanaAnnotationQuery;
|
||||||
const { limit, matchAny, tags, type } = annotationQuery;
|
const { limit, matchAny, tags, type, queryType } = annotationQuery;
|
||||||
const styles = getStyles();
|
let grafanaQueryType = queryType ?? GrafanaQueryType.Annotations;
|
||||||
|
const defaultTimezone = useMemo(() => getDashboardSrv().dashboard?.getTimezone(), []);
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const onFilterByChange = (newValue: SelectableValue<GrafanaAnnotationType>) =>
|
const onFilterByChange = (newValue: SelectableValue<GrafanaAnnotationType>) =>
|
||||||
onChange({
|
onChange({
|
||||||
@ -66,49 +85,86 @@ export default function AnnotationQueryEditor({ query, onChange }: Props) {
|
|||||||
tags,
|
tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onQueryTypeChange = (newValue: SelectableValue<GrafanaQueryType>) => {
|
||||||
|
const newQuery: GrafanaAnnotationQuery = { ...annotationQuery, queryType: newValue.value! };
|
||||||
|
if (newQuery.queryType === GrafanaQueryType.TimeRegions) {
|
||||||
|
if (!newQuery.timeRegion) {
|
||||||
|
newQuery.timeRegion = {
|
||||||
|
timezone: defaultTimezone,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete newQuery.timeRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(newQuery);
|
||||||
|
};
|
||||||
|
const onTimeRegionChange = (timeRegion?: TimeRegionConfig) => {
|
||||||
|
onChange({
|
||||||
|
...annotationQuery,
|
||||||
|
timeRegion,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldSet className={styles.container}>
|
<FieldSet className={styles.container}>
|
||||||
<Field label="Filter by">
|
<Field label="Query type">
|
||||||
<Select
|
<Select
|
||||||
inputId="grafana-annotations__filter-by"
|
inputId="grafana-annotations__query-type"
|
||||||
options={annotationTypes}
|
options={queryTypes}
|
||||||
value={type}
|
value={grafanaQueryType}
|
||||||
onChange={onFilterByChange}
|
onChange={onQueryTypeChange}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Max limit">
|
{grafanaQueryType === GrafanaQueryType.Annotations && (
|
||||||
<Select
|
|
||||||
inputId="grafana-annotations__limit"
|
|
||||||
width={16}
|
|
||||||
options={limitOptions}
|
|
||||||
value={limit}
|
|
||||||
onChange={onMaxLimitChange}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
{type === GrafanaAnnotationType.Tags && (
|
|
||||||
<>
|
<>
|
||||||
<Field label="Match any" description={matchTooltipContent}>
|
<Field label="Filter by">
|
||||||
<Switch id="grafana-annotations__match-any" value={matchAny} onChange={onMatchAnyChange} />
|
<Select
|
||||||
</Field>
|
inputId="grafana-annotations__filter-by"
|
||||||
<Field label="Tags" description={tagsTooltipContent}>
|
options={annotationTypes}
|
||||||
<TagFilter
|
value={type}
|
||||||
allowCustomValue
|
onChange={onFilterByChange}
|
||||||
inputId="grafana-annotations__tags"
|
|
||||||
onChange={onTagsChange}
|
|
||||||
tagOptions={getAnnotationTags}
|
|
||||||
tags={tags ?? []}
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field label="Max limit">
|
||||||
|
<Select
|
||||||
|
inputId="grafana-annotations__limit"
|
||||||
|
width={16}
|
||||||
|
options={limitOptions}
|
||||||
|
value={limit}
|
||||||
|
onChange={onMaxLimitChange}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
{type === GrafanaAnnotationType.Tags && (
|
||||||
|
<>
|
||||||
|
<Field label="Match any" description={matchTooltipContent}>
|
||||||
|
<Switch id="grafana-annotations__match-any" value={matchAny} onChange={onMatchAnyChange} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Tags" description={tagsTooltipContent}>
|
||||||
|
<TagFilter
|
||||||
|
allowCustomValue
|
||||||
|
inputId="grafana-annotations__tags"
|
||||||
|
onChange={onTagsChange}
|
||||||
|
tagOptions={getAnnotationTags}
|
||||||
|
tags={tags ?? []}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{grafanaQueryType === GrafanaQueryType.TimeRegions && annotationQuery.timeRegion && (
|
||||||
|
<TimeRegionEditor value={annotationQuery.timeRegion} onChange={onTimeRegionChange} />
|
||||||
|
)}
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = () => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
container: css`
|
container: css({
|
||||||
max-width: 600px;
|
maxWidth: theme.spacing(60),
|
||||||
`,
|
marginBottom: theme.spacing(2),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,189 @@
|
|||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { Moment } from 'moment';
|
||||||
|
import TimePicker from 'rc-time-picker';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { FormInputSize, Icon, useStyles2 } from '@grafana/ui';
|
||||||
|
import { inputSizes } from '@grafana/ui/src/components/Forms/commonStyles';
|
||||||
|
import { focusCss } from '@grafana/ui/src/themes/mixins';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onChange: (value: Moment) => void;
|
||||||
|
value?: Moment;
|
||||||
|
defaultValue?: Moment;
|
||||||
|
showHour?: boolean;
|
||||||
|
showSeconds?: boolean;
|
||||||
|
minuteStep?: number;
|
||||||
|
size?: FormInputSize;
|
||||||
|
disabled?: boolean;
|
||||||
|
disabledHours?: () => number[];
|
||||||
|
disabledMinutes?: () => number[];
|
||||||
|
disabledSeconds?: () => number[];
|
||||||
|
placeholder?: string;
|
||||||
|
format?: string;
|
||||||
|
allowEmpty?: boolean;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POPUP_CLASS_NAME = 'time-of-day-picker-panel';
|
||||||
|
|
||||||
|
// @TODO fix TimeOfDayPicker and switch?
|
||||||
|
export const TimePickerInput = ({
|
||||||
|
minuteStep = 1,
|
||||||
|
showHour = true,
|
||||||
|
showSeconds = false,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
size = 'auto',
|
||||||
|
disabled,
|
||||||
|
disabledHours,
|
||||||
|
disabledMinutes,
|
||||||
|
disabledSeconds,
|
||||||
|
placeholder,
|
||||||
|
format = 'HH:mm',
|
||||||
|
defaultValue = undefined,
|
||||||
|
allowEmpty = false,
|
||||||
|
width,
|
||||||
|
}: Props) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const getWidth = () => {
|
||||||
|
if (width) {
|
||||||
|
return css`
|
||||||
|
width: ${width}px;
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputSizes()[size];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimePicker
|
||||||
|
value={value}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={(v) => onChange(v)}
|
||||||
|
showHour={showHour}
|
||||||
|
showSecond={showSeconds}
|
||||||
|
format={format}
|
||||||
|
allowEmpty={allowEmpty}
|
||||||
|
className={cx(getWidth(), styles.input)}
|
||||||
|
popupClassName={cx(styles.picker, POPUP_CLASS_NAME)}
|
||||||
|
minuteStep={minuteStep}
|
||||||
|
inputIcon={<Caret wrapperStyle={styles.caretWrapper} />}
|
||||||
|
disabled={disabled}
|
||||||
|
disabledHours={disabledHours}
|
||||||
|
disabledMinutes={disabledMinutes}
|
||||||
|
disabledSeconds={disabledSeconds}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CaretProps {
|
||||||
|
wrapperStyle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Caret = ({ wrapperStyle = '' }: CaretProps) => {
|
||||||
|
return (
|
||||||
|
<div className={wrapperStyle}>
|
||||||
|
<Icon name="angle-down" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
const bgColor = theme.components.input.background;
|
||||||
|
const menuShadowColor = theme.v1.palette.black;
|
||||||
|
const optionBgHover = theme.colors.background.secondary;
|
||||||
|
const borderRadius = theme.shape.borderRadius(1);
|
||||||
|
const borderColor = theme.components.input.borderColor;
|
||||||
|
|
||||||
|
return {
|
||||||
|
caretWrapper: css`
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
`,
|
||||||
|
picker: css`
|
||||||
|
.rc-time-picker-panel-select {
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: ${bgColor};
|
||||||
|
border-color: ${borderColor};
|
||||||
|
li {
|
||||||
|
outline-width: 2px;
|
||||||
|
&.rc-time-picker-panel-select-option-selected {
|
||||||
|
background-color: inherit;
|
||||||
|
border: 1px solid ${theme.v1.palette.orange};
|
||||||
|
border-radius: ${borderRadius};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${optionBgHover};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rc-time-picker-panel-select-option-disabled {
|
||||||
|
color: ${theme.colors.action.disabledText};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-time-picker-panel-inner {
|
||||||
|
box-shadow: 0px 4px 4px ${menuShadowColor};
|
||||||
|
background-color: ${bgColor};
|
||||||
|
border-color: ${borderColor};
|
||||||
|
border-radius: ${borderRadius};
|
||||||
|
margin-top: 3px;
|
||||||
|
|
||||||
|
.rc-time-picker-panel-input-wrap {
|
||||||
|
margin-right: 2px;
|
||||||
|
|
||||||
|
&,
|
||||||
|
.rc-time-picker-panel-input {
|
||||||
|
background-color: ${bgColor};
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-time-picker-panel-combobox {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
input: css`
|
||||||
|
.rc-time-picker-input {
|
||||||
|
background-color: ${bgColor};
|
||||||
|
border-radius: ${borderRadius};
|
||||||
|
border-color: ${borderColor};
|
||||||
|
height: ${theme.spacing(4)};
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
${focusCss(theme)}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: ${theme.colors.action.disabledBackground};
|
||||||
|
color: ${theme.colors.action.disabledText};
|
||||||
|
border: 1px solid ${theme.colors.action.disabledBackground};
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-time-picker-clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,191 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import moment, { Moment } from 'moment/moment';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { getTimeZoneInfo, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
|
import { Button, Field, FieldSet, HorizontalGroup, Select, TimeZonePicker, useStyles2 } from '@grafana/ui';
|
||||||
|
import { TimeZoneOffset } from '@grafana/ui/src/components/DateTimePickers/TimeZonePicker/TimeZoneOffset';
|
||||||
|
import { TimeZoneTitle } from '@grafana/ui/src/components/DateTimePickers/TimeZonePicker/TimeZoneTitle';
|
||||||
|
import { TimeRegionConfig } from 'app/core/utils/timeRegions';
|
||||||
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
|
|
||||||
|
import { TimePickerInput } from './TimePickerInput';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: TimeRegionConfig;
|
||||||
|
onChange: (value?: TimeRegionConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'].map((v, idx) => {
|
||||||
|
return {
|
||||||
|
label: v,
|
||||||
|
value: idx + 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
export const TimeRegionEditor = ({ value, onChange }: Props) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const timezoneInfo = getTimeZoneInfo(value.timezone ?? 'utc', timestamp);
|
||||||
|
const isDashboardTimezone = getDashboardSrv().getCurrent()?.getTimezone() === value.timezone;
|
||||||
|
|
||||||
|
const [isEditing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
const onToggleChangeTimezone = () => {
|
||||||
|
setEditing(!isEditing);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTime = (time: string | undefined): Moment | undefined => {
|
||||||
|
if (!time) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = moment();
|
||||||
|
|
||||||
|
if (time) {
|
||||||
|
const match = time.split(':');
|
||||||
|
date.set('hour', parseInt(match[0], 10));
|
||||||
|
date.set('minute', parseInt(match[1], 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
return date;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getToPlaceholder = () => {
|
||||||
|
let placeholder = 'Everyday';
|
||||||
|
if (value.fromDayOfWeek && !value.toDayOfWeek) {
|
||||||
|
placeholder = days[value.fromDayOfWeek - 1].label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return placeholder;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTimezonePicker = () => {
|
||||||
|
const timezone = (
|
||||||
|
<>
|
||||||
|
<TimeZoneTitle title={timezoneInfo?.name} />
|
||||||
|
<TimeZoneOffset timeZone={value.timezone} timestamp={timestamp} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDashboardTimezone) {
|
||||||
|
return <>Dashboard timezone ({timezone})</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timezone;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTimeChange = (v: Moment, field: string) => {
|
||||||
|
const time = v ? v.format('HH:mm') : undefined;
|
||||||
|
if (field === 'from') {
|
||||||
|
onChange({ ...value, from: time });
|
||||||
|
} else {
|
||||||
|
onChange({ ...value, to: time });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTimezoneChange = (v: string | undefined) => {
|
||||||
|
onChange({ ...value, timezone: v });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFromDayOfWeekChange = (v: SelectableValue<number>) => {
|
||||||
|
const fromDayOfWeek = v ? v.value : undefined;
|
||||||
|
const toDayOfWeek = v ? value.toDayOfWeek : undefined; // clear if everyday
|
||||||
|
onChange({ ...value, fromDayOfWeek, toDayOfWeek });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToDayOfWeekChange = (v: SelectableValue<number>) => {
|
||||||
|
onChange({ ...value, toDayOfWeek: v ? v.value : undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTimezone = () => {
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<TimeZonePicker
|
||||||
|
value={value.timezone}
|
||||||
|
includeInternal={true}
|
||||||
|
onChange={(v) => onTimezoneChange(v)}
|
||||||
|
onBlur={() => setEditing(false)}
|
||||||
|
menuShouldPortal={true}
|
||||||
|
openMenuOnFocus={false}
|
||||||
|
width={100}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.timezoneContainer}>
|
||||||
|
<div className={styles.timezone}>{renderTimezonePicker()}</div>
|
||||||
|
<Button variant="secondary" onClick={onToggleChangeTimezone} size="sm">
|
||||||
|
Change timezone
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet className={styles.wrapper}>
|
||||||
|
<Field label="From">
|
||||||
|
<HorizontalGroup spacing="xs">
|
||||||
|
<Select
|
||||||
|
options={days}
|
||||||
|
isClearable
|
||||||
|
placeholder="Everyday"
|
||||||
|
value={value.fromDayOfWeek ?? null}
|
||||||
|
onChange={(v) => onFromDayOfWeekChange(v)}
|
||||||
|
width={20}
|
||||||
|
/>
|
||||||
|
<TimePickerInput
|
||||||
|
value={getTime(value.from)}
|
||||||
|
onChange={(v) => onTimeChange(v, 'from')}
|
||||||
|
allowEmpty={true}
|
||||||
|
placeholder="HH:mm"
|
||||||
|
width={100}
|
||||||
|
/>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</Field>
|
||||||
|
<Field label="To">
|
||||||
|
<HorizontalGroup spacing="xs">
|
||||||
|
{(value.fromDayOfWeek || value.toDayOfWeek) && (
|
||||||
|
<Select
|
||||||
|
options={days}
|
||||||
|
isClearable
|
||||||
|
placeholder={getToPlaceholder()}
|
||||||
|
value={value.toDayOfWeek ?? null}
|
||||||
|
onChange={(v) => onToDayOfWeekChange(v)}
|
||||||
|
width={20}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<TimePickerInput
|
||||||
|
value={getTime(value.to)}
|
||||||
|
onChange={(v) => onTimeChange(v, 'to')}
|
||||||
|
allowEmpty={true}
|
||||||
|
placeholder="HH:mm"
|
||||||
|
width={100}
|
||||||
|
/>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</Field>
|
||||||
|
<Field label="Timezone">{renderTimezone()}</Field>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
wrapper: css({
|
||||||
|
maxWidth: theme.spacing(60),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
timezoneContainer: css`
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
`,
|
||||||
|
timezone: css`
|
||||||
|
margin-right: 5px;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
@ -30,6 +30,7 @@ import { migrateDatasourceNameToRef } from 'app/features/dashboard/state/Dashboa
|
|||||||
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';
|
||||||
|
|
||||||
import AnnotationQueryEditor from './components/AnnotationQueryEditor';
|
import AnnotationQueryEditor from './components/AnnotationQueryEditor';
|
||||||
|
import { doTimeRegionQuery } from './timeRegions';
|
||||||
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery, GrafanaQueryType } from './types';
|
import { GrafanaAnnotationQuery, GrafanaAnnotationType, GrafanaQuery, GrafanaQueryType } from './types';
|
||||||
|
|
||||||
let counter = 100;
|
let counter = 100;
|
||||||
@ -96,12 +97,25 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
|
|||||||
if (target.queryType === GrafanaQueryType.Snapshot) {
|
if (target.queryType === GrafanaQueryType.Snapshot) {
|
||||||
results.push(
|
results.push(
|
||||||
of({
|
of({
|
||||||
|
// NOTE refId is intentionally missing because:
|
||||||
|
// 1) there is only one snapshot
|
||||||
|
// 2) the payload will reference original refIds
|
||||||
data: (target.snapshot ?? []).map((v) => dataFrameFromJSON(v)),
|
data: (target.snapshot ?? []).map((v) => dataFrameFromJSON(v)),
|
||||||
state: LoadingState.Done,
|
state: LoadingState.Done,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (target.queryType === GrafanaQueryType.TimeRegions) {
|
||||||
|
const frame = doTimeRegionQuery('', target.timeRegion!, request.range, request.timezone);
|
||||||
|
results.push(
|
||||||
|
of({
|
||||||
|
data: frame ? [frame] : [],
|
||||||
|
state: LoadingState.Done,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (target.queryType === GrafanaQueryType.LiveMeasurements) {
|
if (target.queryType === GrafanaQueryType.LiveMeasurements) {
|
||||||
let channel = templateSrv.replace(target.channel, request.scopedVars);
|
let channel = templateSrv.replace(target.channel, request.scopedVars);
|
||||||
const { filter } = target;
|
const { filter } = target;
|
||||||
@ -177,7 +191,17 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAnnotations(options: AnnotationQueryRequest<GrafanaQuery>): Promise<DataQueryResponse> {
|
async getAnnotations(options: AnnotationQueryRequest<GrafanaQuery>): Promise<DataQueryResponse> {
|
||||||
const templateSrv = getTemplateSrv();
|
const query = options.annotation.target as GrafanaQuery;
|
||||||
|
if (query?.queryType === GrafanaQueryType.TimeRegions) {
|
||||||
|
const frame = doTimeRegionQuery(
|
||||||
|
options.annotation.name,
|
||||||
|
query.timeRegion!,
|
||||||
|
options.range,
|
||||||
|
getDashboardSrv().getCurrent()?.timezone // Annotation queries don't include the timezone
|
||||||
|
);
|
||||||
|
return Promise.resolve({ data: frame ? [frame] : [] });
|
||||||
|
}
|
||||||
|
|
||||||
const annotation = options.annotation as unknown as AnnotationQuery<GrafanaAnnotationQuery>;
|
const annotation = options.annotation as unknown as AnnotationQuery<GrafanaAnnotationQuery>;
|
||||||
const target = annotation.target!;
|
const target = annotation.target!;
|
||||||
const params: any = {
|
const params: any = {
|
||||||
@ -202,6 +226,7 @@ export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
|
|||||||
if (!Array.isArray(target.tags) || target.tags.length === 0) {
|
if (!Array.isArray(target.tags) || target.tags.length === 0) {
|
||||||
return Promise.resolve({ data: [] });
|
return Promise.resolve({ data: [] });
|
||||||
}
|
}
|
||||||
|
const templateSrv = getTemplateSrv();
|
||||||
const delimiter = '__delimiter__';
|
const delimiter = '__delimiter__';
|
||||||
const tags = [];
|
const tags = [];
|
||||||
for (const t of params.tags) {
|
for (const t of params.tags) {
|
||||||
|
490
public/app/plugins/datasource/grafana/timeRegions.test.ts
Normal file
490
public/app/plugins/datasource/grafana/timeRegions.test.ts
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
import { dateTime, toDataFrameDTO } from '@grafana/data';
|
||||||
|
|
||||||
|
import { doTimeRegionQuery } from './timeRegions';
|
||||||
|
|
||||||
|
describe('grafana data source', () => {
|
||||||
|
it('supports time region query', () => {
|
||||||
|
const frame = doTimeRegionQuery(
|
||||||
|
'test',
|
||||||
|
{ fromDayOfWeek: 1, toDayOfWeek: 2 },
|
||||||
|
{
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-31'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'utc'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(toDataFrameDTO(frame!)).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678060800000,
|
||||||
|
1678665600000,
|
||||||
|
1679270400000,
|
||||||
|
1679875200000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "timeEnd",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678233599000,
|
||||||
|
1678838399000,
|
||||||
|
1679443199000,
|
||||||
|
1680047999000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"values": [
|
||||||
|
"test",
|
||||||
|
"test",
|
||||||
|
"test",
|
||||||
|
"test",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"meta": undefined,
|
||||||
|
"name": undefined,
|
||||||
|
"refId": undefined,
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles timezone conversion UTC-UTC', () => {
|
||||||
|
// region TZ = UTC
|
||||||
|
// dashboard TZ = UTC
|
||||||
|
// Mon Mar 06 2023 00:00:00 GMT+0000 -> Mon Mar 06 2023 23:59:59 GMT+0000
|
||||||
|
|
||||||
|
const frame = doTimeRegionQuery(
|
||||||
|
'test',
|
||||||
|
{ fromDayOfWeek: 1, timezone: 'utc' },
|
||||||
|
{
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-08'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'utc'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678060800000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "timeEnd",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678147199000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"values": [
|
||||||
|
"test",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles timezone conversion browser-UTC', () => {
|
||||||
|
// region TZ = browser (Pacific/Easter)
|
||||||
|
// dashboard TZ = UTC
|
||||||
|
// Mon Mar 06 2023 00:00:00 GMT-0600 -> Mon Mar 06 2023 23:59:59 GMT-0600
|
||||||
|
// Mon Mar 06 2023 06:00:00 GMT+0000 -> Mon Mar 06 2023 05:59:59 GMT+0000
|
||||||
|
|
||||||
|
const frame = doTimeRegionQuery(
|
||||||
|
'test',
|
||||||
|
{ fromDayOfWeek: 1, timezone: 'browser' },
|
||||||
|
{
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-08'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'utc'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678078800000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "timeEnd",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678165199000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"values": [
|
||||||
|
"test",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles timezone conversion CST-UTC', () => {
|
||||||
|
// region TZ = America/Chicago (CST)
|
||||||
|
// dashboard TZ = UTC
|
||||||
|
// Mon Mar 06 2023 00:00:00 GMT-0600 -> Mon Mar 06 2023 23:59:59 GMT-0600 (CDT)
|
||||||
|
// Mon Mar 06 2023 06:00:00 GMT+0000 -> Tue Mar 07 2023 05:59:59 GMT+0000
|
||||||
|
|
||||||
|
const frame = doTimeRegionQuery(
|
||||||
|
'test',
|
||||||
|
{ fromDayOfWeek: 1, timezone: 'America/Chicago' },
|
||||||
|
{
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-08'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'utc'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678082400000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "timeEnd",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678168799000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"values": [
|
||||||
|
"test",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles timezone conversion Europe/Amsterdam-UTC', () => {
|
||||||
|
// region TZ = Europe/Amsterdam
|
||||||
|
// dashboard TZ = UTC
|
||||||
|
// Mon Mar 06 2023 00:00:00 GMT+0100 -> Mon Mar 06 2023 23:59:59 GMT+0100 (Europe/Amsterdam)
|
||||||
|
// Sun Mar 05 2023 23:00:00 GMT+0000 -> Mon Mar 06 2023 22:59:59 GMT+0000
|
||||||
|
|
||||||
|
const frame = doTimeRegionQuery(
|
||||||
|
'test',
|
||||||
|
{ fromDayOfWeek: 1, timezone: 'Europe/Amsterdam' },
|
||||||
|
{
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-08'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'utc'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678057200000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "timeEnd",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678143599000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"values": [
|
||||||
|
"test",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles timezone conversion Asia/Hovd-UTC', () => {
|
||||||
|
// region TZ = Asia/Hovd
|
||||||
|
// dashboard TZ = UTC
|
||||||
|
// Mon Mar 06 2023 00:00:00 GMT+0700 -> Mon Mar 06 2023 23:59:59 GMT+0700 (Asia/Hovd)
|
||||||
|
// Sun Mar 05 2023 17:00:00 GMT+0000 -> Mon Mar 06 2023 16:59:59 GMT+0000
|
||||||
|
|
||||||
|
const frame = doTimeRegionQuery(
|
||||||
|
'test',
|
||||||
|
{ fromDayOfWeek: 1, timezone: 'Asia/Hovd' },
|
||||||
|
{
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-08'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'utc'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678035600000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "timeEnd",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678121999000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"values": [
|
||||||
|
"test",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles timezone conversion UTC-Asia/Dubai', () => {
|
||||||
|
// region TZ = UTC
|
||||||
|
// dashboard TZ = Asia/Dubai
|
||||||
|
// Mon Mar 06 2023 00:00:00 GMT+0000 -> Mon Mar 06 2023 23:59:59 GMT+0000 (UTC)
|
||||||
|
// Mon Mar 06 2023 04:00:00 GMT+0400 -> Mon Mar 06 2023 03:59:59 GMT+0400 (Asia/Dubai)
|
||||||
|
|
||||||
|
const frame = doTimeRegionQuery(
|
||||||
|
'test',
|
||||||
|
{ fromDayOfWeek: 1, timezone: 'utc' },
|
||||||
|
{
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-08'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Asia/Dubai'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678060800000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "timeEnd",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678147199000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"values": [
|
||||||
|
"test",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles timezone conversion UTC-CST', () => {
|
||||||
|
// region TZ = UTC
|
||||||
|
// dashboard TZ = 'America/Chicago'
|
||||||
|
// Mon Mar 06 2023 08:00:00 GMT+0000 -> Mon Mar 06 2023 08:00:00 GMT+0000 (UTC)
|
||||||
|
// Mon Mar 06 2023 02:00:00 GMT-0600 -> Mon Mar 06 2023 02:00:00 GMT-0600 (CST)
|
||||||
|
|
||||||
|
const frame = doTimeRegionQuery(
|
||||||
|
'test',
|
||||||
|
{ fromDayOfWeek: 1, from: '08:00', timezone: 'utc' },
|
||||||
|
{
|
||||||
|
from: dateTime('2023-03-01'),
|
||||||
|
to: dateTime('2023-03-08'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'America/Chicago'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678089600000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "timeEnd",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1678089600000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"values": [
|
||||||
|
"test",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles timezone conversion UTC-CDT', () => {
|
||||||
|
// region TZ = UTC
|
||||||
|
// dashboard TZ = 'America/Chicago'
|
||||||
|
// Mon Apr 03 2023 08:00:00 GMT+0000 -> Mon Apr 03 2023 08:00:00 GMT+0000 (UTC)
|
||||||
|
// Mon Apr 03 2023 03:00:00 GMT-0500 -> Mon Apr 03 2023 03:00:00 GMT-0500 (CDT)
|
||||||
|
|
||||||
|
const frame = doTimeRegionQuery(
|
||||||
|
'test',
|
||||||
|
{ fromDayOfWeek: 1, from: '08:00', timezone: 'utc' },
|
||||||
|
{
|
||||||
|
from: dateTime('2023-03-30'),
|
||||||
|
to: dateTime('2023-04-06'),
|
||||||
|
raw: {
|
||||||
|
to: '',
|
||||||
|
from: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'America/Chicago'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(toDataFrameDTO(frame!).fields).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1680508800000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "timeEnd",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1680508800000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"labels": undefined,
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"values": [
|
||||||
|
"test",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
48
public/app/plugins/datasource/grafana/timeRegions.ts
Normal file
48
public/app/plugins/datasource/grafana/timeRegions.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { TimeRange, DataFrame, FieldType, getTimeZoneInfo } from '@grafana/data';
|
||||||
|
import { TimeRegionConfig, calculateTimesWithin } from 'app/core/utils/timeRegions';
|
||||||
|
|
||||||
|
export function doTimeRegionQuery(
|
||||||
|
name: string,
|
||||||
|
config: TimeRegionConfig,
|
||||||
|
range: TimeRange,
|
||||||
|
tz: string
|
||||||
|
): DataFrame | undefined {
|
||||||
|
if (!config) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const regions = calculateTimesWithin(config, range); // UTC
|
||||||
|
if (!regions.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const times: number[] = [];
|
||||||
|
const timesEnd: number[] = [];
|
||||||
|
const texts: string[] = [];
|
||||||
|
|
||||||
|
const regionTimezone = config.timezone ?? tz;
|
||||||
|
|
||||||
|
for (const region of regions) {
|
||||||
|
let from = region.from;
|
||||||
|
let to = region.to;
|
||||||
|
|
||||||
|
const info = getTimeZoneInfo(regionTimezone, from);
|
||||||
|
if (info) {
|
||||||
|
const offset = info.offsetInMins * 60 * 1000;
|
||||||
|
from += offset;
|
||||||
|
to += offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
times.push(from);
|
||||||
|
timesEnd.push(to);
|
||||||
|
texts.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: times, config: {} },
|
||||||
|
{ name: 'timeEnd', type: FieldType.time, values: timesEnd, config: {} },
|
||||||
|
{ name: 'text', type: FieldType.string, values: texts, config: {} },
|
||||||
|
],
|
||||||
|
length: times.length,
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { DataFrameJSON } from '@grafana/data';
|
import { DataFrameJSON } from '@grafana/data';
|
||||||
import { LiveDataFilter } from '@grafana/runtime';
|
import { LiveDataFilter } from '@grafana/runtime';
|
||||||
import { DataQuery } from '@grafana/schema';
|
import { DataQuery } from '@grafana/schema';
|
||||||
|
import { TimeRegionConfig } from 'app/core/utils/timeRegions';
|
||||||
import { SearchQuery } from 'app/features/search/service';
|
import { SearchQuery } from 'app/features/search/service';
|
||||||
|
|
||||||
//----------------------------------------------
|
//----------------------------------------------
|
||||||
@ -11,6 +12,7 @@ export enum GrafanaQueryType {
|
|||||||
LiveMeasurements = 'measurements',
|
LiveMeasurements = 'measurements',
|
||||||
Annotations = 'annotations',
|
Annotations = 'annotations',
|
||||||
Snapshot = 'snapshot',
|
Snapshot = 'snapshot',
|
||||||
|
TimeRegions = 'timeRegions',
|
||||||
|
|
||||||
// backend
|
// backend
|
||||||
RandomWalk = 'randomWalk',
|
RandomWalk = 'randomWalk',
|
||||||
@ -27,6 +29,7 @@ export interface GrafanaQuery extends DataQuery {
|
|||||||
path?: string; // for list and read
|
path?: string; // for list and read
|
||||||
search?: SearchQuery;
|
search?: SearchQuery;
|
||||||
snapshot?: DataFrameJSON[];
|
snapshot?: DataFrameJSON[];
|
||||||
|
timeRegion?: TimeRegionConfig;
|
||||||
file?: GrafanaQueryFile;
|
file?: GrafanaQueryFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
import { Field, PanelProps } from '@grafana/data';
|
import { DataFrame, Field, PanelProps } from '@grafana/data';
|
||||||
import { PanelDataErrorView } from '@grafana/runtime';
|
import { PanelDataErrorView } from '@grafana/runtime';
|
||||||
import { TooltipDisplayMode } from '@grafana/schema';
|
import { TooltipDisplayMode } from '@grafana/schema';
|
||||||
import { KeyboardPlugin, TimeSeries, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui';
|
import { KeyboardPlugin, TimeSeries, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui';
|
||||||
@ -37,6 +37,25 @@ export const TimeSeriesPanel = ({
|
|||||||
return getFieldLinksForExplore({ field, rowIndex, splitOpenFn: onSplitOpen, range: timeRange });
|
return getFieldLinksForExplore({ field, rowIndex, splitOpenFn: onSplitOpen, range: timeRange });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { annotations, exemplars } = useMemo(() => {
|
||||||
|
let annotations: DataFrame[] | null = null;
|
||||||
|
let exemplars: DataFrame[] | null = null;
|
||||||
|
|
||||||
|
if (data?.annotations?.length) {
|
||||||
|
annotations = [];
|
||||||
|
exemplars = [];
|
||||||
|
for (let frame of data.annotations) {
|
||||||
|
if (frame.name === 'exemplar') {
|
||||||
|
exemplars.push(frame);
|
||||||
|
} else {
|
||||||
|
annotations.push(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { annotations, exemplars };
|
||||||
|
}, [data.annotations]);
|
||||||
|
|
||||||
const frames = useMemo(() => prepareGraphableFields(data.series, config.theme2, timeRange), [data, timeRange]);
|
const frames = useMemo(() => prepareGraphableFields(data.series, config.theme2, timeRange), [data, timeRange]);
|
||||||
const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]);
|
const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]);
|
||||||
|
|
||||||
@ -88,9 +107,7 @@ export const TimeSeriesPanel = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Renders annotation markers*/}
|
{/* Renders annotation markers*/}
|
||||||
{data.annotations && (
|
{annotations && <AnnotationsPlugin annotations={annotations} config={config} timeZone={timeZone} />}
|
||||||
<AnnotationsPlugin annotations={data.annotations} config={config} timeZone={timeZone} />
|
|
||||||
)}
|
|
||||||
{/* Enables annotations creation*/}
|
{/* Enables annotations creation*/}
|
||||||
{enableAnnotationCreation ? (
|
{enableAnnotationCreation ? (
|
||||||
<AnnotationEditorPlugin data={alignedDataFrame} timeZone={timeZone} config={config}>
|
<AnnotationEditorPlugin data={alignedDataFrame} timeZone={timeZone} config={config}>
|
||||||
@ -132,11 +149,11 @@ export const TimeSeriesPanel = ({
|
|||||||
defaultItems={[]}
|
defaultItems={[]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{data.annotations && (
|
{exemplars && (
|
||||||
<ExemplarsPlugin
|
<ExemplarsPlugin
|
||||||
visibleSeries={getVisibleLabels(config, frames)}
|
visibleSeries={getVisibleLabels(config, frames)}
|
||||||
config={config}
|
config={config}
|
||||||
exemplars={data.annotations}
|
exemplars={exemplars}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
getFieldLinks={getFieldLinks}
|
getFieldLinks={getFieldLinks}
|
||||||
/>
|
/>
|
||||||
|
Reference in New Issue
Block a user