mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Live: Improve the debug panel and add a devenv dashbaord (#83350)
This commit is contained in:
@@ -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"]
|
||||
],
|
||||
|
447
devenv/dev-dashboards/live/live-publish.json
Normal file
447
devenv/dev-dashboards/live/live-publish.json
Normal file
@@ -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'),
|
||||
|
44
public/app/features/live/info.ts
Normal file
44
public/app/features/live/info.ts
Normal file
@@ -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> = {};
|
||||
|
||||
async updateSelectOptions() {
|
||||
this.setState({
|
||||
namespaces: [],
|
||||
paths: [],
|
||||
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]);
|
||||
|
||||
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={this.onScopeChanged} />
|
||||
</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>
|
||||
<div className={style.dropWrap}>
|
||||
<Label>Scope</Label>
|
||||
<Select options={scopes} value={scopes.find((s) => s.value === scope)} onChange={onScopeChanged} />
|
||||
</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={onNamespaceChanged}
|
||||
allowCustomValue={true}
|
||||
backspaceRemovesValue={true}
|
||||
isClearable={true}
|
||||
/>
|
||||
</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>;
|
||||
}
|
||||
|
||||
|
67
public/app/plugins/panel/live/LivePublish.tsx
Normal file
67
public/app/plugins/panel/live/LivePublish.tsx
Normal file
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user