mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TimeSeries: Support multiple timezones in x axis (#52424)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
parent
17ea5f4f3e
commit
2fa10dc903
@ -4532,7 +4532,8 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "26"],
|
[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.", "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": [
|
"public/app/features/dashboard/state/TimeModel.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
@ -173,6 +173,11 @@ OptionsWithLegend: {
|
|||||||
legend: VizLegendOptions
|
legend: VizLegendOptions
|
||||||
} @cuetsy(kind="interface")
|
} @cuetsy(kind="interface")
|
||||||
|
|
||||||
|
// TODO docs
|
||||||
|
OptionsWithTimezones: {
|
||||||
|
timezones?: [...string]
|
||||||
|
} @cuetsy(kind="interface")
|
||||||
|
|
||||||
// TODO docs
|
// TODO docs
|
||||||
OptionsWithTextFormatting: {
|
OptionsWithTextFormatting: {
|
||||||
text?: VizTextDisplayOptions
|
text?: VizTextDisplayOptions
|
||||||
|
@ -208,6 +208,14 @@ export interface OptionsWithLegend {
|
|||||||
legend: VizLegendOptions;
|
legend: VizLegendOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OptionsWithTimezones {
|
||||||
|
timezones?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultOptionsWithTimezones: Partial<OptionsWithTimezones> = {
|
||||||
|
timezones: [],
|
||||||
|
};
|
||||||
|
|
||||||
export interface OptionsWithTextFormatting {
|
export interface OptionsWithTextFormatting {
|
||||||
text?: VizTextDisplayOptions;
|
text?: VizTextDisplayOptions;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ export interface GraphNGProps extends Themeable2 {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
timeZone: TimeZone;
|
timeZones: TimeZone[] | TimeZone;
|
||||||
legend: VizLegendOptions;
|
legend: VizLegendOptions;
|
||||||
fields?: XYFieldMatchers; // default will assume timeseries data
|
fields?: XYFieldMatchers; // default will assume timeseries data
|
||||||
renderers?: Renderers;
|
renderers?: Renderers;
|
||||||
@ -216,17 +216,17 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: GraphNGProps) {
|
componentDidUpdate(prevProps: GraphNGProps) {
|
||||||
const { frames, structureRev, timeZone, propsToDiff } = this.props;
|
const { frames, structureRev, timeZones, propsToDiff } = this.props;
|
||||||
|
|
||||||
const propsChanged = !sameProps(prevProps, this.props, propsToDiff);
|
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);
|
let newState = this.prepState(this.props, false);
|
||||||
|
|
||||||
if (newState) {
|
if (newState) {
|
||||||
const shouldReconfig =
|
const shouldReconfig =
|
||||||
this.state.config === undefined ||
|
this.state.config === undefined ||
|
||||||
timeZone !== prevProps.timeZone ||
|
timeZones !== prevProps.timeZones ||
|
||||||
structureRev !== prevProps.structureRev ||
|
structureRev !== prevProps.structureRev ||
|
||||||
!structureRev ||
|
!structureRev ||
|
||||||
propsChanged;
|
propsChanged;
|
||||||
|
@ -214,7 +214,7 @@ describe('GraphNG utils', () => {
|
|||||||
const result = preparePlotConfigBuilder({
|
const result = preparePlotConfigBuilder({
|
||||||
frame: frame!,
|
frame: frame!,
|
||||||
theme: createTheme(),
|
theme: createTheme(),
|
||||||
timeZone: DefaultTimeZone,
|
timeZones: [DefaultTimeZone],
|
||||||
getTimeRange: getDefaultTimeRange,
|
getTimeRange: getDefaultTimeRange,
|
||||||
eventBus: new EventBusSrv(),
|
eventBus: new EventBusSrv(),
|
||||||
sync: () => DashboardCursorSync.Tooltip,
|
sync: () => DashboardCursorSync.Tooltip,
|
||||||
|
@ -22,12 +22,12 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
|
|||||||
|
|
||||||
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||||
const { eventBus, sync } = this.context as PanelContext;
|
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({
|
return preparePlotConfigBuilder({
|
||||||
frame: alignedFrame,
|
frame: alignedFrame,
|
||||||
theme,
|
theme,
|
||||||
timeZone,
|
timeZones: Array.isArray(timeZones) ? timeZones : [timeZones],
|
||||||
getTimeRange,
|
getTimeRange,
|
||||||
eventBus,
|
eventBus,
|
||||||
sync,
|
sync,
|
||||||
|
@ -48,7 +48,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
|||||||
}> = ({
|
}> = ({
|
||||||
frame,
|
frame,
|
||||||
theme,
|
theme,
|
||||||
timeZone,
|
timeZones,
|
||||||
getTimeRange,
|
getTimeRange,
|
||||||
eventBus,
|
eventBus,
|
||||||
sync,
|
sync,
|
||||||
@ -57,7 +57,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
|||||||
tweakScale = (opts) => opts,
|
tweakScale = (opts) => opts,
|
||||||
tweakAxis = (opts) => opts,
|
tweakAxis = (opts) => opts,
|
||||||
}) => {
|
}) => {
|
||||||
const builder = new UPlotConfigBuilder(timeZone);
|
const builder = new UPlotConfigBuilder(timeZones[0]);
|
||||||
|
|
||||||
let alignedFrame: DataFrame;
|
let alignedFrame: DataFrame;
|
||||||
|
|
||||||
@ -97,16 +97,51 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addAxis({
|
// filters first 2 ticks to make space for timezone labels
|
||||||
scaleKey: xScaleKey,
|
const filterTicks: uPlot.Axis.Filter | undefined =
|
||||||
isTime: true,
|
timeZones.length > 1
|
||||||
placement: xFieldAxisPlacement,
|
? (u, splits) => {
|
||||||
show: xFieldAxisShow,
|
return splits.map((v, i) => (i < 2 ? null : v));
|
||||||
label: xField.config.custom?.axisLabel,
|
}
|
||||||
timeZone,
|
: undefined;
|
||||||
theme,
|
|
||||||
grid: { show: xField.config.custom?.axisGridShow },
|
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 {
|
} else {
|
||||||
// Not time!
|
// Not time!
|
||||||
if (xField.config.unit) {
|
if (xField.config.unit) {
|
||||||
|
@ -151,7 +151,7 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
|||||||
ticks
|
ticks
|
||||||
),
|
),
|
||||||
splits,
|
splits,
|
||||||
values: values,
|
values,
|
||||||
space:
|
space:
|
||||||
space ??
|
space ??
|
||||||
((self, axisIdx, scaleMin, scaleMax, plotDim) => {
|
((self, axisIdx, scaleMin, scaleMax, plotDim) => {
|
||||||
@ -227,7 +227,7 @@ export function formatTime(
|
|||||||
format = systemDateFormats.interval.month;
|
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) {
|
export function getUPlotSideFromAxis(axis: AxisPlacement) {
|
||||||
|
@ -87,8 +87,14 @@ export class UPlotConfigBuilder {
|
|||||||
addAxis(props: AxisProps) {
|
addAxis(props: AxisProps) {
|
||||||
props.placement = props.placement ?? AxisPlacement.Auto;
|
props.placement = props.placement ?? AxisPlacement.Auto;
|
||||||
props.grid = props.grid ?? {};
|
props.grid = props.grid ?? {};
|
||||||
if (this.axes[props.scaleKey]) {
|
let scaleKey = props.scaleKey;
|
||||||
this.axes[props.scaleKey].merge(props);
|
|
||||||
|
if (scaleKey === 'x') {
|
||||||
|
scaleKey += props.timeZone ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.axes[scaleKey]) {
|
||||||
|
this.axes[scaleKey].merge(props);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +112,7 @@ export class UPlotConfigBuilder {
|
|||||||
props.size = 0;
|
props.size = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.axes[props.scaleKey] = new UPlotAxisBuilder(props);
|
this.axes[scaleKey] = new UPlotAxisBuilder(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAxisPlacement(scaleKey: string): AxisPlacement {
|
getAxisPlacement(scaleKey: string): AxisPlacement {
|
||||||
@ -285,7 +291,7 @@ export type Renderers = Array<{
|
|||||||
type UPlotConfigPrepOpts<T extends Record<string, any> = {}> = {
|
type UPlotConfigPrepOpts<T extends Record<string, any> = {}> = {
|
||||||
frame: DataFrame;
|
frame: DataFrame;
|
||||||
theme: GrafanaTheme2;
|
theme: GrafanaTheme2;
|
||||||
timeZone: TimeZone;
|
timeZones: TimeZone[];
|
||||||
getTimeRange: () => TimeRange;
|
getTimeRange: () => TimeRange;
|
||||||
eventBus: EventBus;
|
eventBus: EventBus;
|
||||||
allFrames: DataFrame[];
|
allFrames: DataFrame[];
|
||||||
|
@ -236,7 +236,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
|
|||||||
frame: alignedFrame,
|
frame: alignedFrame,
|
||||||
getTimeRange,
|
getTimeRange,
|
||||||
theme,
|
theme,
|
||||||
timeZone,
|
timeZones: [timeZone],
|
||||||
eventBus,
|
eventBus,
|
||||||
orientation,
|
orientation,
|
||||||
barWidth,
|
barWidth,
|
||||||
@ -266,7 +266,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
|
|||||||
preparePlotFrame={(f) => f[0]} // already processed in by the panel above!
|
preparePlotFrame={(f) => f[0]} // already processed in by the panel above!
|
||||||
renderLegend={renderLegend}
|
renderLegend={renderLegend}
|
||||||
legend={options.legend}
|
legend={options.legend}
|
||||||
timeZone={timeZone}
|
timeZones={timeZone}
|
||||||
timeRange={{ from: 1, to: 1 } as unknown as TimeRange} // HACK
|
timeRange={{ from: 1, to: 1 } as unknown as TimeRange} // HACK
|
||||||
structureRev={structureRev}
|
structureRev={structureRev}
|
||||||
width={width}
|
width={width}
|
||||||
|
@ -116,7 +116,7 @@ describe('BarChart utils', () => {
|
|||||||
orientation: v,
|
orientation: v,
|
||||||
frame: frame!,
|
frame: frame!,
|
||||||
theme: createTheme(),
|
theme: createTheme(),
|
||||||
timeZone: DefaultTimeZone,
|
timeZones: [DefaultTimeZone],
|
||||||
getTimeRange: getDefaultTimeRange,
|
getTimeRange: getDefaultTimeRange,
|
||||||
eventBus: new EventBusSrv(),
|
eventBus: new EventBusSrv(),
|
||||||
allFrames: [frame],
|
allFrames: [frame],
|
||||||
@ -131,7 +131,7 @@ describe('BarChart utils', () => {
|
|||||||
showValue: v,
|
showValue: v,
|
||||||
frame: frame!,
|
frame: frame!,
|
||||||
theme: createTheme(),
|
theme: createTheme(),
|
||||||
timeZone: DefaultTimeZone,
|
timeZones: [DefaultTimeZone],
|
||||||
getTimeRange: getDefaultTimeRange,
|
getTimeRange: getDefaultTimeRange,
|
||||||
eventBus: new EventBusSrv(),
|
eventBus: new EventBusSrv(),
|
||||||
allFrames: [frame],
|
allFrames: [frame],
|
||||||
@ -146,7 +146,7 @@ describe('BarChart utils', () => {
|
|||||||
stacking: v,
|
stacking: v,
|
||||||
frame: frame!,
|
frame: frame!,
|
||||||
theme: createTheme(),
|
theme: createTheme(),
|
||||||
timeZone: DefaultTimeZone,
|
timeZones: [DefaultTimeZone],
|
||||||
getTimeRange: getDefaultTimeRange,
|
getTimeRange: getDefaultTimeRange,
|
||||||
eventBus: new EventBusSrv(),
|
eventBus: new EventBusSrv(),
|
||||||
allFrames: [frame],
|
allFrames: [frame],
|
||||||
|
@ -233,7 +233,7 @@ export const CandlestickPanel: React.FC<CandlestickPanelProps> = ({
|
|||||||
frames={[info.frame]}
|
frames={[info.frame]}
|
||||||
structureRev={data.structureRev}
|
structureRev={data.structureRev}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
timeZone={timeZone}
|
timeZones={timeZone}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
legend={options.legend}
|
legend={options.legend}
|
||||||
|
@ -8,6 +8,7 @@ import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPl
|
|||||||
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
|
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
|
||||||
import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin';
|
import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin';
|
||||||
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
||||||
|
import { getTimezones } from '../timeseries/utils';
|
||||||
|
|
||||||
import { StateTimelineTooltip } from './StateTimelineTooltip';
|
import { StateTimelineTooltip } from './StateTimelineTooltip';
|
||||||
import { TimelineChart } from './TimelineChart';
|
import { TimelineChart } from './TimelineChart';
|
||||||
@ -42,6 +43,8 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
|
|||||||
[frames, options.legend, theme]
|
[frames, options.legend, theme]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const timezones = useMemo(() => getTimezones(options.timezones, timeZone), [options.timezones, timeZone]);
|
||||||
|
|
||||||
const renderCustomTooltip = useCallback(
|
const renderCustomTooltip = useCallback(
|
||||||
(alignedData: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => {
|
(alignedData: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => {
|
||||||
const data = frames ?? [];
|
const data = frames ?? [];
|
||||||
@ -104,7 +107,7 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
|
|||||||
frames={frames}
|
frames={frames}
|
||||||
structureRev={data.structureRev}
|
structureRev={data.structureRev}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
timeZone={timeZone}
|
timeZones={timezones}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
legendItems={legendItems}
|
legendItems={legendItems}
|
||||||
|
@ -61,6 +61,9 @@ export class TimelineChart extends React.Component<TimelineProps> {
|
|||||||
allFrames: this.props.frames,
|
allFrames: this.props.frames,
|
||||||
...this.props,
|
...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
|
// When there is only one row, use the full space
|
||||||
rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1,
|
rowHeight: alignedFrame.fields.length > 2 ? this.props.rowHeight : 1,
|
||||||
getValueColor: this.getValueColor,
|
getValueColor: this.getValueColor,
|
||||||
|
@ -32,6 +32,7 @@ Panel: thema.#Lineage & {
|
|||||||
mode?: TimelineMode
|
mode?: TimelineMode
|
||||||
ui.OptionsWithLegend
|
ui.OptionsWithLegend
|
||||||
ui.OptionsWithTooltip
|
ui.OptionsWithTooltip
|
||||||
|
ui.OptionsWithTimezones
|
||||||
showValue: ui.VisibilityMode | *"auto"
|
showValue: ui.VisibilityMode | *"auto"
|
||||||
rowHeight: number | *0.9
|
rowHeight: number | *0.9
|
||||||
colWidth?: number
|
colWidth?: number
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import { DashboardCursorSync } from '@grafana/data';
|
import { DashboardCursorSync } from '@grafana/data';
|
||||||
import { HideableFieldConfig, OptionsWithLegend, OptionsWithTooltip, VisibilityMode } from '@grafana/schema';
|
import {
|
||||||
|
HideableFieldConfig,
|
||||||
|
OptionsWithLegend,
|
||||||
|
OptionsWithTimezones,
|
||||||
|
OptionsWithTooltip,
|
||||||
|
VisibilityMode,
|
||||||
|
} from '@grafana/schema';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export interface TimelineOptions extends OptionsWithLegend, OptionsWithTooltip {
|
export interface TimelineOptions extends OptionsWithLegend, OptionsWithTooltip, OptionsWithTimezones {
|
||||||
mode: TimelineMode; // not in the saved model!
|
mode: TimelineMode; // not in the saved model!
|
||||||
|
|
||||||
showValue: VisibilityMode;
|
showValue: VisibilityMode;
|
||||||
|
@ -56,7 +56,7 @@ export function mapMouseEventToMode(event: React.MouseEvent): SeriesVisibilityCh
|
|||||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
|
export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
|
||||||
frame,
|
frame,
|
||||||
theme,
|
theme,
|
||||||
timeZone,
|
timeZones,
|
||||||
getTimeRange,
|
getTimeRange,
|
||||||
mode,
|
mode,
|
||||||
eventBus,
|
eventBus,
|
||||||
@ -68,7 +68,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
|
|||||||
mergeValues,
|
mergeValues,
|
||||||
getValueColor,
|
getValueColor,
|
||||||
}) => {
|
}) => {
|
||||||
const builder = new UPlotConfigBuilder(timeZone);
|
const builder = new UPlotConfigBuilder(timeZones[0]);
|
||||||
|
|
||||||
const xScaleUnit = 'time';
|
const xScaleUnit = 'time';
|
||||||
const xScaleKey = 'x';
|
const xScaleKey = 'x';
|
||||||
@ -185,7 +185,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
|
|||||||
isTime: true,
|
isTime: true,
|
||||||
splits: coreConfig.xSplits!,
|
splits: coreConfig.xSplits!,
|
||||||
placement: AxisPlacement.Bottom,
|
placement: AxisPlacement.Bottom,
|
||||||
timeZone,
|
timeZone: timeZones[0],
|
||||||
theme,
|
theme,
|
||||||
grid: { show: true },
|
grid: { show: true },
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,7 @@ import { TimelineChart } from '../state-timeline/TimelineChart';
|
|||||||
import { TimelineMode } from '../state-timeline/types';
|
import { TimelineMode } from '../state-timeline/types';
|
||||||
import { prepareTimelineFields, prepareTimelineLegendItems } from '../state-timeline/utils';
|
import { prepareTimelineFields, prepareTimelineLegendItems } from '../state-timeline/utils';
|
||||||
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
||||||
|
import { getTimezones } from '../timeseries/utils';
|
||||||
|
|
||||||
import { StatusPanelOptions } from './types';
|
import { StatusPanelOptions } from './types';
|
||||||
|
|
||||||
@ -36,6 +37,8 @@ export const StatusHistoryPanel: React.FC<TimelinePanelProps> = ({
|
|||||||
[frames, options.legend, theme]
|
[frames, options.legend, theme]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const timezones = useMemo(() => getTimezones(options.timezones, timeZone), [options.timezones, timeZone]);
|
||||||
|
|
||||||
if (!frames || warn) {
|
if (!frames || warn) {
|
||||||
return (
|
return (
|
||||||
<div className="panel-empty">
|
<div className="panel-empty">
|
||||||
@ -62,7 +65,7 @@ export const StatusHistoryPanel: React.FC<TimelinePanelProps> = ({
|
|||||||
frames={frames}
|
frames={frames}
|
||||||
structureRev={data.structureRev}
|
structureRev={data.structureRev}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
timeZone={timeZone}
|
timeZones={timezones}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
legendItems={legendItems}
|
legendItems={legendItems}
|
||||||
|
@ -28,6 +28,7 @@ Panel: thema.#Lineage & {
|
|||||||
PanelOptions: {
|
PanelOptions: {
|
||||||
ui.OptionsWithLegend
|
ui.OptionsWithLegend
|
||||||
ui.OptionsWithTooltip
|
ui.OptionsWithTooltip
|
||||||
|
ui.OptionsWithTimezones
|
||||||
showValue: ui.VisibilityMode
|
showValue: ui.VisibilityMode
|
||||||
rowHeight: number
|
rowHeight: number
|
||||||
colWidth?: number
|
colWidth?: number
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { HideableFieldConfig, VisibilityMode, OptionsWithTooltip, OptionsWithLegend } from '@grafana/schema';
|
import {
|
||||||
|
HideableFieldConfig,
|
||||||
|
VisibilityMode,
|
||||||
|
OptionsWithTooltip,
|
||||||
|
OptionsWithLegend,
|
||||||
|
OptionsWithTimezones,
|
||||||
|
} from '@grafana/schema';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export interface StatusPanelOptions extends OptionsWithTooltip, OptionsWithLegend {
|
export interface StatusPanelOptions extends OptionsWithTooltip, OptionsWithLegend, OptionsWithTimezones {
|
||||||
showValue: VisibilityMode;
|
showValue: VisibilityMode;
|
||||||
rowHeight: number;
|
rowHeight: number;
|
||||||
colWidth?: number;
|
colWidth?: number;
|
||||||
|
@ -14,7 +14,7 @@ import { ExemplarsPlugin } from './plugins/ExemplarsPlugin';
|
|||||||
import { OutsideRangePlugin } from './plugins/OutsideRangePlugin';
|
import { OutsideRangePlugin } from './plugins/OutsideRangePlugin';
|
||||||
import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin';
|
import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin';
|
||||||
import { TimeSeriesOptions } from './types';
|
import { TimeSeriesOptions } from './types';
|
||||||
import { prepareGraphableFields } from './utils';
|
import { getTimezones, prepareGraphableFields } from './utils';
|
||||||
|
|
||||||
interface TimeSeriesPanelProps extends PanelProps<TimeSeriesOptions> {}
|
interface TimeSeriesPanelProps extends PanelProps<TimeSeriesOptions> {}
|
||||||
|
|
||||||
@ -37,6 +37,7 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
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.timezones, timeZone), [options.timezones, timeZone]);
|
||||||
|
|
||||||
if (!frames) {
|
if (!frames) {
|
||||||
return (
|
return (
|
||||||
@ -57,7 +58,7 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
|
|||||||
frames={frames}
|
frames={frames}
|
||||||
structureRev={data.structureRev}
|
structureRev={data.structureRev}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
timeZone={timeZone}
|
timeZones={timezones}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
legend={options.legend}
|
legend={options.legend}
|
||||||
|
70
public/app/plugins/panel/timeseries/TimezonesEditor.tsx
Normal file
70
public/app/plugins/panel/timeseries/TimezonesEditor.tsx
Normal file
@ -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<string[], unknown, OptionsWithTimezones>;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
{value.map((tz, idx) => (
|
||||||
|
<div className={styles.wrapper} key={`${idx}.${tz}`}>
|
||||||
|
<span className={styles.first}>
|
||||||
|
<TimeZonePicker
|
||||||
|
onChange={(v) => setTimezone(idx, v)}
|
||||||
|
includeInternal={true}
|
||||||
|
value={tz ?? InternalTimeZones.default}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
{idx === value.length - 1 ? (
|
||||||
|
<IconButton ariaLabel="Add timezone" name="plus" onClick={addTimezone} />
|
||||||
|
) : (
|
||||||
|
<IconButton ariaLabel="Remove timezone" name="times" onClick={() => removeTimezone(idx)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
wrapper: css`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: rows;
|
||||||
|
align-items: center;
|
||||||
|
`,
|
||||||
|
first: css`
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-grow: 2;
|
||||||
|
`,
|
||||||
|
});
|
@ -3,6 +3,7 @@ import { GraphFieldConfig } from '@grafana/schema';
|
|||||||
import { commonOptionsBuilder } from '@grafana/ui';
|
import { commonOptionsBuilder } from '@grafana/ui';
|
||||||
|
|
||||||
import { TimeSeriesPanel } from './TimeSeriesPanel';
|
import { TimeSeriesPanel } from './TimeSeriesPanel';
|
||||||
|
import { TimezonesEditor } from './TimezonesEditor';
|
||||||
import { defaultGraphConfig, getGraphFieldConfig } from './config';
|
import { defaultGraphConfig, getGraphFieldConfig } from './config';
|
||||||
import { graphPanelChangedHandler } from './migrations';
|
import { graphPanelChangedHandler } from './migrations';
|
||||||
import { TimeSeriesSuggestionsSupplier } from './suggestions';
|
import { TimeSeriesSuggestionsSupplier } from './suggestions';
|
||||||
@ -14,6 +15,15 @@ export const plugin = new PanelPlugin<TimeSeriesOptions, GraphFieldConfig>(TimeS
|
|||||||
.setPanelOptions((builder) => {
|
.setPanelOptions((builder) => {
|
||||||
commonOptionsBuilder.addTooltipOptions(builder);
|
commonOptionsBuilder.addTooltipOptions(builder);
|
||||||
commonOptionsBuilder.addLegendOptions(builder);
|
commonOptionsBuilder.addLegendOptions(builder);
|
||||||
|
|
||||||
|
builder.addCustomEditor({
|
||||||
|
id: 'timezones',
|
||||||
|
name: 'Timezone',
|
||||||
|
path: 'timezones',
|
||||||
|
category: ['Axis'],
|
||||||
|
editor: TimezonesEditor,
|
||||||
|
defaultValue: undefined,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.setSuggestionsSupplier(new TimeSeriesSuggestionsSupplier())
|
.setSuggestionsSupplier(new TimeSeriesSuggestionsSupplier())
|
||||||
.setDataSupport({ annotations: true, alertStates: true });
|
.setDataSupport({ annotations: true, alertStates: true });
|
||||||
|
@ -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 {}
|
||||||
|
@ -116,3 +116,10 @@ export function prepareGraphableFields(
|
|||||||
|
|
||||||
return null;
|
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));
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user