mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 10:03:33 -06:00
335 lines
8.3 KiB
TypeScript
335 lines
8.3 KiB
TypeScript
import { css, cx } from '@emotion/css';
|
|
import { isEqual } from 'lodash';
|
|
import React, { PureComponent } from 'react';
|
|
import { Unsubscribable, PartialObserver } from 'rxjs';
|
|
|
|
import {
|
|
GrafanaTheme2,
|
|
PanelProps,
|
|
LiveChannelStatusEvent,
|
|
isValidLiveChannelAddress,
|
|
LiveChannelEvent,
|
|
isLiveChannelStatusEvent,
|
|
isLiveChannelMessageEvent,
|
|
LiveChannelConnectionState,
|
|
PanelData,
|
|
LoadingState,
|
|
applyFieldOverrides,
|
|
LiveChannelAddress,
|
|
StreamingDataFrame,
|
|
} from '@grafana/data';
|
|
import { config, getGrafanaLiveSrv } from '@grafana/runtime';
|
|
import { Alert, stylesFactory, Button, JSONFormatter, CustomScrollbar, CodeEditor } from '@grafana/ui';
|
|
|
|
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.theme2);
|
|
|
|
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: PanelProps = {
|
|
...this.props,
|
|
options: { frameIndex: 0, showHeader: true },
|
|
};
|
|
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: GrafanaTheme2) => ({
|
|
statusWrap: css`
|
|
margin: auto;
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
background: ${theme.components.panel.background};
|
|
padding: 10px;
|
|
z-index: ${theme.zIndex.modal};
|
|
`,
|
|
status: {
|
|
[LiveChannelConnectionState.Pending]: css`
|
|
border: 1px solid ${theme.v1.palette.orange};
|
|
`,
|
|
[LiveChannelConnectionState.Connected]: css`
|
|
border: 1px solid ${theme.colors.success.main};
|
|
`,
|
|
[LiveChannelConnectionState.Connecting]: css`
|
|
border: 1px solid ${theme.v1.palette.brandWarning};
|
|
`,
|
|
[LiveChannelConnectionState.Disconnected]: css`
|
|
border: 1px solid ${theme.colors.warning.main};
|
|
`,
|
|
[LiveChannelConnectionState.Shutdown]: css`
|
|
border: 1px solid ${theme.colors.error.main};
|
|
`,
|
|
[LiveChannelConnectionState.Invalid]: css`
|
|
border: 1px solid red;
|
|
`,
|
|
},
|
|
}));
|