diff --git a/.betterer.results b/.betterer.results index 0ff5d1dc90e..a20d93acd02 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4532,7 +4532,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "25"], [0, 0, 0, "Do not use any type assertions.", "26"], [0, 0, 0, "Unexpected any. Specify a different type.", "27"], - [0, 0, 0, "Unexpected any. Specify a different type.", "28"] + [0, 0, 0, "Unexpected any. Specify a different type.", "28"], + [0, 0, 0, "Unexpected any. Specify a different type.", "29"] ], "public/app/features/dashboard/state/TimeModel.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-schema/src/schema/mudball.cue b/packages/grafana-schema/src/schema/mudball.cue index 10e7c9331b5..e91a5ea4356 100644 --- a/packages/grafana-schema/src/schema/mudball.cue +++ b/packages/grafana-schema/src/schema/mudball.cue @@ -173,6 +173,11 @@ OptionsWithLegend: { legend: VizLegendOptions } @cuetsy(kind="interface") +// TODO docs +OptionsWithTimezones: { + timezones?: [...string] +} @cuetsy(kind="interface") + // TODO docs OptionsWithTextFormatting: { text?: VizTextDisplayOptions diff --git a/packages/grafana-schema/src/schema/mudball.gen.ts b/packages/grafana-schema/src/schema/mudball.gen.ts index 1e9ce67d707..5b9aad146ce 100644 --- a/packages/grafana-schema/src/schema/mudball.gen.ts +++ b/packages/grafana-schema/src/schema/mudball.gen.ts @@ -208,6 +208,14 @@ export interface OptionsWithLegend { legend: VizLegendOptions; } +export interface OptionsWithTimezones { + timezones?: string[]; +} + +export const defaultOptionsWithTimezones: Partial = { + timezones: [], +}; + export interface OptionsWithTextFormatting { text?: VizTextDisplayOptions; } diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index 9783be08b37..10202f0a9e5 100644 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -44,7 +44,7 @@ export interface GraphNGProps extends Themeable2 { width: number; height: number; timeRange: TimeRange; - timeZone: TimeZone; + timeZones: TimeZone[] | TimeZone; legend: VizLegendOptions; fields?: XYFieldMatchers; // default will assume timeseries data renderers?: Renderers; @@ -216,17 +216,17 @@ export class GraphNG extends React.Component { } componentDidUpdate(prevProps: GraphNGProps) { - const { frames, structureRev, timeZone, propsToDiff } = this.props; + const { frames, structureRev, timeZones, propsToDiff } = this.props; const propsChanged = !sameProps(prevProps, this.props, propsToDiff); - if (frames !== prevProps.frames || propsChanged || timeZone !== prevProps.timeZone) { + if (frames !== prevProps.frames || propsChanged || timeZones !== prevProps.timeZones) { let newState = this.prepState(this.props, false); if (newState) { const shouldReconfig = this.state.config === undefined || - timeZone !== prevProps.timeZone || + timeZones !== prevProps.timeZones || structureRev !== prevProps.structureRev || !structureRev || propsChanged; diff --git a/packages/grafana-ui/src/components/GraphNG/utils.test.ts b/packages/grafana-ui/src/components/GraphNG/utils.test.ts index ae6ed7a938c..6f44020a288 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.test.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.test.ts @@ -214,7 +214,7 @@ describe('GraphNG utils', () => { const result = preparePlotConfigBuilder({ frame: frame!, theme: createTheme(), - timeZone: DefaultTimeZone, + timeZones: [DefaultTimeZone], getTimeRange: getDefaultTimeRange, eventBus: new EventBusSrv(), sync: () => DashboardCursorSync.Tooltip, diff --git a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx index 39ee24a3bbb..708a504b680 100644 --- a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx +++ b/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx @@ -22,12 +22,12 @@ export class UnthemedTimeSeries extends React.Component { prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { const { eventBus, sync } = this.context as PanelContext; - const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props; + const { theme, timeZones, renderers, tweakAxis, tweakScale } = this.props; return preparePlotConfigBuilder({ frame: alignedFrame, theme, - timeZone, + timeZones: Array.isArray(timeZones) ? timeZones : [timeZones], getTimeRange, eventBus, sync, diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index 9f9f2de8c3e..51703830a03 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -48,7 +48,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ }> = ({ frame, theme, - timeZone, + timeZones, getTimeRange, eventBus, sync, @@ -57,7 +57,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ tweakScale = (opts) => opts, tweakAxis = (opts) => opts, }) => { - const builder = new UPlotConfigBuilder(timeZone); + const builder = new UPlotConfigBuilder(timeZones[0]); let alignedFrame: DataFrame; @@ -97,16 +97,51 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ }, }); - builder.addAxis({ - scaleKey: xScaleKey, - isTime: true, - placement: xFieldAxisPlacement, - show: xFieldAxisShow, - label: xField.config.custom?.axisLabel, - timeZone, - theme, - grid: { show: xField.config.custom?.axisGridShow }, - }); + // filters first 2 ticks to make space for timezone labels + const filterTicks: uPlot.Axis.Filter | undefined = + timeZones.length > 1 + ? (u, splits) => { + return splits.map((v, i) => (i < 2 ? null : v)); + } + : undefined; + + for (let i = 0; i < timeZones.length; i++) { + const timeZone = timeZones[i]; + builder.addAxis({ + scaleKey: xScaleKey, + isTime: true, + placement: xFieldAxisPlacement, + show: xFieldAxisShow, + label: xField.config.custom?.axisLabel, + timeZone, + theme, + grid: { show: i === 0 && xField.config.custom?.axisGridShow }, + filter: filterTicks, + }); + } + + // render timezone labels + if (timeZones.length > 1) { + builder.addHook('drawAxes', (u: uPlot) => { + u.ctx.save(); + + u.ctx.fillStyle = theme.colors.text.primary; + u.ctx.textAlign = 'left'; + u.ctx.textBaseline = 'bottom'; + + let i = 0; + u.axes.forEach((a) => { + if (a.side === 2) { + //@ts-ignore + let cssBaseline: number = a._pos + a._size; + u.ctx.fillText(timeZones[i], u.bbox.left, cssBaseline * uPlot.pxRatio); + i++; + } + }); + + u.ctx.restore(); + }); + } } else { // Not time! if (xField.config.unit) { diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts index 96f6f02e1eb..05a924173d3 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts @@ -151,7 +151,7 @@ export class UPlotAxisBuilder extends PlotConfigBuilder { ticks ), splits, - values: values, + values, space: space ?? ((self, axisIdx, scaleMin, scaleMax, plotDim) => { @@ -227,7 +227,7 @@ export function formatTime( format = systemDateFormats.interval.month; } - return splits.map((v) => dateTimeFormat(v, { format, timeZone })); + return splits.map((v) => (v == null ? '' : dateTimeFormat(v, { format, timeZone }))); } export function getUPlotSideFromAxis(axis: AxisPlacement) { diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index cc2b2124989..034b8eae90e 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -87,8 +87,14 @@ export class UPlotConfigBuilder { addAxis(props: AxisProps) { props.placement = props.placement ?? AxisPlacement.Auto; props.grid = props.grid ?? {}; - if (this.axes[props.scaleKey]) { - this.axes[props.scaleKey].merge(props); + let scaleKey = props.scaleKey; + + if (scaleKey === 'x') { + scaleKey += props.timeZone ?? ''; + } + + if (this.axes[scaleKey]) { + this.axes[scaleKey].merge(props); return; } @@ -106,7 +112,7 @@ export class UPlotConfigBuilder { props.size = 0; } - this.axes[props.scaleKey] = new UPlotAxisBuilder(props); + this.axes[scaleKey] = new UPlotAxisBuilder(props); } getAxisPlacement(scaleKey: string): AxisPlacement { @@ -285,7 +291,7 @@ export type Renderers = Array<{ type UPlotConfigPrepOpts = {}> = { frame: DataFrame; theme: GrafanaTheme2; - timeZone: TimeZone; + timeZones: TimeZone[]; getTimeRange: () => TimeRange; eventBus: EventBus; allFrames: DataFrame[]; diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index 0a58e65beaa..adfa27748cb 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -236,7 +236,7 @@ export const BarChartPanel: React.FunctionComponent = ({ frame: alignedFrame, getTimeRange, theme, - timeZone, + timeZones: [timeZone], eventBus, orientation, barWidth, @@ -266,7 +266,7 @@ export const BarChartPanel: React.FunctionComponent = ({ preparePlotFrame={(f) => f[0]} // already processed in by the panel above! renderLegend={renderLegend} legend={options.legend} - timeZone={timeZone} + timeZones={timeZone} timeRange={{ from: 1, to: 1 } as unknown as TimeRange} // HACK structureRev={structureRev} width={width} diff --git a/public/app/plugins/panel/barchart/utils.test.ts b/public/app/plugins/panel/barchart/utils.test.ts index 124fc030518..f9b90b08ff7 100644 --- a/public/app/plugins/panel/barchart/utils.test.ts +++ b/public/app/plugins/panel/barchart/utils.test.ts @@ -116,7 +116,7 @@ describe('BarChart utils', () => { orientation: v, frame: frame!, theme: createTheme(), - timeZone: DefaultTimeZone, + timeZones: [DefaultTimeZone], getTimeRange: getDefaultTimeRange, eventBus: new EventBusSrv(), allFrames: [frame], @@ -131,7 +131,7 @@ describe('BarChart utils', () => { showValue: v, frame: frame!, theme: createTheme(), - timeZone: DefaultTimeZone, + timeZones: [DefaultTimeZone], getTimeRange: getDefaultTimeRange, eventBus: new EventBusSrv(), allFrames: [frame], @@ -146,7 +146,7 @@ describe('BarChart utils', () => { stacking: v, frame: frame!, theme: createTheme(), - timeZone: DefaultTimeZone, + timeZones: [DefaultTimeZone], getTimeRange: getDefaultTimeRange, eventBus: new EventBusSrv(), allFrames: [frame], diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index aa4544ebb71..6fe572658a1 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -233,7 +233,7 @@ export const CandlestickPanel: React.FC = ({ frames={[info.frame]} structureRev={data.structureRev} timeRange={timeRange} - timeZone={timeZone} + timeZones={timeZone} width={width} height={height} legend={options.legend} diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index d65827d48a9..e189a12e3e7 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -8,6 +8,7 @@ import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPl import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin'; import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin'; import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; +import { getTimezones } from '../timeseries/utils'; import { StateTimelineTooltip } from './StateTimelineTooltip'; import { TimelineChart } from './TimelineChart'; @@ -42,6 +43,8 @@ export const StateTimelinePanel: React.FC = ({ [frames, options.legend, theme] ); + const timezones = useMemo(() => getTimezones(options.timezones, timeZone), [options.timezones, timeZone]); + const renderCustomTooltip = useCallback( (alignedData: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => { const data = frames ?? []; @@ -104,7 +107,7 @@ export const StateTimelinePanel: React.FC = ({ frames={frames} structureRev={data.structureRev} timeRange={timeRange} - timeZone={timeZone} + timeZones={timezones} width={width} height={height} legendItems={legendItems} diff --git a/public/app/plugins/panel/state-timeline/TimelineChart.tsx b/public/app/plugins/panel/state-timeline/TimelineChart.tsx index 3d13fc495cf..f2f59ab7aca 100644 --- a/public/app/plugins/panel/state-timeline/TimelineChart.tsx +++ b/public/app/plugins/panel/state-timeline/TimelineChart.tsx @@ -61,6 +61,9 @@ export class TimelineChart extends React.Component { allFrames: this.props.frames, ...this.props, + // Ensure timezones is passed as an array + timeZones: Array.isArray(this.props.timeZones) ? this.props.timeZones : [this.props.timeZones], + // When there is only one row, use the full space rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1, getValueColor: this.getValueColor, diff --git a/public/app/plugins/panel/state-timeline/models.cue b/public/app/plugins/panel/state-timeline/models.cue index f3523594727..262a6bd6dd0 100644 --- a/public/app/plugins/panel/state-timeline/models.cue +++ b/public/app/plugins/panel/state-timeline/models.cue @@ -32,6 +32,7 @@ Panel: thema.#Lineage & { mode?: TimelineMode ui.OptionsWithLegend ui.OptionsWithTooltip + ui.OptionsWithTimezones showValue: ui.VisibilityMode | *"auto" rowHeight: number | *0.9 colWidth?: number diff --git a/public/app/plugins/panel/state-timeline/types.ts b/public/app/plugins/panel/state-timeline/types.ts index ecd1f5f56e8..fed8714bb21 100644 --- a/public/app/plugins/panel/state-timeline/types.ts +++ b/public/app/plugins/panel/state-timeline/types.ts @@ -1,10 +1,16 @@ import { DashboardCursorSync } from '@grafana/data'; -import { HideableFieldConfig, OptionsWithLegend, OptionsWithTooltip, VisibilityMode } from '@grafana/schema'; +import { + HideableFieldConfig, + OptionsWithLegend, + OptionsWithTimezones, + OptionsWithTooltip, + VisibilityMode, +} from '@grafana/schema'; /** * @alpha */ -export interface TimelineOptions extends OptionsWithLegend, OptionsWithTooltip { +export interface TimelineOptions extends OptionsWithLegend, OptionsWithTooltip, OptionsWithTimezones { mode: TimelineMode; // not in the saved model! showValue: VisibilityMode; diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index 83246e541cf..986970534ea 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -56,7 +56,7 @@ export function mapMouseEventToMode(event: React.MouseEvent): SeriesVisibilityCh export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ frame, theme, - timeZone, + timeZones, getTimeRange, mode, eventBus, @@ -68,7 +68,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ mergeValues, getValueColor, }) => { - const builder = new UPlotConfigBuilder(timeZone); + const builder = new UPlotConfigBuilder(timeZones[0]); const xScaleUnit = 'time'; const xScaleKey = 'x'; @@ -185,7 +185,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ isTime: true, splits: coreConfig.xSplits!, placement: AxisPlacement.Bottom, - timeZone, + timeZone: timeZones[0], theme, grid: { show: true }, }); diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 5ae1d277983..cd3c25f8eb5 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -7,6 +7,7 @@ import { TimelineChart } from '../state-timeline/TimelineChart'; import { TimelineMode } from '../state-timeline/types'; import { prepareTimelineFields, prepareTimelineLegendItems } from '../state-timeline/utils'; import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; +import { getTimezones } from '../timeseries/utils'; import { StatusPanelOptions } from './types'; @@ -36,6 +37,8 @@ export const StatusHistoryPanel: React.FC = ({ [frames, options.legend, theme] ); + const timezones = useMemo(() => getTimezones(options.timezones, timeZone), [options.timezones, timeZone]); + if (!frames || warn) { return (
@@ -62,7 +65,7 @@ export const StatusHistoryPanel: React.FC = ({ frames={frames} structureRev={data.structureRev} timeRange={timeRange} - timeZone={timeZone} + timeZones={timezones} width={width} height={height} legendItems={legendItems} diff --git a/public/app/plugins/panel/status-history/models.cue b/public/app/plugins/panel/status-history/models.cue index 771ee445a0b..22bc7cb1ed0 100644 --- a/public/app/plugins/panel/status-history/models.cue +++ b/public/app/plugins/panel/status-history/models.cue @@ -28,6 +28,7 @@ Panel: thema.#Lineage & { PanelOptions: { ui.OptionsWithLegend ui.OptionsWithTooltip + ui.OptionsWithTimezones showValue: ui.VisibilityMode rowHeight: number colWidth?: number diff --git a/public/app/plugins/panel/status-history/types.ts b/public/app/plugins/panel/status-history/types.ts index 0e4b4b65277..874d1a99def 100644 --- a/public/app/plugins/panel/status-history/types.ts +++ b/public/app/plugins/panel/status-history/types.ts @@ -1,9 +1,15 @@ -import { HideableFieldConfig, VisibilityMode, OptionsWithTooltip, OptionsWithLegend } from '@grafana/schema'; +import { + HideableFieldConfig, + VisibilityMode, + OptionsWithTooltip, + OptionsWithLegend, + OptionsWithTimezones, +} from '@grafana/schema'; /** * @alpha */ -export interface StatusPanelOptions extends OptionsWithTooltip, OptionsWithLegend { +export interface StatusPanelOptions extends OptionsWithTooltip, OptionsWithLegend, OptionsWithTimezones { showValue: VisibilityMode; rowHeight: number; colWidth?: number; diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index dfa3861e293..b2d9b26e78c 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -14,7 +14,7 @@ import { ExemplarsPlugin } from './plugins/ExemplarsPlugin'; import { OutsideRangePlugin } from './plugins/OutsideRangePlugin'; import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin'; import { TimeSeriesOptions } from './types'; -import { prepareGraphableFields } from './utils'; +import { getTimezones, prepareGraphableFields } from './utils'; interface TimeSeriesPanelProps extends PanelProps {} @@ -37,6 +37,7 @@ export const TimeSeriesPanel: React.FC = ({ }; const frames = useMemo(() => prepareGraphableFields(data.series, config.theme2, timeRange), [data, timeRange]); + const timezones = useMemo(() => getTimezones(options.timezones, timeZone), [options.timezones, timeZone]); if (!frames) { return ( @@ -57,7 +58,7 @@ export const TimeSeriesPanel: React.FC = ({ frames={frames} structureRev={data.structureRev} timeRange={timeRange} - timeZone={timeZone} + timeZones={timezones} width={width} height={height} legend={options.legend} diff --git a/public/app/plugins/panel/timeseries/TimezonesEditor.tsx b/public/app/plugins/panel/timeseries/TimezonesEditor.tsx new file mode 100644 index 00000000000..127a0cbd5fe --- /dev/null +++ b/public/app/plugins/panel/timeseries/TimezonesEditor.tsx @@ -0,0 +1,70 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2, InternalTimeZones, StandardEditorProps } from '@grafana/data'; +import { OptionsWithTimezones } from '@grafana/schema'; +import { IconButton, TimeZonePicker, useStyles2 } from '@grafana/ui'; + +type Props = StandardEditorProps; + +export const TimezonesEditor = ({ value, onChange }: Props) => { + const styles = useStyles2(getStyles); + + if (!value || value.length < 1) { + value = ['']; + } + + const addTimezone = () => { + onChange([...value, InternalTimeZones.default]); + }; + + const removeTimezone = (idx: number) => { + const copy = value.slice(); + copy.splice(idx, 1); + onChange(copy); + }; + + const setTimezone = (idx: number, tz?: string) => { + const copy = value.slice(); + copy[idx] = tz ?? InternalTimeZones.default; + if (copy.length === 0 || (copy.length === 1 && copy[0] === '')) { + onChange(undefined); + } else { + onChange(copy); + } + }; + + return ( +
+ {value.map((tz, idx) => ( +
+ + setTimezone(idx, v)} + includeInternal={true} + value={tz ?? InternalTimeZones.default} + /> + + {idx === value.length - 1 ? ( + + ) : ( + removeTimezone(idx)} /> + )} +
+ ))} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + wrapper: css` + width: 100%; + display: flex; + flex-direction: rows; + align-items: center; + `, + first: css` + margin-right: 8px; + flex-grow: 2; + `, +}); diff --git a/public/app/plugins/panel/timeseries/module.tsx b/public/app/plugins/panel/timeseries/module.tsx index c1f4b742ff9..1ded66cd86b 100644 --- a/public/app/plugins/panel/timeseries/module.tsx +++ b/public/app/plugins/panel/timeseries/module.tsx @@ -3,6 +3,7 @@ import { GraphFieldConfig } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; import { TimeSeriesPanel } from './TimeSeriesPanel'; +import { TimezonesEditor } from './TimezonesEditor'; import { defaultGraphConfig, getGraphFieldConfig } from './config'; import { graphPanelChangedHandler } from './migrations'; import { TimeSeriesSuggestionsSupplier } from './suggestions'; @@ -14,6 +15,15 @@ export const plugin = new PanelPlugin(TimeS .setPanelOptions((builder) => { commonOptionsBuilder.addTooltipOptions(builder); commonOptionsBuilder.addLegendOptions(builder); + + builder.addCustomEditor({ + id: 'timezones', + name: 'Timezone', + path: 'timezones', + category: ['Axis'], + editor: TimezonesEditor, + defaultValue: undefined, + }); }) .setSuggestionsSupplier(new TimeSeriesSuggestionsSupplier()) .setDataSupport({ annotations: true, alertStates: true }); diff --git a/public/app/plugins/panel/timeseries/types.ts b/public/app/plugins/panel/timeseries/types.ts index 43cf99e0746..a414735b9d1 100644 --- a/public/app/plugins/panel/timeseries/types.ts +++ b/public/app/plugins/panel/timeseries/types.ts @@ -1,3 +1,3 @@ -import { OptionsWithLegend, OptionsWithTooltip } from '@grafana/schema'; +import { OptionsWithLegend, OptionsWithTimezones, OptionsWithTooltip } from '@grafana/schema'; -export interface TimeSeriesOptions extends OptionsWithLegend, OptionsWithTooltip {} +export interface TimeSeriesOptions extends OptionsWithLegend, OptionsWithTooltip, OptionsWithTimezones {} diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index 4ec50963908..241e775b1ff 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -116,3 +116,10 @@ export function prepareGraphableFields( return null; } + +export function getTimezones(timezones: string[] | undefined, defaultTimezone: string): string[] { + if (!timezones || !timezones.length) { + return [defaultTimezone]; + } + return timezones.map((v) => (v?.length ? v : defaultTimezone)); +}