QueriesTab.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import React, { SFC, PureComponent } from 'react';
  2. import DataSourceOption from './DataSourceOption';
  3. import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
  4. import { EditorTabBody } from './EditorTabBody';
  5. import { DataSourcePicker } from './DataSourcePicker';
  6. import { PanelModel } from '../panel_model';
  7. import { DashboardModel } from '../dashboard_model';
  8. import './../../panel/metrics_tab';
  9. import config from 'app/core/config';
  10. import { QueryInspector } from './QueryInspector';
  11. import { Switch } from 'app/core/components/Switch/Switch';
  12. import { Input } from 'app/core/components/Form';
  13. import { InputStatus, EventsWithValidation } from 'app/core/components/Form/Input';
  14. import { isValidTimeSpan } from 'app/core/utils/rangeutil';
  15. import { ValidationEvents } from 'app/types';
  16. // Services
  17. import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
  18. import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
  19. import { DataSourceSelectItem } from 'app/types';
  20. import Remarkable from 'remarkable';
  21. interface Props {
  22. panel: PanelModel;
  23. dashboard: DashboardModel;
  24. }
  25. interface Help {
  26. isLoading: boolean;
  27. helpHtml: any;
  28. }
  29. interface State {
  30. currentDatasource: DataSourceSelectItem;
  31. help: Help;
  32. hideTimeOverride: boolean;
  33. }
  34. interface LoadingPlaceholderProps {
  35. text: string;
  36. }
  37. const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => <h2>{text}</h2>;
  38. const timeRangeValidationEvents: ValidationEvents = {
  39. [EventsWithValidation.onBlur]: [
  40. {
  41. rule: value => {
  42. if (!value) {
  43. return true;
  44. }
  45. return isValidTimeSpan(value);
  46. },
  47. errorMessage: 'Not a valid timespan',
  48. },
  49. ],
  50. };
  51. export class QueriesTab extends PureComponent<Props, State> {
  52. element: any;
  53. component: AngularComponent;
  54. datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources();
  55. backendSrv: BackendSrv = getBackendSrv();
  56. constructor(props) {
  57. super(props);
  58. const { panel } = props;
  59. this.state = {
  60. currentDatasource: this.datasources.find(datasource => datasource.value === panel.datasource),
  61. help: {
  62. isLoading: false,
  63. helpHtml: null,
  64. },
  65. hideTimeOverride: false,
  66. };
  67. }
  68. componentDidMount() {
  69. if (!this.element) {
  70. return;
  71. }
  72. const { panel, dashboard } = this.props;
  73. const loader = getAngularLoader();
  74. const template = '<metrics-tab />';
  75. const scopeProps = {
  76. ctrl: {
  77. panel: panel,
  78. dashboard: dashboard,
  79. refresh: () => panel.refresh(),
  80. },
  81. };
  82. this.component = loader.load(this.element, scopeProps, template);
  83. }
  84. componentWillUnmount() {
  85. if (this.component) {
  86. this.component.destroy();
  87. }
  88. }
  89. onChangeDataSource = datasource => {
  90. const { panel } = this.props;
  91. const { currentDatasource } = this.state;
  92. // switching to mixed
  93. if (datasource.meta.mixed) {
  94. panel.targets.forEach(target => {
  95. target.datasource = panel.datasource;
  96. if (!target.datasource) {
  97. target.datasource = config.defaultDatasource;
  98. }
  99. });
  100. } else if (currentDatasource && currentDatasource.meta.mixed) {
  101. panel.targets.forEach(target => {
  102. delete target.datasource;
  103. });
  104. }
  105. panel.datasource = datasource.value;
  106. panel.refresh();
  107. this.setState(prevState => ({
  108. ...prevState,
  109. currentDatasource: datasource,
  110. }));
  111. };
  112. loadHelp = () => {
  113. const { currentDatasource } = this.state;
  114. const hasHelp = currentDatasource.meta.hasQueryHelp;
  115. if (hasHelp) {
  116. this.setState(prevState => ({
  117. ...prevState,
  118. help: {
  119. helpHtml: <h2>Loading help...</h2>,
  120. isLoading: true,
  121. },
  122. }));
  123. this.backendSrv
  124. .get(`/api/plugins/${currentDatasource.meta.id}/markdown/query_help`)
  125. .then(res => {
  126. const md = new Remarkable();
  127. const helpHtml = md.render(res); // TODO: Clean out dangerous code? Previous: this.helpHtml = this.$sce.trustAsHtml(md.render(res));
  128. this.setState(prevState => ({
  129. ...prevState,
  130. help: {
  131. helpHtml: <div className="markdown-html" dangerouslySetInnerHTML={{ __html: helpHtml }} />,
  132. isLoading: false,
  133. },
  134. }));
  135. })
  136. .catch(() => {
  137. this.setState(prevState => ({
  138. ...prevState,
  139. help: {
  140. helpHtml: 'Error occured when loading help',
  141. isLoading: false,
  142. },
  143. }));
  144. });
  145. }
  146. };
  147. renderOptions = close => {
  148. const { currentDatasource } = this.state;
  149. const { queryOptions } = currentDatasource.meta;
  150. const { panel } = this.props;
  151. const onChangeFn = (panelKey: string) => {
  152. return (value: string | number) => {
  153. panel[panelKey] = value;
  154. panel.refresh();
  155. };
  156. };
  157. const allOptions = {
  158. cacheTimeout: {
  159. label: 'Cache timeout',
  160. placeholder: '60',
  161. name: 'cacheTimeout',
  162. value: panel.cacheTimeout,
  163. tooltipInfo: (
  164. <>
  165. If your time series store has a query cache this option can override the default cache timeout. Specify a
  166. numeric value in seconds.
  167. </>
  168. ),
  169. },
  170. maxDataPoints: {
  171. label: 'Max data points',
  172. placeholder: 'auto',
  173. name: 'maxDataPoints',
  174. value: panel.maxDataPoints,
  175. tooltipInfo: (
  176. <>
  177. The maximum data points the query should return. For graphs this is automatically set to one data point per
  178. pixel.
  179. </>
  180. ),
  181. },
  182. minInterval: {
  183. label: 'Min time interval',
  184. placeholder: '0',
  185. name: 'minInterval',
  186. value: panel.interval,
  187. panelKey: 'interval',
  188. tooltipInfo: (
  189. <>
  190. A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example{' '}
  191. <code>1m</code> if your data is written every minute. Access auto interval via variable{' '}
  192. <code>$__interval</code> for time range string and <code>$__interval_ms</code> for numeric variable that can
  193. be used in math expressions.
  194. </>
  195. ),
  196. },
  197. };
  198. return Object.keys(queryOptions).map(key => {
  199. const options = allOptions[key];
  200. return <DataSourceOption key={key} {...options} onChange={onChangeFn(allOptions[key].panelKey || key)} />;
  201. });
  202. };
  203. renderQueryInspector = () => {
  204. const { panel } = this.props;
  205. return <QueryInspector panel={panel} LoadingPlaceholder={LoadingPlaceholder} />;
  206. };
  207. renderHelp = () => {
  208. const { helpHtml, isLoading } = this.state.help;
  209. return isLoading ? <LoadingPlaceholder text="Loading help..." /> : helpHtml;
  210. };
  211. emptyToNull = (value: string) => {
  212. return value === '' ? null : value;
  213. };
  214. onOverrideTime = (evt, status: InputStatus) => {
  215. const { value } = evt.target;
  216. const { panel } = this.props;
  217. const emptyToNullValue = this.emptyToNull(value);
  218. if (status === InputStatus.Valid && panel.timeFrom !== emptyToNullValue) {
  219. panel.timeFrom = emptyToNullValue;
  220. panel.refresh();
  221. }
  222. };
  223. onTimeShift = (evt, status: InputStatus) => {
  224. const { value } = evt.target;
  225. const { panel } = this.props;
  226. const emptyToNullValue = this.emptyToNull(value);
  227. if (status === InputStatus.Valid && panel.timeShift !== emptyToNullValue) {
  228. panel.timeShift = emptyToNullValue;
  229. panel.refresh();
  230. }
  231. };
  232. onToggleTimeOverride = () => {
  233. const { panel } = this.props;
  234. panel.hideTimeOverride = !panel.hideTimeOverride;
  235. panel.refresh();
  236. };
  237. render() {
  238. const { currentDatasource } = this.state;
  239. const hideTimeOverride = this.props.panel.hideTimeOverride;
  240. console.log('hideTimeOverride', hideTimeOverride);
  241. const { hasQueryHelp, queryOptions } = currentDatasource.meta;
  242. const hasQueryOptions = !!queryOptions;
  243. const dsInformation = {
  244. title: currentDatasource.name,
  245. imgSrc: currentDatasource.meta.info.logos.small,
  246. render: closeOpenView => (
  247. <DataSourcePicker
  248. datasources={this.datasources}
  249. onChangeDataSource={ds => {
  250. closeOpenView();
  251. this.onChangeDataSource(ds);
  252. }}
  253. />
  254. ),
  255. };
  256. const queryInspector = {
  257. title: 'Query Inspector',
  258. render: this.renderQueryInspector,
  259. };
  260. const dsHelp = {
  261. title: '',
  262. icon: 'fa fa-question',
  263. disabled: !hasQueryHelp,
  264. onClick: this.loadHelp,
  265. render: this.renderHelp,
  266. };
  267. const options = {
  268. title: '',
  269. icon: 'fa fa-cog',
  270. disabled: !hasQueryOptions,
  271. render: this.renderOptions,
  272. };
  273. return (
  274. <EditorTabBody heading="Queries" main={dsInformation} toolbarItems={[options, queryInspector, dsHelp]}>
  275. <>
  276. <div ref={element => (this.element = element)} style={{ width: '100%' }} />
  277. <h5 className="section-heading">Time Range</h5>
  278. <div className="gf-form-group">
  279. <div className="gf-form">
  280. <span className="gf-form-label">
  281. <i className="fa fa-clock-o" />
  282. </span>
  283. <span className="gf-form-label width-12">Override relative time</span>
  284. <span className="gf-form-label width-6">Last</span>
  285. <Input
  286. type="text"
  287. className="gf-form-input max-width-8"
  288. placeholder="1h"
  289. onBlur={this.onOverrideTime}
  290. validationEvents={timeRangeValidationEvents}
  291. hideErrorMessage={true}
  292. />
  293. </div>
  294. <div className="gf-form">
  295. <span className="gf-form-label">
  296. <i className="fa fa-clock-o" />
  297. </span>
  298. <span className="gf-form-label width-12">Add time shift</span>
  299. <span className="gf-form-label width-6">Amount</span>
  300. <Input
  301. type="text"
  302. className="gf-form-input max-width-8"
  303. placeholder="1h"
  304. onBlur={this.onTimeShift}
  305. validationEvents={timeRangeValidationEvents}
  306. hideErrorMessage={true}
  307. />
  308. </div>
  309. <div className="gf-form-inline">
  310. <div className="gf-form">
  311. <span className="gf-form-label">
  312. <i className="fa fa-clock-o" />
  313. </span>
  314. </div>
  315. <Switch label="Hide time override info" checked={hideTimeOverride} onChange={this.onToggleTimeOverride} />
  316. </div>
  317. </div>
  318. </>
  319. </EditorTabBody>
  320. );
  321. }
  322. }