Просмотр исходного кода

[workflow] add issue popover links to short IDs

David Cramer 7 лет назад
Родитель
Сommit
ef7c903bd3

+ 1 - 0
CHANGES

@@ -9,6 +9,7 @@ Version 8.17 (Unreleased)
 - Added per-key (DSN) rate limits (``project:rate-limits`` feature).
 - Added tsdb statistics for events per-key.
 - Added basic workspace for Visual Studio Code.
+- Added hovercards for Issue IDs in activity entries.
 
 Schema Changes
 ~~~~~~~~~~~~~~

+ 5 - 1
src/sentry/static/sentry/app/components/activity/item.jsx

@@ -4,6 +4,7 @@ import React from 'react';
 import {CommitLink} from '../../views/releases/releaseCommits';
 import Duration from '../../components/duration';
 import Avatar from '../../components/avatar';
+import IssueLink from '../../components/issueLink';
 import {Link} from 'react-router';
 import MemberListStore from '../../stores/memberListStore';
 import TimeSince from '../../components/timeSince';
@@ -52,8 +53,11 @@ const ActivityItem = React.createClass({
     let orgId = this.props.orgId;
     let project = item.project;
     let issue = item.issue;
+
     let issueLink = issue
-      ? <Link to={`/${orgId}/${project.slug}/issues/${issue.id}/`}>{issue.shortId}</Link>
+      ? (<IssueLink orgId={orgId} projectId={project.slug} issue={issue}>
+          {issue.shortId}
+        </IssueLink>)
       : null;
 
     switch (item.type) {

+ 153 - 0
src/sentry/static/sentry/app/components/issueLink.jsx

@@ -0,0 +1,153 @@
+import React from 'react';
+import {Link} from 'react-router';
+
+import ApiMixin from '../mixins/apiMixin';
+import Count from './count';
+import GroupTitle from './group/title';
+import TimeSince from './timeSince';
+
+export default React.createClass({
+  propTypes: {
+    orgId: React.PropTypes.string.isRequired,
+    projectId: React.PropTypes.string.isRequired,
+    issue: React.PropTypes.object.isRequired,
+    card: React.PropTypes.bool
+  },
+
+  mixins: [ApiMixin],
+
+  getDefaultProps() {
+    return {
+      card: true
+    };
+  },
+
+  getInitialState() {
+    return {
+      visible: false
+    };
+  },
+
+  toggleHovercard() {
+    this.setState({
+      visible: !this.state.visible
+    });
+  },
+
+  getMessage(data) {
+    let metadata = data.metadata;
+    switch (data.type) {
+      case 'error':
+        return metadata.value;
+      case 'csp':
+        return metadata.message;
+      default:
+        return data.culprit || '';
+    }
+  },
+
+  renderBody() {
+    let {issue, orgId, projectId} = this.props;
+    let message = this.getMessage(issue);
+
+    let className = '';
+    className += ' type-' + issue.type;
+    className += ' level-' + issue.level;
+    if (issue.isBookmarked) {
+      className += ' isBookmarked';
+    }
+    if (issue.hasSeen) {
+      className += ' hasSeen';
+    }
+    if (issue.status === 'resolved') {
+      className += ' isResolved';
+    }
+
+    return (
+      <div>
+        <div className="hovercard-header">
+          <span>{issue.shortId}</span>
+        </div>
+        <div className="hovercard-body">
+          <div className={className}>
+            <div style={{marginBottom: 20}}>
+              <h3>
+                <GroupTitle data={issue} />
+              </h3>
+              <div className="event-message">
+                <span className="error-level">{issue.level}</span>
+                {message && <span className="message">{message}</span>}
+                {issue.logger &&
+                  <span className="event-annotation">
+                    <Link
+                      to={{
+                        pathname: `/${orgId}/${projectId}/`,
+                        query: {query: 'logger:' + issue.logger}
+                      }}>
+                      {issue.logger}
+                    </Link>
+                  </span>}
+                {issue.annotations.map((annotation, i) => {
+                  return (
+                    <span
+                      className="event-annotation"
+                      key={i}
+                      dangerouslySetInnerHTML={{__html: annotation}}
+                    />
+                  );
+                })}
+              </div>
+            </div>
+            <div className="row row-flex" style={{marginBottom: 20}}>
+              <div className="col-xs-6">
+                <h6>First Seen</h6>
+                <TimeSince date={issue.firstSeen} />
+              </div>
+              <div className="col-xs-6">
+                <h6>Last Seen</h6>
+                <TimeSince date={issue.lastSeen} />
+              </div>
+            </div>
+            <div className="row row-flex">
+              <div className="col-xs-6">
+                <h6>Occurrences</h6>
+                <Count value={issue.count} />
+              </div>
+              <div className="col-xs-6">
+                <h6>Users Affected</h6>
+                <Count value={issue.userCount} />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  },
+
+  render() {
+    let {card, issue, orgId, projectId} = this.props;
+    let {visible} = this.state;
+    if (!card)
+      return (
+        <Link to={`/${orgId}/${projectId}/issues/${issue.id}/`}>
+          {this.props.children}
+        </Link>
+      );
+
+    return (
+      <span
+        onMouseEnter={this.toggleHovercard}
+        onMouseLeave={this.toggleHovercard}
+        style={{position: 'relative'}}>
+        <Link to={`/${orgId}/${projectId}/issues/${issue.id}/`}>
+          {this.props.children}
+        </Link>
+        {visible &&
+          <div className="hovercard">
+            <div className="hovercard-hoverlap" />
+            {this.renderBody()}
+          </div>}
+      </span>
+    );
+  }
+});

+ 64 - 0
src/sentry/static/sentry/less/sentry-hovercard.less

@@ -179,4 +179,68 @@
       color: @70;
     }
   }
+
+  /** ISSUE LINK HAX **/
+  time {
+    padding-left: 0 !important;
+    color: inherit !important;
+  }
+  h3 {
+    color: @gray-darkest;
+    font-size: 14px;
+    margin: 0 0 8px;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+    line-height: 1.1;
+
+    em {
+      font-style: normal;
+      font-weight: 400;
+      color: @70;
+      font-size: 90%;
+    }
+  }
+  .event-message, .event-meta {
+    line-height: 1.2;
+    font-size: 12px;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+  }
+  .error-level,
+  .event-type {
+    padding: 0;
+    border-radius: 2px;
+    margin-right: 5px;
+    display: inline-block;
+    position: relative;
+    top: 3px;
+    .square(9px);
+    text-indent: -9999em;
+    display: inline-block;
+    border-radius: 9px;
+  }
+  .event-meta {
+    font-size: 80%;
+    margin-top: 6px;
+  }
+  .event-annotation {
+    display: inline-block;
+    margin: 0 0 5px 9px;
+    font-size: 13px;
+    line-height: 1;
+    border-left: 1px solid lighten(@trim, 6);
+    padding-left: 9px;
+    height: 13px;
+
+    a {
+      color: lighten(@gray, 12);
+
+      &:hover {
+        color: @gray;
+      }
+    }
+  }
+  /** /ISSUE GROUP HAX **/
 }