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

Assistant updates (#13852)

- Converts the ping/drawer model to a hovercard model (see screenshot)
- Removes "cues" -- now all guides start automatically (most users were not even noticing the cue)
- Guide text is less verbose now
- Removes some unnecessary guides
- Removes the feedback button at the end of a guide
- Adds amplitude tracking
adhiraj 5 лет назад
Родитель
Сommit
2de30158a4

+ 13 - 0
docs-ui/components/hovercard.stories.js

@@ -12,6 +12,17 @@ const positionOptions = {
   right: 'right',
 };
 
+const showOptions = {
+  true: true,
+  false: false,
+  null: null,
+};
+
+const tipColorOptions = {
+  red: 'red',
+  null: null,
+};
+
 storiesOf('UI|Hovercard', module).add(
   'Hovercard',
   withInfo(
@@ -29,6 +40,8 @@ storiesOf('UI|Hovercard', module).add(
         header={text('Header', 'Hovercard Header')}
         body={text('Body', 'Hovercard body (can also be a React node)')}
         position={select('position', positionOptions, 'top', 'Hovercard positioning')}
+        show={select('show', showOptions, null, 'Force show/unshow')}
+        tipColor={select('tipColor', tipColorOptions, null, 'Tip color')}
       >
         Hover over me
       </Hovercard>

+ 31 - 136
src/sentry/assistant/guides.py

@@ -5,7 +5,6 @@ from django.utils.translation import ugettext_lazy as _
 
 # Guide Schema
 # id (text, required): unique id
-# cue (text):  The text used to prompt the user to initiate the guide.
 # required_targets (list): An empty list will cause the guide to be shown regardless
 #                          of page/targets presence.
 # steps (list): List of steps
@@ -20,23 +19,21 @@ from django.utils.translation import ugettext_lazy as _
 #                          otherwise the anchor will be pinged and scrolled to. If you'd like
 #                          your step to show always or have a step is not tied to a specific
 #                          element but you'd still like it to be shown, set this as None.
-# guide_type (text, optional): "guide" or "tip" (defaults to guide). If it's a tip, the cue won't
-#     be shown, and you should also specify the fields "cta_text" and "cta_link", which would
-#     replace the "Was this guide useful" message at the end with the CTA and a dismiss button.
-# cta_text (text, conditional): CTA button text on the last step of a tip. Must be present if
-#     guide_type = tip.
-# cta_link (text, conditional): Where the CTA button points to. Must be present if guide_type = tip.
 
 GUIDES = {
     'issue': {
         'id': 1,
-        'cue': _('Get a tour of the issue page'),
-        'required_targets': ['exception'],
+        'required_targets': ['issue_title', 'exception'],
         'steps': [
+            {
+                'title': _('Issue Details'),
+                'message': _('The issue page contains all the details about an issue. Let\'s get started.'),
+                'target': 'issue_title',
+            },
             {
                 'title': _('Stacktrace'),
                 'message': _(
-                    'See the sequence of function calls that led to the error, and in some cases '
+                    'See the sequence of function calls that led to the error, and '
                     'global/local variables for each stack frame.'),
                 'target': 'exception',
             },
@@ -52,157 +49,55 @@ GUIDES = {
             {
                 'title': _('Tags'),
                 'message': _(
-                    'Tags are arbitrary key-value pairs you can send with an event. Events can be '
-                    'filtered by tags, allowing you to do things like search for all events from '
-                    'a specific machine, browser or release. The sidebar on the right shows you '
-                    'the distribution of tags for all events in this event group.'),
+                    'Attach arbitrary key-value pairs to each event which you can search and filter on. '
+                    'View a heatmap of all tags for an issue on the right panel. '),
                 'target': 'tags',
             },
             {
                 'title': _('Resolve'),
                 'message': _(
-                    'Resolving an issue removes it from the default dashboard view of unresolved '
-                    'issues. You can ask Sentry to <a href="/settings/account/notifications/" target="_blank"> '
-                    'alert you</a> when a resolved issue re-occurs.'),
+                    'Resolve an issue to remove it from your issue list. '
+                    'Sentry can also <a href="/settings/account/notifications/" target="_blank"> '
+                    'alert you</a> when a new issue occurs or a resolved issue re-occurs.'),
                 'target': 'resolve',
             },
             {
-                'title': _('Issue Number'),
-                'message': _(
-                    'This is a unique identifier for the issue and can be included in a commit '
-                    'message to tell Sentry to resolve the issue when the commit gets deployed. '
-                    'See <a href="https://docs.sentry.io/learn/releases/" target="_blank">Releases</a> '
-                    'to learn more.'),
-                'target': 'issue_number',
-            },
-            {
-                'title': _('Issue Tracking'),
+                'title': _('Delete and Ignore'),
                 'message': _(
-                    'Create issues in your project management tool from within Sentry. See a list '
-                    'of all integrations <a href="https://docs.sentry.io/integrations/" target="_blank">here</a>.'),
-                'target': 'issue_tracking',
+                    'Delete an issue to remove it from your issue list until it happens again. '
+                    'Ignore an issue to remove it permanently or until certain conditions are met.'),
+                'target': 'ignore_delete_discard',
             },
             {
-                'title': _('Ignore, Delete and Discard'),
+                'title': _('Issue Number'),
                 'message': _(
-                    'Ignoring an issue silences notifications and removes it from your feeds. '
-                    'Deleting an issue deletes its data and causes a new issue to be created if it '
-                    'happens again. Delete & Discard (available on the medium plan and higher) '
-                    'deletes most of the issue\'s data and discards future events matching the '
-                    'issue before they reach your stream. This is useful to permanently ignore '
-                    'errors you don\'t care about.'),
-                'target': 'ignore_delete_discard',
+                    'Include this unique identifier in your commit message to have Sentry automatically '
+                    'resolve the issue when your code is deployed. '
+                    '<a href="https://docs.sentry.io/learn/releases/" target="_blank">Learn more</a>.'),
+                'target': 'issue_number',
             },
             {
-                'title': _('Issue Owners'),
+                'title': _('Ownership Rules'),
                 'message': _(
-                    'Define users or teams that are responsible for specific paths or URLS so '
-                    'that notifications can be routed to the correct people. Learn more '
-                    '<a href="https://docs.sentry.io/learn/issue-owners/" target="_blank">here</a>.'),
+                    'Define users or teams responsible for specific file paths or URLs so '
+                    'that alerts can be routed to the right person. '
+                    '<a href="https://docs.sentry.io/learn/issue-owners/" target="_blank">Learn more</a>.'),
                 'target': 'owners',
             },
         ],
     },
-    'releases': {
-        'id': 2,
-        'cue': _('What are releases?'),
-        'required_targets': ['releases'],
-        'steps': [
-            {
-                'title': _('Releases'),
-                'message': _('A release is a specific version of your code.'
-                             'When you tell Sentry about your releases, it can '
-                             'predict which commits caused an error and who might be a likely '
-                             'owner.'),
-                'target': 'releases',
-            },
-            {
-                'title': _('Releases'),
-                'message': _('Sentry does this by tying together commits in the release, files '
-                             'touched by those commits, files observed in the stacktrace, and '
-                             'authors of those files. Learn more about releases '
-                             '<a href="https://docs.sentry.io/learn/releases/" target="_blank">here</a>.'),
-                'target': 'releases',
-            },
-        ]
-    },
-
-    'event_issue': {
+    'issue_stream': {
         'id': 3,
-        'cue': _('Learn about the issue stream'),
-        'required_targets': ['issues'],
+        'required_targets': ['issue_stream'],
         'steps': [
-            {
-                'title': _('Events'),
-                'message': _(
-                    'When your application throws an error, that error is captured by Sentry as an event.'),
-                'target': 'events',
-            },
             {
                 'title': _('Issues'),
                 'message': _(
-                    'Individual events are then automatically rolled up and grouped into Issues with other similar events. '
-                    'A single issue can represent anywhere from one to thousands of individual events, depending on how many '
-                    'times a specific error is thrown. '),
-                'target': 'issues',
-            },
-            {
-                'title': _('Users'),
-                'message': _(
-                    'Sending user data to Sentry will unlock a number of features, primarily the ability to drill '
-                    'down into the number of users affected by an issue. '
-                    'Learn how easy it is to '
-                    '<a href="https://docs.sentry.io/learn/context/#capturing-the-user" target="_blank">set this up </a>today.'),
-                'target': 'users',
-            },
-        ]
-    },
-    # Ideally, this would only be sent if the organization has never
-    # customized alert rules (as per FeatureAdoption)
-    'alert_rules': {
-        'id': 5,
-        'cue': _('Tips for alert rules'),
-        'required_targets': ['alert_conditions'],
-        'steps': [
-            {
-                'title': _('Reduce Inbox Noise'),
-                'message': _('Sentry, by default, alerts on every new issue via email. '
-                             'If that\'s too noisy, send the alerts to a service like Slack to '
-                             'reduce inbox noise.<br><br> Enabling <a href="https://sentry.io/settings/account/notifications/#weeklyReports" target="_blank">'
-                             'weekly reports</a> can also help you stay on top of issues without '
-                             'getting overwhelmed.'),
-                'target': 'alert_conditions',
-            },
-            {
-                'title': _('Prioritize Alerts'),
-                'message': _('Not all alerts are equally important. Send the important ones to a '
-                             'service like PagerDuty. <a href="https://blog.sentry.io/2017/10/12/proactive-alert-rules" target="_blank">Learn more</a> '
-                             'about prioritizing alerts.'),
-                'target': 'alert_actions',
-            },
-            {
-                'title': _('Fine-tune notifications'),
-                'message': _('You can control alerts both at the project and the user level. '
-                             'Go to <a href="/account/settings/notifications/" target="_blank">Account Notifications</a> '
-                             'to choose which project\'s alert or workflow notifications you\'d like to receive.'),
-                'target': None,
-            },
-        ],
-    },
-    'alert_reminder_1': {
-        'id': 6,
-        'guide_type': 'tip',
-        'required_targets': ['project_details'],
-        'steps': [
-            {
-                'title': _('Alert Rules'),
-                'message': _('This project received ${numEvents} events in the last 30 days but doesn\'t have '
-                             'custom alerts. Customizing alerts gives you more control over how you get '
-                             'notified of issues. Learn more <a href="https://sentry.io/_/resources/customer-success/alert-rules/?referrer=assistant" target="_blank">here</a>.'),
-                'target': 'project_details',
+                    'Sentry automatically groups similar events together into an issue. Similarity '
+                    'is determined by stacktrace and other factors. '
+                    '<a href="https://docs.sentry.io/data-management/rollups/" target="_blank">Learn more</a>. '),
+                'target': 'issue_stream',
             },
         ],
-        'cta_text': _('Customize Alerts'),
-        'cta_link': '/settings/${orgSlug}/projects/${projectSlug}/alerts/rules/',
     },
 }

