Live: Improve the debug panel and add a devenv dashbaord (#83350)

pull/83417/head
Ryan McKinley 1 year ago committed by GitHub
parent 715ea44466
commit d122af6b97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 14
      .betterer.results
  2. 447
      devenv/dev-dashboards/live/live-publish.json
  3. 1
      devenv/jsonnet/dev-dashboards.libsonnet
  4. 44
      public/app/features/live/info.ts
  5. 36
      public/app/plugins/datasource/grafana/components/QueryEditor.tsx
  6. 174
      public/app/plugins/panel/live/LiveChannelEditor.tsx
  7. 69
      public/app/plugins/panel/live/LivePanel.tsx
  8. 67
      public/app/plugins/panel/live/LivePublish.tsx
  9. 17
      public/app/plugins/panel/live/module.tsx
  10. 12
      public/app/plugins/panel/live/types.ts

@ -4864,14 +4864,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/grafana/components/QueryEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"]
[0, 0, 0, "Styles should be written using objects.", "4"]
],
"public/app/plugins/datasource/grafana/components/TimePickerInput.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
@ -5942,9 +5939,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"]
],
"public/app/plugins/panel/live/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/logs/LogsPanel.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],

@ -0,0 +1,447 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 209,
"links": [],
"panels": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"gridPos": {
"h": 2,
"w": 24,
"x": 0,
"y": 0
},
"id": 9,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "## This dashboard requires alpha panels to be enabled!",
"mode": "markdown"
},
"pluginVersion": "11.0.0-pre",
"type": "text"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"gridPos": {
"h": 4,
"w": 15,
"x": 0,
"y": 2
},
"id": 2,
"options": {
"channel": {
"namespace": "devenv",
"path": "weather",
"scope": "stream"
},
"display": "none",
"json": {
"hello": "world"
},
"message": "weather,location=west,sensor=A temperature=82\nweather,location=east,sensor=A temperature=76",
"publish": "influx"
},
"title": "Panel Title",
"type": "live"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 9,
"x": 15,
"y": 2
},
"id": 4,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "11.0.0-pre",
"targets": [
{
"channel": "stream/devenv/weather",
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"queryType": "measurements",
"refId": "A"
}
],
"title": "Weather (values)",
"transformations": [
{
"id": "sortBy",
"options": {
"fields": {},
"sort": [
{
"desc": true,
"field": "time"
}
]
}
}
],
"type": "table"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"gridPos": {
"h": 4,
"w": 15,
"x": 0,
"y": 6
},
"id": 5,
"options": {
"channel": {
"namespace": "devenv",
"path": "weather",
"scope": "stream"
},
"display": "none",
"json": {
"hello": "world"
},
"message": "weather,location=west,sensor=A temperature=90\nweather,location=east,sensor=A temperature=80",
"publish": "influx"
},
"title": "Panel Title",
"type": "live"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 24,
"x": 0,
"y": 10
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"channel": "stream/devenv/weather",
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"queryType": "measurements",
"refId": "A"
},
{
"channel": "stream/devenv/weatherX",
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"hide": false,
"queryType": "measurements",
"refId": "B"
}
],
"title": "Panel Title",
"type": "timeseries"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"gridPos": {
"h": 4,
"w": 15,
"x": 0,
"y": 17
},
"id": 6,
"options": {
"channel": {
"namespace": "devenv",
"path": "weather",
"scope": "stream"
},
"display": "none",
"json": {
"hello": "world"
},
"message": "weatherX,location=west,sensor=X temperature=82\nweatherX,location=east,sensor=X temperature=76",
"publish": "influx"
},
"title": "Panel Title",
"type": "live"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 9,
"x": 15,
"y": 17
},
"id": 7,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "11.0.0-pre",
"targets": [
{
"channel": "stream/devenv/weatherX",
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"queryType": "measurements",
"refId": "A"
}
],
"title": "WeatherX (values)",
"transformations": [
{
"id": "sortBy",
"options": {
"fields": {},
"sort": [
{
"desc": true,
"field": "time"
}
]
}
}
],
"type": "table"
},
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"gridPos": {
"h": 4,
"w": 15,
"x": 0,
"y": 21
},
"id": 8,
"options": {
"channel": {
"namespace": "devenv",
"path": "weather",
"scope": "stream"
},
"display": "none",
"json": {
"hello": "world"
},
"message": "weatherX,location=west,sensor=X temperature=90\nweatherX,location=east,sensor=X temperature=22",
"publish": "influx"
},
"title": "Panel Title",
"type": "live"
}
],
"schemaVersion": 39,
"tags": [
"gdev",
"live-tests"
],
"templating": {
"list": []
},
"time": {
"from": "now-1m",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "live test",
"uid": "addoomtlivedev",
"version": 17,
"weekStart": ""
}

