Browse Source

ref(external-repositories-settings): Add feature flag restrictions (#30820)

Priscila Oliveira 3 years ago
parent
commit
122950d344

+ 1 - 1
src/sentry/conf/server.py

@@ -920,7 +920,7 @@ SENTRY_FEATURES = {
     "organizations:filters-and-sampling": False,
     # Enable Dynamic Sampling errors in the org settings
     "organizations:filters-and-sampling-error-rules": False,
-    # Allow organizations to configure built-in symbol sources.
+    # Allow organizations to configure all symbol sources.
     "organizations:symbol-sources": True,
     # Allow organizations to configure custom external symbol sources.
     "organizations:custom-symbol-sources": True,

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

@@ -72,6 +72,12 @@ type DisabledAppStoreConnectItem = {
   children: React.ReactElement;
 };
 
+type DisabledCustomSymbolSources = {
+  organization: Organization;
+  disabled: boolean;
+  children: React.ReactNode;
+};
+
 type DisabledMemberTooltipProps = {children: React.ReactNode};
 
 type DashboardHeadersProps = {organization: Organization};
@@ -95,6 +101,7 @@ type FirstPartyIntegrationAdditionalCTAProps = {
  * Component wrapping hooks
  */
 export type ComponentHooks = {
+  'component:disabled-custom-symbol-sources': () => React.ComponentType<DisabledCustomSymbolSources>;
   'component:header-date-range': () => React.ComponentType<DateRangeProps>;
   'component:header-selector-items': () => React.ComponentType<SelectorItemsProps>;
   'component:global-notifications': () => React.ComponentType<GlobalNotificationProps>;

+ 5 - 4
static/app/views/settings/projectDebugFiles/index.tsx

@@ -19,7 +19,7 @@ import TextBlock from 'sentry/views/settings/components/text/textBlock';
 import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
 
 import DebugFileRow from './debugFileRow';
-import ExternalSources from './externalSources';
+import Sources from './sources';
 
 type Props = RouteComponentProps<{orgId: string; projectId: string}, {}> & {
   organization: Organization;
@@ -166,7 +166,6 @@ class ProjectDebugSymbols extends AsyncView<Props, State> {
     const {organization, project, router, location} = this.props;
     const {loading, showDetails, builtinSymbolSources, debugFiles, debugFilesPageLinks} =
       this.state;
-    const {features} = organization;
 
     return (
       <Fragment>
@@ -180,10 +179,11 @@ class ProjectDebugSymbols extends AsyncView<Props, State> {
           `)}
         </TextBlock>
 
-        {features.includes('symbol-sources') && (
+        {organization.features.includes('symbol-sources') && (
           <Fragment>
             <PermissionAlert />
-            <ExternalSources
+
+            <Sources
               api={this.api}
               location={location}
               router={router}
@@ -196,6 +196,7 @@ class ProjectDebugSymbols extends AsyncView<Props, State> {
               }
               builtinSymbolSources={project.builtinSymbolSources ?? []}
               builtinSymbolSourceOptions={builtinSymbolSources ?? []}
+              isLoading={loading}
             />
           </Fragment>
         )}

+ 41 - 25
static/app/views/settings/projectDebugFiles/externalSources/builtInRepositories.tsx → static/app/views/settings/projectDebugFiles/sources/builtInRepositories.tsx

@@ -4,18 +4,22 @@ import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicato
 import ProjectActions from 'sentry/actions/projectActions';
 import {Client} from 'sentry/api';
 import Access from 'sentry/components/acl/access';
+import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {Panel, PanelBody, PanelHeader} from 'sentry/components/panels';
 import {t} from 'sentry/locale';
 import {Organization, Project} from 'sentry/types';
 import {BuiltinSymbolSource} from 'sentry/types/debugFiles';
 import SelectField from 'sentry/views/settings/components/forms/selectField';
 
+const SECTION_TITLE = t('Built-in Repositories');
+
 type Props = {
   api: Client;
   organization: Organization;
   projSlug: Project['slug'];
   builtinSymbolSourceOptions: BuiltinSymbolSource[];
   builtinSymbolSources: string[];
+  isLoading: boolean;
 };
 
 function BuiltInRepositories({
@@ -24,6 +28,7 @@ function BuiltInRepositories({
   builtinSymbolSourceOptions,
   builtinSymbolSources,
   projSlug,
+  isLoading,
 }: Props) {
   // If the project details object has an unknown built-in source, this will be filtered here.
   // This prevents the UI from showing the wrong feedback message when updating the field
@@ -74,32 +79,43 @@ function BuiltInRepositories({
 
   return (
     <Panel>
-      <PanelHeader>{t('Built-in Repositories')}</PanelHeader>
+      <PanelHeader>{SECTION_TITLE}</PanelHeader>
       <PanelBody>
-        <Access access={['project:write']}>
-          {({hasAccess}) => (
-            <StyledSelectField
-              disabled={!hasAccess}
-              name="builtinSymbolSources"
-              label={t('Built-in Repositories')}
-              help={t(
-                'Configures which built-in repositories Sentry should use to resolve debug files.'
-              )}
-              placeholder={t('Select built-in repository')}
-              value={validBuiltInSymbolSources}
-              onChange={handleChange}
-              options={builtinSymbolSourceOptions
-                .filter(source => !source.hidden)
-                .map(source => ({
-                  value: source.sentry_key,
-                  label: source.name,
-                }))}
-              getValue={value => (value === null ? [] : value)}
-              flexibleControlStateSize
-              multiple
-            />
-          )}
-        </Access>
+        {isLoading ? (
+          <LoadingIndicator />
+        ) : (
+          <Access access={['project:write']}>
+            {({hasAccess}) => (
+              <StyledSelectField
+                disabledReason={
+                  !hasAccess
+                    ? t(
+                        'You do not have permission to edit built-in repositories configurations.'
+                      )
+                    : undefined
+                }
+                disabled={!hasAccess}
+                name="builtinSymbolSources"
+                label={SECTION_TITLE}
+                help={t(
+                  'Configures which built-in repositories Sentry should use to resolve debug files.'
+                )}
+                placeholder={t('Select built-in repository')}
+                value={validBuiltInSymbolSources}
+                onChange={handleChange}
+                options={builtinSymbolSourceOptions
+                  .filter(source => !source.hidden)
+                  .map(source => ({
+                    value: source.sentry_key,
+                    label: source.name,
+                  }))}
+                getValue={value => (value === null ? [] : value)}
+                flexibleControlStateSize
+                multiple
+              />
+            )}
+          </Access>
+        )}
       </PanelBody>
     </Panel>
   );

+ 72 - 64
static/app/views/settings/projectDebugFiles/externalSources/customRepositories/actions.tsx → static/app/views/settings/projectDebugFiles/sources/customRepositories/actions.tsx

@@ -1,7 +1,6 @@
 import {Fragment} from 'react';
 import styled from '@emotion/styled';
 
-import Access from 'sentry/components/acl/access';
 import ActionButton from 'sentry/components/actions/button';
 import MenuItemActionLink from 'sentry/components/actions/menuItemActionLink';
 import Button from 'sentry/components/button';
@@ -25,6 +24,8 @@ type Props = {
   onEdit: () => void;
   onDelete: () => void;
   showDetails: boolean;
+  hasFeature: boolean;
+  hasAccess: boolean;
 };
 
 function Actions({
@@ -36,6 +37,8 @@ function Actions({
   onEdit,
   onDelete,
   showDetails,
+  hasFeature,
+  hasAccess,
 }: Props) {
   function renderConfirmDelete(element: React.ReactElement) {
     return (
@@ -80,6 +83,9 @@ function Actions({
       </ConfirmDelete>
     );
   }
+
+  const actionsDisabled = !hasAccess || isDetailsDisabled || !hasFeature;
+
   return (
     <StyledButtonBar gap={1}>
       {showDetails && (
@@ -93,70 +99,72 @@ function Actions({
           {t('Details')}
         </StyledDropdownButton>
       )}
-      <Access access={['project:write']}>
-        {({hasAccess}) => (
-          <Fragment>
-            <ButtonTooltip
-              title={t(
-                'You do not have permission to edit custom repository configurations.'
-              )}
-              disabled={hasAccess}
-            >
-              <ActionBtn
-                disabled={!hasAccess || isDetailsDisabled}
-                onClick={onEdit}
-                size="small"
-              >
-                {t('Configure')}
-              </ActionBtn>
-            </ButtonTooltip>
 
-            {!hasAccess || isDetailsDisabled ? (
-              <ButtonTooltip
-                title={t(
-                  'You do not have permission to delete custom repository configurations.'
-                )}
-                disabled={hasAccess}
-              >
-                <ActionBtn size="small" disabled>
-                  {t('Delete')}
-                </ActionBtn>
-              </ButtonTooltip>
-            ) : (
-              renderConfirmDelete(<ActionBtn size="small">{t('Delete')}</ActionBtn>)
-            )}
-            <DropDownWrapper>
-              <DropdownLink
-                caret={false}
-                customTitle={
-                  <StyledActionButton
-                    label={t('Actions')}
-                    disabled={!hasAccess || isDetailsDisabled}
-                    title={
-                      !hasAccess
-                        ? t(
-                            'You do not have permission to edit and delete custom repository configurations.'
-                          )
-                        : undefined
-                    }
-                    icon={<IconEllipsis />}
-                  />
-                }
-                anchorRight
-              >
-                <MenuItemActionLink title={t('Configure')} onClick={onEdit}>
-                  {t('Configure')}
-                </MenuItemActionLink>
-                {renderConfirmDelete(
-                  <MenuItemActionLink title={t('Delete')}>
-                    {t('Delete')}
-                  </MenuItemActionLink>
-                )}
-              </DropdownLink>
-            </DropDownWrapper>
-          </Fragment>
-        )}
-      </Access>
+      <ButtonTooltip
+        title={
+          !hasFeature
+            ? undefined
+            : !hasAccess
+            ? t('You do not have permission to edit custom repositories configurations.')
+            : undefined
+        }
+        disabled={actionsDisabled}
+      >
+        <ActionBtn disabled={actionsDisabled} onClick={onEdit} size="small">
+          {t('Configure')}
+        </ActionBtn>
+      </ButtonTooltip>
+
+      {actionsDisabled ? (
+        <ButtonTooltip
+          title={
+            !hasFeature
+              ? undefined
+              : !hasAccess
+              ? t(
+                  'You do not have permission to delete custom repositories configurations.'
+                )
+              : undefined
+          }
+          disabled={actionsDisabled}
+        >
+          <ActionBtn size="small" disabled>
+            {t('Delete')}
+          </ActionBtn>
+        </ButtonTooltip>
+      ) : (
+        renderConfirmDelete(<ActionBtn size="small">{t('Delete')}</ActionBtn>)
+      )}
+      <DropDownWrapper>
+        <DropdownLink
+          caret={false}
+          disabled={actionsDisabled}
+          customTitle={
+            <StyledActionButton
+              label={t('Actions')}
+              disabled={actionsDisabled}
+              title={
+                !hasFeature
+                  ? undefined
+                  : !hasAccess
+                  ? t(
+                      'You do not have permission to edit and delete custom repositories configurations.'
+                    )
+                  : undefined
+              }
+              icon={<IconEllipsis />}
+            />
+          }
+          anchorRight
+        >
+          <MenuItemActionLink title={t('Configure')} onClick={onEdit}>
+            {t('Configure')}
+          </MenuItemActionLink>
+          {renderConfirmDelete(
+            <MenuItemActionLink title={t('Delete')}>{t('Delete')}</MenuItemActionLink>
+          )}
+        </DropdownLink>
+      </DropDownWrapper>
     </StyledButtonBar>
   );
 }

+ 0 - 0
static/app/views/settings/projectDebugFiles/externalSources/customRepositories/details.tsx → static/app/views/settings/projectDebugFiles/sources/customRepositories/details.tsx


+ 103 - 24
static/app/views/settings/projectDebugFiles/externalSources/customRepositories/index.tsx → static/app/views/settings/projectDebugFiles/sources/customRepositories/index.tsx

@@ -8,13 +8,18 @@ import {openDebugFileSourceModal} from 'sentry/actionCreators/modal';
 import ProjectActions from 'sentry/actions/projectActions';
 import {Client} from 'sentry/api';
 import Access from 'sentry/components/acl/access';
+import Feature from 'sentry/components/acl/feature';
+import FeatureDisabled from 'sentry/components/acl/featureDisabled';
 import DropdownAutoComplete from 'sentry/components/dropdownAutoComplete';
 import DropdownButton from 'sentry/components/dropdownButton';
 import EmptyStateWarning from 'sentry/components/emptyStateWarning';
 import HookOrDefault from 'sentry/components/hookOrDefault';
+import LoadingIndicator from 'sentry/components/loadingIndicator';
 import MenuItem from 'sentry/components/menuItem';
 import {Panel, PanelBody, PanelHeader} from 'sentry/components/panels';
+import PanelAlert from 'sentry/components/panels/panelAlert';
 import AppStoreConnectContext from 'sentry/components/projects/appStoreConnectContext';
+import Tooltip from 'sentry/components/tooltip';
 import {t} from 'sentry/locale';
 import {Organization, Project} from 'sentry/types';
 import {CustomRepo, CustomRepoType} from 'sentry/types/debugFiles';
@@ -28,11 +33,18 @@ import {
   getRequestMessages,
 } from './utils';
 
+const SECTION_TITLE = t('Custom Repositories');
+
 const HookedAppStoreConnectItem = HookOrDefault({
   hookName: 'component:disabled-app-store-connect-item',
   defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
 });
 
+const HookedCustomSymbolSources = HookOrDefault({
+  hookName: 'component:disabled-custom-symbol-sources',
+  defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
+});
+
 type Props = {
   api: Client;
   organization: Organization;
@@ -40,6 +52,7 @@ type Props = {
   customRepositories: CustomRepo[];
   router: InjectedRouter;
   location: Location;
+  isLoading: boolean;
 };
 
 function CustomRepositories({
@@ -49,6 +62,7 @@ function CustomRepositories({
   projSlug,
   router,
   location,
+  isLoading,
 }: Props) {
   const appStoreConnectContext = useContext(AppStoreConnectContext);
 
@@ -193,12 +207,26 @@ function CustomRepositories({
     });
   }
 
-  return (
-    <Panel>
-      <PanelHeader hasButtons>
-        {t('Custom Repositories')}
+  function renderAddRepositoryButton({
+    hasAccess,
+    hasFeature,
+  }: {
+    hasAccess: boolean;
+    hasFeature: boolean;
+  }) {
+    return (
+      <Tooltip
+        title={
+          !hasFeature
+            ? undefined
+            : !hasAccess
+            ? t('You do not have permission to add custom repositories.')
+            : undefined
+        }
+      >
         <DropdownAutoComplete
           alignMenu="right"
+          disabled={!hasAccess || !hasFeature || isLoading}
           onSelect={item => {
             handleAddRepository(item.value);
           }}
@@ -228,26 +256,42 @@ function CustomRepositories({
           })}
         >
           {({isOpen}) => (
-            <Access access={['project:write']}>
-              {({hasAccess}) => (
-                <DropdownButton
-                  isOpen={isOpen}
-                  title={
-                    !hasAccess
-                      ? t('You do not have permission to add custom repositories.')
-                      : undefined
-                  }
-                  disabled={!hasAccess}
-                  size="small"
-                >
-                  {t('Add Repository')}
-                </DropdownButton>
-              )}
-            </Access>
+            <DropdownButton
+              isOpen={isOpen}
+              disabled={!hasAccess || !hasFeature || isLoading}
+              size="small"
+            >
+              {t('Add Repository')}
+            </DropdownButton>
           )}
         </DropdownAutoComplete>
-      </PanelHeader>
-      <PanelBody>
+      </Tooltip>
+    );
+  }
+
+  function renderContent({
+    hasAccess,
+    hasFeature,
+    features,
+  }: {
+    hasAccess: boolean;
+    hasFeature: boolean;
+    features: string[];
+  }) {
+    if (isLoading) {
+      return <LoadingIndicator />;
+    }
+
+    return (
+      <HookedCustomSymbolSources disabled={hasFeature} organization={organization}>
+        {!hasFeature && (
+          <FeatureDisabled
+            features={features}
+            alert={PanelAlert}
+            message={t('This feature is not enabled on your Sentry installation.')}
+            featureName={SECTION_TITLE}
+          />
+        )}
         {!repositories.length ? (
           <EmptyStateWarning>
             <p>{t('No custom repositories configured')}</p>
@@ -264,18 +308,53 @@ function CustomRepositories({
                     }
                   : repository
               }
+              hasFeature={hasFeature}
+              hasAccess={hasAccess}
               onDelete={handleDeleteRepository}
               onEdit={handleEditRepository}
             />
           ))
         )}
-      </PanelBody>
-    </Panel>
+      </HookedCustomSymbolSources>
+    );
+  }
+
+  return (
+    <Feature features={['custom-symbol-sources']} organization={organization}>
+      {({hasFeature, features}) => (
+        <Access access={['project:write']}>
+          {({hasAccess}) => {
+            return (
+              <Content hasFeature={hasFeature}>
+                <PanelHeader hasButtons>
+                  {SECTION_TITLE}
+                  {renderAddRepositoryButton({
+                    hasAccess,
+                    hasFeature,
+                  })}
+                </PanelHeader>
+                <PanelBody>
+                  {renderContent({
+                    hasAccess,
+                    hasFeature,
+                    features,
+                  })}
+                </PanelBody>
+              </Content>
+            );
+          }}
+        </Access>
+      )}
+    </Feature>
   );
 }
 
 export default CustomRepositories;
 
+const Content = styled(Panel)<{hasFeature: boolean}>`
+  ${p => !p.hasFeature && `overflow: hidden;`}
+`;
+
 const StyledMenuItem = styled(MenuItem)`
   color: ${p => p.theme.textColor};
   font-size: ${p => p.theme.fontSizeMedium};

+ 5 - 1
static/app/views/settings/projectDebugFiles/externalSources/customRepositories/repository.tsx → static/app/views/settings/projectDebugFiles/sources/customRepositories/repository.tsx

@@ -14,9 +14,11 @@ type Props = {
   repository: CustomRepo;
   onDelete: (repositoryId: string) => void;
   onEdit: (repositoryId: string) => void;
+  hasFeature: boolean;
+  hasAccess: boolean;
 };
 
-function Repository({repository, onDelete, onEdit}: Props) {
+function Repository({repository, onDelete, onEdit, hasFeature, hasAccess}: Props) {
   const [isDetailsExpanded, setIsDetailsExpanded] = useState(false);
   const {id, name, type} = repository;
 
@@ -32,6 +34,8 @@ function Repository({repository, onDelete, onEdit}: Props) {
       <CustomRepositoryActions
         repositoryName={name}
         repositoryType={type}
+        hasFeature={hasFeature}
+        hasAccess={hasAccess}
         onDelete={() => onDelete(id)}
         onEdit={() => onEdit(id)}
         showDetails={repository.type === CustomRepoType.APP_STORE_CONNECT}

+ 0 - 0
static/app/views/settings/projectDebugFiles/externalSources/customRepositories/status.tsx → static/app/views/settings/projectDebugFiles/sources/customRepositories/status.tsx


+ 0 - 0
static/app/views/settings/projectDebugFiles/externalSources/customRepositories/utils.tsx → static/app/views/settings/projectDebugFiles/sources/customRepositories/utils.tsx


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