Browse Source

feat(ui): project selector hook (#29447)

This PR adds a hook called feature-disabled:project-selector-all-projects which allows getsentry to customize a disabled view we show in the global header for showing the button which allows the user to select all/their projects. Note we also changed the text from View All Projects to Select All Projects to make it more clear what it does.
Stephen Cefali 3 years ago
parent
commit
fc8d21c409

+ 0 - 1
static/app/components/acl/feature.tsx

@@ -177,7 +177,6 @@ class Feature extends React.Component<Props> {
         customDisabledRender = hooks[0];
       }
     }
-
     const renderProps = {
       organization,
       project,

+ 1 - 7
static/app/components/organizations/globalSelectionHeader/globalSelectionHeader.tsx

@@ -171,9 +171,6 @@ class GlobalSelectionHeader extends React.Component<Props, State> {
     searchQuery: '',
   };
 
-  hasMultipleProjectSelection = () =>
-    new Set(this.props.organization.features).has('global-views');
-
   // Returns an options object for `update*` actions
   getUpdateOptions = () => ({
     save: true,
@@ -346,10 +343,7 @@ class GlobalSelectionHeader extends React.Component<Props, State> {
                     value={this.state.projects || this.props.selection.projects}
                     onChange={this.handleChangeProjects}
                     onUpdate={this.handleUpdateProjects}
-                    multi={
-                      !disableMultipleProjectSelection &&
-                      this.hasMultipleProjectSelection()
-                    }
+                    disableMultipleProjectSelection={disableMultipleProjectSelection}
                     {...(loadingProjects ? paginatedProjectSelectorCallbacks : {})}
                     showIssueStreamLink={showIssueStreamLink}
                     showProjectSettingsLink={showProjectSettingsLink}

+ 77 - 41
static/app/components/organizations/multipleProjectSelector.tsx

@@ -3,6 +3,7 @@ import {withRouter, WithRouterProps} from 'react-router';
 import {ClassNames} from '@emotion/react';
 import styled from '@emotion/styled';
 
+import Feature from 'app/components/acl/feature';
 import Button from 'app/components/button';
 import Link from 'app/components/links/link';
 import HeaderItem from 'app/components/organizations/headerItem';
@@ -27,7 +28,7 @@ type Props = WithRouterProps & {
   onChange: (selected: number[]) => unknown;
   onUpdate: () => unknown;
   isGlobalSelectionReady?: boolean;
-  multi?: boolean;
+  disableMultipleProjectSelection?: boolean;
   shouldForceProject?: boolean;
   forceProject?: MinimalProject | null;
   showIssueStreamLink?: boolean;
@@ -42,7 +43,6 @@ type State = {
 
 class MultipleProjectSelector extends React.PureComponent<Props, State> {
   static defaultProps = {
-    multi: true,
     lockedMessageSubject: t('page'),
   };
 
@@ -50,6 +50,13 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
     hasChanges: false,
   };
 
+  get multi() {
+    const {organization, disableMultipleProjectSelection} = this.props;
+    return (
+      !disableMultipleProjectSelection && organization.features.includes('global-views')
+    );
+  }
+
   // Reset "hasChanges" state and call `onUpdate` callback
   doUpdate = () => {
     this.setState({hasChanges: false}, this.props.onUpdate);
@@ -93,12 +100,12 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
       return;
     }
 
-    const {value, multi} = this.props;
+    const {value} = this.props;
     analytics('projectselector.update', {
       count: value.length,
       path: getRouteStringFromRoutes(this.props.router.routes),
       org_id: parseInt(this.props.organization.id, 10),
-      multi,
+      multi: this.multi,
     });
 
     this.doUpdate();
@@ -139,9 +146,9 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
   };
 
   renderProjectName() {
-    const {forceProject, location, multi, organization, showIssueStreamLink} = this.props;
+    const {forceProject, location, organization, showIssueStreamLink} = this.props;
 
-    if (showIssueStreamLink && forceProject && multi) {
+    if (showIssueStreamLink && forceProject && this.multi) {
       return (
         <Tooltip title={t('Issues Stream')} position="bottom">
           <StyledLink
@@ -181,8 +188,8 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
       value,
       projects,
       isGlobalSelectionReady,
+      disableMultipleProjectSelection,
       nonMemberProjects,
-      multi,
       organization,
       shouldForceProject,
       forceProject,
@@ -190,6 +197,7 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
       footerMessage,
     } = this.props;
     const selectedProjectIds = new Set(value);
+    const multi = this.multi;
 
     const allProjects = [...projects, ...nonMemberProjects];
     const selected = allProjects.filter(project =>
@@ -246,7 +254,7 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
             menuFooter={({actions}) => (
               <SelectorFooterControls
                 selected={selectedProjectIds}
-                multi={multi}
+                disableMultipleProjectSelection={disableMultipleProjectSelection}
                 organization={organization}
                 hasChanges={this.state.hasChanges}
                 onApply={() => this.handleUpdate(actions)}
@@ -305,20 +313,28 @@ class MultipleProjectSelector extends React.PureComponent<Props, State> {
   }
 }
 
+type FeatureRenderProps = {
+  hasFeature: boolean;
+  renderShowAllButton?: (p: {
+    canShowAllProjects: boolean;
+    onButtonClick: () => void;
+  }) => React.ReactNode;
+};
+
 type ControlProps = {
   organization: Organization;
-  onApply: (e: React.MouseEvent) => void;
-  onShowAllProjects: (e: React.MouseEvent) => void;
-  onShowMyProjects: (e: React.MouseEvent) => void;
+  onApply: () => void;
+  onShowAllProjects: () => void;
+  onShowMyProjects: () => void;
   selected?: Set<number>;
-  multi?: boolean;
+  disableMultipleProjectSelection?: boolean;
   hasChanges?: boolean;
   message?: React.ReactNode;
 };
 
 const SelectorFooterControls = ({
   selected,
-  multi,
+  disableMultipleProjectSelection,
   hasChanges,
   onApply,
   onShowAllProjects,
@@ -326,41 +342,55 @@ const SelectorFooterControls = ({
   organization,
   message,
 }: ControlProps) => {
-  let showMyProjects = false;
-  let showAllProjects = false;
-  if (multi) {
-    showMyProjects = true;
-
-    const hasGlobalRole =
-      organization.role === 'owner' || organization.role === 'manager';
-    const hasOpenMembership = organization.features.includes('open-membership');
-    const allSelected = selected && selected.has(ALL_ACCESS_PROJECTS);
-    if ((hasGlobalRole || hasOpenMembership) && !allSelected) {
-      showAllProjects = true;
-      showMyProjects = false;
-    }
-  }
-
   // Nothing to show.
-  if (!(showAllProjects || showMyProjects || hasChanges || message)) {
+  if (disableMultipleProjectSelection && !hasChanges && !message) {
     return null;
   }
 
+  // see if we should show "All Projects" or "My Projects" if disableMultipleProjectSelection isn't true
+  const hasGlobalRole = organization.role === 'owner' || organization.role === 'manager';
+  const hasOpenMembership = organization.features.includes('open-membership');
+  const allSelected = selected && selected.has(ALL_ACCESS_PROJECTS);
+
+  const canShowAllProjects = (hasGlobalRole || hasOpenMembership) && !allSelected;
+  const onProjectClick = canShowAllProjects ? onShowAllProjects : onShowMyProjects;
+  const buttonText = canShowAllProjects
+    ? t('Select All Projects')
+    : t('Select My Projects');
+
   return (
     <FooterContainer>
       {message && <FooterMessage>{message}</FooterMessage>}
-
       <FooterActions>
-        {showAllProjects && (
-          <Button onClick={onShowAllProjects} priority="default" size="xsmall">
-            {t('View All Projects')}
-          </Button>
-        )}
-        {showMyProjects && (
-          <Button onClick={onShowMyProjects} priority="default" size="xsmall">
-            {t('View My Projects')}
-          </Button>
+        {!disableMultipleProjectSelection && (
+          <Feature
+            features={['organizations:global-views']}
+            organization={organization}
+            hookName="feature-disabled:project-selector-all-projects"
+            renderDisabled={false}
+          >
+            {({renderShowAllButton, hasFeature}: FeatureRenderProps) => {
+              // if our hook is adding renderShowAllButton, render that
+              if (renderShowAllButton) {
+                return renderShowAllButton({
+                  onButtonClick: onProjectClick,
+                  canShowAllProjects,
+                });
+              }
+              // if no hook, render null if feature is disabled
+              if (!hasFeature) {
+                return null;
+              }
+              // otherwise render the buton
+              return (
+                <Button priority="default" size="xsmall" onClick={onProjectClick}>
+                  {buttonText}
+                </Button>
+              );
+            }}
+          </Feature>
         )}
+
         {hasChanges && (
           <SubmitButton onClick={onApply} size="xsmall" priority="primary">
             {t('Apply Filter')}
@@ -374,14 +404,20 @@ const SelectorFooterControls = ({
 export default withRouter(MultipleProjectSelector);
 
 const FooterContainer = styled('div')`
-  padding: ${space(1)} 0;
+  display: flex;
+  justify-content: space-between;
 `;
+
 const FooterActions = styled('div')`
+  padding: ${space(1)} 0;
   display: flex;
   justify-content: flex-end;
   & > * {
     margin-left: ${space(0.5)};
   }
+  &:empty {
+    display: none;
+  }
 `;
 const SubmitButton = styled(Button)`
   animation: 0.1s ${growIn} ease-in;
@@ -389,7 +425,7 @@ const SubmitButton = styled(Button)`
 
 const FooterMessage = styled('div')`
   font-size: ${p => p.theme.fontSizeSmall};
-  padding: 0 ${space(0.5)};
+  padding: ${space(1)} ${space(0.5)};
 `;
 
 const StyledProjectSelector = styled(ProjectSelector)`

+ 1 - 0
static/app/types/hooks.tsx

@@ -145,6 +145,7 @@ export type FeatureDisabledHooks = {
   'feature-disabled:sso-saml2': FeatureDisabledHook;
   'feature-disabled:trace-view-link': FeatureDisabledHook;
   'feature-disabled:alert-wizard-performance': FeatureDisabledHook;
+  'feature-disabled:project-selector-all-projects': FeatureDisabledHook;
 };
 
 /**

+ 4 - 4
tests/js/spec/components/organizations/globalSelectionHeader.spec.jsx

@@ -1031,7 +1031,7 @@ describe('GlobalSelectionHeader', function () {
       // My projects in the footer
       expect(
         projectSelector.find('SelectorFooterControls Button').first().text()
-      ).toEqual('View My Projects');
+      ).toEqual('Select My Projects');
     });
 
     it('shows "All Projects" button based on features', async function () {
@@ -1056,7 +1056,7 @@ describe('GlobalSelectionHeader', function () {
       // All projects in the footer
       expect(
         projectSelector.find('SelectorFooterControls Button').first().text()
-      ).toEqual('View All Projects');
+      ).toEqual('Select All Projects');
     });
 
     it('shows "All Projects" button based on role', async function () {
@@ -1081,7 +1081,7 @@ describe('GlobalSelectionHeader', function () {
       // All projects in the footer
       expect(
         projectSelector.find('SelectorFooterControls Button').first().text()
-      ).toEqual('View All Projects');
+      ).toEqual('Select All Projects');
     });
 
     it('shows "My Projects" when "all projects" is selected', async function () {
@@ -1106,7 +1106,7 @@ describe('GlobalSelectionHeader', function () {
       // My projects in the footer
       expect(
         projectSelector.find('SelectorFooterControls Button').first().text()
-      ).toEqual('View My Projects');
+      ).toEqual('Select My Projects');
     });
   });