AnnoListPanel.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. // Libraries
  2. import React, { PureComponent } from 'react';
  3. // Types
  4. import { AnnoOptions } from './types';
  5. import { dateTime, DurationUnit, AnnotationEvent } from '@grafana/data';
  6. import { PanelProps, Tooltip } from '@grafana/ui';
  7. import { getBackendSrv } from 'app/core/services/backend_srv';
  8. import { AbstractList } from '@grafana/ui/src/components/List/AbstractList';
  9. import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
  10. import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
  11. import appEvents from 'app/core/app_events';
  12. import { updateLocation } from 'app/core/actions';
  13. import { store } from 'app/store/store';
  14. import { cx, css } from 'emotion';
  15. interface UserInfo {
  16. id: number;
  17. login: string;
  18. email: string;
  19. }
  20. interface Props extends PanelProps<AnnoOptions> {}
  21. interface State {
  22. annotations: AnnotationEvent[];
  23. timeInfo: string;
  24. loaded: boolean;
  25. queryUser?: UserInfo;
  26. queryTags: string[];
  27. }
  28. export class AnnoListPanel extends PureComponent<Props, State> {
  29. constructor(props: Props) {
  30. super(props);
  31. this.state = {
  32. annotations: [],
  33. timeInfo: '',
  34. loaded: false,
  35. queryTags: [],
  36. };
  37. }
  38. componentDidMount() {
  39. this.doSearch();
  40. }
  41. componentDidUpdate(prevProps: Props, prevState: State) {
  42. const { options, timeRange } = this.props;
  43. const needsQuery =
  44. options !== prevProps.options ||
  45. this.state.queryTags !== prevState.queryTags ||
  46. this.state.queryUser !== prevState.queryUser ||
  47. timeRange !== prevProps.timeRange;
  48. if (needsQuery) {
  49. this.doSearch();
  50. }
  51. }
  52. async doSearch() {
  53. // http://docs.grafana.org/http_api/annotations/
  54. // https://github.com/grafana/grafana/blob/master/public/app/core/services/backend_srv.ts
  55. // https://github.com/grafana/grafana/blob/master/public/app/features/annotations/annotations_srv.ts
  56. const { options } = this.props;
  57. const { queryUser, queryTags } = this.state;
  58. const params: any = {
  59. tags: options.tags,
  60. limit: options.limit,
  61. type: 'annotation', // Skip the Annotations that are really alerts. (Use the alerts panel!)
  62. };
  63. if (options.onlyFromThisDashboard) {
  64. params.dashboardId = getDashboardSrv().getCurrent().id;
  65. }
  66. let timeInfo = '';
  67. if (options.onlyInTimeRange) {
  68. const { timeRange } = this.props;
  69. params.from = timeRange.from.valueOf();
  70. params.to = timeRange.to.valueOf();
  71. } else {
  72. timeInfo = 'All Time';
  73. }
  74. if (queryUser) {
  75. params.userId = queryUser.id;
  76. }
  77. if (options.tags && options.tags.length) {
  78. params.tags = options.tags;
  79. }
  80. if (queryTags.length) {
  81. params.tags = params.tags ? [...params.tags, ...queryTags] : queryTags;
  82. }
  83. const annotations = await getBackendSrv().get('/api/annotations', params);
  84. this.setState({
  85. annotations,
  86. timeInfo,
  87. loaded: true,
  88. });
  89. }
  90. onAnnoClick = (e: React.SyntheticEvent, anno: AnnotationEvent) => {
  91. e.stopPropagation();
  92. const { options } = this.props;
  93. const dashboardSrv = getDashboardSrv();
  94. const current = dashboardSrv.getCurrent();
  95. const params: any = {
  96. from: this._timeOffset(anno.time, options.navigateBefore, true),
  97. to: this._timeOffset(anno.time, options.navigateAfter, false),
  98. };
  99. if (options.navigateToPanel) {
  100. params.panelId = anno.panelId;
  101. params.fullscreen = true;
  102. }
  103. if (current.id === anno.dashboardId) {
  104. store.dispatch(
  105. updateLocation({
  106. query: params,
  107. partial: true,
  108. })
  109. );
  110. return;
  111. }
  112. getBackendSrv()
  113. .get('/api/search', { dashboardIds: anno.dashboardId })
  114. .then((res: any[]) => {
  115. if (res && res.length && res[0].id === anno.dashboardId) {
  116. const dash = res[0];
  117. store.dispatch(
  118. updateLocation({
  119. query: params,
  120. path: dash.url,
  121. })
  122. );
  123. return;
  124. }
  125. appEvents.emit('alert-warning', ['Unknown Dashboard: ' + anno.dashboardId]);
  126. });
  127. };
  128. _timeOffset(time: number, offset: string, subtract = false): number {
  129. let incr = 5;
  130. let unit = 'm';
  131. const parts = /^(\d+)(\w)/.exec(offset);
  132. if (parts && parts.length === 3) {
  133. incr = parseInt(parts[1], 10);
  134. unit = parts[2];
  135. }
  136. const t = dateTime(time);
  137. if (subtract) {
  138. incr *= -1;
  139. }
  140. return t.add(incr, unit as DurationUnit).valueOf();
  141. }
  142. onTagClick = (e: React.SyntheticEvent, tag: string, remove: boolean) => {
  143. e.stopPropagation();
  144. const queryTags = remove ? this.state.queryTags.filter(item => item !== tag) : [...this.state.queryTags, tag];
  145. this.setState({ queryTags });
  146. };
  147. onUserClick = (e: React.SyntheticEvent, anno: AnnotationEvent) => {
  148. e.stopPropagation();
  149. this.setState({
  150. queryUser: {
  151. id: anno.userId,
  152. login: anno.login,
  153. email: anno.email,
  154. },
  155. });
  156. };
  157. onClearUser = () => {
  158. this.setState({
  159. queryUser: undefined,
  160. });
  161. };
  162. renderTags = (tags: string[], remove: boolean): JSX.Element => {
  163. if (!tags || !tags.length) {
  164. return null;
  165. }
  166. return (
  167. <>
  168. {tags.map(tag => {
  169. return (
  170. <span key={tag} onClick={e => this.onTagClick(e, tag, remove)} className="pointer">
  171. <TagBadge label={tag} removeIcon={remove} count={0} />
  172. </span>
  173. );
  174. })}
  175. </>
  176. );
  177. };
  178. renderItem = (anno: AnnotationEvent, index: number): JSX.Element => {
  179. const { options } = this.props;
  180. const { showUser, showTags, showTime } = options;
  181. const dashboard = getDashboardSrv().getCurrent();
  182. return (
  183. <div className="dashlist-item">
  184. <span
  185. className="dashlist-link pointer"
  186. onClick={e => {
  187. this.onAnnoClick(e, anno);
  188. }}
  189. >
  190. <span
  191. className={cx([
  192. 'dashlist-title',
  193. css`
  194. margin-right: 8px;
  195. `,
  196. ])}
  197. >
  198. {anno.text}
  199. </span>
  200. <span className="pluginlist-message">
  201. {anno.login && showUser && (
  202. <span className="graph-annotation">
  203. <Tooltip
  204. content={
  205. <span>
  206. Created by:
  207. <br /> {anno.email}
  208. </span>
  209. }
  210. theme="info"
  211. placement="top"
  212. >
  213. <span onClick={e => this.onUserClick(e, anno)} className="graph-annotation__user">
  214. <img src={anno.avatarUrl} />
  215. </span>
  216. </Tooltip>
  217. </span>
  218. )}
  219. {showTags && this.renderTags(anno.tags, false)}
  220. </span>
  221. <span className="pluginlist-version">{showTime && <span>{dashboard.formatDate(anno.time)}</span>}</span>
  222. </span>
  223. </div>
  224. );
  225. };
  226. render() {
  227. const { height } = this.props;
  228. const { loaded, annotations, queryUser, queryTags } = this.state;
  229. if (!loaded) {
  230. return <div>loading...</div>;
  231. }
  232. // Previously we showed inidication that it covered all time
  233. // { timeInfo && (
  234. // <span className="panel-time-info">
  235. // <i className="fa fa-clock-o" /> {timeInfo}
  236. // </span>
  237. // )}
  238. const hasFilter = queryUser || queryTags.length > 0;
  239. return (
  240. <div style={{ height, overflow: 'scroll' }}>
  241. {hasFilter && (
  242. <div>
  243. <b>Filter: &nbsp; </b>
  244. {queryUser && (
  245. <span onClick={this.onClearUser} className="pointer">
  246. {queryUser.email}
  247. </span>
  248. )}
  249. {queryTags.length > 0 && this.renderTags(queryTags, true)}
  250. </div>
  251. )}
  252. {annotations.length < 1 && <div className="panel-alert-list__no-alerts">No Annotations Found</div>}
  253. <AbstractList
  254. items={annotations}
  255. renderItem={this.renderItem}
  256. getItemKey={item => {
  257. return item.id + '';
  258. }}
  259. className="dashlist"
  260. />
  261. </div>
  262. );
  263. }
  264. }