import React, { PureComponent } from 'react'; import { Unsubscribable, PartialObserver } from 'rxjs'; import { FeatureInfoBox, stylesFactory, Button, JSONFormatter, CustomScrollbar, CodeEditor } from '@grafana/ui'; import { GrafanaTheme, PanelProps, LiveChannelStatusEvent, isValidLiveChannelAddress, LiveChannel, LiveChannelEvent, isLiveChannelStatusEvent, isLiveChannelMessageEvent, LiveChannelConnectionState, PanelData, LoadingState, applyFieldOverrides, } from '@grafana/data'; import { TablePanel } from '../table/TablePanel'; import { LivePanelOptions, MessageDisplayMode } from './types'; import { config, getGrafanaLiveSrv, MeasurementCollector } from '@grafana/runtime'; import { css, cx } from 'emotion'; interface Props extends PanelProps {} interface State { error?: any; channel?: LiveChannel; status?: LiveChannelStatusEvent; message?: any; changed: number; } export class LivePanel extends PureComponent { private readonly isValid: boolean; subscription?: Unsubscribable; styles = getStyles(config.theme); constructor(props: Props) { super(props); this.isValid = !!getGrafanaLiveSrv(); this.state = { changed: 0 }; } async componentDidMount() { this.loadChannel(); } componentWillUnmount() { if (this.subscription) { this.subscription.unsubscribe(); } } componentDidUpdate(prevProps: Props): void { if (this.props.options?.channel !== prevProps.options?.channel) { this.loadChannel(); } } streamObserver: PartialObserver = { next: (event: LiveChannelEvent) => { if (isLiveChannelStatusEvent(event)) { this.setState({ status: event, changed: Date.now() }); } else if (isLiveChannelMessageEvent(event)) { this.setState({ message: event.message, changed: Date.now() }); } else { console.log('ignore', event); } }, }; unsubscribe = () => { if (this.subscription) { this.subscription.unsubscribe(); this.subscription = undefined; } }; async loadChannel() { const addr = this.props.options?.channel; if (!isValidLiveChannelAddress(addr)) { console.log('INVALID', addr); this.unsubscribe(); this.setState({ channel: undefined, }); return; } const channel = getGrafanaLiveSrv().getChannel(addr); const changed = channel.id !== this.state.channel?.id; console.log('LOAD', addr, changed, channel); if (changed) { this.unsubscribe(); // Subscribe to new events try { this.subscription = channel.getStream().subscribe(this.streamObserver); this.setState({ channel, error: undefined }); } catch (err) { this.setState({ channel: undefined, error: err }); } } else { console.log('Same channel', channel); } } renderNotEnabled() { const preformatted = `[feature_toggles] enable = live`; return (

Grafana live requires a feature flag to run

custom.ini:
{preformatted}
); } 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 { channel } = this.state; if (!channel?.publish) { console.log('channel does not support publishing'); return; } const json = this.props.options?.json; if (json) { const rsp = await channel.publish(json); console.log('GOT', rsp); } else { console.log('nothing to publish'); } }; renderMessage(height: number) { const { options } = this.props; const { message } = this.state; if (!message) { return (

Waiting for data:

{options.channel?.scope}/{options.channel?.namespace}/{options.channel?.path}
); } if (options.message === MessageDisplayMode.JSON) { return ; } if (options.message === MessageDisplayMode.Auto) { if (message instanceof MeasurementCollector) { const data: PanelData = { series: applyFieldOverrides({ data: message.getData(), theme: config.theme, replaceVariables: (v: string) => v, fieldConfig: { defaults: {}, overrides: [], }, }), state: LoadingState.Streaming, } as PanelData; const props = { ...this.props, options: { frameIndex: 0, showHeader: true }, } as PanelProps; return ; } } return
{JSON.stringify(message)}
; } renderPublish(height: number) { const { channel } = this.state; if (!channel?.publish) { return
This channel does not support publishing
; } const { options } = this.props; return ( <>
); } renderStatus() { const { status } = this.state; if (status?.state === LiveChannelConnectionState.Connected) { return; // nothing } let statusClass = ''; if (status) { statusClass = this.styles.status[status.state]; } return
{status?.state}
; } renderBody() { const { status } = this.state; const { options, height } = this.props; if (options.publish) { // Only the publish form if (options.message === MessageDisplayMode.None) { return
{this.renderPublish(height)}
; } // Both message and publish const halfHeight = height / 2; return (
{this.renderMessage(halfHeight)}
{this.renderPublish(halfHeight)}
); } if (options.message === MessageDisplayMode.None) { return
{JSON.stringify(status)}
; } // Only message return (
{this.renderMessage(height)}
); } render() { if (!this.isValid) { return this.renderNotEnabled(); } const { channel, error } = this.state; if (!channel) { return (

Use the panel editor to pick a channel

); } if (error) { return (

ERROR

{JSON.stringify(error)}
); } return ( <> {this.renderStatus()} {this.renderBody()} ); } } const getStyles = stylesFactory((theme: GrafanaTheme) => ({ statusWrap: css` margin: auto; position: absolute; top: 0; right: 0; background: ${theme.colors.panelBg}; padding: 10px; z-index: ${theme.zIndex.modal}; `, status: { [LiveChannelConnectionState.Pending]: css` border: 1px solid ${theme.palette.brandPrimary}; `, [LiveChannelConnectionState.Connected]: css` border: 1px solid ${theme.palette.brandSuccess}; `, [LiveChannelConnectionState.Disconnected]: css` border: 1px solid ${theme.palette.brandWarning}; `, [LiveChannelConnectionState.Shutdown]: css` border: 1px solid ${theme.palette.brandDanger}; `, [LiveChannelConnectionState.Invalid]: css` border: 1px solid red; `, }, }));