QueryInspector.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import { css } from '@emotion/css';
  2. import React, { PureComponent } from 'react';
  3. import { Subscription } from 'rxjs';
  4. import { AppEvents, DataFrame } from '@grafana/data';
  5. import { selectors } from '@grafana/e2e-selectors';
  6. import { Stack } from '@grafana/experimental';
  7. import { config, RefreshEvent } from '@grafana/runtime';
  8. import { Button, ClipboardButton, JSONFormatter, LoadingPlaceholder } from '@grafana/ui';
  9. import appEvents from 'app/core/app_events';
  10. import { backendSrv } from 'app/core/services/backend_srv';
  11. import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
  12. import { PanelModel } from 'app/features/dashboard/state';
  13. import { getPanelInspectorStyles } from './styles';
  14. interface DsQuery {
  15. isLoading: boolean;
  16. response: {};
  17. }
  18. interface ExecutedQueryInfo {
  19. refId: string;
  20. query: string;
  21. frames: number;
  22. rows: number;
  23. }
  24. interface Props {
  25. data: DataFrame[];
  26. onRefreshQuery: () => void;
  27. panel?: PanelModel;
  28. }
  29. interface State {
  30. allNodesExpanded: boolean | null;
  31. isMocking: boolean;
  32. mockedResponse: string;
  33. dsQuery: DsQuery;
  34. executedQueries: ExecutedQueryInfo[];
  35. }
  36. export class QueryInspector extends PureComponent<Props, State> {
  37. private formattedJson: any;
  38. private subs = new Subscription();
  39. constructor(props: Props) {
  40. super(props);
  41. this.state = {
  42. executedQueries: [],
  43. allNodesExpanded: null,
  44. isMocking: false,
  45. mockedResponse: '',
  46. dsQuery: {
  47. isLoading: false,
  48. response: {},
  49. },
  50. };
  51. }
  52. componentDidMount() {
  53. const { panel } = this.props;
  54. this.subs.add(
  55. backendSrv.getInspectorStream().subscribe({
  56. next: (response) => this.onDataSourceResponse(response),
  57. })
  58. );
  59. if (panel) {
  60. this.subs.add(panel.events.subscribe(RefreshEvent, this.onPanelRefresh));
  61. this.updateQueryList();
  62. }
  63. }
  64. componentDidUpdate(oldProps: Props) {
  65. if (this.props.data !== oldProps.data) {
  66. this.updateQueryList();
  67. }
  68. }
  69. /**
  70. * Find the list of executed queries
  71. */
  72. updateQueryList() {
  73. const { data } = this.props;
  74. const executedQueries: ExecutedQueryInfo[] = [];
  75. if (data?.length) {
  76. let last: ExecutedQueryInfo | undefined = undefined;
  77. data.forEach((frame, idx) => {
  78. const query = frame.meta?.executedQueryString;
  79. if (query) {
  80. const refId = frame.refId || '?';
  81. if (last?.refId === refId) {
  82. last.frames++;
  83. last.rows += frame.length;
  84. } else {
  85. last = {
  86. refId,
  87. frames: 0,
  88. rows: frame.length,
  89. query,
  90. };
  91. executedQueries.push(last);
  92. }
  93. }
  94. });
  95. }
  96. this.setState({ executedQueries });
  97. }
  98. componentWillUnmount() {
  99. this.subs.unsubscribe();
  100. }
  101. onPanelRefresh = () => {
  102. this.setState((prevState) => ({
  103. ...prevState,
  104. dsQuery: {
  105. isLoading: true,
  106. response: {},
  107. },
  108. }));
  109. };
  110. onDataSourceResponse(response: any) {
  111. // ignore silent requests
  112. if (response.config?.hideFromInspector) {
  113. return;
  114. }
  115. response = { ...response }; // clone - dont modify the response
  116. if (response.headers) {
  117. delete response.headers;
  118. }
  119. if (response.config) {
  120. response.request = response.config;
  121. delete response.config;
  122. delete response.request.transformRequest;
  123. delete response.request.transformResponse;
  124. delete response.request.paramSerializer;
  125. delete response.request.jsonpCallbackParam;
  126. delete response.request.headers;
  127. delete response.request.requestId;
  128. delete response.request.inspect;
  129. delete response.request.retry;
  130. delete response.request.timeout;
  131. }
  132. if (response.data) {
  133. response.response = response.data;
  134. delete response.config;
  135. delete response.data;
  136. delete response.status;
  137. delete response.statusText;
  138. delete response.ok;
  139. delete response.url;
  140. delete response.redirected;
  141. delete response.type;
  142. delete response.$$config;
  143. }
  144. this.setState((prevState) => ({
  145. ...prevState,
  146. dsQuery: {
  147. isLoading: false,
  148. response: response,
  149. },
  150. }));
  151. }
  152. setFormattedJson = (formattedJson: any) => {
  153. this.formattedJson = formattedJson;
  154. };
  155. getTextForClipboard = () => {
  156. return JSON.stringify(this.formattedJson, null, 2);
  157. };
  158. onClipboardSuccess = () => {
  159. appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']);
  160. };
  161. onToggleExpand = () => {
  162. this.setState((prevState) => ({
  163. ...prevState,
  164. allNodesExpanded: !this.state.allNodesExpanded,
  165. }));
  166. };
  167. onToggleMocking = () => {
  168. this.setState((prevState) => ({
  169. ...prevState,
  170. isMocking: !this.state.isMocking,
  171. }));
  172. };
  173. getNrOfOpenNodes = () => {
  174. if (this.state.allNodesExpanded === null) {
  175. return 3; // 3 is default, ie when state is null
  176. } else if (this.state.allNodesExpanded) {
  177. return 20;
  178. }
  179. return 1;
  180. };
  181. setMockedResponse = (evt: any) => {
  182. const mockedResponse = evt.target.value;
  183. this.setState((prevState) => ({
  184. ...prevState,
  185. mockedResponse,
  186. }));
  187. };
  188. renderExecutedQueries(executedQueries: ExecutedQueryInfo[]) {
  189. if (!executedQueries.length) {
  190. return null;
  191. }
  192. const styles = {
  193. refId: css`
  194. font-weight: ${config.theme.typography.weight.semibold};
  195. color: ${config.theme.colors.textBlue};
  196. margin-right: 8px;
  197. `,
  198. };
  199. return (
  200. <div>
  201. {executedQueries.map((info) => {
  202. return (
  203. <Stack key={info.refId} gap={1} direction="column">
  204. <div>
  205. <span className={styles.refId}>{info.refId}:</span>
  206. {info.frames > 1 && <span>{info.frames} frames, </span>}
  207. <span>{info.rows} rows</span>
  208. </div>
  209. <pre>{info.query}</pre>
  210. </Stack>
  211. );
  212. })}
  213. </div>
  214. );
  215. }
  216. render() {
  217. const { allNodesExpanded, executedQueries } = this.state;
  218. const { panel, onRefreshQuery } = this.props;
  219. const { response, isLoading } = this.state.dsQuery;
  220. const openNodes = this.getNrOfOpenNodes();
  221. const styles = getPanelInspectorStyles();
  222. const haveData = Object.keys(response).length > 0;
  223. if (panel && !supportsDataQuery(panel.plugin)) {
  224. return null;
  225. }
  226. return (
  227. <div className={styles.wrap}>
  228. <div aria-label={selectors.components.PanelInspector.Query.content}>
  229. <h3 className="section-heading">Query inspector</h3>
  230. <p className="small muted">
  231. Query inspector allows you to view raw request and response. To collect this data Grafana needs to issue a
  232. new query. Click refresh button below to trigger a new query.
  233. </p>
  234. </div>
  235. {this.renderExecutedQueries(executedQueries)}
  236. <div className={styles.toolbar}>
  237. <Button
  238. icon="sync"
  239. onClick={onRefreshQuery}
  240. aria-label={selectors.components.PanelInspector.Query.refreshButton}
  241. >
  242. Refresh
  243. </Button>
  244. {haveData && allNodesExpanded && (
  245. <Button icon="minus" variant="secondary" className={styles.toolbarItem} onClick={this.onToggleExpand}>
  246. Collapse all
  247. </Button>
  248. )}
  249. {haveData && !allNodesExpanded && (
  250. <Button icon="plus" variant="secondary" className={styles.toolbarItem} onClick={this.onToggleExpand}>
  251. Expand all
  252. </Button>
  253. )}
  254. {haveData && (
  255. <ClipboardButton
  256. getText={this.getTextForClipboard}
  257. onClipboardCopy={this.onClipboardSuccess}
  258. className={styles.toolbarItem}
  259. icon="copy"
  260. variant="secondary"
  261. >
  262. Copy to clipboard
  263. </ClipboardButton>
  264. )}
  265. <div className="flex-grow-1" />
  266. </div>
  267. <div className={styles.content}>
  268. {isLoading && <LoadingPlaceholder text="Loading query inspector..." />}
  269. {!isLoading && haveData && (
  270. <JSONFormatter json={response} open={openNodes} onDidRender={this.setFormattedJson} />
  271. )}
  272. {!isLoading && !haveData && (
  273. <p className="muted">No request and response collected yet. Hit refresh button</p>
  274. )}
  275. </div>
  276. </div>
  277. );
  278. }
  279. }