2025-04-01 10:38:02 +09:00

302 lines
7.6 KiB
TypeScript

import { css, cx } from '@emotion/css';
import { isEqual } from 'lodash';
import { 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, JSONFormatter, CustomScrollbar } from '@grafana/ui';
import { TablePanel } from '../table/TablePanel';
import { LivePublish } from './LivePublish';
import { LivePanelOptions, MessageDisplayMode, MessagePublishMode } from './types';
interface Props extends PanelProps<LivePanelOptions> {}
interface State {
error?: unknown;
addr?: LiveChannelAddress;
status?: LiveChannelStatusEvent;
message?: unknown;
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>
);
}
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.display === MessageDisplayMode.JSON) {
return <JSONFormatter json={message} open={5} />;
}
if (options.display === 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 (
<LivePublish
height={height}
body={options.message}
mode={options.publish ?? MessagePublishMode.JSON}
onSave={(message) => this.props.onOptionsChange({ ...options, message })}
addr={this.state.addr}
/>
);
}
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;
const publish = options.publish === MessagePublishMode.JSON || options.publish === MessagePublishMode.Influx;
if (publish) {
if (options.display === MessageDisplayMode.None) {
return this.renderPublish(height);
}
// 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.display === 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',
zIndex: 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',
}),
},
}));