Browse Source

feat(owners): Owner Rule Builder (#7684)

Max Bittker 7 years ago
parent
commit
bbd273db87

+ 4 - 4
src/sentry/static/sentry/app/components/dropdownAutoCompleteMenu.jsx

@@ -74,7 +74,7 @@ class DropdownAutoCompleteMenu extends React.Component {
       //if the first item has children, we assume it is a group
       //if the first item has children, we assume it is a group
       return _.flatMap(this.filterGroupedItems(items, inputValue), item => {
       return _.flatMap(this.filterGroupedItems(items, inputValue), item => {
         return [
         return [
-          {...item.group, groupLabel: true},
+          {...item, groupLabel: true},
           ...item.items.map(groupedItem => ({...groupedItem, index: itemCount++})),
           ...item.items.map(groupedItem => ({...groupedItem, index: itemCount++})),
         ];
         ];
       });
       });
@@ -139,10 +139,10 @@ class DropdownAutoCompleteMenu extends React.Component {
                     {this.autoCompleteFilter(items, inputValue).map(
                     {this.autoCompleteFilter(items, inputValue).map(
                       (item, index) =>
                       (item, index) =>
                         item.groupLabel ? (
                         item.groupLabel ? (
-                          <StyledLabel key={item.value}>{item.label}</StyledLabel>
+                          <StyledLabel key={index}>{item.label}</StyledLabel>
                         ) : (
                         ) : (
                           <AutoCompleteItem
                           <AutoCompleteItem
-                            key={item.value}
+                            key={`${item.value}-${item.label}`}
                             highlightedIndex={highlightedIndex}
                             highlightedIndex={highlightedIndex}
                             index={item.index}
                             index={item.index}
                             {...getItemProps({item, index: item.index})}
                             {...getItemProps({item, index: item.index})}
@@ -191,7 +191,7 @@ const AutoCompleteItem = styled('div')`
 `;
 `;
 
 
 const StyledLabel = styled('div')`
 const StyledLabel = styled('div')`
-  padding: 0 8px;
+  padding: 2px 8px;
   background-color: ${p => p.theme.offWhite};
   background-color: ${p => p.theme.offWhite};
   border: 1px solid ${p => p.theme.borderLight};
   border: 1px solid ${p => p.theme.borderLight};
   border-width: 1px 0;
   border-width: 1px 0;

+ 1 - 0
src/sentry/static/sentry/app/components/selectInput.jsx

@@ -67,6 +67,7 @@ class SelectInput extends React.Component {
   };
   };
 
 
   destroy = () => {
   destroy = () => {
+    this.select2.off('change', this.onChange);
     jQuery(this.refs.select).select2('destroy');
     jQuery(this.refs.select).select2('destroy');
   };
   };
 
 

+ 1 - 1
src/sentry/static/sentry/app/components/teamAvatar.jsx

@@ -25,7 +25,7 @@ class TeamAvatar extends React.Component {
     let {team, hasTooltip} = this.props;
     let {team, hasTooltip} = this.props;
     let displayName = this.getDisplayName();
     let displayName = this.getDisplayName();
     return (
     return (
-      <Tooltip title={displayName} disabled={!hasTooltip}>
+      <Tooltip title={`#${displayName}`} disabled={!hasTooltip}>
         <span className={classNames('avatar', this.props.className)}>
         <span className={classNames('avatar', this.props.className)}>
           <LetterAvatar identifier={team.slug} displayName={displayName} />
           <LetterAvatar identifier={team.slug} displayName={displayName} />
         </span>
         </span>

+ 2 - 0
src/sentry/static/sentry/app/utils.jsx

@@ -217,6 +217,8 @@ export function sortProjects(projects) {
 export const buildUserId = id => `user:${id}`;
 export const buildUserId = id => `user:${id}`;
 export const buildTeamId = id => `team:${id}`;
 export const buildTeamId = id => `team:${id}`;
 
 
+export const actorEquality = (a, b) => a.type === b.type && a.id === b.id;
+
 // re-export under utils
 // re-export under utils
 export {parseLinkHeader, deviceNameMapper, Collection, PendingChangeQueue, CursorPoller};
 export {parseLinkHeader, deviceNameMapper, Collection, PendingChangeQueue, CursorPoller};
 
 

+ 25 - 2
src/sentry/static/sentry/app/views/settings/project/projectOwnership/ownerInput.jsx

@@ -11,6 +11,7 @@ import SentryTypes from '../../../../proptypes';
 
 
 import {addErrorMessage, addSuccessMessage} from '../../../../actionCreators/indicator';
 import {addErrorMessage, addSuccessMessage} from '../../../../actionCreators/indicator';
 import {t} from '../../../../locale';
 import {t} from '../../../../locale';
+import RuleBuilder from './ruleBuilder';
 
 
 const SyntaxOverlay = styled.div`
 const SyntaxOverlay = styled.div`
   margin: 5px;
   margin: 5px;
@@ -24,6 +25,11 @@ const SyntaxOverlay = styled.div`
   top: ${({line}) => line}em;
   top: ${({line}) => line}em;
 `;
 `;
 
 
+const SaveButton = styled.div`
+  text-align: end;
+  padding-top: 10px;
+`;
+
 class OwnerInput extends React.Component {
 class OwnerInput extends React.Component {
   static propTypes = {
   static propTypes = {
     organization: SentryTypes.Organization,
     organization: SentryTypes.Organization,
@@ -103,11 +109,24 @@ class OwnerInput extends React.Component {
   onChange(e) {
   onChange(e) {
     this.setState({text: e.target.value});
     this.setState({text: e.target.value});
   }
   }
+
+  handleAddRule(rule) {
+    this.setState(({text}) => ({
+      text: text + '\n' + rule,
+    }));
+  }
+
   render() {
   render() {
+    let {project, organization} = this.props;
     let {text, error, initialText} = this.state;
     let {text, error, initialText} = this.state;
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
+        <RuleBuilder
+          organization={organization}
+          project={project}
+          onAddRule={this.handleAddRule.bind(this)}
+        />
         <div
         <div
           style={{position: 'relative'}}
           style={{position: 'relative'}}
           onKeyDown={e => {
           onKeyDown={e => {
@@ -135,13 +154,17 @@ class OwnerInput extends React.Component {
             }}
             }}
             onChange={this.onChange.bind(this)}
             onChange={this.onChange.bind(this)}
             value={text}
             value={text}
+            spellCheck="false"
+            autoComplete="off"
+            autoCorrect="off"
+            autoCapitalize="off"
           />
           />
           {error &&
           {error &&
             error.raw && (
             error.raw && (
               <SyntaxOverlay line={error.raw[0].match(/line (\d*),/)[1] - 1} />
               <SyntaxOverlay line={error.raw[0].match(/line (\d*),/)[1] - 1} />
             )}
             )}
           {error && error.raw && error.raw.toString()}
           {error && error.raw && error.raw.toString()}
-          <div style={{textAlign: 'end', paddingTop: '10px'}}>
+          <SaveButton>
             <Button
             <Button
               size="small"
               size="small"
               priority="primary"
               priority="primary"
@@ -150,7 +173,7 @@ class OwnerInput extends React.Component {
             >
             >
               {t('Save Changes')}
               {t('Save Changes')}
             </Button>
             </Button>
-          </div>
+          </SaveButton>
         </div>
         </div>
       </React.Fragment>
       </React.Fragment>
     );
     );

+ 242 - 0
src/sentry/static/sentry/app/views/settings/project/projectOwnership/ruleBuilder.jsx

@@ -0,0 +1,242 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import styled from 'react-emotion';
+import {Flex} from 'grid-emotion';
+
+import memberListStore from '../../../../stores/memberListStore';
+import ProjectsStore from '../../../../stores/projectsStore';
+import Button from '../../../../components/buttons/button';
+import SelectInput from '../../../../components/selectInput';
+import InlineSvg from '../../../../components/inlineSvg';
+import Input from '../../../../views/settings/components/forms/controls/input';
+import DropdownAutoComplete from '../../../../components/dropdownAutoComplete';
+import DropdownButton from '../../../../components/dropdownButton';
+import ActorAvatar from '../../../../components/actorAvatar';
+import SentryTypes from '../../../../proptypes';
+import {buildUserId, buildTeamId, actorEquality} from '../../../../utils';
+import {addErrorMessage} from '../../../../actionCreators/indicator';
+
+import {t} from '../../../../locale';
+
+const BuilderBar = styled('div')`
+  display: flex;
+  height: 40px;
+  align-items: center;
+  margin-bottom: 1em;
+`;
+
+const BuilderSelect = styled(SelectInput)`
+  padding: 0.5em;
+  margin-right: 5px;
+  width: 80px;
+  flex-shrink: 0;
+`;
+
+const BuilderInput = styled(Input)`
+  padding: 0.5em;
+  margin-right: 5px;
+`;
+
+const Divider = styled(InlineSvg)`
+  color: ${p => p.theme.borderDark};
+  flex-shrink: 0;
+  margin-right: 5px;
+`;
+
+const Owners = styled('div')`
+  justify-content: flex-end;
+  display: flex;
+  span {
+    margin-right: 2px;
+  }
+
+  .avatar {
+    width: 28px;
+    height: 28px;
+  }
+`;
+
+const BuilderDropdownButton = styled(DropdownButton)`
+  margin-right: 5px;
+  flex: 1;
+  white-space: nowrap;
+  height: 37px;
+  .button-label {
+    font-size: 14px;
+    padding: 4px 8px;
+    padding-left 4px;
+  }
+`;
+
+const RuleAddButton = styled(Button)`
+  width: 37px;
+  height: 37px;
+  flex-shrink: 0;
+
+  display: flex;
+  justify-content: center;
+
+  .button-label {
+    padding: 0.5em;
+  }
+
+  div {
+    margin: 0px !important;
+  }
+`;
+
+const AddOwnersLabel = styled.div`
+  margin-left: 3px;
+  padding: 0px;
+`;
+
+const initialState = {
+  text: '',
+  type: 'path',
+  owners: [],
+};
+
+class RuleBuilder extends React.Component {
+  static propTypes = {
+    project: SentryTypes.Project,
+    onAddRule: PropTypes.func,
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = initialState;
+  }
+
+  mentionableUsers() {
+    return memberListStore.getAll().map(member => ({
+      value: buildUserId(member.id),
+      label: member.email,
+      searchKey: `${member.email}  ${name}`,
+      actor: {
+        type: 'user',
+        id: member.id,
+        name: member.name,
+      },
+    }));
+  }
+
+  mentionableTeams() {
+    let {project} = this.props;
+    let projectData = ProjectsStore.getAll().find(p => p.slug == project.slug);
+
+    if (!projectData) {
+      return [];
+    }
+
+    return projectData.teams.map(team => ({
+      value: buildTeamId(team.id),
+      label: `#${team.slug}`,
+      searchKey: team.slug,
+      actor: {
+        type: 'team',
+        id: team.id,
+        name: team.slug,
+      },
+    }));
+  }
+
+  handleTypeChange = e => {
+    this.setState({type: e[0].value});
+  };
+
+  handleChangeValue = e => {
+    this.setState({text: e.target.value});
+  };
+
+  onAddActor = ({actor}) => {
+    this.setState(({owners}) => {
+      if (!owners.find(i => actorEquality(i, actor))) {
+        return {owners: [...owners, actor]};
+      } else return {};
+    });
+  };
+
+  handleRemoveActor(toRemove, e) {
+    this.setState(({owners}) => ({
+      owners: owners.filter(actor => !actorEquality(actor, toRemove)),
+    }));
+    e.stopPropagation();
+  }
+
+  handleAddRule = () => {
+    let {type, text, owners} = this.state;
+
+    if (!text || owners.length == 0) {
+      addErrorMessage('A Rule needs a type, a value, and one or more owners.');
+      return;
+    }
+    let ownerText = owners
+      .map(actor => `${actor.type == 'team' ? '#' : ''}${actor.name}`)
+      .join(' ');
+
+    let rule = `${type}:${text} ${ownerText}`;
+    this.props.onAddRule(rule);
+    this.setState(initialState);
+  };
+
+  render() {
+    let {type, text, owners} = this.state;
+
+    return (
+      <BuilderBar>
+        <BuilderSelect value={type} onChange={this.handleTypeChange}>
+          <option value="path">Path</option>
+          <option value="url">URL</option>
+        </BuilderSelect>
+        <BuilderInput
+          controlled
+          value={text}
+          onChange={this.handleChangeValue}
+          placeholder={type === 'path' ? 'src/example/*' : 'example.com/settings/*'}
+        />
+        <Divider src="icon-chevron-right" />
+        <Flex flex="1" align="center">
+          <DropdownAutoComplete
+            items={[
+              {
+                value: 'team',
+                label: 'Teams',
+                items: this.mentionableTeams(),
+              },
+              {
+                value: 'user',
+                label: 'Users',
+                items: this.mentionableUsers(),
+              },
+            ]}
+            onSelect={this.onAddActor}
+          >
+            {({isOpen, selectedItem}) => (
+              <BuilderDropdownButton isOpen={isOpen}>
+                <Owners>
+                  {owners.map(owner => (
+                    <span
+                      key={`${owner.type}-${owner.id}`}
+                      onClick={this.handleRemoveActor.bind(this, owner)}
+                    >
+                      <ActorAvatar actor={owner} />
+                    </span>
+                  ))}
+                </Owners>
+                <AddOwnersLabel>{t('Add Owners')}</AddOwnersLabel>
+              </BuilderDropdownButton>
+            )}
+          </DropdownAutoComplete>
+        </Flex>
+
+        <RuleAddButton
+          priority="primary"
+          onClick={this.handleAddRule}
+          icon="icon-circle-add"
+        />
+      </BuilderBar>
+    );
+  }
+}
+
+export default RuleBuilder;

+ 1 - 1
tests/js/spec/components/__snapshots__/teamAvatar.spec.jsx.snap

@@ -3,7 +3,7 @@
 exports[`TeamAvatar render() renders 1`] = `
 exports[`TeamAvatar render() renders 1`] = `
 <Tooltip
 <Tooltip
   disabled={true}
   disabled={true}
-  title="team-slug"
+  title="#team-slug"
 >
 >
   <span
   <span
     className="avatar avatar"
     className="avatar avatar"

+ 643 - 31
tests/js/spec/views/__snapshots__/ownershipInput.spec.jsx.snap

@@ -1,8 +1,114 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
 exports[`ProjectTeamsSettings render() renders 1`] = `
 exports[`ProjectTeamsSettings render() renders 1`] = `
+.glamor-64 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  height: 40px;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  margin-bottom: 1em;
+}
+
 .glamor-0 {
 .glamor-0 {
+  padding: 0.5em;
+  margin-right: 5px;
+  width: 80px;
+  -webkit-flex-shrink: 0;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+}
+
+.glamor-4 {
+  display: block;
+  width: 100%;
+  background: #fff;
+  border: 1px solid;
+  box-shadow: inset;
+  padding: 0.5em;
+  -webkit-transition: border 0.1s linear;
+  transition: border 0.1s linear;
+  resize: vertical;
+  padding: 0.5em;
+  margin-right: 5px;
+}
+
+.glamor-4 {
+  display: block;
+  width: 100%;
+  background: #fff;
+  border: 1px solid;
+  box-shadow: inset;
+  padding: 0.5em;
+  -webkit-transition: border 0.1s linear;
+  transition: border 0.1s linear;
+  resize: vertical;
+  padding: 0.5em;
+  margin-right: 5px;
+}
+
+.glamor-4 {
+  display: block;
+  width: 100%;
+  background: #fff;
+  border: 1px solid;
+  box-shadow: inset;
+  padding: 0.5em;
+  -webkit-transition: border 0.1s linear;
+  transition: border 0.1s linear;
+  resize: vertical;
+  padding: 0.5em;
+  margin-right: 5px;
+}
+
+.glamor-4 {
+  display: block;
+  width: 100%;
+  background: #fff;
+  border: 1px solid;
+  box-shadow: inset;
+  padding: 0.5em;
+  -webkit-transition: border 0.1s linear;
+  transition: border 0.1s linear;
+  resize: vertical;
+  padding: 0.5em;
+  margin-right: 5px;
+}
+
+.glamor-4:focus {
+  outline: none;
+}
+
+.glamor-4:hover,
+.glamor-4:focus,
+.glamor-4:active {
+  border: 1px solid;
+}
+
+.glamor-9 {
+  -webkit-flex-shrink: 0;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  margin-right: 5px;
+}
+
+.glamor-7 {
+  vertical-align: middle;
+  -webkit-flex-shrink: 0;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  margin-right: 5px;
+}
+
+.glamor-45 {
   box-sizing: border-box;
   box-sizing: border-box;
+  -webkit-flex: 1;
+  -ms-flex: 1;
+  flex: 1;
   display: -webkit-box;
   display: -webkit-box;
   display: -webkit-flex;
   display: -webkit-flex;
   display: -ms-flexbox;
   display: -ms-flexbox;
@@ -13,6 +119,134 @@ exports[`ProjectTeamsSettings render() renders 1`] = `
   align-items: center;
   align-items: center;
 }
 }
 
 
+.glamor-41 {
+  position: relative;
+  display: inline-block;
+}
+
+.glamor-37 {
+  margin-right: 5px;
+  -webkit-flex: 1;
+  -ms-flex: 1;
+  flex: 1;
+  white-space: nowrap;
+  height: 37px;
+}
+
+.glamor-37 .button-label {
+  font-size: 14px;
+  padding: 4px 8px;
+  padding-left: 4px;
+}
+
+.glamor-29 {
+  position: relative;
+  z-index: 2;
+  box-shadow: none;
+  margin-right: 5px;
+  -webkit-flex: 1;
+  -ms-flex: 1;
+  flex: 1;
+  white-space: nowrap;
+  height: 37px;
+}
+
+.glamor-29 .button-label {
+  font-size: 14px;
+  padding: 4px 8px;
+  padding-left: 4px;
+}
+
+.glamor-26 {
+  box-sizing: border-box;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+}
+
+.glamor-13 {
+  -webkit-box-pack: end;
+  -webkit-justify-content: flex-end;
+  -ms-flex-pack: end;
+  justify-content: flex-end;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+}
+
+.glamor-13 span {
+  margin-right: 2px;
+}
+
+.glamor-13 .avatar {
+  width: 28px;
+  height: 28px;
+}
+
+.glamor-15 {
+  margin-left: 3px;
+  padding: 0px;
+}
+
+.glamor-20 {
+  margin-left: 0.33em;
+}
+
+.glamor-18 {
+  vertical-align: middle;
+  margin-left: 0.33em;
+}
+
+.glamor-60 {
+  width: 37px;
+  height: 37px;
+  -webkit-flex-shrink: 0;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-pack: center;
+  -webkit-justify-content: center;
+  -ms-flex-pack: center;
+  justify-content: center;
+}
+
+.glamor-60 .button-label {
+  padding: 0.5em;
+}
+
+.glamor-60 div {
+  margin: 0px !important;
+}
+
+.glamor-54 {
+  box-sizing: border-box;
+  margin-right: 8px;
+  margin-left: -2px;
+}
+
+.glamor-50 {
+  display: block;
+}
+
+.glamor-48 {
+  vertical-align: middle;
+  display: block;
+}
+
+.glamor-68 {
+  text-align: end;
+  padding-top: 10px;
+}
+
 <OwnerInput
 <OwnerInput
   initialText="url:src @dummy@example.com"
   initialText="url:src @dummy@example.com"
   organization={
   organization={
@@ -59,6 +293,379 @@ exports[`ProjectTeamsSettings render() renders 1`] = `
     }
     }
   }
   }
 >
 >
+  <RuleBuilder
+    onAddRule={[Function]}
+    organization={
+      Object {
+        "access": Array [
+          "org:read",
+          "org:write",
+          "org:admin",
+          "project:read",
+          "project:write",
+          "project:admin",
+          "team:read",
+          "team:write",
+          "team:admin",
+        ],
+        "features": Array [],
+        "id": "3",
+        "name": "Organization Name",
+        "onboardingTasks": Array [],
+        "projects": Array [],
+        "slug": "org-slug",
+        "status": Object {
+          "id": "active",
+          "name": "active",
+        },
+        "teams": Array [],
+      }
+    }
+    project={
+      Object {
+        "hasAccess": true,
+        "id": "2",
+        "isBookmarked": false,
+        "isMember": true,
+        "name": "Project Name",
+        "slug": "project-slug",
+        "teams": Array [],
+      }
+    }
+  >
+    <BuilderBar>
+      <div
+        className="glamor-64 glamor-65"
+      >
+        <BuilderSelect
+          onChange={[Function]}
+          value="path"
+        >
+          <SelectInput
+            className="glamor-0 glamor-1"
+            disabled={false}
+            multiple={false}
+            onChange={[Function]}
+            placeholder="Select an option..."
+            required={false}
+            value="path"
+          >
+            <select
+              className="glamor-0 glamor-1"
+              disabled={false}
+              multiple={false}
+              onChange={[Function]}
+              placeholder="Select an option..."
+              required={false}
+              value="path"
+            >
+              <option
+                value="path"
+              >
+                Path
+              </option>
+              <option
+                value="url"
+              >
+                URL
+              </option>
+            </select>
+          </SelectInput>
+        </BuilderSelect>
+        <BuilderInput
+          controlled={true}
+          onChange={[Function]}
+          placeholder="src/example/*"
+          value=""
+        >
+          <input
+            className="glamor-4 glamor-5"
+            onChange={[Function]}
+            placeholder="src/example/*"
+            value=""
+          />
+        </BuilderInput>
+        <Divider
+          src="icon-chevron-right"
+        >
+          <InlineSvg
+            className="glamor-9 glamor-6"
+            src="icon-chevron-right"
+          >
+            <StyledSvg
+              className="glamor-9 glamor-6"
+              height="1em"
+              style={Object {}}
+              viewBox={Object {}}
+              width="1em"
+            >
+              <svg
+                className="glamor-6 glamor-7 glamor-8"
+                height="1em"
+                style={Object {}}
+                viewBox={Object {}}
+                width="1em"
+              >
+                <use
+                  href="#test"
+                  xlinkHref="#test"
+                />
+              </svg>
+            </StyledSvg>
+          </InlineSvg>
+        </Divider>
+        <Flex
+          align="center"
+          flex="1"
+        >
+          <Base
+            align="center"
+            className="glamor-45"
+            flex="1"
+          >
+            <div
+              className="glamor-45"
+              is={null}
+            >
+              <DropdownAutoComplete
+                alignMenu="right"
+                items={
+                  Array [
+                    Object {
+                      "items": Array [],
+                      "label": "Teams",
+                      "value": "team",
+                    },
+                    Object {
+                      "items": Array [],
+                      "label": "Users",
+                      "value": "user",
+                    },
+                  ]
+                }
+                onSelect={[Function]}
+              >
+                <DropdownAutoCompleteMenu
+                  alignMenu="right"
+                  items={
+                    Array [
+                      Object {
+                        "items": Array [],
+                        "label": "Teams",
+                        "value": "team",
+                      },
+                      Object {
+                        "items": Array [],
+                        "label": "Users",
+                        "value": "user",
+                      },
+                    ]
+                  }
+                  onSelect={[Function]}
+                >
+                  <AutoComplete
+                    itemToString={[Function]}
+                    onSelect={[Function]}
+                  >
+                    <DropdownMenu
+                      isOpen={false}
+                      keepMenuOpen={false}
+                      onClickOutside={[Function]}
+                    >
+                      <AutoCompleteRoot>
+                        <Component
+                          className="glamor-41 glamor-42"
+                        >
+                          <div
+                            className="glamor-41 glamor-42"
+                          >
+                            <div
+                              onClick={[Function]}
+                              onMouseEnter={[Function]}
+                              onMouseLeave={[Function]}
+                              role="button"
+                            >
+                              <BuilderDropdownButton
+                                isOpen={false}
+                              >
+                                <DropdownButton
+                                  className="glamor-37 glamor-28"
+                                  isOpen={false}
+                                >
+                                  <StyledButton
+                                    className="glamor-37 glamor-28"
+                                    isOpen={false}
+                                  >
+                                    <Component
+                                      className="glamor-28 glamor-29 glamor-30"
+                                      isOpen={false}
+                                    >
+                                      <Button
+                                        className="glamor-28 glamor-29 glamor-30"
+                                        disabled={false}
+                                      >
+                                        <button
+                                          className="glamor-28 glamor-29 glamor-30 button button-default"
+                                          disabled={false}
+                                          onClick={[Function]}
+                                          role="button"
+                                        >
+                                          <Flex
+                                            align="center"
+                                            className="button-label"
+                                          >
+                                            <Base
+                                              align="center"
+                                              className="button-label glamor-26"
+                                            >
+                                              <div
+                                                className="button-label glamor-26"
+                                                is={null}
+                                              >
+                                                <Owners>
+                                                  <div
+                                                    className="glamor-13 glamor-14"
+                                                  />
+                                                </Owners>
+                                                <AddOwnersLabel>
+                                                  <div
+                                                    className="glamor-15 glamor-16"
+                                                  >
+                                                    Add Owners
+                                                  </div>
+                                                </AddOwnersLabel>
+                                                <StyledChevronDown>
+                                                  <Component
+                                                    className="glamor-20 glamor-17"
+                                                  >
+                                                    <InlineSvg
+                                                      className="glamor-20 glamor-17"
+                                                      src="icon-chevron-down"
+                                                    >
+                                                      <StyledSvg
+                                                        className="glamor-20 glamor-17"
+                                                        height="1em"
+                                                        style={Object {}}
+                                                        viewBox={Object {}}
+                                                        width="1em"
+                                                      >
+                                                        <svg
+                                                          className="glamor-17 glamor-18 glamor-8"
+                                                          height="1em"
+                                                          style={Object {}}
+                                                          viewBox={Object {}}
+                                                          width="1em"
+                                                        >
+                                                          <use
+                                                            href="#test"
+                                                            xlinkHref="#test"
+                                                          />
+                                                        </svg>
+                                                      </StyledSvg>
+                                                    </InlineSvg>
+                                                  </Component>
+                                                </StyledChevronDown>
+                                              </div>
+                                            </Base>
+                                          </Flex>
+                                        </button>
+                                      </Button>
+                                    </Component>
+                                  </StyledButton>
+                                </DropdownButton>
+                              </BuilderDropdownButton>
+                            </div>
+                          </div>
+                        </Component>
+                      </AutoCompleteRoot>
+                    </DropdownMenu>
+                  </AutoComplete>
+                </DropdownAutoCompleteMenu>
+              </DropdownAutoComplete>
+            </div>
+          </Base>
+        </Flex>
+        <RuleAddButton
+          icon="icon-circle-add"
+          onClick={[Function]}
+          priority="primary"
+        >
+          <Button
+            className="glamor-60 glamor-61"
+            disabled={false}
+            icon="icon-circle-add"
+            onClick={[Function]}
+            priority="primary"
+          >
+            <button
+              className="glamor-60 glamor-61 button button-primary"
+              disabled={false}
+              onClick={[Function]}
+              role="button"
+            >
+              <Flex
+                align="center"
+                className="button-label"
+              >
+                <Base
+                  align="center"
+                  className="button-label glamor-26"
+                >
+                  <div
+                    className="button-label glamor-26"
+                    is={null}
+                  >
+                    <Icon>
+                      <Base
+                        className="glamor-54 glamor-55"
+                      >
+                        <div
+                          className="glamor-54 glamor-55"
+                          is={null}
+                        >
+                          <StyledInlineSvg
+                            size="16px"
+                            src="icon-circle-add"
+                          >
+                            <InlineSvg
+                              className="glamor-50 glamor-47"
+                              size="16px"
+                              src="icon-circle-add"
+                            >
+                              <StyledSvg
+                                className="glamor-50 glamor-47"
+                                height="16px"
+                                style={Object {}}
+                                viewBox={Object {}}
+                                width="16px"
+                              >
+                                <svg
+                                  className="glamor-47 glamor-48 glamor-8"
+                                  height="16px"
+                                  style={Object {}}
+                                  viewBox={Object {}}
+                                  width="16px"
+                                >
+                                  <use
+                                    href="#test"
+                                    xlinkHref="#test"
+                                  />
+                                </svg>
+                              </StyledSvg>
+                            </InlineSvg>
+                          </StyledInlineSvg>
+                        </div>
+                      </Base>
+                    </Icon>
+                  </div>
+                </Base>
+              </Flex>
+            </button>
+          </Button>
+        </RuleAddButton>
+      </div>
+    </BuilderBar>
+  </RuleBuilder>
   <div
   <div
     onKeyDown={[Function]}
     onKeyDown={[Function]}
     style={
     style={
@@ -68,6 +675,9 @@ exports[`ProjectTeamsSettings render() renders 1`] = `
     }
     }
   >
   >
     <TextareaAutosize
     <TextareaAutosize
+      autoCapitalize="off"
+      autoComplete="off"
+      autoCorrect="off"
       onChange={[Function]}
       onChange={[Function]}
       placeholder="#example usage
       placeholder="#example usage
 
 
@@ -75,6 +685,7 @@ path:src/example/pipeline/* person@sentry.io #infrastructure
 
 
 url:http://example.com/settings/* #product"
 url:http://example.com/settings/* #product"
       rows={1}
       rows={1}
+      spellCheck="false"
       style={
       style={
         Object {
         Object {
           "border": "1 solid",
           "border": "1 solid",
@@ -93,6 +704,9 @@ url:http://example.com/settings/* #product"
       value="new"
       value="new"
     >
     >
       <textarea
       <textarea
+        autoCapitalize="off"
+        autoComplete="off"
+        autoCorrect="off"
         onChange={[Function]}
         onChange={[Function]}
         placeholder="#example usage
         placeholder="#example usage
 
 
@@ -100,6 +714,7 @@ path:src/example/pipeline/* person@sentry.io #infrastructure
 
 
 url:http://example.com/settings/* #product"
 url:http://example.com/settings/* #product"
         rows={1}
         rows={1}
+        spellCheck="false"
         style={
         style={
           Object {
           Object {
             "border": "1 solid",
             "border": "1 solid",
@@ -118,45 +733,42 @@ url:http://example.com/settings/* #product"
         value="new"
         value="new"
       />
       />
     </TextareaAutosize>
     </TextareaAutosize>
-    <div
-      style={
-        Object {
-          "paddingTop": "10px",
-          "textAlign": "end",
-        }
-      }
-    >
-      <Button
-        disabled={false}
-        onClick={[Function]}
-        priority="primary"
-        size="small"
+    <SaveButton>
+      <div
+        className="glamor-68 glamor-69"
       >
       >
-        <button
-          className="button button-primary button-sm"
+        <Button
           disabled={false}
           disabled={false}
           onClick={[Function]}
           onClick={[Function]}
-          role="button"
+          priority="primary"
+          size="small"
         >
         >
-          <Flex
-            align="center"
-            className="button-label"
+          <button
+            className="button button-primary button-sm"
+            disabled={false}
+            onClick={[Function]}
+            role="button"
           >
           >
-            <Base
+            <Flex
               align="center"
               align="center"
-              className="button-label glamor-0"
+              className="button-label"
             >
             >
-              <div
-                className="button-label glamor-0"
-                is={null}
+              <Base
+                align="center"
+                className="button-label glamor-26"
               >
               >
-                Save Changes
-              </div>
-            </Base>
-          </Flex>
-        </button>
-      </Button>
-    </div>
+                <div
+                  className="button-label glamor-26"
+                  is={null}
+                >
+                  Save Changes
+                </div>
+              </Base>
+            </Flex>
+          </button>
+        </Button>
+      </div>
+    </SaveButton>
   </div>
   </div>
 </OwnerInput>
 </OwnerInput>
 `;
 `;

+ 642 - 0
tests/js/spec/views/__snapshots__/ruleBuilder.spec.jsx.snap

@@ -0,0 +1,642 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProjectTeamsSettings render() renders 1`] = `
+.glamor-64 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  height: 40px;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  margin-bottom: 1em;
+}
+
+.glamor-0 {
+  padding: 0.5em;
+  margin-right: 5px;
+  width: 80px;
+  -webkit-flex-shrink: 0;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+}
+
+.glamor-4 {
+  display: block;
+  width: 100%;
+  background: #fff;
+  border: 1px solid;
+  box-shadow: inset;
+  padding: 0.5em;
+  -webkit-transition: border 0.1s linear;
+  transition: border 0.1s linear;
+  resize: vertical;
+  padding: 0.5em;
+  margin-right: 5px;
+}
+
+.glamor-4 {
+  display: block;
+  width: 100%;
+  background: #fff;
+  border: 1px solid;
+  box-shadow: inset;
+  padding: 0.5em;
+  -webkit-transition: border 0.1s linear;
+  transition: border 0.1s linear;
+  resize: vertical;
+  padding: 0.5em;
+  margin-right: 5px;
+}
+
+.glamor-4 {
+  display: block;
+  width: 100%;
+  background: #fff;
+  border: 1px solid;
+  box-shadow: inset;
+  padding: 0.5em;
+  -webkit-transition: border 0.1s linear;
+  transition: border 0.1s linear;
+  resize: vertical;
+  padding: 0.5em;
+  margin-right: 5px;
+}
+
+.glamor-4 {
+  display: block;
+  width: 100%;
+  background: #fff;
+  border: 1px solid;
+  box-shadow: inset;
+  padding: 0.5em;
+  -webkit-transition: border 0.1s linear;
+  transition: border 0.1s linear;
+  resize: vertical;
+  padding: 0.5em;
+  margin-right: 5px;
+}
+
+.glamor-4:focus {
+  outline: none;
+}
+
+.glamor-4:hover,
+.glamor-4:focus,
+.glamor-4:active {
+  border: 1px solid;
+}
+
+.glamor-9 {
+  -webkit-flex-shrink: 0;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  margin-right: 5px;
+}
+
+.glamor-7 {
+  vertical-align: middle;
+  -webkit-flex-shrink: 0;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  margin-right: 5px;
+}
+
+.glamor-45 {
+  box-sizing: border-box;
+  -webkit-flex: 1;
+  -ms-flex: 1;
+  flex: 1;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+}
+
+.glamor-41 {
+  position: relative;
+  display: inline-block;
+}
+
+.glamor-37 {
+  margin-right: 5px;
+  -webkit-flex: 1;
+  -ms-flex: 1;
+  flex: 1;
+  white-space: nowrap;
+  height: 37px;
+}
+
+.glamor-37 .button-label {
+  font-size: 14px;
+  padding: 4px 8px;
+  padding-left: 4px;
+}
+
+.glamor-29 {
+  position: relative;
+  z-index: 2;
+  box-shadow: none;
+  margin-right: 5px;
+  -webkit-flex: 1;
+  -ms-flex: 1;
+  flex: 1;
+  white-space: nowrap;
+  height: 37px;
+}
+
+.glamor-29 .button-label {
+  font-size: 14px;
+  padding: 4px 8px;
+  padding-left: 4px;
+}
+
+.glamor-26 {
+  box-sizing: border-box;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+}
+
+.glamor-13 {
+  -webkit-box-pack: end;
+  -webkit-justify-content: flex-end;
+  -ms-flex-pack: end;
+  justify-content: flex-end;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+}
+
+.glamor-13 span {
+  margin-right: 2px;
+}
+
+.glamor-13 .avatar {
+  width: 28px;
+  height: 28px;
+}
+
+.glamor-15 {
+  margin-left: 3px;
+  padding: 0px;
+}
+
+.glamor-20 {
+  margin-left: 0.33em;
+}
+
+.glamor-18 {
+  vertical-align: middle;
+  margin-left: 0.33em;
+}
+
+.glamor-60 {
+  width: 37px;
+  height: 37px;
+  -webkit-flex-shrink: 0;
+  -ms-flex-negative: 0;
+  flex-shrink: 0;
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  -webkit-box-pack: center;
+  -webkit-justify-content: center;
+  -ms-flex-pack: center;
+  justify-content: center;
+}
+
+.glamor-60 .button-label {
+  padding: 0.5em;
+}
+
+.glamor-60 div {
+  margin: 0px !important;
+}
+
+.glamor-54 {
+  box-sizing: border-box;
+  margin-right: 8px;
+  margin-left: -2px;
+}
+
+.glamor-50 {
+  display: block;
+}
+
+.glamor-48 {
+  vertical-align: middle;
+  display: block;
+}
+
+<RuleBuilder
+  onAddRule={
+    [MockFunction] {
+      "calls": Array [
+        Array [
+          "path:some/path/* Jane Doe",
+        ],
+      ],
+    }
+  }
+  project={
+    Object {
+      "hasAccess": true,
+      "id": "2",
+      "isBookmarked": false,
+      "isMember": true,
+      "name": "Project Name",
+      "slug": "project-slug",
+      "teams": Array [],
+    }
+  }
+>
+  <BuilderBar>
+    <div
+      className="glamor-64 glamor-65"
+    >
+      <BuilderSelect
+        onChange={[Function]}
+        value="path"
+      >
+        <SelectInput
+          className="glamor-0 glamor-1"
+          disabled={false}
+          multiple={false}
+          onChange={[Function]}
+          placeholder="Select an option..."
+          required={false}
+          value="path"
+        >
+          <select
+            className="glamor-0 glamor-1"
+            disabled={false}
+            multiple={false}
+            onChange={[Function]}
+            placeholder="Select an option..."
+            required={false}
+            value="path"
+          >
+            <option
+              value="path"
+            >
+              Path
+            </option>
+            <option
+              value="url"
+            >
+              URL
+            </option>
+          </select>
+        </SelectInput>
+      </BuilderSelect>
+      <BuilderInput
+        controlled={true}
+        onChange={[Function]}
+        placeholder="src/example/*"
+        value=""
+      >
+        <input
+          className="glamor-4 glamor-5"
+          onChange={[Function]}
+          placeholder="src/example/*"
+          value=""
+        />
+      </BuilderInput>
+      <Divider
+        src="icon-chevron-right"
+      >
+        <InlineSvg
+          className="glamor-9 glamor-6"
+          src="icon-chevron-right"
+        >
+          <StyledSvg
+            className="glamor-9 glamor-6"
+            height="1em"
+            style={Object {}}
+            viewBox={Object {}}
+            width="1em"
+          >
+            <svg
+              className="glamor-6 glamor-7 glamor-8"
+              height="1em"
+              style={Object {}}
+              viewBox={Object {}}
+              width="1em"
+            >
+              <use
+                href="#test"
+                xlinkHref="#test"
+              />
+            </svg>
+          </StyledSvg>
+        </InlineSvg>
+      </Divider>
+      <Flex
+        align="center"
+        flex="1"
+      >
+        <Base
+          align="center"
+          className="glamor-45"
+          flex="1"
+        >
+          <div
+            className="glamor-45"
+            is={null}
+          >
+            <DropdownAutoComplete
+              alignMenu="right"
+              items={
+                Array [
+                  Object {
+                    "items": Array [],
+                    "label": "Teams",
+                    "value": "team",
+                  },
+                  Object {
+                    "items": Array [
+                      Object {
+                        "actor": Object {
+                          "id": "1",
+                          "name": "Jane Doe",
+                          "type": "user",
+                        },
+                        "label": "janedoe@example.com",
+                        "searchKey": "janedoe@example.com  nodejs",
+                        "value": "user:1",
+                      },
+                      Object {
+                        "actor": Object {
+                          "id": "2",
+                          "name": "John Smith",
+                          "type": "user",
+                        },
+                        "label": "johnsmith@example.com",
+                        "searchKey": "johnsmith@example.com  nodejs",
+                        "value": "user:2",
+                      },
+                    ],
+                    "label": "Users",
+                    "value": "user",
+                  },
+                ]
+              }
+              onSelect={[Function]}
+            >
+              <DropdownAutoCompleteMenu
+                alignMenu="right"
+                items={
+                  Array [
+                    Object {
+                      "items": Array [],
+                      "label": "Teams",
+                      "value": "team",
+                    },
+                    Object {
+                      "items": Array [
+                        Object {
+                          "actor": Object {
+                            "id": "1",
+                            "name": "Jane Doe",
+                            "type": "user",
+                          },
+                          "label": "janedoe@example.com",
+                          "searchKey": "janedoe@example.com  nodejs",
+                          "value": "user:1",
+                        },
+                        Object {
+                          "actor": Object {
+                            "id": "2",
+                            "name": "John Smith",
+                            "type": "user",
+                          },
+                          "label": "johnsmith@example.com",
+                          "searchKey": "johnsmith@example.com  nodejs",
+                          "value": "user:2",
+                        },
+                      ],
+                      "label": "Users",
+                      "value": "user",
+                    },
+                  ]
+                }
+                onSelect={[Function]}
+              >
+                <AutoComplete
+                  itemToString={[Function]}
+                  onSelect={[Function]}
+                >
+                  <DropdownMenu
+                    isOpen={false}
+                    keepMenuOpen={false}
+                    onClickOutside={[Function]}
+                  >
+                    <AutoCompleteRoot>
+                      <Component
+                        className="glamor-41 glamor-42"
+                      >
+                        <div
+                          className="glamor-41 glamor-42"
+                        >
+                          <div
+                            onClick={[Function]}
+                            onMouseEnter={[Function]}
+                            onMouseLeave={[Function]}
+                            role="button"
+                          >
+                            <BuilderDropdownButton
+                              isOpen={false}
+                            >
+                              <DropdownButton
+                                className="glamor-37 glamor-28"
+                                isOpen={false}
+                              >
+                                <StyledButton
+                                  className="glamor-37 glamor-28"
+                                  isOpen={false}
+                                >
+                                  <Component
+                                    className="glamor-28 glamor-29 glamor-30"
+                                    isOpen={false}
+                                  >
+                                    <Button
+                                      className="glamor-28 glamor-29 glamor-30"
+                                      disabled={false}
+                                    >
+                                      <button
+                                        className="glamor-28 glamor-29 glamor-30 button button-default"
+                                        disabled={false}
+                                        onClick={[Function]}
+                                        role="button"
+                                      >
+                                        <Flex
+                                          align="center"
+                                          className="button-label"
+                                        >
+                                          <Base
+                                            align="center"
+                                            className="button-label glamor-26"
+                                          >
+                                            <div
+                                              className="button-label glamor-26"
+                                              is={null}
+                                            >
+                                              <Owners>
+                                                <div
+                                                  className="glamor-13 glamor-14"
+                                                />
+                                              </Owners>
+                                              <AddOwnersLabel>
+                                                <div
+                                                  className="glamor-15 glamor-16"
+                                                >
+                                                  Add Owners
+                                                </div>
+                                              </AddOwnersLabel>
+                                              <StyledChevronDown>
+                                                <Component
+                                                  className="glamor-20 glamor-17"
+                                                >
+                                                  <InlineSvg
+                                                    className="glamor-20 glamor-17"
+                                                    src="icon-chevron-down"
+                                                  >
+                                                    <StyledSvg
+                                                      className="glamor-20 glamor-17"
+                                                      height="1em"
+                                                      style={Object {}}
+                                                      viewBox={Object {}}
+                                                      width="1em"
+                                                    >
+                                                      <svg
+                                                        className="glamor-17 glamor-18 glamor-8"
+                                                        height="1em"
+                                                        style={Object {}}
+                                                        viewBox={Object {}}
+                                                        width="1em"
+                                                      >
+                                                        <use
+                                                          href="#test"
+                                                          xlinkHref="#test"
+                                                        />
+                                                      </svg>
+                                                    </StyledSvg>
+                                                  </InlineSvg>
+                                                </Component>
+                                              </StyledChevronDown>
+                                            </div>
+                                          </Base>
+                                        </Flex>
+                                      </button>
+                                    </Button>
+                                  </Component>
+                                </StyledButton>
+                              </DropdownButton>
+                            </BuilderDropdownButton>
+                          </div>
+                        </div>
+                      </Component>
+                    </AutoCompleteRoot>
+                  </DropdownMenu>
+                </AutoComplete>
+              </DropdownAutoCompleteMenu>
+            </DropdownAutoComplete>
+          </div>
+        </Base>
+      </Flex>
+      <RuleAddButton
+        icon="icon-circle-add"
+        onClick={[Function]}
+        priority="primary"
+      >
+        <Button
+          className="glamor-60 glamor-61"
+          disabled={false}
+          icon="icon-circle-add"
+          onClick={[Function]}
+          priority="primary"
+        >
+          <button
+            className="glamor-60 glamor-61 button button-primary"
+            disabled={false}
+            onClick={[Function]}
+            role="button"
+          >
+            <Flex
+              align="center"
+              className="button-label"
+            >
+              <Base
+                align="center"
+                className="button-label glamor-26"
+              >
+                <div
+                  className="button-label glamor-26"
+                  is={null}
+                >
+                  <Icon>
+                    <Base
+                      className="glamor-54 glamor-55"
+                    >
+                      <div
+                        className="glamor-54 glamor-55"
+                        is={null}
+                      >
+                        <StyledInlineSvg
+                          size="16px"
+                          src="icon-circle-add"
+                        >
+                          <InlineSvg
+                            className="glamor-50 glamor-47"
+                            size="16px"
+                            src="icon-circle-add"
+                          >
+                            <StyledSvg
+                              className="glamor-50 glamor-47"
+                              height="16px"
+                              style={Object {}}
+                              viewBox={Object {}}
+                              width="16px"
+                            >
+                              <svg
+                                className="glamor-47 glamor-48 glamor-8"
+                                height="16px"
+                                style={Object {}}
+                                viewBox={Object {}}
+                                width="16px"
+                              >
+                                <use
+                                  href="#test"
+                                  xlinkHref="#test"
+                                />
+                              </svg>
+                            </StyledSvg>
+                          </InlineSvg>
+                        </StyledInlineSvg>
+                      </div>
+                    </Base>
+                  </Icon>
+                </div>
+              </Base>
+            </Flex>
+          </button>
+        </Button>
+      </RuleAddButton>
+    </div>
+  </BuilderBar>
+</RuleBuilder>
+`;

+ 2 - 1
tests/js/spec/views/ownershipInput.spec.jsx

@@ -4,6 +4,7 @@ import {mount} from 'enzyme';
 import {Client} from 'app/api';
 import {Client} from 'app/api';
 import OwnerInput from 'app/views/settings/project/projectOwnership/ownerInput';
 import OwnerInput from 'app/views/settings/project/projectOwnership/ownerInput';
 
 
+jest.mock('jquery');
 describe('ProjectTeamsSettings', function() {
 describe('ProjectTeamsSettings', function() {
   let org;
   let org;
   let project;
   let project;
@@ -32,7 +33,7 @@ describe('ProjectTeamsSettings', function() {
         TestStubs.routerContext()
         TestStubs.routerContext()
       );
       );
 
 
-      let submit = wrapper.find('button');
+      let submit = wrapper.find('SaveButton button');
 
 
       expect(put).not.toHaveBeenCalled();
       expect(put).not.toHaveBeenCalled();
 
 

Some files were not shown because too many files changed in this diff