mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TrendPanel: Add new trend panel (Alpha) (#65740)
This commit is contained in:
parent
313b3dd2af
commit
d974e5f25a
@ -6009,6 +6009,10 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
],
|
],
|
||||||
|
"public/app/plugins/panel/trend/suggestions.ts:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
|
],
|
||||||
"public/app/plugins/panel/xychart/AutoEditor.tsx:5381": [
|
"public/app/plugins/panel/xychart/AutoEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -407,6 +407,7 @@ lerna.json @grafana/frontend-ops
|
|||||||
/public/app/plugins/panel/table/ @grafana/grafana-bi-squad
|
/public/app/plugins/panel/table/ @grafana/grafana-bi-squad
|
||||||
/public/app/plugins/panel/table-old/ @grafana/grafana-bi-squad
|
/public/app/plugins/panel/table-old/ @grafana/grafana-bi-squad
|
||||||
/public/app/plugins/panel/timeseries/ @grafana/dataviz-squad
|
/public/app/plugins/panel/timeseries/ @grafana/dataviz-squad
|
||||||
|
/public/app/plugins/panel/trend/ @grafana/dataviz-squad
|
||||||
/public/app/plugins/panel/geomap/ @grafana/dataviz-squad
|
/public/app/plugins/panel/geomap/ @grafana/dataviz-squad
|
||||||
/public/app/plugins/panel/canvas/ @grafana/dataviz-squad
|
/public/app/plugins/panel/canvas/ @grafana/dataviz-squad
|
||||||
/public/app/plugins/panel/candlestick/ @grafana/dataviz-squad
|
/public/app/plugins/panel/candlestick/ @grafana/dataviz-squad
|
||||||
|
@ -156,6 +156,11 @@ export class UPlotScaleBuilder extends PlotConfigBuilder<ScaleProps, Scale> {
|
|||||||
|
|
||||||
let minMax: uPlot.Range.MinMax = [dataMin, dataMax];
|
let minMax: uPlot.Range.MinMax = [dataMin, dataMax];
|
||||||
|
|
||||||
|
// don't pad numeric x scales
|
||||||
|
if (scaleKey === 'x' && !isTime) {
|
||||||
|
return minMax;
|
||||||
|
}
|
||||||
|
|
||||||
// happens when all series on a scale are `show: false`, re-returning nulls will auto-disable axis
|
// happens when all series on a scale are `show: false`, re-returning nulls will auto-disable axis
|
||||||
if (!hasFixedRange && dataMin == null && dataMax == null) {
|
if (!hasFixedRange && dataMin == null && dataMax == null) {
|
||||||
return minMax;
|
return minMax;
|
||||||
|
@ -189,6 +189,7 @@ func verifyCorePluginCatalogue(t *testing.T, ctx context.Context, ps *store.Serv
|
|||||||
"table-old": {},
|
"table-old": {},
|
||||||
"text": {},
|
"text": {},
|
||||||
"timeseries": {},
|
"timeseries": {},
|
||||||
|
"trend": {},
|
||||||
"welcome": {},
|
"welcome": {},
|
||||||
"xychart": {},
|
"xychart": {},
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,7 @@ func corePlugins(rt *thema.Runtime) []pfs.ParsedPlugin {
|
|||||||
parsePluginOrPanic("public/app/plugins/panel/text", "text", rt),
|
parsePluginOrPanic("public/app/plugins/panel/text", "text", rt),
|
||||||
parsePluginOrPanic("public/app/plugins/panel/timeseries", "timeseries", rt),
|
parsePluginOrPanic("public/app/plugins/panel/timeseries", "timeseries", rt),
|
||||||
parsePluginOrPanic("public/app/plugins/panel/traces", "traces", rt),
|
parsePluginOrPanic("public/app/plugins/panel/traces", "traces", rt),
|
||||||
|
parsePluginOrPanic("public/app/plugins/panel/trend", "trend", rt),
|
||||||
parsePluginOrPanic("public/app/plugins/panel/welcome", "welcome", rt),
|
parsePluginOrPanic("public/app/plugins/panel/welcome", "welcome", rt),
|
||||||
parsePluginOrPanic("public/app/plugins/panel/xychart", "xychart", rt),
|
parsePluginOrPanic("public/app/plugins/panel/xychart", "xychart", rt),
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ seqs: [
|
|||||||
// grafana.com, then the plugin `id` has to follow the naming
|
// grafana.com, then the plugin `id` has to follow the naming
|
||||||
// conventions.
|
// conventions.
|
||||||
id: string & strings.MinRunes(1)
|
id: string & strings.MinRunes(1)
|
||||||
id: =~"^([0-9a-z]+\\-([0-9a-z]+\\-)?(\(strings.Join([ for t in _types {t}], "|"))))|(alertGroups|alertlist|annolist|barchart|bargauge|candlestick|canvas|dashlist|debug|gauge|geomap|gettingstarted|graph|heatmap|histogram|icon|live|logs|news|nodeGraph|piechart|pluginlist|stat|state-timeline|status-history|table|table-old|text|timeseries|traces|welcome|xychart|alertmanager|cloudwatch|dashboard|elasticsearch|grafana|grafana-azure-monitor-datasource|graphite|influxdb|jaeger|loki|mixed|mssql|mysql|opentsdb|postgres|prometheus|stackdriver|tempo|testdata|zipkin|phlare|parca)$"
|
id: =~"^([0-9a-z]+\\-([0-9a-z]+\\-)?(\(strings.Join([ for t in _types {t}], "|"))))|(alertGroups|alertlist|annolist|barchart|bargauge|candlestick|canvas|dashlist|debug|gauge|geomap|gettingstarted|graph|heatmap|histogram|icon|live|logs|news|nodeGraph|piechart|pluginlist|stat|state-timeline|status-history|table|table-old|text|timeseries|trend|traces|welcome|xychart|alertmanager|cloudwatch|dashboard|elasticsearch|grafana|grafana-azure-monitor-datasource|graphite|influxdb|jaeger|loki|mixed|mssql|mysql|opentsdb|postgres|prometheus|stackdriver|tempo|testdata|zipkin|phlare|parca)$"
|
||||||
|
|
||||||
// Human-readable name of the plugin that is shown to the user in
|
// Human-readable name of the plugin that is shown to the user in
|
||||||
// the UI.
|
// the UI.
|
||||||
|
@ -66,6 +66,7 @@ import * as tablePanel from 'app/plugins/panel/table/module';
|
|||||||
import * as textPanel from 'app/plugins/panel/text/module';
|
import * as textPanel from 'app/plugins/panel/text/module';
|
||||||
import * as timeseriesPanel from 'app/plugins/panel/timeseries/module';
|
import * as timeseriesPanel from 'app/plugins/panel/timeseries/module';
|
||||||
import * as tracesPanel from 'app/plugins/panel/traces/module';
|
import * as tracesPanel from 'app/plugins/panel/traces/module';
|
||||||
|
import * as trendPanel from 'app/plugins/panel/trend/module';
|
||||||
import * as welcomeBanner from 'app/plugins/panel/welcome/module';
|
import * as welcomeBanner from 'app/plugins/panel/welcome/module';
|
||||||
import * as xyChartPanel from 'app/plugins/panel/xychart/module';
|
import * as xyChartPanel from 'app/plugins/panel/xychart/module';
|
||||||
|
|
||||||
@ -106,6 +107,7 @@ const builtInPlugins: any = {
|
|||||||
|
|
||||||
'app/plugins/panel/text/module': textPanel,
|
'app/plugins/panel/text/module': textPanel,
|
||||||
'app/plugins/panel/timeseries/module': timeseriesPanel,
|
'app/plugins/panel/timeseries/module': timeseriesPanel,
|
||||||
|
'app/plugins/panel/trend/module': trendPanel,
|
||||||
'app/plugins/panel/state-timeline/module': stateTimelinePanel,
|
'app/plugins/panel/state-timeline/module': stateTimelinePanel,
|
||||||
'app/plugins/panel/status-history/module': statusHistoryPanel,
|
'app/plugins/panel/status-history/module': statusHistoryPanel,
|
||||||
'app/plugins/panel/candlestick/module': candlestickPanel,
|
'app/plugins/panel/candlestick/module': candlestickPanel,
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import { PanelPlugin } from '@grafana/data';
|
import { PanelPlugin } from '@grafana/data';
|
||||||
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 { TimezonesEditor } from './TimezonesEditor';
|
||||||
import { defaultGraphConfig, getGraphFieldConfig } from './config';
|
import { defaultGraphConfig, getGraphFieldConfig } from './config';
|
||||||
import { graphPanelChangedHandler } from './migrations';
|
import { graphPanelChangedHandler } from './migrations';
|
||||||
import { PanelOptions } from './panelcfg.gen';
|
import { PanelFieldConfig, PanelOptions } from './panelcfg.gen';
|
||||||
import { TimeSeriesSuggestionsSupplier } from './suggestions';
|
import { TimeSeriesSuggestionsSupplier } from './suggestions';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(TimeSeriesPanel)
|
export const plugin = new PanelPlugin<PanelOptions, PanelFieldConfig>(TimeSeriesPanel)
|
||||||
.setPanelChangeHandler(graphPanelChangedHandler)
|
.setPanelChangeHandler(graphPanelChangedHandler)
|
||||||
.useFieldConfig(getGraphFieldConfig(defaultGraphConfig))
|
.useFieldConfig(getGraphFieldConfig(defaultGraphConfig))
|
||||||
.setPanelOptions((builder) => {
|
.setPanelOptions((builder) => {
|
||||||
|
@ -22,12 +22,26 @@ import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue';
|
|||||||
export function prepareGraphableFields(
|
export function prepareGraphableFields(
|
||||||
series: DataFrame[],
|
series: DataFrame[],
|
||||||
theme: GrafanaTheme2,
|
theme: GrafanaTheme2,
|
||||||
timeRange?: TimeRange
|
timeRange?: TimeRange,
|
||||||
|
// numeric X requires a single frame where the first field is numeric
|
||||||
|
xNumFieldIdx?: number
|
||||||
): DataFrame[] | null {
|
): DataFrame[] | null {
|
||||||
if (!series?.length) {
|
if (!series?.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let useNumericX = xNumFieldIdx != null;
|
||||||
|
|
||||||
|
// Make sure the numeric x field is first in the frame
|
||||||
|
if (xNumFieldIdx != null && xNumFieldIdx > 0) {
|
||||||
|
series = [
|
||||||
|
{
|
||||||
|
...series[0],
|
||||||
|
fields: [series[0].fields[xNumFieldIdx], ...series[0].fields.filter((f, i) => i !== xNumFieldIdx)],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// some datasources simply tag the field as time, but don't convert to milli epochs
|
// some datasources simply tag the field as time, but don't convert to milli epochs
|
||||||
// so we're stuck with doing the parsing here to avoid Moment slowness everywhere later
|
// so we're stuck with doing the parsing here to avoid Moment slowness everywhere later
|
||||||
// this mutates (once)
|
// this mutates (once)
|
||||||
@ -49,20 +63,26 @@ export function prepareGraphableFields(
|
|||||||
let hasTimeField = false;
|
let hasTimeField = false;
|
||||||
let hasValueField = false;
|
let hasValueField = false;
|
||||||
|
|
||||||
let nulledFrame = applyNullInsertThreshold({
|
let nulledFrame = useNumericX
|
||||||
frame,
|
? frame
|
||||||
refFieldPseudoMin: timeRange?.from.valueOf(),
|
: applyNullInsertThreshold({
|
||||||
refFieldPseudoMax: timeRange?.to.valueOf(),
|
frame,
|
||||||
});
|
refFieldPseudoMin: timeRange?.from.valueOf(),
|
||||||
|
refFieldPseudoMax: timeRange?.to.valueOf(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const frameFields = nullToValue(nulledFrame).fields;
|
||||||
|
|
||||||
|
for (let fieldIdx = 0; fieldIdx < frameFields?.length ?? 0; fieldIdx++) {
|
||||||
|
const field = frameFields[fieldIdx];
|
||||||
|
|
||||||
for (const field of nullToValue(nulledFrame).fields) {
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case FieldType.time:
|
case FieldType.time:
|
||||||
hasTimeField = true;
|
hasTimeField = true;
|
||||||
fields.push(field);
|
fields.push(field);
|
||||||
break;
|
break;
|
||||||
case FieldType.number:
|
case FieldType.number:
|
||||||
hasValueField = true;
|
hasValueField = useNumericX ? fieldIdx > 0 : true;
|
||||||
copy = {
|
copy = {
|
||||||
...field,
|
...field,
|
||||||
values: new ArrayVector(
|
values: new ArrayVector(
|
||||||
@ -124,7 +144,7 @@ export function prepareGraphableFields(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasTimeField && hasValueField) {
|
if ((useNumericX || hasTimeField) && hasValueField) {
|
||||||
frames.push({
|
frames.push({
|
||||||
...frame,
|
...frame,
|
||||||
length: nulledFrame.length,
|
length: nulledFrame.length,
|
||||||
@ -134,20 +154,19 @@ export function prepareGraphableFields(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (frames.length) {
|
if (frames.length) {
|
||||||
setClassicPaletteIdxs(frames, theme);
|
setClassicPaletteIdxs(frames, theme, 0);
|
||||||
return frames;
|
return frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setClassicPaletteIdxs = (frames: DataFrame[], theme: GrafanaTheme2) => {
|
const setClassicPaletteIdxs = (frames: DataFrame[], theme: GrafanaTheme2, skipFieldIdx?: number) => {
|
||||||
let seriesIndex = 0;
|
let seriesIndex = 0;
|
||||||
|
|
||||||
frames.forEach((frame) => {
|
frames.forEach((frame) => {
|
||||||
frame.fields.forEach((field) => {
|
frame.fields.forEach((field, fieldIdx) => {
|
||||||
// TODO: also add FieldType.enum type here after https://github.com/grafana/grafana/pull/60491
|
// TODO: also add FieldType.enum type here after https://github.com/grafana/grafana/pull/60491
|
||||||
if (field.type === FieldType.number || field.type === FieldType.boolean) {
|
if (fieldIdx !== skipFieldIdx && (field.type === FieldType.number || field.type === FieldType.boolean)) {
|
||||||
field.state = {
|
field.state = {
|
||||||
...field.state,
|
...field.state,
|
||||||
seriesIndex: seriesIndex++, // TODO: skip this for fields with custom renderers (e.g. Candlestick)?
|
seriesIndex: seriesIndex++, // TODO: skip this for fields with custom renderers (e.g. Candlestick)?
|
||||||
|
137
public/app/plugins/panel/trend/TrendPanel.tsx
Normal file
137
public/app/plugins/panel/trend/TrendPanel.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { FieldType, PanelProps } from '@grafana/data';
|
||||||
|
import { config, PanelDataErrorView } from '@grafana/runtime';
|
||||||
|
import { KeyboardPlugin, TimeSeries, TooltipDisplayMode, TooltipPlugin, usePanelContext } from '@grafana/ui';
|
||||||
|
import { findFieldIndex } from 'app/features/dimensions';
|
||||||
|
|
||||||
|
import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin';
|
||||||
|
import { prepareGraphableFields, regenerateLinksSupplier } from '../timeseries/utils';
|
||||||
|
|
||||||
|
import { PanelOptions } from './panelcfg.gen';
|
||||||
|
|
||||||
|
export const TrendPanel = ({
|
||||||
|
data,
|
||||||
|
timeRange,
|
||||||
|
timeZone,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
options,
|
||||||
|
fieldConfig,
|
||||||
|
replaceVariables,
|
||||||
|
id,
|
||||||
|
}: PanelProps<PanelOptions>) => {
|
||||||
|
const { sync } = usePanelContext();
|
||||||
|
|
||||||
|
const info = useMemo(() => {
|
||||||
|
if (data.series.length > 1) {
|
||||||
|
return {
|
||||||
|
warning: 'Only one frame is supported, consider adding a join transformation',
|
||||||
|
frames: data.series,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let frames = data.series;
|
||||||
|
let xFieldIdx: number | undefined;
|
||||||
|
if (options.xField) {
|
||||||
|
xFieldIdx = findFieldIndex(frames[0], options.xField);
|
||||||
|
if (xFieldIdx == null) {
|
||||||
|
return {
|
||||||
|
warning: 'Unable to find field: ' + options.xField,
|
||||||
|
frames: data.series,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// first number field
|
||||||
|
// Perhaps we can/should support any ordinal rather than an error here
|
||||||
|
xFieldIdx = frames[0].fields.findIndex((f) => f.type === FieldType.number);
|
||||||
|
if (xFieldIdx === -1) {
|
||||||
|
return {
|
||||||
|
warning: 'No numeric fields found for X axis',
|
||||||
|
frames,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure values are ascending
|
||||||
|
if (xFieldIdx != null) {
|
||||||
|
const field = frames[0].fields[xFieldIdx];
|
||||||
|
if (field.type === FieldType.number) {
|
||||||
|
// we may support ordinal soon
|
||||||
|
let last = Number.NEGATIVE_INFINITY;
|
||||||
|
const values = field.values.toArray();
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
const v = values[i];
|
||||||
|
if (last > v) {
|
||||||
|
return {
|
||||||
|
warning: `Values must be in ascending order (index: ${i}, ${last} > ${v})`,
|
||||||
|
frames,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
last = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { frames: prepareGraphableFields(frames, config.theme2, undefined, xFieldIdx) };
|
||||||
|
}, [data, options.xField]);
|
||||||
|
|
||||||
|
if (info.warning || !info.frames) {
|
||||||
|
return (
|
||||||
|
<PanelDataErrorView
|
||||||
|
panelId={id}
|
||||||
|
fieldConfig={fieldConfig}
|
||||||
|
data={data}
|
||||||
|
message={info.warning}
|
||||||
|
needsNumberField={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimeSeries // Name change!
|
||||||
|
frames={info.frames}
|
||||||
|
structureRev={data.structureRev}
|
||||||
|
timeRange={timeRange}
|
||||||
|
timeZone={timeZone}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
legend={options.legend}
|
||||||
|
options={options}
|
||||||
|
>
|
||||||
|
{(config, alignedDataFrame) => {
|
||||||
|
if (
|
||||||
|
alignedDataFrame.fields.filter((f) => f.config.links !== undefined && f.config.links.length > 0).length > 0
|
||||||
|
) {
|
||||||
|
alignedDataFrame = regenerateLinksSupplier(alignedDataFrame, info.frames!, replaceVariables, timeZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<KeyboardPlugin config={config} />
|
||||||
|
{options.tooltip.mode === TooltipDisplayMode.None || (
|
||||||
|
<TooltipPlugin
|
||||||
|
frames={info.frames!}
|
||||||
|
data={alignedDataFrame}
|
||||||
|
config={config}
|
||||||
|
mode={options.tooltip.mode}
|
||||||
|
sortOrder={options.tooltip.sort}
|
||||||
|
sync={sync}
|
||||||
|
timeZone={timeZone}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ContextMenuPlugin
|
||||||
|
data={alignedDataFrame}
|
||||||
|
frames={info.frames!}
|
||||||
|
config={config}
|
||||||
|
timeZone={timeZone}
|
||||||
|
replaceVariables={replaceVariables}
|
||||||
|
defaultItems={[]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</TimeSeries>
|
||||||
|
);
|
||||||
|
};
|
9
public/app/plugins/panel/trend/img/trend.svg
Normal file
9
public/app/plugins/panel/trend/img/trend.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 82.48 77.59">
|
||||||
|
<defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}.cls-3{fill:#84aff1;}</style>
|
||||||
|
<linearGradient id="linear-gradient" y1="12" x2="82.48" y2="12" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient><linearGradient id="linear-gradient-2" x1="41.14" y1="77.98" x2="41.91" y2="13.73" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1f60c4" stop-opacity="0"/><stop offset="1" stop-color="#3865ab"/></linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g id="Icons">
|
||||||
|
<path class="cls-1" d="M33.26,24a2,2,0,0,1-.83-.18l-14.7-6.66-15,5.71a2,2,0,1,1-1.42-3.74l15.8-6a2,2,0,0,1,1.53,0l14.16,6.41,15-16A2,2,0,0,1,50,3.16L65.15,9.73,79.38.33a2,2,0,1,1,2.2,3.34l-15.13,10a2,2,0,0,1-1.9.16L49.72,7.39l-15,16A2,2,0,0,1,33.26,24Z"/>
|
||||||
|
<path class="cls-3" d="M2.11,45.72H2A2,2,0,0,1,.73,42.18l15.8-13a2,2,0,0,1,1.65-.42l14.59,2.83L48,20.12a2,2,0,0,1,2.4,0l14.7,10.94L79,16.35a2,2,0,1,1,2.9,2.75L66.8,35.1a2,2,0,0,1-2.64.23L49.23,24.22,34.46,35.32a2,2,0,0,1-1.58.37L18.34,32.87l-14.6,12A2,2,0,0,1,2.11,45.72Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
30
public/app/plugins/panel/trend/module.tsx
Normal file
30
public/app/plugins/panel/trend/module.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { PanelPlugin } from '@grafana/data';
|
||||||
|
import { commonOptionsBuilder } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { defaultGraphConfig, getGraphFieldConfig } from '../timeseries/config';
|
||||||
|
|
||||||
|
import { TrendPanel } from './TrendPanel';
|
||||||
|
import { PanelFieldConfig, PanelOptions } from './panelcfg.gen';
|
||||||
|
import { TrendSuggestionsSupplier } from './suggestions';
|
||||||
|
|
||||||
|
export const plugin = new PanelPlugin<PanelOptions, PanelFieldConfig>(TrendPanel)
|
||||||
|
.useFieldConfig(getGraphFieldConfig(defaultGraphConfig))
|
||||||
|
.setPanelOptions((builder) => {
|
||||||
|
const category = ['X Axis'];
|
||||||
|
builder.addFieldNamePicker({
|
||||||
|
path: 'xField',
|
||||||
|
name: 'X Field',
|
||||||
|
description: 'An increasing numeric value',
|
||||||
|
category,
|
||||||
|
defaultValue: undefined,
|
||||||
|
settings: {
|
||||||
|
isClearable: true,
|
||||||
|
placeholderText: 'First numeric value',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
commonOptionsBuilder.addTooltipOptions(builder);
|
||||||
|
commonOptionsBuilder.addLegendOptions(builder);
|
||||||
|
})
|
||||||
|
.setSuggestionsSupplier(new TrendSuggestionsSupplier());
|
||||||
|
//.setDataSupport({ annotations: true, alertStates: true });
|
42
public/app/plugins/panel/trend/panelcfg.cue
Normal file
42
public/app/plugins/panel/trend/panelcfg.cue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// Copyright 2021 Grafana Labs
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package grafanaplugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
composableKinds: PanelCfg: {
|
||||||
|
lineage: {
|
||||||
|
seqs: [
|
||||||
|
{
|
||||||
|
schemas: [
|
||||||
|
{
|
||||||
|
// Identical to timeseries... except it does not have timezone settings
|
||||||
|
PanelOptions: {
|
||||||
|
legend: common.VizLegendOptions
|
||||||
|
tooltip: common.VizTooltipOptions
|
||||||
|
|
||||||
|
// Name of the x field to use (defaults to first number)
|
||||||
|
xField?: string
|
||||||
|
} @cuetsy(kind="interface")
|
||||||
|
|
||||||
|
PanelFieldConfig: common.GraphFieldConfig & {} @cuetsy(kind="interface")
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
27
public/app/plugins/panel/trend/panelcfg.gen.ts
Normal file
27
public/app/plugins/panel/trend/panelcfg.gen.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// Generated by:
|
||||||
|
// public/app/plugins/gen.go
|
||||||
|
// Using jennies:
|
||||||
|
// TSTypesJenny
|
||||||
|
// PluginTSTypesJenny
|
||||||
|
//
|
||||||
|
// Run 'make gen-cue' from repository root to regenerate.
|
||||||
|
|
||||||
|
import * as common from '@grafana/schema';
|
||||||
|
|
||||||
|
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identical to timeseries... except it does not have timezone settings
|
||||||
|
*/
|
||||||
|
export interface PanelOptions {
|
||||||
|
legend: common.VizLegendOptions;
|
||||||
|
tooltip: common.VizTooltipOptions;
|
||||||
|
/**
|
||||||
|
* Name of the x field to use (defaults to first number)
|
||||||
|
*/
|
||||||
|
xField?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PanelFieldConfig extends common.GraphFieldConfig {}
|
19
public/app/plugins/panel/trend/plugin.json
Normal file
19
public/app/plugins/panel/trend/plugin.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"type": "panel",
|
||||||
|
"name": "Trend",
|
||||||
|
"id": "trend",
|
||||||
|
|
||||||
|
"state": "alpha",
|
||||||
|
|
||||||
|
"info": {
|
||||||
|
"description": "Like timeseries, but when x != time",
|
||||||
|
"author": {
|
||||||
|
"name": "Grafana Labs",
|
||||||
|
"url": "https://grafana.com"
|
||||||
|
},
|
||||||
|
"logos": {
|
||||||
|
"small": "img/trend.svg",
|
||||||
|
"large": "img/trend.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
public/app/plugins/panel/trend/suggestions.ts
Normal file
40
public/app/plugins/panel/trend/suggestions.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { VisualizationSuggestionsBuilder } from '@grafana/data';
|
||||||
|
import { GraphDrawStyle, GraphFieldConfig } from '@grafana/schema';
|
||||||
|
import { SuggestionName } from 'app/types/suggestions';
|
||||||
|
|
||||||
|
import { PanelOptions } from './panelcfg.gen';
|
||||||
|
|
||||||
|
export class TrendSuggestionsSupplier {
|
||||||
|
getSuggestionsForData(builder: VisualizationSuggestionsBuilder) {
|
||||||
|
const { dataSummary } = builder;
|
||||||
|
|
||||||
|
if (dataSummary.numberFieldCount < 2 || dataSummary.rowCountTotal < 2 || dataSummary.rowCountTotal < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Super basic
|
||||||
|
const list = builder.getListAppender<PanelOptions, GraphFieldConfig>({
|
||||||
|
name: SuggestionName.LineChart,
|
||||||
|
pluginId: 'trend',
|
||||||
|
options: {
|
||||||
|
legend: {} as any,
|
||||||
|
},
|
||||||
|
fieldConfig: {
|
||||||
|
defaults: {
|
||||||
|
custom: {},
|
||||||
|
},
|
||||||
|
overrides: [],
|
||||||
|
},
|
||||||
|
cardOptions: {
|
||||||
|
previewModifier: (s) => {
|
||||||
|
s.options!.legend.showLegend = false;
|
||||||
|
|
||||||
|
if (s.fieldConfig?.defaults.custom?.drawStyle !== GraphDrawStyle.Bars) {
|
||||||
|
s.fieldConfig!.defaults.custom!.lineWidth = Math.max(s.fieldConfig!.defaults.custom!.lineWidth ?? 1, 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user