+ 24 - 9
src/sentry/static/sentry/app/actionCreators/guides.jsx

@@ -1,6 +1,6 @@
 import {Client} from 'app/api';
 import GuideActions from 'app/actions/guideActions';
-import {analytics} from 'app/utils/analytics';
+import {trackAnalyticsEvent} from 'app/utils/analytics';
 
 const api = new Client();
 
@@ -29,22 +29,31 @@ export function closeGuide() {
   GuideActions.closeGuide();
 }
 
-export function recordFinish(guideId, useful) {
+export function dismissGuide(guideId, step, org) {
+  recordDismiss(guideId, step, org);
+  closeGuide();
+}
+
+export function recordFinish(guideId, org) {
   api.request('/assistant/', {
     method: 'PUT',
     data: {
       guide_id: guideId,
       status: 'viewed',
-      useful,
     },
   });
-  analytics('assistant.guide_finished', {
+  const data = {
+    eventKey: 'assistant.guide_finished',
+    eventName: 'Assistant Guide Finished',
     guide: guideId,
-    useful,
-  });
+  };
+  if (org) {
+    data.organization_id = org.id;
+  }
+  trackAnalyticsEvent(data);
 }
 
-export function recordDismiss(guideId, step) {
+export function recordDismiss(guideId, step, org) {
   api.request('/assistant/', {
     method: 'PUT',
     data: {
@@ -52,8 +61,14 @@ export function recordDismiss(guideId, step) {
       status: 'dismissed',
     },
   });
-  analytics('assistant.guide_dismissed', {
+  const data = {
+    eventKey: 'assistant.guide_dismissed',
+    eventName: 'Assistant Guide Dismissed',
     guide: guideId,
     step,
-  });
+  };
+  if (org) {
+    data.organization_id = org.id;
+  }
+  trackAnalyticsEvent(data);
 }

+ 0 - 2
src/sentry/static/sentry/app/components/actions/ignore.jsx

@@ -10,7 +10,6 @@ import CustomIgnoreCountModal from 'app/components/customIgnoreCountModal';
 import CustomIgnoreDurationModal from 'app/components/customIgnoreDurationModal';
 import ActionLink from 'app/components/actions/actionLink';
 import Tooltip from 'app/components/tooltip';
-import GuideAnchor from 'app/components/assistant/guideAnchor';
 
 export default class IgnoreActions extends React.Component {
   static propTypes = {
@@ -123,7 +122,6 @@ export default class IgnoreActions extends React.Component {
           windowChoices={this.getIgnoreWindows()}
         />
         <div className="btn-group">
-          <GuideAnchor target="ignore_delete_discard" type="text" />
           <ActionLink
             {...actionLinkProps}
             title="Ignore"

+ 75 - 74
src/sentry/static/sentry/app/components/actions/resolve.jsx

@@ -126,81 +126,82 @@ export default class ResolveActions extends React.Component {
           orgId={orgId}
           projectId={projectId}
         />
-        <div className="btn-group">
-          <GuideAnchor target="resolve" type="text" />
-          <ActionLink
-            {...actionLinkProps}
-            title="Resolve"
-            className={buttonClass}
-            onAction={() => onUpdate({status: 'resolved'})}
-          >
-            <span className="icon-checkmark hidden-xs" style={{marginRight: 5}} />
-            {t('Resolve')}
-          </ActionLink>
+        <GuideAnchor target="resolve">
+          <div className="btn-group">
+            <ActionLink
+              {...actionLinkProps}
+              title="Resolve"
+              className={buttonClass}
+              onAction={() => onUpdate({status: 'resolved'})}
+            >
+              <span className="icon-checkmark hidden-xs" style={{marginRight: 5}} />
+              {t('Resolve')}
+            </ActionLink>
 
-          <DropdownLink
-            key="resolve-dropdown"
-            caret={true}
-            className={buttonClass}
-            title=""
-            alwaysRenderMenu
-            disabled={disableDropdown || disabled}
-          >
-            <MenuItem header={true}>{t('Resolved In')}</MenuItem>
-            <MenuItem noAnchor={true}>
-              <Tooltip title={actionTitle} containerDisplayMode="block">
-                <ActionLink
-                  {...actionLinkProps}
-                  onAction={() => {
-                    return (
-                      hasRelease &&
-                      onUpdate({
-                        status: 'resolved',
-                        statusDetails: {
-                          inNextRelease: true,
-                        },
-                      })
-                    );
-                  }}
-                >
-                  {t('The next release')}
-                </ActionLink>
-              </Tooltip>
-              <Tooltip title={actionTitle} containerDisplayMode="block">
-                <ActionLink
-                  {...actionLinkProps}
-                  onAction={() => {
-                    return (
-                      hasRelease &&
-                      onUpdate({
-                        status: 'resolved',
-                        statusDetails: {
-                          inRelease: latestRelease ? latestRelease.version : 'latest',
-                        },
-                      })
-                    );
-                  }}
-                >
-                  {latestRelease
-                    ? t(
-                        'The current release (%s)',
-                        getShortVersion(latestRelease.version)
-                      )
-                    : t('The current release')}
-                </ActionLink>
-              </Tooltip>
-              <Tooltip title={actionTitle} containerDisplayMode="block">
-                <ActionLink
-                  {...actionLinkProps}
-                  onAction={() => hasRelease && this.setState({modal: true})}
-                  shouldConfirm={false}
-                >
-                  {t('Another version\u2026')}
-                </ActionLink>
-              </Tooltip>
-            </MenuItem>
-          </DropdownLink>
-        </div>
+            <DropdownLink
+              key="resolve-dropdown"
+              caret={true}
+              className={buttonClass}
+              title=""
+              alwaysRenderMenu
+              disabled={disableDropdown || disabled}
+            >
+              <MenuItem header={true}>{t('Resolved In')}</MenuItem>
+              <MenuItem noAnchor={true}>
+                <Tooltip title={actionTitle} containerDisplayMode="block">
+                  <ActionLink
+                    {...actionLinkProps}
+                    onAction={() => {
+                      return (
+                        hasRelease &&
+                        onUpdate({
+                          status: 'resolved',
+                          statusDetails: {
+                            inNextRelease: true,
+                          },
+                        })
+                      );
+                    }}
+                  >
+                    {t('The next release')}
+                  </ActionLink>
+                </Tooltip>
+                <Tooltip title={actionTitle} containerDisplayMode="block">
+                  <ActionLink
+                    {...actionLinkProps}
+                    onAction={() => {
+                      return (
+                        hasRelease &&
+                        onUpdate({
+                          status: 'resolved',
+                          statusDetails: {
+                            inRelease: latestRelease ? latestRelease.version : 'latest',
+                          },
+                        })
+                      );
+                    }}
+                  >
+                    {latestRelease
+                      ? t(
+                          'The current release (%s)',
+                          getShortVersion(latestRelease.version)
+                        )
+                      : t('The current release')}
+                  </ActionLink>
+                </Tooltip>
+                <Tooltip title={actionTitle} containerDisplayMode="block">
+                  <ActionLink
+                    {...actionLinkProps}
+                    onAction={() => hasRelease && this.setState({modal: true})}
+                    shouldConfirm={false}
+                  >
+                    {t('Another version\u2026')}
+                  </ActionLink>
+                </Tooltip>
+              </MenuItem>
+            </DropdownLink>
+          </div>
+        </GuideAnchor>
       </div>
     );
   }

+ 135 - 111
src/sentry/static/sentry/app/components/assistant/guideAnchor.jsx

@@ -1,25 +1,34 @@
 import PropTypes from 'prop-types';
-import classNames from 'classnames';
 import React from 'react';
-import styled, {keyframes} from 'react-emotion';
+import styled from 'react-emotion';
+import {css} from 'emotion';
+import $ from 'jquery';
 import createReactClass from 'create-react-class';
 import Reflux from 'reflux';
-import $ from 'jquery';
-import {registerAnchor, unregisterAnchor} from 'app/actionCreators/guides';
+import theme from 'app/utils/theme';
+import {
+  registerAnchor,
+  unregisterAnchor,
+  nextStep,
+  closeGuide,
+  recordFinish,
+  dismissGuide,
+} from 'app/actionCreators/guides';
 import GuideStore from 'app/stores/guideStore';
-import {expandOut} from 'app/styles/animations';
-
-// A guide anchor provides a ripple-effect on an element to draw attention to it.
-// Guide anchors register with the guide store, which uses this information to
-// determine which guides can be shown on the page. Multiple guide anchors on
-// a page can have the same `target` property, which will make all of them glow
-// when a step in the guide matches that target (although only one of them will
-// be scrolled to).
+import Hovercard from 'app/components/hovercard';
+import Button from 'app/components/button';
+import space from 'app/styles/space';
+import {t} from 'app/locale';
+import {CloseIcon} from 'app/components/assistant/styles';
+
+// A GuideAnchor puts an informative hovercard around an element.
+// Guide anchors register with the GuideStore, which uses registrations
+// from one or more anchors on the page to determine which guides can
+// be shown on the page.
 const GuideAnchor = createReactClass({
   propTypes: {
     target: PropTypes.string.isRequired,
-    // The `invisible` anchor type can be used for guides not attached to specific elements.
-    type: PropTypes.oneOf(['text', 'button', 'invisible']),
+    position: PropTypes.string,
   },
 
   mixins: [Reflux.listenTo(GuideStore, 'onGuideStateChange')],
@@ -35,10 +44,10 @@ const GuideAnchor = createReactClass({
   },
 
   componentDidUpdate(prevProps, prevState) {
-    if (!prevState.active && this.state.active && this.props.type !== 'invisible') {
+    if (this.containerElement && !prevState.active && this.state.active) {
       const windowHeight = $(window).height();
       $('html,body').animate({
-        scrollTop: $(this.anchorElement).offset().top - windowHeight / 4,
+        scrollTop: $(this.containerElement).offset().top - windowHeight / 2,
       });
     }
   },
@@ -48,123 +57,138 @@ const GuideAnchor = createReactClass({
   },
 
   onGuideStateChange(data) {
-    if (
+    const active =
       data.currentGuide &&
-      data.currentStep > 0 &&
-      data.currentGuide.steps[data.currentStep - 1].target === this.props.target &&
-      // TODO(adhiraj): It would be more correct to let invisible anchors become active,
-      // and use CSS to make them invisible.
-      this.props.type !== 'invisible'
-    ) {
-      this.setState({active: true});
-    } else {
-      this.setState({active: false});
-    }
+      data.currentGuide.steps[data.currentStep].target === this.props.target;
+    this.setState({
+      active,
+      guide: data.currentGuide,
+      step: data.currentStep,
+      org: data.org,
+      messageVariables: {
+        orgSlug: data.org && data.org.slug,
+        projectSlug: data.project && data.project.slug,
+      },
+    });
+  },
+
+  interpolate(template, variables) {
+    const regex = /\${([^{]+)}/g;
+    return template.replace(regex, (match, g1) => {
+      return variables[g1.trim()];
+    });
+  },
+
+  /* Terminology:
+   - A guide can be FINISHED by clicking one of the buttons in the last step.
+   - A guide can be DISMISSED by x-ing out of it at any step except the last (where there is no x).
+   - In both cases we consider it CLOSED.
+  */
+  handleFinish(e) {
+    e.stopPropagation();
+    const {guide, org} = this.state;
+    recordFinish(guide.id, org);
+    closeGuide();
+  },
+
+  handleNextStep(e) {
+    e.stopPropagation();
+    nextStep();
+  },
+
+  handleDismiss(e) {
+    e.stopPropagation();
+    const {guide, step, org} = this.state;
+    dismissGuide(guide.id, step, org);
   },
 
   render() {
-    const {target, type} = this.props;
-    const {active} = this.state;
+    const {active, guide, step, messageVariables} = this.state;
     if (!active) {
       return this.props.children ? this.props.children : null;
     }
 
+    const body = (
+      <GuideContainer>
+        <GuideInputRow>
+          <StyledTitle>{guide.steps[step].title}</StyledTitle>
+          {step < guide.steps.length - 1 && (
+            <CloseLink onClick={this.handleDismiss} href="#" data-test-id="close-button">
+              <CloseIcon />
+            </CloseLink>
+          )}
+        </GuideInputRow>
+        <StyledContent>
+          <div
+            dangerouslySetInnerHTML={{
+              __html: this.interpolate(guide.steps[step].message, messageVariables),
+            }}
+          />
+          <div css={{marginTop: '1em'}}>
+            <div>
+              {step < guide.steps.length - 1 ? (
+                <Button priority="success" size="small" onClick={this.handleNextStep}>
+                  {t('Next')} &rarr;
+                </Button>
+              ) : (
+                <Button priority="success" size="small" onClick={this.handleFinish}>
+                  {t(guide.steps.length === 1 ? 'Got It' : 'Done')}
+                </Button>
+              )}
+            </div>
+          </div>
+        </StyledContent>
+      </GuideContainer>
+    );
+
     return (
-      <GuideAnchorContainer innerRef={el => (this.anchorElement = el)} type={type}>
-        {this.props.children}
-        <StyledGuideAnchor
-          className={classNames('guide-anchor-ping', target)}
-          active={this.state.active}
-        >
-          <StyledGuideAnchorRipples />
-        </StyledGuideAnchor>
-      </GuideAnchorContainer>
+      <Hovercard
+        show={true}
+        body={body}
+        bodyClassName={css`
+          background-color: ${theme.greenDark};
+          margin: -1px;
+        `}
+        tipColor={theme.greenDark}
+        position={this.props.position}
+      >
+        <span ref={el => (this.containerElement = el)}>{this.props.children}</span>
+      </Hovercard>
     );
   },
 });
 
-export const conditionalGuideAnchor = (condition, target, type, children) => {
-  return condition
-    ? React.createElement(GuideAnchor, {target, type}, children)
-    : children;
-};
-
-const recedeAnchor = keyframes`
-  0% {
-    transform: scale(3, 3);
-    opacity: 1;
-  }
+const GuideContainer = styled('div')`
+  background-color: ${p => p.theme.greenDark};
+  border-color: ${p => p.theme.greenLight};
+  color: ${p => p.theme.offWhite};
+`;
 
-  100% {
-    transform: scale(1, 1);
-    opacity: 0.75;
+const CloseLink = styled('a')`
+  color: ${p => p.theme.offWhite};
+  &:hover {
+    color: ${p => p.theme.offWhite};
   }
+  display: flex;
 `;
 
-const GuideAnchorContainer = styled('div')`
-  ${p =>
-    p.type === 'text' &&
-    `
-      display: inline-block;
-      position: relative;
-    `};
+const GuideInputRow = styled('div')`
+  display: flex;
+  align-items: center;
 `;
 
-const StyledGuideAnchor = styled('div')`
-  width: 20px;
-  height: 20px;
-  cursor: pointer;
-  z-index: 999;
-  position: absolute;
-  pointer-events: none;
-  visibility: hidden;
-
-  ${p =>
-    p.active
-      ? `
-    visibility: visible;
-    animation: ${recedeAnchor} 5s ease-in forwards;
-  `
-      : ''};
+const StyledTitle = styled('div')`
+  font-weight: bold;
+  font-size: 1.3em;
+  flex-grow: 1;
 `;
 
-const StyledGuideAnchorRipples = styled('div')`
-  animation: ${expandOut} 1.5s ease-out infinite;
-  width: 100%;
-  height: 100%;
-  top: 0;
-  left: 0;
-
-  &,
-  &:before,
-  &:after {
-    position: absolute;
-    display: block;
-    left: calc(50% - 10px);
-    top: calc(50% - 10px);
-    background-color: ${p => p.theme.greenTransparent};
-    border-radius: 50%;
-  }
-
-  &:before,
-  &:after {
-    content: '';
-  }
-
-  &:before {
-    width: 70%;
-    height: 70%;
-    left: calc(50% - 7px);
-    top: calc(50% - 7px);
-    background-color: ${p => p.theme.greenTransparent};
-  }
+const StyledContent = styled('div')`
+  margin-top: ${space(1)};
+  line-height: 1.5;
 
-  &:after {
-    width: 50%;
-    height: 50%;
-    left: calc(50% - 5px);
-    top: calc(50% - 5px);
-    color: ${p => p.theme.green};
+  a {
+    color: ${p => p.theme.greenLight};
   }
 `;
 

+ 0 - 211
src/sentry/static/sentry/app/components/assistant/guideDrawer.jsx

@@ -1,211 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {withRouter} from 'react-router';
-import Reflux from 'reflux';
-import createReactClass from 'create-react-class';
-import styled from 'react-emotion';
-import Button from 'app/components/button';
-import GuideStore from 'app/stores/guideStore';
-import space from 'app/styles/space';
-import {t} from 'app/locale';
-import {
-  closeGuide,
-  fetchGuides,
-  nextStep,
-  recordDismiss,
-  recordFinish,
-} from 'app/actionCreators/guides';
-import {
-  AssistantContainer,
-  CloseIcon,
-  CueContainer,
-  CueIcon,
-  CueText,
-} from 'app/components/assistant/styles';
-
-/* GuideDrawer is what slides up when the user clicks on a guide cue. */
-const GuideDrawer = createReactClass({
-  displayName: 'GuideDrawer',
-  propTypes: {
-    router: PropTypes.object,
-  },
-  mixins: [Reflux.listenTo(GuideStore, 'onGuideStateChange')],
-
-  getInitialState() {
-    return {
-      guide: null,
-      step: 0,
-      messageVariables: {},
-    };
-  },
-
-  componentDidMount() {
-    fetchGuides();
-  },
-
-  onGuideStateChange(data) {
-    this.setState({
-      guide: data.currentGuide,
-      step: data.currentStep,
-      messageVariables: {
-        orgSlug: data.org && data.org.slug,
-        projectSlug: data.project && data.project.slug,
-        numEvents: data.project && data.projectStats.get(parseInt(data.project.id, 10)),
-      },
-    });
-  },
-
-  /* Terminology:
-   - A guide can be FINISHED by clicking one of the buttons in the last step.
-   - A guide can be DISMISSED by x-ing out of it at any step except the last (where there is no x).
-   - In both cases we consider it CLOSED.
-  */
-  handleFinish(useful) {
-    recordFinish(this.state.guide.id, useful);
-    closeGuide();
-    // This is a bit racy. Technically the correct thing to do would be to wait until closeGuide
-    // has updated the guide store and triggered a component state change. But it doesn't seem
-    // to cause any issues in practice.
-    if (useful && this.state.guide.cta_link) {
-      const link = this.interpolate(
-        this.state.guide.cta_link,
-        this.state.messageVariables
-      );
-      this.props.router.push(link);
-    }
-  },
-
-  handleDismiss(e) {
-    e.stopPropagation();
-    recordDismiss(this.state.guide.id, this.state.step);
-    closeGuide();
-  },
-
-  interpolate(template, variables) {
-    const regex = /\${([^{]+)}/g;
-    return template.replace(regex, (match, g1) => {
-      return variables[g1.trim()];
-    });
-  },
-
-  render() {
-    const {guide, step, messageVariables} = this.state;
-
-    if (!guide) {
-      return null;
-    }
-
-    if (step === 0) {
-      return (
-        <StyledCueContainer onClick={nextStep} data-test-id="assistant-cue">
-          {<CueIcon hasGuide={true} />}
-          <StyledCueText>{guide.cue}</StyledCueText>
-          <div style={{display: 'flex'}} onClick={this.handleDismiss}>
-            <CloseIcon />
-          </div>
-        </StyledCueContainer>
-      );
-    }
-
-    const isTip = guide.guide_type === 'tip';
-
-    return (
-      <GuideContainer>
-        <GuideInputRow>
-          <CueIcon hasGuide={true} />
-          <StyledTitle>{guide.steps[step - 1].title}</StyledTitle>
-          {step < guide.steps.length && (
-            <div
-              className="close-button"
-              style={{display: 'flex'}}
-              onClick={this.handleDismiss}
-            >
-              <CloseIcon />
-            </div>
-          )}
-        </GuideInputRow>
-        <StyledContent>
-          <div
-            dangerouslySetInnerHTML={{
-              __html: this.interpolate(guide.steps[step - 1].message, messageVariables),
-            }}
-          />
-          <div style={{marginTop: '1em'}}>
-            {step < guide.steps.length ? (
-              <div>
-                <Button priority="success" size="small" onClick={nextStep}>
-                  {t('Next')} &rarr;
-                </Button>
-              </div>
-            ) : (
-              <div style={{textAlign: 'center'}}>
-                {!isTip && <p>{t('Did you find this guide useful?')}</p>}
-                <Button
-                  priority="success"
-                  size="small"
-                  onClick={() => this.handleFinish(true)}
-                >
-                  {isTip ? guide.cta_text : <span>{t('Yes')} &nbsp; &#x2714;</span>}
-                </Button>
-                <Button
-                  priority="success"
-                  size="small"
-                  style={{marginLeft: '0.25em'}}
-                  onClick={() => this.handleFinish(false)}
-                >
-                  {isTip ? t('Dismiss') : <span>{t('No')} &nbsp; &#x2716;</span>}
-                </Button>
-              </div>
-            )}
-          </div>
-        </StyledContent>
-      </GuideContainer>
-    );
-  },
-});
-
-const StyledCueText = styled(CueText)`
-  width: auto;
-  opacity: 1;
-  margin-left: ${space(1)};
-`;
-
-const StyledCueContainer = styled(CueContainer)`
-  right: 50%;
-  transform: translateX(50%);
-  background-color: ${p => p.theme.greenDark};
-  border-color: ${p => p.theme.greenLight};
-  color: ${p => p.theme.offWhite};
-`;
-
-const GuideContainer = styled(AssistantContainer)`
-  background-color: ${p => p.theme.greenDark};
-  border-color: ${p => p.theme.greenLight};
-  color: ${p => p.theme.offWhite};
-  height: auto;
-  right: 50%;
-  transform: translateX(50%);
-`;
-
-const GuideInputRow = styled('div')`
-  display: flex;
-  align-items: center;
-`;
-
-const StyledTitle = styled('div')`
-  font-size: 1.5em;
-  margin-left: 0.5em;
-  flex-grow: 1;
-`;
-
-const StyledContent = styled('div')`
-  margin-top: ${space(1)};
-  line-height: 1.5;
-
-  a {
-    color: ${p => p.theme.greenLight};
-  }
-`;
-
-export {GuideDrawer};
-export default withRouter(GuideDrawer);

+ 0 - 22
src/sentry/static/sentry/app/components/assistant/helper.jsx

@@ -1,22 +0,0 @@
-import React from 'react';
-import styled from 'react-emotion';
-import GuideDrawer from 'app/components/assistant/guideDrawer';
-
-/* AssistantHelper is responsible for rendering the guide and support drawers. */
-export default class AssistantHelper extends React.Component {
-  render() {
-    return (
-      <StyledHelper>
-        <GuideDrawer />
-      </StyledHelper>
-    );
-  }
-}
-
-/* this globally controls the size of the component */
-const StyledHelper = styled('div')`
-  font-size: 1.4rem;
-  @media (max-width: 600px) {
-    display: none;
-  }
-`;

+ 10 - 15
src/sentry/static/sentry/app/components/events/eventDataSection.jsx

@@ -50,10 +50,15 @@ class EventDataSection extends React.Component {
       raw,
       wrapTitle,
     } = this.props;
-    const guideAnchor =
-      type === 'tags' && hideGuide === false ? (
-        <GuideAnchor target="tags" type="text" />
-      ) : null;
+
+    let titleNode = wrapTitle ? <h3>{title}</h3> : <div>{title}</div>;
+    if (type === 'tags' && hideGuide === false) {
+      titleNode = (
+        <GuideAnchor target="tags" position="top">
+          {titleNode}
+        </GuideAnchor>
+      );
+    }
 
     return (
       <div className={(className || '') + ' box'}>
@@ -62,17 +67,7 @@ class EventDataSection extends React.Component {
             <a href={'#' + type} className="permalink">
               <em className="icon-anchor" />
             </a>
-            {wrapTitle ? (
-              <h3>
-                {guideAnchor}
-                {title}
-              </h3>
-            ) : (
-              <div>
-                {guideAnchor}
-                {title}
-              </div>
-            )}
+            {titleNode}
             {type === 'extra' && (
               <div className="btn-group pull-right">
                 <a

+ 5 - 4
src/sentry/static/sentry/app/components/events/interfaces/breadcrumbs.jsx

@@ -174,10 +174,11 @@ class BreadcrumbsInterface extends React.Component {
 
     const title = (
       <div>
-        <GuideAnchor target="breadcrumbs" type="text" />
-        <h3>
-          <strong>{t('Breadcrumbs')}</strong>
-        </h3>
+        <GuideAnchor target="breadcrumbs" position="top">
+          <h3>
+            <strong>{t('Breadcrumbs')}</strong>
+          </h3>
+        </GuideAnchor>
         {this.getSearchField()}
       </div>
     );

Некоторые файлы не были показаны из-за большого количества измененных файлов