123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- import { css, cx } from '@emotion/css';
- import { isEqual } from 'lodash';
- import React, { PureComponent } from 'react';
- import { Unsubscribable, PartialObserver } from 'rxjs';
- import {
- GrafanaTheme,
- PanelProps,
- LiveChannelStatusEvent,
- isValidLiveChannelAddress,
- LiveChannelEvent,
- isLiveChannelStatusEvent,
- isLiveChannelMessageEvent,
- LiveChannelConnectionState,
- PanelData,
- LoadingState,
- applyFieldOverrides,
- LiveChannelAddress,
- } from '@grafana/data';
- import { config, getGrafanaLiveSrv } from '@grafana/runtime';
- import { Alert, stylesFactory, Button, JSONFormatter, CustomScrollbar, CodeEditor } from '@grafana/ui';
- import { StreamingDataFrame } from 'app/features/live/data/StreamingDataFrame';
- import { TablePanel } from '../table/TablePanel';
- import { LivePanelOptions, MessageDisplayMode } from './types';
- interface Props extends PanelProps<LivePanelOptions> {}
- interface State {
- error?: any;
- addr?: LiveChannelAddress;
- status?: LiveChannelStatusEvent;
- message?: any;
- changed: number;
- }
- export class LivePanel extends PureComponent<Props, State> {
- 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<LiveChannelEvent> = {
- 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({
- addr: undefined,
- });
- return;
- }
- if (isEqual(addr, this.state.addr)) {
- console.log('Same channel', this.state.addr);
- return;
- }
- const live = getGrafanaLiveSrv();
- if (!live) {
- console.log('INVALID', addr);
- this.unsubscribe();
- this.setState({
- addr: undefined,
- });
- return;
- }
- this.unsubscribe();
- console.log('LOAD', addr);
- // Subscribe to new events
- try {
- this.subscription = live.getStream(addr).subscribe(this.streamObserver);
- this.setState({ addr, error: undefined });
- } catch (err) {
- this.setState({ addr: undefined, error: err });
- }
- }
- renderNotEnabled() {
- const preformatted = `[feature_toggles]
- enable = live`;
- return (
- <Alert title="Grafana Live" severity="info">
- <p>Grafana live requires a feature flag to run</p>
- <b>custom.ini:</b>
- <pre>{preformatted}</pre>
- </Alert>
- );
- }
- 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;
- if (!message) {
- return (
- <div>
- <h4>Waiting for data:</h4>
- {options.channel?.scope}/{options.channel?.namespace}/{options.channel?.path}
- </div>
- );
- }
- if (options.message === MessageDisplayMode.JSON) {
- return <JSONFormatter json={message} open={5} />;
- }
- if (options.message === MessageDisplayMode.Auto) {
- if (message instanceof StreamingDataFrame) {
- const data: PanelData = {
- series: applyFieldOverrides({
- data: [message],
- theme: config.theme2,
- replaceVariables: (v: string) => v,
- fieldConfig: {
- defaults: {},
- overrides: [],
- },
- }),
- state: LoadingState.Streaming,
- } as PanelData;
- const props = {
- ...this.props,
- options: { frameIndex: 0, showHeader: true },
- } as PanelProps<any>;
- return <TablePanel {...props} data={data} height={height} />;
- }
- }
- return <pre>{JSON.stringify(message)}</pre>;
- }
- 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>
- </>
- );
- }
- renderStatus() {
- const { status } = this.state;
- if (status?.state === LiveChannelConnectionState.Connected) {
- return; // nothing
- }
- let statusClass = '';
- if (status) {
- statusClass = this.styles.status[status.state];
- }
- return <div className={cx(statusClass, this.styles.statusWrap)}>{status?.state}</div>;
- }
- renderBody() {
- const { status } = this.state;
- const { options, height } = this.props;
- if (options.publish) {
- // Only the publish form
- if (options.message === MessageDisplayMode.None) {
- return <div>{this.renderPublish(height)}</div>;
- }
- // Both message and publish
- const halfHeight = height / 2;
- return (
- <div>
- <div style={{ height: halfHeight, overflow: 'hidden' }}>
- <CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
- {this.renderMessage(halfHeight)}
- </CustomScrollbar>
- </div>
- <div>{this.renderPublish(halfHeight)}</div>
- </div>
- );
- }
- if (options.message === MessageDisplayMode.None) {
- return <pre>{JSON.stringify(status)}</pre>;
- }
- // Only message
- return (
- <div style={{ overflow: 'hidden', height }}>
- <CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
- {this.renderMessage(height)}
- </CustomScrollbar>
- </div>
- );
- }
- render() {
- if (!this.isValid) {
- return this.renderNotEnabled();
- }
- const { addr, error } = this.state;
- if (!addr) {
- return (
- <Alert title="Grafana Live" severity="info">
- Use the panel editor to pick a channel
- </Alert>
- );
- }
- if (error) {
- return (
- <div>
- <h2>ERROR</h2>
- <div>{JSON.stringify(error)}</div>
- </div>
- );
- }
- 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;
- `,
- },
- }));
|