@ -58,6 +58,7 @@
"join-by-labels": (import '../dev-dashboards/transforms/join-by-labels.json'),
"lazy_loading": (import '../dev-dashboards/panel-common/lazy_loading.json'),
"linked-viz": (import '../dev-dashboards/panel-common/linked-viz.json'),
"live-publish": (import '../dev-dashboards/live/live-publish.json'),
"loki_fakedata": (import '../dev-dashboards/datasource-loki/loki_fakedata.json'),
"loki_query_splitting": (import '../dev-dashboards/datasource-loki/loki_query_splitting.json'),
"migrations": (import '../dev-dashboards/migrations/migrations.json'),

@ -0,0 +1,44 @@
import { SelectableValue, dataFrameFromJSON } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
interface ChannelInfo {
channel: string;
minute_rate: number; //
data: unknown; // the last payload
}
interface ManagedChannels {
channels: ChannelInfo[];
}
interface ChannelSelectionInfo {
channels: Array<SelectableValue<string>>;
channelFields: Record<string, Array<SelectableValue<string>>>;
}
export async function getManagedChannelInfo(): Promise<ChannelSelectionInfo> {
return getBackendSrv()
.get<ManagedChannels>('api/live/list')
.then((v) => {
const channelInfo = v.channels ?? [];
const channelFields: Record<string, Array<SelectableValue<string>>> = {};
const channels: Array<SelectableValue<string>> = channelInfo.map((c) => {
if (c.data) {
const distinctFields = new Set<string>();
const frame = dataFrameFromJSON(c.data);
for (const f of frame.fields) {
distinctFields.add(f.name);
}
channelFields[c.channel] = Array.from(distinctFields).map((n) => ({
value: n,
label: n,
}));
}
return {
value: c.channel,
label: c.channel + ' [' + c.minute_rate + ' msg/min]',
};
});
return { channelFields, channels };
});
}

