mirror of https://github.com/grafana/grafana
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>pull/66869/head
parent
a2b97547a6
commit
faad4b92ad
@ -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; |
||||
`,
|
||||
}; |
||||
}; |
@ -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", |
||||
], |
||||
}, |
||||
] |
||||
`);
|
||||
}); |
||||
}); |
@ -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, |
||||
}; |
||||
} |
Loading…
Reference in new issue