123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- import { css } from '@emotion/css';
- import { FocusScope } from '@react-aria/focus';
- import React, { PureComponent } from 'react';
- import { Subscription } from 'rxjs';
- import {
- AnnotationChangeEvent,
- AnnotationEvent,
- AppEvents,
- dateTime,
- DurationUnit,
- GrafanaTheme,
- locationUtil,
- PanelProps,
- } from '@grafana/data';
- import { config, getBackendSrv, locationService } from '@grafana/runtime';
- import { CustomScrollbar, stylesFactory, TagList } from '@grafana/ui';
- import { AbstractList } from '@grafana/ui/src/components/List/AbstractList';
- import appEvents from 'app/core/app_events';
- import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
- import { AnnotationListItem } from './AnnotationListItem';
- import { AnnoOptions } from './types';
- interface UserInfo {
- id?: number;
- login?: string;
- email?: string;
- }
- export interface Props extends PanelProps<AnnoOptions> {}
- interface State {
- annotations: AnnotationEvent[];
- timeInfo: string;
- loaded: boolean;
- queryUser?: UserInfo;
- queryTags: string[];
- }
- export class AnnoListPanel extends PureComponent<Props, State> {
- style = getStyles(config.theme);
- subs = new Subscription();
- tagListRef = React.createRef<HTMLUListElement>();
- constructor(props: Props) {
- super(props);
- this.state = {
- annotations: [],
- timeInfo: '',
- loaded: false,
- queryTags: [],
- };
- }
- componentDidMount() {
- this.doSearch();
- // When an annotation on this dashboard changes, re-run the query
- this.subs.add(
- this.props.eventBus.getStream(AnnotationChangeEvent).subscribe({
- next: () => {
- this.doSearch();
- },
- })
- );
- }
- componentWillUnmount() {
- this.subs.unsubscribe();
- }
- componentDidUpdate(prevProps: Props, prevState: State) {
- const { options, timeRange } = this.props;
- const needsQuery =
- options !== prevProps.options ||
- this.state.queryTags !== prevState.queryTags ||
- this.state.queryUser !== prevState.queryUser ||
- (options.onlyInTimeRange && timeRange !== prevProps.timeRange);
- if (needsQuery) {
- this.doSearch();
- }
- }
- async doSearch() {
- // http://docs.grafana.org/http_api/annotations/
- // https://github.com/grafana/grafana/blob/main/public/app/core/services/backend_srv.ts
- // https://github.com/grafana/grafana/blob/main/public/app/features/annotations/annotations_srv.ts
- const { options } = this.props;
- const { queryUser, queryTags } = this.state;
- const params: any = {
- tags: options.tags,
- limit: options.limit,
- type: 'annotation', // Skip the Annotations that are really alerts. (Use the alerts panel!)
- };
- if (options.onlyFromThisDashboard) {
- params.dashboardId = getDashboardSrv().getCurrent()?.id;
- }
- let timeInfo = '';
- if (options.onlyInTimeRange) {
- const { timeRange } = this.props;
- params.from = timeRange.from.valueOf();
- params.to = timeRange.to.valueOf();
- } else {
- timeInfo = 'All Time';
- }
- if (queryUser) {
- params.userId = queryUser.id;
- }
- if (options.tags && options.tags.length) {
- params.tags = options.tags.map((tag) => this.props.replaceVariables(tag));
- }
- if (queryTags.length) {
- params.tags = params.tags ? [...params.tags, ...queryTags] : queryTags;
- }
- const annotations = await getBackendSrv().get('/api/annotations', params, `anno-list-panel-${this.props.id}`);
- this.setState({
- annotations,
- timeInfo,
- loaded: true,
- });
- }
- onAnnoClick = async (anno: AnnotationEvent) => {
- if (!anno.time) {
- return;
- }
- const { options } = this.props;
- const dashboardSrv = getDashboardSrv();
- const current = dashboardSrv.getCurrent();
- const params: any = {
- from: this._timeOffset(anno.time, options.navigateBefore, true),
- to: this._timeOffset(anno.timeEnd ?? anno.time, options.navigateAfter, false),
- };
- if (options.navigateToPanel) {
- params.viewPanel = anno.panelId;
- }
- if (current?.id === anno.dashboardId) {
- locationService.partial(params);
- return;
- }
- const result = await getBackendSrv().get('/api/search', { dashboardIds: anno.dashboardId });
- if (result && result.length && result[0].id === anno.dashboardId) {
- const dash = result[0];
- const url = new URL(dash.url, window.location.origin);
- url.searchParams.set('from', params.from);
- url.searchParams.set('to', params.to);
- locationService.push(locationUtil.stripBaseFromUrl(url.toString()));
- return;
- }
- appEvents.emit(AppEvents.alertWarning, ['Unknown Dashboard: ' + anno.dashboardId]);
- };
- _timeOffset(time: number, offset: string, subtract = false): number {
- let incr = 5;
- let unit = 'm';
- const parts = /^(\d+)(\w)/.exec(offset);
- if (parts && parts.length === 3) {
- incr = parseInt(parts[1], 10);
- unit = parts[2];
- }
- const t = dateTime(time);
- if (subtract) {
- incr *= -1;
- }
- return t.add(incr, unit as DurationUnit).valueOf();
- }
- onTagClick = (tag: string, remove?: boolean) => {
- if (!remove && this.state.queryTags.includes(tag)) {
- return;
- }
- const queryTags = remove ? this.state.queryTags.filter((item) => item !== tag) : [...this.state.queryTags, tag];
- // Logic to ensure keyboard focus isn't lost when the currently
- // focused tag is removed
- let nextTag: HTMLElement | undefined = undefined;
- if (remove) {
- const focusedTag = document.activeElement;
- const dataTagId = focusedTag?.getAttribute('data-tag-id');
- if (this.tagListRef.current?.contains(focusedTag) && dataTagId) {
- const parsedTagId = Number.parseInt(dataTagId, 10);
- const possibleNextTag =
- this.tagListRef.current.querySelector(`[data-tag-id="${parsedTagId + 1}"]`) ??
- this.tagListRef.current.querySelector(`[data-tag-id="${parsedTagId - 1}"]`);
- if (possibleNextTag instanceof HTMLElement) {
- nextTag = possibleNextTag;
- }
- }
- }
- this.setState({ queryTags }, () => nextTag?.focus());
- };
- onUserClick = (anno: AnnotationEvent) => {
- this.setState({
- queryUser: {
- id: anno.userId,
- login: anno.login,
- email: anno.email,
- },
- });
- };
- onClearUser = () => {
- this.setState({
- queryUser: undefined,
- });
- };
- renderItem = (anno: AnnotationEvent, index: number): JSX.Element => {
- const { options } = this.props;
- const dashboard = getDashboardSrv().getCurrent();
- if (!dashboard) {
- return <></>;
- }
- return (
- <AnnotationListItem
- annotation={anno}
- formatDate={dashboard.formatDate}
- onClick={this.onAnnoClick}
- onAvatarClick={this.onUserClick}
- onTagClick={this.onTagClick}
- options={options}
- />
- );
- };
- render() {
- const { loaded, annotations, queryUser, queryTags } = this.state;
- if (!loaded) {
- return <div>loading...</div>;
- }
- // Previously we showed inidication that it covered all time
- // { timeInfo && (
- // <span className="panel-time-info">
- // <Icon name="clock-nine" /> {timeInfo}
- // </span>
- // )}
- const hasFilter = queryUser || queryTags.length > 0;
- return (
- <CustomScrollbar autoHeightMin="100%">
- {hasFilter && (
- <div className={this.style.filter}>
- <b>Filter:</b>
- {queryUser && (
- <span onClick={this.onClearUser} className="pointer">
- {queryUser.email}
- </span>
- )}
- {queryTags.length > 0 && (
- <FocusScope restoreFocus>
- <TagList
- icon="times"
- tags={queryTags}
- onClick={(tag) => this.onTagClick(tag, true)}
- getAriaLabel={(name) => `Remove ${name} tag`}
- className={this.style.tagList}
- ref={this.tagListRef}
- />
- </FocusScope>
- )}
- </div>
- )}
- {annotations.length < 1 && <div className={this.style.noneFound}>No Annotations Found</div>}
- <AbstractList items={annotations} renderItem={this.renderItem} getItemKey={(item) => `${item.id}`} />
- </CustomScrollbar>
- );
- }
- }
- const getStyles = stylesFactory((theme: GrafanaTheme) => ({
- noneFound: css`
- display: flex;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: calc(100% - 30px);
- `,
- filter: css({
- display: 'flex',
- padding: `0px ${theme.spacing.xs}`,
- b: {
- paddingRight: theme.spacing.sm,
- },
- }),
- tagList: css({
- justifyContent: 'flex-start',
- 'li > button': {
- paddingLeft: '3px',
- },
- }),
- }));
|