LivePanel.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import { css, cx } from '@emotion/css';
  2. import { isEqual } from 'lodash';
  3. import React, { PureComponent } from 'react';
  4. import { Unsubscribable, PartialObserver } from 'rxjs';
  5. import {
  6. GrafanaTheme,
  7. PanelProps,
  8. LiveChannelStatusEvent,
  9. isValidLiveChannelAddress,
  10. LiveChannelEvent,
  11. isLiveChannelStatusEvent,
  12. isLiveChannelMessageEvent,
  13. LiveChannelConnectionState,
  14. PanelData,
  15. LoadingState,
  16. applyFieldOverrides,
  17. LiveChannelAddress,
  18. } from '@grafana/data';
  19. import { config, getGrafanaLiveSrv } from '@grafana/runtime';
  20. import { Alert, stylesFactory, Button, JSONFormatter, CustomScrollbar, CodeEditor } from '@grafana/ui';
  21. import { StreamingDataFrame } from 'app/features/live/data/StreamingDataFrame';
  22. import { TablePanel } from '../table/TablePanel';
  23. import { LivePanelOptions, MessageDisplayMode } from './types';
  24. interface Props extends PanelProps<LivePanelOptions> {}
  25. interface State {
  26. error?: any;
  27. addr?: LiveChannelAddress;
  28. status?: LiveChannelStatusEvent;
  29. message?: any;
  30. changed: number;
  31. }
  32. export class LivePanel extends PureComponent<Props, State> {
  33. private readonly isValid: boolean;
  34. subscription?: Unsubscribable;
  35. styles = getStyles(config.theme);
  36. constructor(props: Props) {
  37. super(props);
  38. this.isValid = !!getGrafanaLiveSrv();
  39. this.state = { changed: 0 };
  40. }
  41. async componentDidMount() {
  42. this.loadChannel();
  43. }
  44. componentWillUnmount() {
  45. if (this.subscription) {
  46. this.subscription.unsubscribe();
  47. }
  48. }
  49. componentDidUpdate(prevProps: Props): void {
  50. if (this.props.options?.channel !== prevProps.options?.channel) {
  51. this.loadChannel();
  52. }
  53. }
  54. streamObserver: PartialObserver<LiveChannelEvent> = {
  55. next: (event: LiveChannelEvent) => {
  56. if (isLiveChannelStatusEvent(event)) {
  57. this.setState({ status: event, changed: Date.now() });
  58. } else if (isLiveChannelMessageEvent(event)) {
  59. this.setState({ message: event.message, changed: Date.now() });
  60. } else {
  61. console.log('ignore', event);
  62. }
  63. },
  64. };
  65. unsubscribe = () => {
  66. if (this.subscription) {
  67. this.subscription.unsubscribe();
  68. this.subscription = undefined;
  69. }
  70. };
  71. async loadChannel() {
  72. const addr = this.props.options?.channel;
  73. if (!isValidLiveChannelAddress(addr)) {
  74. console.log('INVALID', addr);
  75. this.unsubscribe();
  76. this.setState({
  77. addr: undefined,
  78. });
  79. return;
  80. }
  81. if (isEqual(addr, this.state.addr)) {
  82. console.log('Same channel', this.state.addr);
  83. return;
  84. }
  85. const live = getGrafanaLiveSrv();
  86. if (!live) {
  87. console.log('INVALID', addr);
  88. this.unsubscribe();
  89. this.setState({
  90. addr: undefined,
  91. });
  92. return;
  93. }
  94. this.unsubscribe();
  95. console.log('LOAD', addr);
  96. // Subscribe to new events
  97. try {
  98. this.subscription = live.getStream(addr).subscribe(this.streamObserver);
  99. this.setState({ addr, error: undefined });
  100. } catch (err) {
  101. this.setState({ addr: undefined, error: err });
  102. }
  103. }
  104. renderNotEnabled() {
  105. const preformatted = `[feature_toggles]
  106. enable = live`;
  107. return (
  108. <Alert title="Grafana Live" severity="info">
  109. <p>Grafana live requires a feature flag to run</p>
  110. <b>custom.ini:</b>
  111. <pre>{preformatted}</pre>
  112. </Alert>
  113. );
  114. }
  115. onSaveJSON = (text: string) => {
  116. const { options, onOptionsChange } = this.props;
  117. try {
  118. const json = JSON.parse(text);
  119. onOptionsChange({ ...options, json });
  120. } catch (err) {
  121. console.log('Error reading JSON', err);
  122. }
  123. };
  124. onPublishClicked = async () => {
  125. const { addr } = this.state;
  126. if (!addr) {
  127. console.log('invalid address');
  128. return;
  129. }
  130. const data = this.props.options?.json;
  131. if (!data) {
  132. console.log('nothing to publish');
  133. return;
  134. }
  135. const rsp = await getGrafanaLiveSrv().publish(addr, data);
  136. console.log('onPublishClicked (response from publish)', rsp);
  137. };
  138. renderMessage(height: number) {
  139. const { options } = this.props;
  140. const { message } = this.state;
  141. if (!message) {
  142. return (
  143. <div>
  144. <h4>Waiting for data:</h4>
  145. {options.channel?.scope}/{options.channel?.namespace}/{options.channel?.path}
  146. </div>
  147. );
  148. }
  149. if (options.message === MessageDisplayMode.JSON) {
  150. return <JSONFormatter json={message} open={5} />;
  151. }
  152. if (options.message === MessageDisplayMode.Auto) {
  153. if (message instanceof StreamingDataFrame) {
  154. const data: PanelData = {
  155. series: applyFieldOverrides({
  156. data: [message],
  157. theme: config.theme2,
  158. replaceVariables: (v: string) => v,
  159. fieldConfig: {
  160. defaults: {},
  161. overrides: [],
  162. },
  163. }),
  164. state: LoadingState.Streaming,
  165. } as PanelData;
  166. const props = {
  167. ...this.props,
  168. options: { frameIndex: 0, showHeader: true },
  169. } as PanelProps<any>;
  170. return <TablePanel {...props} data={data} height={height} />;
  171. }
  172. }
  173. return <pre>{JSON.stringify(message)}</pre>;
  174. }
  175. renderPublish(height: number) {
  176. const { options } = this.props;
  177. return (
  178. <>
  179. <CodeEditor
  180. height={height - 32}
  181. language="json"
  182. value={options.json ? JSON.stringify(options.json, null, 2) : '{ }'}
  183. onBlur={this.onSaveJSON}
  184. onSave={this.onSaveJSON}
  185. showMiniMap={false}
  186. showLineNumbers={true}
  187. />
  188. <div style={{ height: 32 }}>
  189. <Button onClick={this.onPublishClicked}>Publish</Button>
  190. </div>
  191. </>
  192. );
  193. }
  194. renderStatus() {
  195. const { status } = this.state;
  196. if (status?.state === LiveChannelConnectionState.Connected) {
  197. return; // nothing
  198. }
  199. let statusClass = '';
  200. if (status) {
  201. statusClass = this.styles.status[status.state];
  202. }
  203. return <div className={cx(statusClass, this.styles.statusWrap)}>{status?.state}</div>;
  204. }
  205. renderBody() {
  206. const { status } = this.state;
  207. const { options, height } = this.props;
  208. if (options.publish) {
  209. // Only the publish form
  210. if (options.message === MessageDisplayMode.None) {
  211. return <div>{this.renderPublish(height)}</div>;
  212. }
  213. // Both message and publish
  214. const halfHeight = height / 2;
  215. return (
  216. <div>
  217. <div style={{ height: halfHeight, overflow: 'hidden' }}>
  218. <CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
  219. {this.renderMessage(halfHeight)}
  220. </CustomScrollbar>
  221. </div>
  222. <div>{this.renderPublish(halfHeight)}</div>
  223. </div>
  224. );
  225. }
  226. if (options.message === MessageDisplayMode.None) {
  227. return <pre>{JSON.stringify(status)}</pre>;
  228. }
  229. // Only message
  230. return (
  231. <div style={{ overflow: 'hidden', height }}>
  232. <CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
  233. {this.renderMessage(height)}
  234. </CustomScrollbar>
  235. </div>
  236. );
  237. }
  238. render() {
  239. if (!this.isValid) {
  240. return this.renderNotEnabled();
  241. }
  242. const { addr, error } = this.state;
  243. if (!addr) {
  244. return (
  245. <Alert title="Grafana Live" severity="info">
  246. Use the panel editor to pick a channel
  247. </Alert>
  248. );
  249. }
  250. if (error) {
  251. return (
  252. <div>
  253. <h2>ERROR</h2>
  254. <div>{JSON.stringify(error)}</div>
  255. </div>
  256. );
  257. }
  258. return (
  259. <>
  260. {this.renderStatus()}
  261. {this.renderBody()}
  262. </>
  263. );
  264. }
  265. }
  266. const getStyles = stylesFactory((theme: GrafanaTheme) => ({
  267. statusWrap: css`
  268. margin: auto;
  269. position: absolute;
  270. top: 0;
  271. right: 0;
  272. background: ${theme.colors.panelBg};
  273. padding: 10px;
  274. z-index: ${theme.zIndex.modal};
  275. `,
  276. status: {
  277. [LiveChannelConnectionState.Pending]: css`
  278. border: 1px solid ${theme.palette.brandPrimary};
  279. `,
  280. [LiveChannelConnectionState.Connected]: css`
  281. border: 1px solid ${theme.palette.brandSuccess};
  282. `,
  283. [LiveChannelConnectionState.Disconnected]: css`
  284. border: 1px solid ${theme.palette.brandWarning};
  285. `,
  286. [LiveChannelConnectionState.Shutdown]: css`
  287. border: 1px solid ${theme.palette.brandDanger};
  288. `,
  289. [LiveChannelConnectionState.Invalid]: css`
  290. border: 1px solid red;
  291. `,
  292. },
  293. }));