@ -6,7 +6,6 @@ import { DropEvent, FileRejection } from 'react-dropzone';
import {
QueryEditorProps,
SelectableValue,
dataFrameFromJSON,
rangeUtil,
DataQueryRequest,
DataFrame,
@ -16,7 +15,7 @@ import {
getValueFormat,
formattedValueToString,
} from '@grafana/data';
import { config, getBackendSrv, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import {
InlineField,
Select,
@ -33,6 +32,7 @@ import {
} from '@grafana/ui';
import { hasAlphaPanels } from 'app/core/config';
import * as DFImport from 'app/features/dataframe-import';
import { getManagedChannelInfo } from 'app/features/live/info';
import { SearchQuery } from 'app/features/search/service';
import { GrafanaDatasource } from '../datasource';
@ -91,35 +91,9 @@ export class UnthemedQueryEditor extends PureComponent<Props, State> {
}
loadChannelInfo() {
getBackendSrv()
.fetch({ url: 'api/live/list' })
.subscribe({
next: (v: any) => {
const channelInfo = v.data?.channels as any[];
if (channelInfo?.length) {
const channelFields: Record<string, Array<SelectableValue<string>>> = {};
const channels: Array<SelectableValue<string>> = channelInfo.map((c) => {
if (c.data) {
const distinctFields = new Set<string>();
const frame = dataFrameFromJSON(c.data);
for (const f of frame.fields) {
distinctFields.add(f.name);
}
channelFields[c.channel] = Array.from(distinctFields).map((n) => ({
value: n,
label: n,
}));
}
return {
value: c.channel,
label: c.channel + ' [' + c.minute_rate + ' msg/min]',
};
});
this.setState({ channelFields, channels });
}
},
});
getManagedChannelInfo().then((v) => {
this.setState(v);
});
}
loadFolderInfo() {

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import React, { PureComponent } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
LiveChannelScope,
@ -7,9 +7,11 @@ import {
SelectableValue,
StandardEditorProps,
GrafanaTheme2,
parseLiveChannelAddress,
} from '@grafana/data';
import { Select, Alert, Label, stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config';
import { getManagedChannelInfo } from 'app/features/live/info';
import { LivePanelOptions } from './types';
@ -19,39 +21,54 @@ const scopes: Array<SelectableValue<LiveChannelScope>> = [
{ label: 'Grafana', value: LiveChannelScope.Grafana, description: 'Core grafana live features' },
{ label: 'Data Sources', value: LiveChannelScope.DataSource, description: 'Data sources with live support' },
{ label: 'Plugins', value: LiveChannelScope.Plugin, description: 'Plugins with live support' },
{ label: 'Stream', value: LiveChannelScope.Stream, description: 'data streams (eg, influx style)' },
];
interface State {
namespaces: Array<SelectableValue<string>>;
paths: Array<SelectableValue<string>>;
}
export class LiveChannelEditor extends PureComponent<Props, State> {
state: State = {
namespaces: [],
paths: [],
};
async componentDidMount() {
this.updateSelectOptions();
}
async componentDidUpdate(oldProps: Props) {
if (this.props.value !== oldProps.value) {
this.updateSelectOptions();
export function LiveChannelEditor(props: Props) {
const [channels, setChannels] = useState<Array<SelectableValue<string>>>([]);
const [namespaces, paths] = useMemo(() => {
const namespaces: Array<SelectableValue<string>> = [];
const paths: Array<SelectableValue<string>> = [];
const scope = props.value.scope;
const namespace = props.value.namespace;
if (!scope?.length) {
return [namespaces, paths];
}
}
const used: Record<string, boolean> = {};
for (let channel of channels) {
const addr = parseLiveChannelAddress(channel.value);
if (!addr || addr.scope !== scope) {
continue;
}
if (!used[addr.namespace]) {
namespaces.push({
value: addr.namespace,
label: addr.namespace,
});
used[addr.namespace] = true;
}
if (namespace?.length && namespace === addr.namespace) {
paths.push({
...channel,
value: addr.path,
});
}
}
return [namespaces, paths];
}, [channels, props.value.scope, props.value.namespace]);
async updateSelectOptions() {
this.setState({
namespaces: [],
paths: [],
useEffect(() => {
getManagedChannelInfo().then((v) => {
setChannels(v.channels);
});
}
}, [props.value.scope]);
onScopeChanged = (v: SelectableValue<LiveChannelScope>) => {
const onScopeChanged = (v: SelectableValue<LiveChannelScope>) => {
if (v.value) {
this.props.onChange({
props.onChange({
scope: v.value,
namespace: undefined,
path: undefined,
@ -59,73 +76,72 @@ export class LiveChannelEditor extends PureComponent<Props, State> {
}
};
onNamespaceChanged = (v: SelectableValue<string>) => {
this.props.onChange({
scope: this.props.value?.scope,
namespace: v.value,
const onNamespaceChanged = (v: SelectableValue<string>) => {
props.onChange({
scope: props.value?.scope,
namespace: v?.value,
path: undefined,
});
};
onPathChanged = (v: SelectableValue<string>) => {
const { value, onChange } = this.props;
const onPathChanged = (v: SelectableValue<string>) => {
const { value, onChange } = props;
onChange({
scope: value.scope,
namespace: value.namespace,
path: v.value,
path: v?.value,
});
};
render() {
const { namespaces, paths } = this.state;
const { scope, namespace, path } = this.props.value;
const style = getStyles(config.theme2);
const { scope, namespace, path } = props.value;
const style = getStyles(config.theme2);
return (
<>
<Alert title="Grafana Live" severity="info">
This supports real-time event streams in grafana core. This feature is under heavy development. Expect the
intefaces and structures to change as this becomes more production ready.
</Alert>
return (
<>
<Alert title="Grafana Live" severity="info">
This supports real-time event streams in grafana core. This feature is under heavy development. Expect the
intefaces and structures to change as this becomes more production ready.
</Alert>
<div>
<div className={style.dropWrap}>
<Label>Scope</Label>
<Select options={scopes} value={scopes.find((s) => s.value === scope)} onChange={onScopeChanged} />
</div>
<div>
{scope && (
<div className={style.dropWrap}>
<Label>Scope</Label>
<Select options={scopes} value={scopes.find((s) => s.value === scope)} onChange={this.onScopeChanged} />
<Label>Namespace</Label>
<Select
options={namespaces}
value={
namespaces.find((s) => s.value === namespace) ??
(namespace ? { label: namespace, value: namespace } : undefined)
}
onChange={onNamespaceChanged}
allowCustomValue={true}
backspaceRemovesValue={true}
isClearable={true}
/>
</div>
)}
{scope && (
<div className={style.dropWrap}>
<Label>Namespace</Label>
<Select
options={namespaces}
value={
namespaces.find((s) => s.value === namespace) ??
(namespace ? { label: namespace, value: namespace } : undefined)
}
onChange={this.onNamespaceChanged}
allowCustomValue={true}
backspaceRemovesValue={true}
/>
</div>
)}
{scope && namespace && (
<div className={style.dropWrap}>
<Label>Path</Label>
<Select
options={paths}
value={findPathOption(paths, path)}
onChange={this.onPathChanged}
allowCustomValue={true}
backspaceRemovesValue={true}
/>
</div>
)}
</div>
</>
);
}
{scope && namespace && (
<div className={style.dropWrap}>
<Label>Path</Label>
<Select
options={paths}
value={findPathOption(paths, path)}
onChange={onPathChanged}
allowCustomValue={true}
backspaceRemovesValue={true}
isClearable={true}
/>
</div>
)}
</div>
</>
);
}
function findPathOption(paths: Array<SelectableValue<string>>, path?: string): SelectableValue<string> | undefined {

@ -19,11 +19,12 @@ import {
StreamingDataFrame,
} from '@grafana/data';
import { config, getGrafanaLiveSrv } from '@grafana/runtime';
import { Alert, stylesFactory, Button, JSONFormatter, CustomScrollbar, CodeEditor } from '@grafana/ui';
import { Alert, stylesFactory, JSONFormatter, CustomScrollbar } from '@grafana/ui';
import { TablePanel } from '../table/TablePanel';
import { LivePanelOptions, MessageDisplayMode } from './types';
import { LivePublish } from './LivePublish';
import { LivePanelOptions, MessageDisplayMode, MessagePublishMode } from './types';
interface Props extends PanelProps<LivePanelOptions> {}
@ -133,34 +134,6 @@ export class LivePanel extends PureComponent<Props, State> {
);
}
onSaveJSON = (text: string) => {
const { options, onOptionsChange } = this.props;
try {
const json = JSON.parse(text);
onOptionsChange({ ...options, json });
} catch (err) {
console.log('Error reading JSON', err);
}
};
onPublishClicked = async () => {
const { addr } = this.state;
if (!addr) {
console.log('invalid address');
return;
}
const data = this.props.options?.json;
if (!data) {
console.log('nothing to publish');
return;
}
const rsp = await getGrafanaLiveSrv().publish(addr, data);
console.log('onPublishClicked (response from publish)', rsp);
};
renderMessage(height: number) {
const { options } = this.props;
const { message } = this.state;
@ -174,11 +147,11 @@ export class LivePanel extends PureComponent<Props, State> {
);
}
if (options.message === MessageDisplayMode.JSON) {
if (options.display === MessageDisplayMode.JSON) {
return <JSONFormatter json={message} open={5} />;
}
if (options.message === MessageDisplayMode.Auto) {
if (options.display === MessageDisplayMode.Auto) {
if (message instanceof StreamingDataFrame) {
const data: PanelData = {
series: applyFieldOverrides({
@ -206,20 +179,13 @@ export class LivePanel extends PureComponent<Props, State> {
renderPublish(height: number) {
const { options } = this.props;
return (
<>
<CodeEditor
height={height - 32}
language="json"
value={options.json ? JSON.stringify(options.json, null, 2) : '{ }'}
onBlur={this.onSaveJSON}
onSave={this.onSaveJSON}
showMiniMap={false}
showLineNumbers={true}
/>
<div style={{ height: 32 }}>
<Button onClick={this.onPublishClicked}>Publish</Button>
</div>
</>
<LivePublish
height={height}
body={options.message}
mode={options.publish ?? MessagePublishMode.JSON}
onSave={(message) => this.props.onOptionsChange({ ...options, message })}
addr={this.state.addr}
/>
);
}
@ -239,12 +205,13 @@ export class LivePanel extends PureComponent<Props, State> {
renderBody() {
const { status } = this.state;
const { options, height } = this.props;
const publish = options.publish === MessagePublishMode.JSON || options.publish === MessagePublishMode.Influx;
if (options.publish) {
// Only the publish form
if (options.message === MessageDisplayMode.None) {
return <div>{this.renderPublish(height)}</div>;
if (publish) {
if (options.display === MessageDisplayMode.None) {
return this.renderPublish(height);
}
// Both message and publish
const halfHeight = height / 2;
return (
@ -258,7 +225,7 @@ export class LivePanel extends PureComponent<Props, State> {
</div>
);
}
if (options.message === MessageDisplayMode.None) {
if (options.display === MessageDisplayMode.None) {
return <pre>{JSON.stringify(status)}</pre>;
}

@ -0,0 +1,67 @@
import React, { useMemo } from 'react';
import { LiveChannelAddress, isValidLiveChannelAddress } from '@grafana/data';
import { getBackendSrv, getGrafanaLiveSrv } from '@grafana/runtime';
import { CodeEditor, Button } from '@grafana/ui';
import { MessagePublishMode } from './types';
interface Props {
height: number;
addr?: LiveChannelAddress;
mode: MessagePublishMode;
body?: string | object;
onSave: (v: string | object) => void;
}
export function LivePublish({ height, mode, body, addr, onSave }: Props) {
const txt = useMemo(() => {
if (mode === MessagePublishMode.JSON) {
return body ? JSON.stringify(body, null, 2) : '{ }';
}
return body == null ? '' : `${body}`;
}, [mode, body]);
const doSave = (v: string) => {
if (mode === MessagePublishMode.JSON) {
onSave(JSON.parse(v));
} else {
onSave(v);
}
};
const onPublishClicked = async () => {
if (mode === MessagePublishMode.Influx) {
if (addr?.scope !== 'stream') {
alert('expected stream scope!');
return;
}
return getBackendSrv().post(`api/live/push/${addr.namespace}`, body);
}
if (!isValidLiveChannelAddress(addr)) {
alert('invalid address');
return;
}
const rsp = await getGrafanaLiveSrv().publish(addr, body);
console.log('onPublishClicked (response from publish)', rsp);
};
return (
<>
<CodeEditor
height={height - 32}
language={mode === MessagePublishMode.JSON ? 'json' : 'text'}
value={txt}
onBlur={doSave}
onSave={doSave}
showMiniMap={false}
showLineNumbers={true}
/>
<div style={{ height: 32 }}>
<Button onClick={onPublishClicked}>Publish</Button>
</div>
</>
);
}

@ -2,7 +2,7 @@ import { PanelPlugin } from '@grafana/data';
import { LiveChannelEditor } from './LiveChannelEditor';
import { LivePanel } from './LivePanel';
import { LivePanelOptions, MessageDisplayMode } from './types';
import { LivePanelOptions, MessageDisplayMode, MessagePublishMode } from './types';
export const plugin = new PanelPlugin<LivePanelOptions>(LivePanel).setPanelOptions((builder) => {
builder.addCustomEditor({
@ -16,7 +16,7 @@ export const plugin = new PanelPlugin<LivePanelOptions>(LivePanel).setPanelOptio
builder
.addRadio({
path: 'message',
path: 'display',
name: 'Show Message',
description: 'Display the last message received on this channel',
settings: {
@ -29,10 +29,17 @@ export const plugin = new PanelPlugin<LivePanelOptions>(LivePanel).setPanelOptio
},
defaultValue: MessageDisplayMode.JSON,
})
.addBooleanSwitch({
.addRadio({
path: 'publish',
name: 'Show Publish',
name: 'Publish',
description: 'Display a form to publish values',
defaultValue: false,
settings: {
options: [
{ value: MessagePublishMode.None, label: 'None' },
{ value: MessagePublishMode.JSON, label: 'JSON' },
{ value: MessagePublishMode.Influx, label: 'Influx' },
],
},
defaultValue: MessagePublishMode.None,
});
});

@ -7,9 +7,15 @@ export enum MessageDisplayMode {
None = 'none', // do not display
}
export enum MessagePublishMode {
None = 'none', // do not display
JSON = 'json', // formatted JSON
Influx = 'influx', // influx line protocol
}
export interface LivePanelOptions {
channel?: LiveChannelAddress;
message?: MessageDisplayMode;
publish?: boolean;
json?: any; // object
display?: MessageDisplayMode;
publish?: MessagePublishMode;
message?: string | object; // likely JSON
}

Loading…
Cancel
Save