InspectDataTab.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import { css } from '@emotion/css';
  2. import { saveAs } from 'file-saver';
  3. import React, { PureComponent } from 'react';
  4. import AutoSizer from 'react-virtualized-auto-sizer';
  5. import {
  6. applyFieldOverrides,
  7. applyRawFieldOverrides,
  8. CSVConfig,
  9. DataFrame,
  10. DataTransformerID,
  11. dateTimeFormat,
  12. dateTimeFormatISO,
  13. MutableDataFrame,
  14. SelectableValue,
  15. toCSV,
  16. transformDataFrame,
  17. TimeZone,
  18. CoreApp,
  19. } from '@grafana/data';
  20. import { selectors } from '@grafana/e2e-selectors';
  21. import { reportInteraction } from '@grafana/runtime';
  22. import { Button, Spinner, Table } from '@grafana/ui';
  23. import { config } from 'app/core/config';
  24. import { dataFrameToLogsModel } from 'app/core/logs_model';
  25. import { PanelModel } from 'app/features/dashboard/state';
  26. import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
  27. import { transformToJaeger } from 'app/plugins/datasource/jaeger/responseTransform';
  28. import { transformToOTLP } from 'app/plugins/datasource/tempo/resultTransformer';
  29. import { transformToZipkin } from 'app/plugins/datasource/zipkin/utils/transforms';
  30. import { InspectDataOptions } from './InspectDataOptions';
  31. import { getPanelInspectorStyles } from './styles';
  32. interface Props {
  33. isLoading: boolean;
  34. options: GetDataOptions;
  35. timeZone: TimeZone;
  36. app?: CoreApp;
  37. data?: DataFrame[];
  38. panel?: PanelModel;
  39. onOptionsChange?: (options: GetDataOptions) => void;
  40. }
  41. interface State {
  42. /** The string is seriesToColumns transformation. Otherwise it is a dataframe index */
  43. selectedDataFrame: number | DataTransformerID;
  44. transformId: DataTransformerID;
  45. dataFrameIndex: number;
  46. transformationOptions: Array<SelectableValue<DataTransformerID>>;
  47. transformedData: DataFrame[];
  48. downloadForExcel: boolean;
  49. }
  50. export class InspectDataTab extends PureComponent<Props, State> {
  51. constructor(props: Props) {
  52. super(props);
  53. this.state = {
  54. selectedDataFrame: 0,
  55. dataFrameIndex: 0,
  56. transformId: DataTransformerID.noop,
  57. transformationOptions: buildTransformationOptions(),
  58. transformedData: props.data ?? [],
  59. downloadForExcel: false,
  60. };
  61. }
  62. componentDidUpdate(prevProps: Props, prevState: State) {
  63. if (!this.props.data) {
  64. this.setState({ transformedData: [] });
  65. return;
  66. }
  67. if (this.props.options.withTransforms) {
  68. this.setState({ transformedData: this.props.data });
  69. return;
  70. }
  71. if (prevProps.data !== this.props.data || prevState.transformId !== this.state.transformId) {
  72. const currentTransform = this.state.transformationOptions.find((item) => item.value === this.state.transformId);
  73. if (currentTransform && currentTransform.transformer.id !== DataTransformerID.noop) {
  74. const selectedDataFrame = this.state.selectedDataFrame;
  75. const dataFrameIndex = this.state.dataFrameIndex;
  76. const subscription = transformDataFrame([currentTransform.transformer], this.props.data).subscribe((data) => {
  77. this.setState({ transformedData: data, selectedDataFrame, dataFrameIndex }, () => subscription.unsubscribe());
  78. });
  79. return;
  80. }
  81. this.setState({ transformedData: this.props.data });
  82. return;
  83. }
  84. }
  85. exportCsv = (dataFrame: DataFrame, csvConfig: CSVConfig = {}) => {
  86. const { panel } = this.props;
  87. const { transformId } = this.state;
  88. const dataFrameCsv = toCSV([dataFrame], csvConfig);
  89. const blob = new Blob([String.fromCharCode(0xfeff), dataFrameCsv], {
  90. type: 'text/csv;charset=utf-8',
  91. });
  92. const displayTitle = panel ? panel.getDisplayTitle() : 'Explore';
  93. const transformation = transformId !== DataTransformerID.noop ? '-as-' + transformId.toLocaleLowerCase() : '';
  94. const fileName = `${displayTitle}-data${transformation}-${dateTimeFormat(new Date())}.csv`;
  95. saveAs(blob, fileName);
  96. };
  97. exportLogsAsTxt = () => {
  98. const { data, panel, app } = this.props;
  99. reportInteraction('grafana_logs_download_logs_clicked', {
  100. app,
  101. format: 'logs',
  102. });
  103. const logsModel = dataFrameToLogsModel(data || [], undefined);
  104. let textToDownload = '';
  105. logsModel.meta?.forEach((metaItem) => {
  106. const string = `${metaItem.label}: ${JSON.stringify(metaItem.value)}\n`;
  107. textToDownload = textToDownload + string;
  108. });
  109. textToDownload = textToDownload + '\n\n';
  110. logsModel.rows.forEach((row) => {
  111. const newRow = dateTimeFormatISO(row.timeEpochMs) + '\t' + row.entry + '\n';
  112. textToDownload = textToDownload + newRow;
  113. });
  114. const blob = new Blob([textToDownload], {
  115. type: 'text/plain;charset=utf-8',
  116. });
  117. const displayTitle = panel ? panel.getDisplayTitle() : 'Explore';
  118. const fileName = `${displayTitle}-logs-${dateTimeFormat(new Date())}.txt`;
  119. saveAs(blob, fileName);
  120. };
  121. exportTracesAsJson = () => {
  122. const { data, panel } = this.props;
  123. if (!data) {
  124. return;
  125. }
  126. for (const df of data) {
  127. // Only export traces
  128. if (df.meta?.preferredVisualisationType !== 'trace') {
  129. continue;
  130. }
  131. switch (df.meta?.custom?.traceFormat) {
  132. case 'jaeger': {
  133. let res = transformToJaeger(new MutableDataFrame(df));
  134. this.saveTraceJson(res, panel);
  135. break;
  136. }
  137. case 'zipkin': {
  138. let res = transformToZipkin(new MutableDataFrame(df));
  139. this.saveTraceJson(res, panel);
  140. break;
  141. }
  142. case 'otlp':
  143. default: {
  144. let res = transformToOTLP(new MutableDataFrame(df));
  145. this.saveTraceJson(res, panel);
  146. break;
  147. }
  148. }
  149. }
  150. };
  151. saveTraceJson = (json: any, panel?: PanelModel) => {
  152. const blob = new Blob([JSON.stringify(json)], {
  153. type: 'application/json',
  154. });
  155. const displayTitle = panel ? panel.getDisplayTitle() : 'Explore';
  156. const fileName = `${displayTitle}-traces-${dateTimeFormat(new Date())}.json`;
  157. saveAs(blob, fileName);
  158. };
  159. onDataFrameChange = (item: SelectableValue<DataTransformerID | number>) => {
  160. this.setState({
  161. transformId:
  162. item.value === DataTransformerID.seriesToColumns ? DataTransformerID.seriesToColumns : DataTransformerID.noop,
  163. dataFrameIndex: typeof item.value === 'number' ? item.value : 0,
  164. selectedDataFrame: item.value!,
  165. });
  166. };
  167. toggleDownloadForExcel = () => {
  168. this.setState((prevState) => ({
  169. downloadForExcel: !prevState.downloadForExcel,
  170. }));
  171. };
  172. getProcessedData(): DataFrame[] {
  173. const { options, panel, timeZone } = this.props;
  174. const data = this.state.transformedData;
  175. if (!options.withFieldConfig || !panel) {
  176. return applyRawFieldOverrides(data);
  177. }
  178. // We need to apply field config even though it was already applied in the PanelQueryRunner.
  179. // That's because transformers create new fields and data frames, so i.e. display processor is no longer there
  180. return applyFieldOverrides({
  181. data,
  182. theme: config.theme2,
  183. fieldConfig: panel.fieldConfig,
  184. timeZone,
  185. replaceVariables: (value: string) => {
  186. return value;
  187. },
  188. });
  189. }
  190. render() {
  191. const { isLoading, options, data, panel, onOptionsChange, app } = this.props;
  192. const { dataFrameIndex, transformId, transformationOptions, selectedDataFrame, downloadForExcel } = this.state;
  193. const styles = getPanelInspectorStyles();
  194. if (isLoading) {
  195. return (
  196. <div>
  197. <Spinner inline={true} /> Loading
  198. </div>
  199. );
  200. }
  201. const dataFrames = this.getProcessedData();
  202. if (!dataFrames || !dataFrames.length) {
  203. return <div>No Data</div>;
  204. }
  205. // let's make sure we don't try to render a frame that doesn't exists
  206. const index = !dataFrames[dataFrameIndex] ? 0 : dataFrameIndex;
  207. const dataFrame = dataFrames[index];
  208. const hasLogs = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'logs');
  209. const hasTraces = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'trace');
  210. return (
  211. <div className={styles.wrap} aria-label={selectors.components.PanelInspector.Data.content}>
  212. <div className={styles.toolbar}>
  213. <InspectDataOptions
  214. data={data}
  215. panel={panel}
  216. options={options}
  217. dataFrames={dataFrames}
  218. transformId={transformId}
  219. transformationOptions={transformationOptions}
  220. selectedDataFrame={selectedDataFrame}
  221. downloadForExcel={downloadForExcel}
  222. onOptionsChange={onOptionsChange}
  223. onDataFrameChange={this.onDataFrameChange}
  224. toggleDownloadForExcel={this.toggleDownloadForExcel}
  225. />
  226. <Button
  227. variant="primary"
  228. onClick={() => {
  229. if (hasLogs) {
  230. reportInteraction('grafana_logs_download_clicked', {
  231. app,
  232. format: 'csv',
  233. });
  234. }
  235. this.exportCsv(dataFrames[dataFrameIndex], { useExcelHeader: this.state.downloadForExcel });
  236. }}
  237. className={css`
  238. margin-bottom: 10px;
  239. `}
  240. >
  241. Download CSV
  242. </Button>
  243. {hasLogs && (
  244. <Button
  245. variant="primary"
  246. onClick={this.exportLogsAsTxt}
  247. className={css`
  248. margin-bottom: 10px;
  249. margin-left: 10px;
  250. `}
  251. >
  252. Download logs
  253. </Button>
  254. )}
  255. {hasTraces && (
  256. <Button
  257. variant="primary"
  258. onClick={this.exportTracesAsJson}
  259. className={css`
  260. margin-bottom: 10px;
  261. margin-left: 10px;
  262. `}
  263. >
  264. Download traces
  265. </Button>
  266. )}
  267. </div>
  268. <div className={styles.content}>
  269. <AutoSizer>
  270. {({ width, height }) => {
  271. if (width === 0) {
  272. return null;
  273. }
  274. return (
  275. <div style={{ width, height }}>
  276. <Table width={width} height={height} data={dataFrame} showTypeIcons={true} />
  277. </div>
  278. );
  279. }}
  280. </AutoSizer>
  281. </div>
  282. </div>
  283. );
  284. }
  285. }
  286. function buildTransformationOptions() {
  287. const transformations: Array<SelectableValue<DataTransformerID>> = [
  288. {
  289. value: DataTransformerID.seriesToColumns,
  290. label: 'Series joined by time',
  291. transformer: {
  292. id: DataTransformerID.seriesToColumns,
  293. options: { byField: 'Time' },
  294. },
  295. },
  296. ];
  297. return transformations;
  298. }