Browse Source

feat(saved-search): Add components to save current search (#12534)

Ref: SEN-396
Lyn Nagara 6 years ago
parent
commit
df35bd7803

+ 1 - 0
src/sentry/static/sentry/app/views/stream/filters.jsx

@@ -93,6 +93,7 @@ class StreamFilters extends React.Component {
                 savedSearchList={savedSearchList}
                 onSavedSearchSelect={onSavedSearchSelect}
                 onSavedSearchDelete={onSavedSearchDelete}
+                query={query}
                 queryCount={queryCount}
                 queryMaxCount={queryMaxCount}
               />

+ 120 - 2
src/sentry/static/sentry/app/views/stream/organizationSavedSearchSelector.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import styled from 'react-emotion';
+import Modal from 'react-bootstrap/lib/Modal';
 
 import {t} from 'app/locale';
 import Access from 'app/components/acl/access';
@@ -11,7 +12,9 @@ import DropdownLink from 'app/components/dropdownLink';
 import QueryCount from 'app/components/queryCount';
 import InlineSvg from 'app/components/inlineSvg';
 import SentryTypes from 'app/sentryTypes';
+import {TextField} from 'app/components/forms';
 import space from 'app/styles/space';
+import withApi from 'app/utils/withApi';
 
 export default class OrganizationSavedSearchSelector extends React.Component {
   static propTypes = {
@@ -19,7 +22,7 @@ export default class OrganizationSavedSearchSelector extends React.Component {
     savedSearchList: PropTypes.array.isRequired,
     onSavedSearchSelect: PropTypes.func.isRequired,
     onSavedSearchDelete: PropTypes.func.isRequired,
-    query: PropTypes.string,
+    query: PropTypes.string.isRequired,
     queryCount: PropTypes.number,
     queryMaxCount: PropTypes.number,
     searchId: PropTypes.string,
@@ -79,7 +82,7 @@ export default class OrganizationSavedSearchSelector extends React.Component {
   }
 
   render() {
-    const {queryCount, queryMaxCount} = this.props;
+    const {organization, query, queryCount, queryMaxCount} = this.props;
 
     return (
       <Container>
@@ -92,12 +95,115 @@ export default class OrganizationSavedSearchSelector extends React.Component {
           }
         >
           {this.renderList()}
+          <Access
+            organization={organization}
+            access={['org:write']}
+            renderNoAccessMessage={false}
+          >
+            <StyledMenuItem divider={true} />
+            <ButtonBar>
+              <SaveSearchButton query={query} organization={organization} />
+            </ButtonBar>
+          </Access>
         </StyledDropdownLink>
       </Container>
     );
   }
 }
 
+const SaveSearchButton = withApi(
+  class SaveSearchButton extends React.Component {
+    static propTypes = {
+      // api: PropTypes.object.isRequired,
+      query: PropTypes.string.isRequired,
+      // organization: SentryTypes.Organization.isRequired,
+    };
+
+    constructor(props) {
+      super(props);
+      this.state = {
+        isModalOpen: false,
+        isSaving: false,
+        query: props.query,
+        name: '',
+      };
+    }
+
+    onSubmit = e => {
+      e.preventDefault();
+
+      // TODO: implement saving
+    };
+
+    onToggle = () => {
+      this.setState({
+        isModalOpen: !this.state.isModalOpen,
+      });
+    };
+
+    handleChangeName = val => {
+      this.setState({name: val});
+    };
+
+    handleChangeQuery = val => {
+      this.setState({query: val});
+    };
+
+    render() {
+      const {isSaving, isModalOpen} = this.state;
+
+      return (
+        <React.Fragment>
+          <Button size="xsmall" onClick={this.onToggle}>
+            {t('Save Current Search')}
+          </Button>
+          <Modal show={isModalOpen} animation={false} onHide={this.onToggle}>
+            <form onSubmit={this.onSubmit}>
+              <div className="modal-header">
+                <h4>{t('Save Current Search')}</h4>
+              </div>
+
+              <div className="modal-body">
+                <p>{t('All team members will now have access to this search.')}</p>
+                <TextField
+                  key="name"
+                  name="name"
+                  label={t('Name')}
+                  placeholder="e.g. My Search Results"
+                  required={true}
+                  onChange={this.handleChangeName}
+                />
+                <TextField
+                  key="query"
+                  name="query"
+                  label={t('Query')}
+                  value={this.props.query}
+                  required={true}
+                  onChange={this.handleChangeQuery}
+                />
+              </div>
+              <div className="modal-footer">
+                <Button
+                  priority="default"
+                  size="small"
+                  disabled={isSaving}
+                  onClick={this.onToggle}
+                  style={{marginRight: space(1)}}
+                >
+                  {t('Cancel')}
+                </Button>
+                <Button priority="primary" size="small" disabled={isSaving}>
+                  {t('Save')}
+                </Button>
+              </div>
+            </form>
+          </Modal>
+        </React.Fragment>
+      );
+    }
+  }
+);
+
 const Container = styled.div`
   & .dropdown-menu {
     max-width: 350px;
@@ -176,3 +282,15 @@ const EmptyItem = styled.li`
   padding: 8px 10px 5px;
   font-style: italic;
 `;
+
+const ButtonBar = styled.li`
+  padding: ${space(0.5)} ${space(1)};
+  display: flex;
+  justify-content: space-between;
+
+  & a {
+    /* need to override .dropdown-menu li a in shared-components.less */
+    padding: 0 !important;
+    line-height: 1 !important;
+  }
+`;

+ 25 - 1
tests/js/spec/views/stream/organizationSavedSearchSelector.spec.jsx

@@ -6,7 +6,7 @@ import OrganizationSavedSearchSelector from 'app/views/stream/organizationSavedS
 describe('OrganizationSavedSearchSelector', function() {
   let wrapper, onSelect, onDelete, organization, savedSearchList;
   beforeEach(function() {
-    organization = TestStubs.Organization();
+    organization = TestStubs.Organization({access: ['org:write']});
     onSelect = jest.fn();
     onDelete = jest.fn();
     savedSearchList = [
@@ -31,6 +31,7 @@ describe('OrganizationSavedSearchSelector', function() {
         savedSearchList={savedSearchList}
         onSavedSearchSelect={onSelect}
         onSavedSearchDelete={onDelete}
+        query={'is:unresolved assigned:lyn@sentry.io'}
       />,
       TestStubs.routerContext()
     );
@@ -120,4 +121,27 @@ describe('OrganizationSavedSearchSelector', function() {
       expect(onDelete).toHaveBeenCalledWith(savedSearchList[1]);
     });
   });
+
+  describe('saves a search', function() {
+    it('clicking save search opens modal', function() {
+      wrapper.find('DropdownLink').simulate('click');
+      expect(wrapper.find('ModalDialog')).toHaveLength(0);
+      wrapper
+        .find('button')
+        .at(0)
+        .simulate('click');
+
+      expect(wrapper.find('ModalDialog')).toHaveLength(1);
+    });
+
+    it('hides save search button if no access', function() {
+      const orgWithoutAccess = TestStubs.Organization({access: ['org:read']});
+
+      wrapper.setProps({organization: orgWithoutAccess});
+
+      const button = wrapper.find('button');
+
+      expect(button).toHaveLength(0);
+    });
+  });
 });