Browse Source

consolidate all changes (#32740)

Taylan Gocmen 3 years ago
parent
commit
6660d7e6ee

+ 0 - 139
static/app/views/organizationGroupDetails/actions/deleteAction.tsx

@@ -1,139 +0,0 @@
-import {Fragment} from 'react';
-import styled from '@emotion/styled';
-
-import {ModalRenderProps, openModal} from 'sentry/actionCreators/modal';
-import Feature from 'sentry/components/acl/feature';
-import FeatureDisabled from 'sentry/components/acl/featureDisabled';
-import ActionButton from 'sentry/components/actions/button';
-import Button from 'sentry/components/button';
-import ButtonBar from 'sentry/components/buttonBar';
-import Confirm from 'sentry/components/confirm';
-import DropdownMenuControlV2 from 'sentry/components/dropdownMenuControlV2';
-import {IconChevron, IconDelete} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import space from 'sentry/styles/space';
-import {Organization, Project} from 'sentry/types';
-import {analytics} from 'sentry/utils/analytics';
-
-type Props = {
-  disabled: boolean;
-  onDelete: () => void;
-  onDiscard: () => void;
-  organization: Organization;
-  project: Project;
-};
-
-function DeleteAction({disabled, project, organization, onDiscard, onDelete}: Props) {
-  function renderDiscardDisabled({children, ...props}) {
-    return children({
-      ...props,
-      renderDisabled: ({features}: {features: string[]}) => (
-        <FeatureDisabled alert featureName="Discard and Delete" features={features} />
-      ),
-    });
-  }
-
-  function renderDiscardModal({Body, Footer, closeModal}: ModalRenderProps) {
-    return (
-      <Feature
-        features={['projects:discard-groups']}
-        hookName="feature-disabled:discard-groups"
-        organization={organization}
-        project={project}
-        renderDisabled={renderDiscardDisabled}
-      >
-        {({hasFeature, renderDisabled, ...props}) => (
-          <Fragment>
-            <Body>
-              {!hasFeature &&
-                typeof renderDisabled === 'function' &&
-                renderDisabled({...props, hasFeature, children: null})}
-              {t(
-                `Discarding this event will result in the deletion of most data associated with this issue and future events being discarded before reaching your stream. Are you sure you wish to continue?`
-              )}
-            </Body>
-            <Footer>
-              <Button onClick={closeModal}>{t('Cancel')}</Button>
-              <Button
-                style={{marginLeft: space(1)}}
-                priority="primary"
-                onClick={onDiscard}
-                disabled={!hasFeature}
-              >
-                {t('Discard Future Events')}
-              </Button>
-            </Footer>
-          </Fragment>
-        )}
-      </Feature>
-    );
-  }
-
-  function openDiscardModal() {
-    openModal(renderDiscardModal);
-    analytics('feature.discard_group.modal_opened', {
-      org_id: parseInt(organization.id, 10),
-    });
-  }
-
-  return (
-    <ButtonBar merged>
-      <Confirm
-        message={t(
-          'Deleting this issue is permanent. Are you sure you wish to continue?'
-        )}
-        onConfirm={onDelete}
-        disabled={disabled}
-      >
-        <DeleteButton
-          disabled={disabled}
-          title={t('Deletes the issue. A new issue will be created if it happens again.')}
-          tooltipProps={{delay: 300}}
-          aria-label={t('Delete issue')}
-          icon={<IconDelete size="xs" />}
-        />
-      </Confirm>
-      <DropdownMenuControlV2
-        isDisabled={disabled}
-        trigger={({props: triggerProps, ref: triggerRef}) => (
-          <DropdownTrigger
-            ref={triggerRef}
-            {...triggerProps}
-            aria-label={t('More delete options')}
-            size="xsmall"
-            icon={<IconChevron direction="down" size="xs" />}
-            disabled={disabled}
-          />
-        )}
-        menuTitle={t('Delete & Discard')}
-        items={[
-          {
-            key: 'delete',
-            label: t('Delete and discard future events'),
-            onAction: () => openDiscardModal(),
-          },
-        ]}
-      />
-    </ButtonBar>
-  );
-}
-
-const DeleteButton = styled(ActionButton)`
-  ${p =>
-    !p.disabled &&
-    `
-    &:hover {
-      background-color: ${p.theme.button.danger.background};
-      color: ${p.theme.button.danger.color};
-      border-color: ${p.theme.button.danger.border};
-    }
-  `}
-`;
-
-const DropdownTrigger = styled(Button)`
-  box-shadow: none;
-  border-radius: ${p => p.theme.borderRadiusRight};
-  border-left: none;
-`;
-
-export default DeleteAction;

+ 154 - 73
static/app/views/organizationGroupDetails/actions/index.tsx

@@ -1,4 +1,4 @@
-import * as React from 'react';
+import {Component, Fragment, MouseEvent} from 'react';
 import {browserHistory} from 'react-router';
 import styled from '@emotion/styled';
 import {Query} from 'history';
@@ -9,18 +9,23 @@ import {
   addLoadingMessage,
   clearIndicators,
 } from 'sentry/actionCreators/indicator';
-import {openReprocessEventModal} from 'sentry/actionCreators/modal';
+import {
+  ModalRenderProps,
+  openModal,
+  openReprocessEventModal,
+} from 'sentry/actionCreators/modal';
 import GroupActions from 'sentry/actions/groupActions';
 import {Client} from 'sentry/api';
 import Access from 'sentry/components/acl/access';
 import Feature from 'sentry/components/acl/feature';
-import ActionButton from 'sentry/components/actions/button';
+import FeatureDisabled from 'sentry/components/acl/featureDisabled';
 import IgnoreActions from 'sentry/components/actions/ignore';
 import ResolveActions from 'sentry/components/actions/resolve';
 import GuideAnchor from 'sentry/components/assistant/guideAnchor';
+import Button from 'sentry/components/button';
+import DropdownMenuControlV2 from 'sentry/components/dropdownMenuControlV2';
 import Tooltip from 'sentry/components/tooltip';
-import {IconStar} from 'sentry/icons';
-import {IconRefresh} from 'sentry/icons/iconRefresh';
+import {IconEllipsis} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
 import {
@@ -32,6 +37,7 @@ import {
   UpdateResolutionStatus,
 } from 'sentry/types';
 import {Event} from 'sentry/types/event';
+import {analytics} from 'sentry/utils/analytics';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import {getUtcDateString} from 'sentry/utils/dates';
 import EventView from 'sentry/utils/discover/eventView';
@@ -42,7 +48,6 @@ import withOrganization from 'sentry/utils/withOrganization';
 import ReviewAction from 'sentry/views/issueList/actions/reviewAction';
 import ShareIssue from 'sentry/views/organizationGroupDetails/actions/shareIssue';
 
-import DeleteAction from './deleteAction';
 import SubscribeAction from './subscribeAction';
 
 type Props = {
@@ -59,7 +64,7 @@ type State = {
   shareBusy: boolean;
 };
 
-class Actions extends React.Component<Props, State> {
+class Actions extends Component<Props, State> {
   state: State = {
     shareBusy: false,
   };
@@ -234,6 +239,14 @@ class Actions extends React.Component<Props, State> {
     this.trackIssueAction('subscribed');
   };
 
+  onRedirectDiscover = () => {
+    const {organization} = this.props;
+    trackAdvancedAnalyticsEvent('growth.issue_open_in_discover_btn_clicked', {
+      organization,
+    });
+    browserHistory.push(this.getDiscoverUrl());
+  };
+
   onDiscard = () => {
     const {group, project, organization, api} = this.props;
     const id = uniqueId();
@@ -256,8 +269,64 @@ class Actions extends React.Component<Props, State> {
     this.trackIssueAction('discarded');
   };
 
-  handleClick(disabled: boolean, onClick: (event: React.MouseEvent) => void) {
-    return function (event: React.MouseEvent) {
+  renderDiscardModal = ({Body, Footer, closeModal}: ModalRenderProps) => {
+    const {organization, project} = this.props;
+
+    function renderDiscardDisabled({children, ...props}) {
+      return children({
+        ...props,
+        renderDisabled: ({features}: {features: string[]}) => (
+          <FeatureDisabled alert featureName="Discard and Delete" features={features} />
+        ),
+      });
+    }
+
+    return (
+      <Feature
+        features={['projects:discard-groups']}
+        hookName="feature-disabled:discard-groups"
+        organization={organization}
+        project={project}
+        renderDisabled={renderDiscardDisabled}
+      >
+        {({hasFeature, renderDisabled, ...props}) => (
+          <Fragment>
+            <Body>
+              {!hasFeature &&
+                typeof renderDisabled === 'function' &&
+                renderDisabled({...props, hasFeature, children: null})}
+              {t(
+                `Discarding this event will result in the deletion of most data associated with this issue and future events being discarded before reaching your stream. Are you sure you wish to continue?`
+              )}
+            </Body>
+            <Footer>
+              <Button onClick={closeModal}>{t('Cancel')}</Button>
+              <Button
+                style={{marginLeft: space(1)}}
+                priority="primary"
+                onClick={this.onDiscard}
+                disabled={!hasFeature}
+              >
+                {t('Discard Future Events')}
+              </Button>
+            </Footer>
+          </Fragment>
+        )}
+      </Feature>
+    );
+  };
+
+  openDiscardModal = () => {
+    const {organization} = this.props;
+
+    openModal(this.renderDiscardModal);
+    analytics('feature.discard_group.modal_opened', {
+      org_id: parseInt(organization.id, 10),
+    });
+  };
+
+  handleClick(disabled: boolean, onClick: (event?: MouseEvent) => void) {
+    return function (event: MouseEvent) {
       if (disabled) {
         event.preventDefault();
         event.stopPropagation();
@@ -311,17 +380,6 @@ class Actions extends React.Component<Props, State> {
         >
           <ReviewAction onUpdate={this.onUpdate} disabled={!group.inbox || disabled} />
         </Tooltip>
-        <Access organization={organization} access={['event:admin']}>
-          {({hasAccess}) => (
-            <DeleteAction
-              disabled={disabled || !hasAccess}
-              organization={organization}
-              project={project}
-              onDelete={this.onDelete}
-              onDiscard={this.onDiscard}
-            />
-          )}
-        </Access>
         {orgFeatures.has('shared-issues') && (
           <ShareIssue
             disabled={disabled}
@@ -332,70 +390,93 @@ class Actions extends React.Component<Props, State> {
             onReshare={() => this.onShare(true)}
           />
         )}
-
-        <Feature
-          hookName="feature-disabled:open-in-discover"
-          features={['discover-basic']}
-          organization={organization}
-        >
-          <ActionButton
-            disabled={disabled}
-            to={disabled ? '' : this.getDiscoverUrl()}
-            onClick={() => {
-              trackAdvancedAnalyticsEvent('growth.issue_open_in_discover_btn_clicked', {
-                organization,
-              });
-            }}
-          >
-            <GuideAnchor target="open_in_discover">{t('Open in Discover')}</GuideAnchor>
-          </ActionButton>
-        </Feature>
-
-        <BookmarkButton
-          disabled={disabled}
-          isActive={group.isBookmarked}
-          title={bookmarkTitle}
-          tooltipProps={{delay: 300}}
-          aria-label={bookmarkTitle}
-          onClick={this.handleClick(disabled, this.onToggleBookmark)}
-          icon={<IconStar isSolid size="xs" />}
-        />
-
         <SubscribeAction
           disabled={disabled}
           group={group}
           onClick={this.handleClick(disabled, this.onToggleSubscribe)}
         />
 
-        {displayReprocessEventAction(organization.features, event) && (
-          <ReprocessAction
-            disabled={disabled}
-            icon={<IconRefresh size="xs" />}
-            title={t('Reprocess this issue')}
-            aria-label={t('Reprocess this issue')}
-            onClick={this.handleClick(disabled, this.onReprocessEvent)}
-          />
-        )}
+        <Access organization={organization} access={['event:admin']}>
+          {({hasAccess}) => (
+            <Feature
+              hookName="feature-disabled:open-in-discover"
+              features={['discover-basic']}
+              organization={organization}
+            >
+              {({hasFeature}) => (
+                <GuideAnchor target="open_in_discover">
+                  <DropdownMenuControlV2
+                    triggerProps={{
+                      'aria-label': t('More Actions'),
+                      icon: <IconEllipsis size="xs" />,
+                      showChevron: false,
+                      size: 'xsmall',
+                    }}
+                    items={[
+                      {
+                        key: 'bookmark',
+                        label: bookmarkTitle,
+                        hidden: false,
+                        onAction: this.onToggleBookmark,
+                      },
+                      {
+                        key: 'open-in-discover',
+                        label: t('Open in Discover'),
+                        hidden: !hasFeature,
+                        onAction: this.onRedirectDiscover,
+                      },
+                      {
+                        key: 'reprocess',
+                        label: t('Reprocess events'),
+                        hidden: !displayReprocessEventAction(
+                          organization.features,
+                          event
+                        ),
+                        onAction: this.onReprocessEvent,
+                      },
+                      {
+                        key: 'delete-issue',
+                        label: t('Delete'),
+                        hidden: !hasAccess,
+                        onAction: () =>
+                          openModal(({Body, Footer, closeModal}: ModalRenderProps) => (
+                            <Fragment>
+                              <Body>
+                                {t(
+                                  'Deleting this issue is permanent. Are you sure you wish to continue?'
+                                )}
+                              </Body>
+                              <Footer>
+                                <Button onClick={closeModal}>{t('Cancel')}</Button>
+                                <Button
+                                  style={{marginLeft: space(1)}}
+                                  priority="primary"
+                                  onClick={this.onDelete}
+                                >
+                                  {t('Delete')}
+                                </Button>
+                              </Footer>
+                            </Fragment>
+                          )),
+                      },
+                      {
+                        key: 'delete-and-discard',
+                        label: t('Delete and discard future events'),
+                        hidden: !hasAccess,
+                        onAction: () => this.openDiscardModal(),
+                      },
+                    ]}
+                  />
+                </GuideAnchor>
+              )}
+            </Feature>
+          )}
+        </Access>
       </Wrapper>
     );
   }
 }
 
-const ReprocessAction = styled(ActionButton)``;
-
-const BookmarkButton = styled(ActionButton)<{isActive: boolean}>`
-  ${p =>
-    p.isActive &&
-    `
-   && {
- background: ${p.theme.yellow100};
- color: ${p.theme.yellow300};
- border-color: ${p.theme.yellow300};
- text-shadow: 0 1px 0 rgba(0, 0, 0, 0.15);
-}
-  `}
-`;
-
 const Wrapper = styled('div')`
   display: grid;
   justify-content: flex-start;

+ 4 - 2
tests/acceptance/page_objects/issue_details.py

@@ -52,8 +52,10 @@ class IssueDetailsPage(BasePage):
         self.browser.wait_until('[aria-label="Unignore"]')
 
     def bookmark_issue(self):
-        self.browser.click('[aria-label="Bookmark"]')
-        self.browser.wait_until('[aria-label="Remove bookmark"]')
+        self.browser.click('button[aria-label="More Actions"]')
+        self.browser.wait_until('[data-test-id="bookmark"]')
+        button = self.browser.element('[data-test-id="bookmark"]')
+        button.click()
 
     def assign_to(self, user):
         assignee = self.browser.find_element_by_css_selector(".assigned-to")

+ 48 - 17
tests/js/spec/views/organizationGroupDetails/actions.spec.tsx

@@ -1,4 +1,5 @@
 import {mountWithTheme} from 'sentry-test/enzyme';
+import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
 
 import ModalActions from 'sentry/actions/modalActions';
 import ConfigStore from 'sentry/stores/configStore';
@@ -81,10 +82,20 @@ describe('GroupActions', function () {
       });
     });
 
-    it('can bookmark', function () {
-      const wrapper = renderComponent();
-      const btn = wrapper.find('button[aria-label="Bookmark"]');
-      btn.simulate('click');
+    it('can bookmark', async function () {
+      render(
+        <GroupActions
+          group={group}
+          project={project}
+          organization={organization}
+          disabled={false}
+        />
+      );
+
+      userEvent.click(screen.getByLabelText('More Actions'));
+
+      const bookmark = await screen.findByTestId('bookmark');
+      userEvent.click(bookmark);
 
       expect(issuesApi).toHaveBeenCalledWith(
         expect.anything(),
@@ -96,11 +107,25 @@ describe('GroupActions', function () {
   });
 
   describe('reprocessing', function () {
-    it('renders ReprocessAction component if org has feature flag reprocessing-v2', function () {
-      const wrapper = renderComponent();
+    it('renders ReprocessAction component if org has feature flag reprocessing-v2 and native exception event', async function () {
+      const event = TestStubs.EventStacktraceException({
+        platform: 'native',
+      });
+
+      render(
+        <GroupActions
+          group={group}
+          project={project}
+          organization={organization}
+          event={event}
+          disabled={false}
+        />
+      );
+
+      userEvent.click(screen.getByLabelText('More Actions'));
 
-      const reprocessActionButton = wrapper.find('ReprocessAction button');
-      expect(reprocessActionButton).toBeTruthy();
+      const reprocessActionButton = await screen.findByTestId('reprocess');
+      expect(reprocessActionButton).toBeInTheDocument();
     });
 
     it('open dialog by clicking on the ReprocessAction component', async function () {
@@ -108,18 +133,24 @@ describe('GroupActions', function () {
         platform: 'native',
       });
 
-      const onReprocessEventFunc = jest.spyOn(ModalActions, 'openModal');
-
-      const wrapper = renderComponent(event);
+      render(
+        <GroupActions
+          group={group}
+          project={project}
+          organization={organization}
+          event={event}
+          disabled={false}
+        />
+      );
 
-      const reprocessActionButton = wrapper.find('ReprocessAction button');
-      expect(reprocessActionButton).toBeTruthy();
+      const onReprocessEventFunc = jest.spyOn(ModalActions, 'openModal');
 
-      reprocessActionButton.simulate('click');
-      await tick();
-      wrapper.update();
+      userEvent.click(screen.getByLabelText('More Actions'));
 
-      expect(onReprocessEventFunc).toHaveBeenCalled();
+      const reprocessActionButton = await screen.findByTestId('reprocess');
+      expect(reprocessActionButton).toBeInTheDocument();
+      userEvent.click(reprocessActionButton);
+      await waitFor(() => expect(onReprocessEventFunc).toHaveBeenCalled());
     });
   });
 });