Browse Source

AnnoList: add alpha annotations list plugin (#17187)

adding an alpha panel
Ryan McKinley 6 years ago
parent
commit
6599bdc7f1

+ 3 - 0
packages/grafana-data/src/types/data.ts

@@ -97,6 +97,9 @@ export interface AnnotationEvent {
   dashboardId?: number;
   dashboardId?: number;
   panelId?: number;
   panelId?: number;
   userId?: number;
   userId?: number;
+  login?: string;
+  email?: string;
+  avatarUrl?: string;
   time?: number;
   time?: number;
   timeEnd?: number;
   timeEnd?: number;
   isRegion?: boolean;
   isRegion?: boolean;

+ 7 - 2
public/app/core/components/TagFilter/TagFilter.tsx

@@ -11,9 +11,14 @@ import { TagBadge } from './TagBadge';
 import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui';
 import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui';
 import { escapeStringForRegex } from '../FilterInput/FilterInput';
 import { escapeStringForRegex } from '../FilterInput/FilterInput';
 
 
+export interface TermCount {
+  term: string;
+  count: number;
+}
+
 export interface Props {
 export interface Props {
   tags: string[];
   tags: string[];
-  tagOptions: () => any;
+  tagOptions: () => Promise<TermCount[]>;
   onChange: (tags: string[]) => void;
   onChange: (tags: string[]) => void;
 }
 }
 
 
@@ -25,7 +30,7 @@ export class TagFilter extends React.Component<Props, any> {
   }
   }
 
 
   onLoadOptions = (query: string) => {
   onLoadOptions = (query: string) => {
-    return this.props.tagOptions().then((options: any[]) => {
+    return this.props.tagOptions().then(options => {
       return options.map(option => ({
       return options.map(option => ({
         value: option.term,
         value: option.term,
         label: option.term,
         label: option.term,

+ 2 - 0
public/app/features/plugins/built_in_plugins.ts

@@ -22,6 +22,7 @@ import * as graphPanel from 'app/plugins/panel/graph/module';
 import * as dashListPanel from 'app/plugins/panel/dashlist/module';
 import * as dashListPanel from 'app/plugins/panel/dashlist/module';
 import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
 import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module';
 import * as alertListPanel from 'app/plugins/panel/alertlist/module';
 import * as alertListPanel from 'app/plugins/panel/alertlist/module';
+import * as annoListPanel from 'app/plugins/panel/annolist/module';
 import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
 import * as heatmapPanel from 'app/plugins/panel/heatmap/module';
 import * as tablePanel from 'app/plugins/panel/table/module';
 import * as tablePanel from 'app/plugins/panel/table/module';
 import * as table2Panel from 'app/plugins/panel/table2/module';
 import * as table2Panel from 'app/plugins/panel/table2/module';
@@ -59,6 +60,7 @@ const builtInPlugins = {
   'app/plugins/panel/dashlist/module': dashListPanel,
   'app/plugins/panel/dashlist/module': dashListPanel,
   'app/plugins/panel/pluginlist/module': pluginsListPanel,
   'app/plugins/panel/pluginlist/module': pluginsListPanel,
   'app/plugins/panel/alertlist/module': alertListPanel,
   'app/plugins/panel/alertlist/module': alertListPanel,
+  'app/plugins/panel/annolist/module': annoListPanel,
   'app/plugins/panel/heatmap/module': heatmapPanel,
   'app/plugins/panel/heatmap/module': heatmapPanel,
   'app/plugins/panel/table/module': tablePanel,
   'app/plugins/panel/table/module': tablePanel,
   'app/plugins/panel/table2/module': table2Panel,
   'app/plugins/panel/table2/module': table2Panel,

+ 194 - 0
public/app/plugins/panel/annolist/AnnoListEditor.tsx

@@ -0,0 +1,194 @@
+// Libraries
+import React, { PureComponent, ChangeEvent } from 'react';
+
+// Components
+import { PanelEditorProps, PanelOptionsGroup, PanelOptionsGrid, Switch, FormField, FormLabel } from '@grafana/ui';
+
+import { toIntegerOrUndefined, toNumberString } from '@grafana/data';
+
+// Types
+import { AnnoOptions } from './types';
+import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
+
+interface State {
+  tag: string;
+}
+
+export class AnnoListEditor extends PureComponent<PanelEditorProps<AnnoOptions>, State> {
+  constructor(props: PanelEditorProps<AnnoOptions>) {
+    super(props);
+
+    this.state = {
+      tag: '',
+    };
+  }
+
+  // Display
+  //-----------
+
+  onToggleShowUser = () =>
+    this.props.onOptionsChange({ ...this.props.options, showUser: !this.props.options.showUser });
+
+  onToggleShowTime = () =>
+    this.props.onOptionsChange({ ...this.props.options, showTime: !this.props.options.showTime });
+
+  onToggleShowTags = () =>
+    this.props.onOptionsChange({ ...this.props.options, showTags: !this.props.options.showTags });
+
+  // Navigate
+  //-----------
+
+  onNavigateBeforeChange = (event: ChangeEvent<HTMLInputElement>) => {
+    this.props.onOptionsChange({ ...this.props.options, navigateBefore: event.target.value });
+  };
+
+  onNavigateAfterChange = (event: ChangeEvent<HTMLInputElement>) => {
+    this.props.onOptionsChange({ ...this.props.options, navigateAfter: event.target.value });
+  };
+
+  onToggleNavigateToPanel = () =>
+    this.props.onOptionsChange({ ...this.props.options, navigateToPanel: !this.props.options.navigateToPanel });
+
+  // Search
+  //-----------
+  onLimitChange = (event: ChangeEvent<HTMLInputElement>) => {
+    const v = toIntegerOrUndefined(event.target.value);
+    this.props.onOptionsChange({ ...this.props.options, limit: v });
+  };
+
+  onToggleOnlyFromThisDashboard = () =>
+    this.props.onOptionsChange({
+      ...this.props.options,
+      onlyFromThisDashboard: !this.props.options.onlyFromThisDashboard,
+    });
+
+  onToggleOnlyInTimeRange = () =>
+    this.props.onOptionsChange({ ...this.props.options, onlyInTimeRange: !this.props.options.onlyInTimeRange });
+
+  // Tags
+  //-----------
+
+  onTagTextChange = (event: ChangeEvent<HTMLInputElement>) => {
+    this.setState({ tag: event.target.value });
+  };
+
+  onTagClick = (e: React.SyntheticEvent, tag: string) => {
+    e.stopPropagation();
+
+    const tags = this.props.options.tags.filter(item => item !== tag);
+    this.props.onOptionsChange({
+      ...this.props.options,
+      tags,
+    });
+  };
+
+  renderTags = (tags: string[]): JSX.Element => {
+    if (!tags || !tags.length) {
+      return null;
+    }
+    return (
+      <>
+        {tags.map(tag => {
+          return (
+            <span key={tag} onClick={e => this.onTagClick(e, tag)} className="pointer">
+              <TagBadge label={tag} removeIcon={true} count={0} />
+            </span>
+          );
+        })}
+      </>
+    );
+  };
+
+  render() {
+    const { options } = this.props;
+    const labelWidth = 8;
+
+    return (
+      <PanelOptionsGrid>
+        <PanelOptionsGroup title="Display">
+          <Switch
+            label="Show User"
+            labelClass={`width-${labelWidth}`}
+            checked={options.showUser}
+            onChange={this.onToggleShowUser}
+          />
+          <Switch
+            label="Show Time"
+            labelClass={`width-${labelWidth}`}
+            checked={options.showTime}
+            onChange={this.onToggleShowTime}
+          />
+          <Switch
+            label="Show Tags"
+            labelClass={`width-${labelWidth}`}
+            checked={options.showTags}
+            onChange={this.onToggleShowTags}
+          />
+        </PanelOptionsGroup>
+        <PanelOptionsGroup title="Navigate">
+          <FormField
+            label="Before"
+            labelWidth={labelWidth}
+            onChange={this.onNavigateBeforeChange}
+            value={options.navigateBefore}
+          />
+          <FormField
+            label="After"
+            labelWidth={labelWidth}
+            onChange={this.onNavigateAfterChange}
+            value={options.navigateAfter}
+          />
+          <Switch
+            label="To Panel"
+            labelClass={`width-${labelWidth}`}
+            checked={options.navigateToPanel}
+            onChange={this.onToggleNavigateToPanel}
+          />
+        </PanelOptionsGroup>
+        <PanelOptionsGroup title="Search">
+          <Switch
+            label="Only This Dashboard"
+            labelClass={`width-12`}
+            checked={options.onlyFromThisDashboard}
+            onChange={this.onToggleOnlyFromThisDashboard}
+          />
+          <Switch
+            label="Within Time Range"
+            labelClass={`width-12`}
+            checked={options.onlyInTimeRange}
+            onChange={this.onToggleOnlyInTimeRange}
+          />
+          <div className="form-field">
+            <FormLabel width={6}>Tags</FormLabel>
+            {this.renderTags(options.tags)}
+            <input
+              type="text"
+              className={`gf-form-input width-${8}`}
+              value={this.state.tag}
+              onChange={this.onTagTextChange}
+              onKeyPress={ev => {
+                if (this.state.tag && ev.key === 'Enter') {
+                  const tags = [...options.tags, this.state.tag];
+                  this.props.onOptionsChange({
+                    ...this.props.options,
+                    tags,
+                  });
+                  this.setState({ tag: '' });
+                  ev.preventDefault();
+                }
+              }}
+            />
+          </div>
+
+          <FormField
+            label="Limit"
+            labelWidth={6}
+            onChange={this.onLimitChange}
+            value={toNumberString(options.limit)}
+            type="number"
+          />
+        </PanelOptionsGroup>
+      </PanelOptionsGrid>
+    );
+  }
+}

+ 304 - 0
public/app/plugins/panel/annolist/AnnoListPanel.tsx

@@ -0,0 +1,304 @@
+// Libraries
+import React, { PureComponent } from 'react';
+
+// Types
+import { AnnoOptions } from './types';
+import { dateTime, DurationUnit, AnnotationEvent } from '@grafana/data';
+import { PanelProps, Tooltip } from '@grafana/ui';
+import { getBackendSrv } from 'app/core/services/backend_srv';
+import { AbstractList } from '@grafana/ui/src/components/List/AbstractList';
+import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
+import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
+import appEvents from 'app/core/app_events';
+
+import { updateLocation } from 'app/core/actions';
+import { store } from 'app/store/store';
+import { cx, css } from 'emotion';
+
+interface UserInfo {
+  id: number;
+  login: string;
+  email: string;
+}
+
+interface Props extends PanelProps<AnnoOptions> {}
+interface State {
+  annotations: AnnotationEvent[];
+  timeInfo: string;
+  loaded: boolean;
+  queryUser?: UserInfo;
+  queryTags: string[];
+}
+
+export class AnnoListPanel extends PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      annotations: [],
+      timeInfo: '',
+      loaded: false,
+      queryTags: [],
+    };
+  }
+
+  componentDidMount() {
+    this.doSearch();
+  }
+
+  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 ||
+      timeRange !== prevProps.timeRange;
+
+    if (needsQuery) {
+      this.doSearch();
+    }
+  }
+
+  async doSearch() {
+    // http://docs.grafana.org/http_api/annotations/
+    // https://github.com/grafana/grafana/blob/master/public/app/core/services/backend_srv.ts
+    // https://github.com/grafana/grafana/blob/master/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;
+    }
+
+    if (queryTags.length) {
+      params.tags = params.tags ? [...params.tags, ...queryTags] : queryTags;
+    }
+
+    const annotations = await getBackendSrv().get('/api/annotations', params);
+    this.setState({
+      annotations,
+      timeInfo,
+      loaded: true,
+    });
+  }
+
+  onAnnoClick = (e: React.SyntheticEvent, anno: AnnotationEvent) => {
+    e.stopPropagation();
+    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.time, options.navigateAfter, false),
+    };
+
+    if (options.navigateToPanel) {
+      params.panelId = anno.panelId;
+      params.fullscreen = true;
+    }
+
+    if (current.id === anno.dashboardId) {
+      store.dispatch(
+        updateLocation({
+          query: params,
+          partial: true,
+        })
+      );
+      return;
+    }
+
+    getBackendSrv()
+      .get('/api/search', { dashboardIds: anno.dashboardId })
+      .then((res: any[]) => {
+        if (res && res.length && res[0].id === anno.dashboardId) {
+          const dash = res[0];
+          store.dispatch(
+            updateLocation({
+              query: params,
+              path: dash.url,
+            })
+          );
+          return;
+        }
+        appEvents.emit('alert-warning', ['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 = (e: React.SyntheticEvent, tag: string, remove: boolean) => {
+    e.stopPropagation();
+    const queryTags = remove ? this.state.queryTags.filter(item => item !== tag) : [...this.state.queryTags, tag];
+
+    this.setState({ queryTags });
+  };
+
+  onUserClick = (e: React.SyntheticEvent, anno: AnnotationEvent) => {
+    e.stopPropagation();
+    this.setState({
+      queryUser: {
+        id: anno.userId,
+        login: anno.login,
+        email: anno.email,
+      },
+    });
+  };
+
+  onClearUser = () => {
+    this.setState({
+      queryUser: undefined,
+    });
+  };
+
+  renderTags = (tags: string[], remove: boolean): JSX.Element => {
+    if (!tags || !tags.length) {
+      return null;
+    }
+    return (
+      <>
+        {tags.map(tag => {
+          return (
+            <span key={tag} onClick={e => this.onTagClick(e, tag, remove)} className="pointer">
+              <TagBadge label={tag} removeIcon={remove} count={0} />
+            </span>
+          );
+        })}
+      </>
+    );
+  };
+
+  renderItem = (anno: AnnotationEvent, index: number): JSX.Element => {
+    const { options } = this.props;
+    const { showUser, showTags, showTime } = options;
+    const dashboard = getDashboardSrv().getCurrent();
+
+    return (
+      <div className="dashlist-item">
+        <span
+          className="dashlist-link pointer"
+          onClick={e => {
+            this.onAnnoClick(e, anno);
+          }}
+        >
+          <span
+            className={cx([
+              'dashlist-title',
+              css`
+                margin-right: 8px;
+              `,
+            ])}
+          >
+            {anno.text}
+          </span>
+
+          <span className="pluginlist-message">
+            {anno.login && showUser && (
+              <span className="graph-annotation">
+                <Tooltip
+                  content={
+                    <span>
+                      Created by:
+                      <br /> {anno.email}
+                    </span>
+                  }
+                  theme="info"
+                  placement="top"
+                >
+                  <span onClick={e => this.onUserClick(e, anno)} className="graph-annotation__user">
+                    <img src={anno.avatarUrl} />
+                  </span>
+                </Tooltip>
+              </span>
+            )}
+            {showTags && this.renderTags(anno.tags, false)}
+          </span>
+
+          <span className="pluginlist-version">{showTime && <span>{dashboard.formatDate(anno.time)}</span>}</span>
+        </span>
+      </div>
+    );
+  };
+
+  render() {
+    const { height } = this.props;
+    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">
+    //     <i className="fa fa-clock-o" /> {timeInfo}
+    //   </span>
+    // )}
+
+    const hasFilter = queryUser || queryTags.length > 0;
+
+    return (
+      <div style={{ height, overflow: 'scroll' }}>
+        {hasFilter && (
+          <div>
+            <b>Filter: &nbsp; </b>
+            {queryUser && (
+              <span onClick={this.onClearUser} className="pointer">
+                {queryUser.email}
+              </span>
+            )}
+            {queryTags.length > 0 && this.renderTags(queryTags, true)}
+          </div>
+        )}
+
+        {annotations.length < 1 && <div className="panel-alert-list__no-alerts">No Annotations Found</div>}
+
+        <AbstractList
+          items={annotations}
+          renderItem={this.renderItem}
+          getItemKey={item => {
+            return item.id + '';
+          }}
+          className="dashlist"
+        />
+      </div>
+    );
+  }
+}

+ 4 - 0
public/app/plugins/panel/annolist/README.md

@@ -0,0 +1,4 @@
+# Annotation List Panel -  Native Plugin
+
+This Annotations List panel is **included** with Grafana.
+

+ 119 - 0
public/app/plugins/panel/annolist/img/icn-annolist-panel.svg

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="100px" height="100px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
+<g>
+	<g>
+		<path style="fill:#666666;" d="M8.842,11.219h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,11.219z"/>
+		<path style="fill:#666666;" d="M0.008,2.113l2.054-2.054C0.966,0.139,0.089,1.016,0.008,2.113z"/>
+		<polygon style="fill:#666666;" points="0,2.998 0,5.533 5.484,0.05 2.948,0.05 		"/>
+		<polygon style="fill:#666666;" points="6.361,0.05 0,6.411 0,8.946 8.896,0.05 		"/>
+		<path style="fill:#666666;" d="M11.169,2.277c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V2.277z"/>
+		<path style="fill:#666666;" d="M9.654,0.169L0.119,9.704c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,0.812,10.247,0.37,9.654,0.169z"/>
+		<polygon style="fill:#666666;" points="11.169,5.479 5.429,11.219 7.964,11.219 11.169,8.014 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,11.031H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,10.212,89.157,11.031,88.146,11.031z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,23.902h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,23.902z"/>
+		<path style="fill:#666666;" d="M0.008,14.796l2.054-2.054C0.966,12.822,0.089,13.699,0.008,14.796z"/>
+		<polygon style="fill:#666666;" points="0,15.681 0,18.216 5.484,12.733 2.948,12.733 		"/>
+		<polygon style="fill:#666666;" points="6.361,12.733 0,19.094 0,21.629 8.896,12.733 		"/>
+		<path style="fill:#666666;" d="M11.169,14.96c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V14.96z"/>
+		<path style="fill:#666666;" d="M9.654,12.852l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,13.495,10.247,13.053,9.654,12.852z"/>
+		<polygon style="fill:#666666;" points="11.169,18.162 5.429,23.902 7.964,23.902 11.169,20.697 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,23.714H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.83,1.83-1.83h73.281
+		c1.011,0,1.83,0.82,1.83,1.83v7.37C89.977,22.895,89.157,23.714,88.146,23.714z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,36.585h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,36.585z"/>
+		<path style="fill:#666666;" d="M0.008,27.479l2.054-2.054C0.966,25.505,0.089,26.382,0.008,27.479z"/>
+		<polygon style="fill:#666666;" points="0,28.364 0,30.899 5.484,25.416 2.948,25.416 		"/>
+		<polygon style="fill:#666666;" points="6.361,25.416 0,31.777 0,34.312 8.896,25.416 		"/>
+		<path style="fill:#666666;" d="M11.169,27.643c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V27.643z"/>
+		<path style="fill:#666666;" d="M9.654,25.535L0.119,35.07c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,26.178,10.247,25.736,9.654,25.535z"/>
+		<polygon style="fill:#666666;" points="11.169,30.845 5.429,36.585 7.964,36.585 11.169,33.38 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,36.397H14.866c-1.011,0-1.83-0.82-1.83-1.831v-7.37c0-1.011,0.82-1.83,1.83-1.83h73.281
+		c1.011,0,1.83,0.82,1.83,1.83v7.37C89.977,35.578,89.157,36.397,88.146,36.397z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,49.268h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,49.268z"/>
+		<path style="fill:#666666;" d="M0.008,40.162l2.054-2.054C0.966,38.188,0.089,39.065,0.008,40.162z"/>
+		<polygon style="fill:#666666;" points="0,41.047 0,43.582 5.484,38.099 2.948,38.099 		"/>
+		<polygon style="fill:#666666;" points="6.361,38.099 0,44.46 0,46.995 8.896,38.099 		"/>
+		<path style="fill:#666666;" d="M11.169,40.326c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V40.326z"/>
+		<path style="fill:#666666;" d="M9.654,38.218l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,38.861,10.247,38.419,9.654,38.218z"/>
+		<polygon style="fill:#666666;" points="11.169,43.528 5.429,49.268 7.964,49.268 11.169,46.063 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,49.08H14.866c-1.011,0-1.83-0.82-1.83-1.831v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,48.261,89.157,49.08,88.146,49.08z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,61.951h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,61.951z"/>
+		<path style="fill:#666666;" d="M0.008,52.845l2.054-2.054C0.966,50.871,0.089,51.748,0.008,52.845z"/>
+		<polygon style="fill:#666666;" points="0,53.73 0,56.265 5.484,50.782 2.948,50.782 		"/>
+		<polygon style="fill:#666666;" points="6.361,50.782 0,57.143 0,59.678 8.896,50.782 		"/>
+		<path style="fill:#666666;" d="M11.169,53.009c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V53.009z"/>
+		<path style="fill:#666666;" d="M9.654,50.901l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,51.544,10.247,51.102,9.654,50.901z"/>
+		<polygon style="fill:#666666;" points="11.169,56.211 5.429,61.951 7.964,61.951 11.169,58.746 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,61.763H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,60.944,89.157,61.763,88.146,61.763z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,74.634h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,74.634z"/>
+		<path style="fill:#666666;" d="M0.008,65.528l2.054-2.054C0.966,63.554,0.089,64.431,0.008,65.528z"/>
+		<polygon style="fill:#666666;" points="0,66.413 0,68.948 5.484,63.465 2.948,63.465 		"/>
+		<polygon style="fill:#666666;" points="6.361,63.465 0,69.826 0,72.361 8.896,63.465 		"/>
+		<path style="fill:#666666;" d="M11.169,65.692c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V65.692z"/>
+		<path style="fill:#666666;" d="M9.654,63.584l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,64.227,10.247,63.785,9.654,63.584z"/>
+		<polygon style="fill:#666666;" points="11.169,68.894 5.429,74.634 7.964,74.634 11.169,71.429 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,74.446H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,73.627,89.157,74.446,88.146,74.446z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,87.317h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,87.317z"/>
+		<path style="fill:#666666;" d="M0.008,78.211l2.054-2.054C0.966,76.237,0.089,77.114,0.008,78.211z"/>
+		<polygon style="fill:#666666;" points="0,79.096 0,81.631 5.484,76.148 2.948,76.148 		"/>
+		<polygon style="fill:#666666;" points="6.361,76.148 0,82.509 0,85.044 8.896,76.148 		"/>
+		<path style="fill:#666666;" d="M11.169,78.375c0-0.068-0.004-0.134-0.01-0.2l-9.132,9.132c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V78.375z"/>
+		<path style="fill:#666666;" d="M9.654,76.267l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,76.91,10.247,76.468,9.654,76.267z"/>
+		<polygon style="fill:#666666;" points="11.169,81.577 5.429,87.317 7.964,87.317 11.169,84.112 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,87.129H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.831,1.83-1.831h73.281
+		c1.011,0,1.83,0.82,1.83,1.831v7.37C89.977,86.31,89.157,87.129,88.146,87.129z"/>
+	<g>
+		<path style="fill:#666666;" d="M8.842,100h0.1c1.228,0,2.227-0.999,2.227-2.227v-0.1L8.842,100z"/>
+		<path style="fill:#666666;" d="M0.008,90.894l2.054-2.054C0.966,88.92,0.089,89.797,0.008,90.894z"/>
+		<polygon style="fill:#666666;" points="0,91.779 0,94.314 5.484,88.831 2.948,88.831 		"/>
+		<polygon style="fill:#666666;" points="6.361,88.831 0,95.192 0,97.727 8.896,88.831 		"/>
+		<path style="fill:#666666;" d="M11.169,91.058c0-0.068-0.004-0.134-0.01-0.2L2.027,99.99c0.066,0.006,0.133,0.01,0.2,0.01h2.325
+			l6.617-6.617V91.058z"/>
+		<path style="fill:#666666;" d="M9.654,88.95l-9.536,9.536c0.201,0.592,0.643,1.073,1.211,1.324l9.649-9.649
+			C10.728,89.593,10.247,89.151,9.654,88.95z"/>
+		<polygon style="fill:#666666;" points="11.169,94.26 5.429,100 7.964,100 11.169,96.795 		"/>
+	</g>
+	<path style="fill:#898989;" d="M88.146,99.812H14.866c-1.011,0-1.83-0.82-1.83-1.83v-7.37c0-1.011,0.82-1.83,1.83-1.83h73.281
+		c1.011,0,1.83,0.82,1.83,1.83v7.37C89.977,98.993,89.157,99.812,88.146,99.812z"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="5.637" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="18.37" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="31.104" r="3.875"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="43.837" r="3.875"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="56.57" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="69.304" r="3.875"/>
+	<circle style="fill:#F7941E;" cx="96.125" cy="82.037" r="3.875"/>
+	<circle style="fill:#898989;" cx="96.125" cy="94.77" r="3.875"/>
+</g>
+</svg>

+ 16 - 0
public/app/plugins/panel/annolist/module.ts

@@ -0,0 +1,16 @@
+import { AnnoListPanel } from './AnnoListPanel';
+import { AnnoOptions, defaults } from './types';
+import { AnnoListEditor } from './AnnoListEditor';
+import { PanelPlugin } from '@grafana/ui';
+
+export const plugin = new PanelPlugin<AnnoOptions>(AnnoListPanel)
+  .setDefaults(defaults)
+  .setEditor(AnnoListEditor)
+
+  // TODO, we should support this directly in the plugin infrastructure
+  .setPanelChangeHandler((options: AnnoOptions, prevPluginId: string, prevOptions: any) => {
+    if (prevPluginId === 'ryantxu-annolist-panel') {
+      return prevOptions as AnnoOptions;
+    }
+    return options;
+  });

+ 20 - 0
public/app/plugins/panel/annolist/plugin.json

@@ -0,0 +1,20 @@
+{
+  "type": "panel",
+  "name": "Annotations list (alpha)",
+  "id": "annolist",
+  "state": "alpha",
+
+  "skipDataQuery": true,
+
+  "info": {
+    "description": "List annotations",
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/icn-annolist-panel.svg",
+      "large": "img/icn-annolist-panel.svg"
+    }
+  }
+}

+ 29 - 0
public/app/plugins/panel/annolist/types.ts

@@ -0,0 +1,29 @@
+export interface AnnoOptions {
+  limit: number;
+  tags: string[];
+  onlyFromThisDashboard: boolean;
+  onlyInTimeRange: boolean;
+
+  showTags: boolean;
+  showUser: boolean;
+  showTime: boolean;
+
+  navigateBefore: string;
+  navigateAfter: string;
+  navigateToPanel: boolean;
+}
+
+export const defaults: AnnoOptions = {
+  limit: 10,
+  tags: [],
+  onlyFromThisDashboard: false,
+  onlyInTimeRange: false,
+
+  showTags: true,
+  showUser: true,
+  showTime: true,
+
+  navigateBefore: '10m',
+  navigateAfter: '10m',
+  navigateToPanel: true,
+};