Browse Source

ref(app-store-connect-dialog): Applies Internal Design Feedback (#26074)

Priscila Oliveira 3 years ago
parent
commit
568fd70013

+ 4 - 0
static/app/components/dropdownAutoComplete/types.tsx

@@ -4,6 +4,10 @@ export type Item = {
   index: number;
   searchKey?: string;
   groupLabel?: boolean;
+  /**
+   * Error message to display for the field
+   */
+  error?: React.ReactNode;
 } & Record<string, any>;
 
 type Items<T> = Array<

+ 49 - 0
static/app/components/modals/debugFileCustomRepository/appStoreConnect/accordion.tsx

@@ -0,0 +1,49 @@
+import React, {useState} from 'react';
+import styled from '@emotion/styled';
+
+import ListItem from 'app/components/list/listItem';
+import {Panel} from 'app/components/panels';
+import {IconChevron} from 'app/icons';
+import space from 'app/styles/space';
+
+type Props = {
+  summary: string;
+  children: React.ReactNode;
+  defaultExpanded?: boolean;
+};
+
+function Accordion({summary, defaultExpanded, children}: Props) {
+  const [isExpanded, setIsExpanded] = useState(!!defaultExpanded);
+
+  return (
+    <ListItem>
+      <StyledPanel>
+        <Summary onClick={() => setIsExpanded(!isExpanded)}>
+          {summary}
+          <IconChevron direction={isExpanded ? 'down' : 'right'} color="gray400" />
+        </Summary>
+        {isExpanded && <Details>{children}</Details>}
+      </StyledPanel>
+    </ListItem>
+  );
+}
+
+export default Accordion;
+
+const StyledPanel = styled(Panel)`
+  padding: ${space(1.5)};
+  margin-bottom: 0;
+`;
+
+const Summary = styled('div')`
+  display: grid;
+  grid-template-columns: 1fr max-content;
+  grid-gap: ${space(1)};
+  cursor: pointer;
+  padding-left: calc(${space(3)} + ${space(1)});
+  align-items: center;
+`;
+
+const Details = styled('div')`
+  padding-top: ${space(1.5)};
+`;

+ 38 - 43
static/app/components/modals/debugFileCustomRepository/appStoreConnect/index.tsx

@@ -9,8 +9,6 @@ import Alert from 'app/components/alert';
 import Button from 'app/components/button';
 import ButtonBar from 'app/components/buttonBar';
 import List from 'app/components/list';
-import ListItem from 'app/components/list/listItem';
-import {Panel} from 'app/components/panels';
 import {IconWarning} from 'app/icons';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
@@ -18,6 +16,7 @@ import {Organization, Project} from 'app/types';
 import withApi from 'app/utils/withApi';
 import AppStoreConnectContext from 'app/views/settings/project/appStoreConnectContext';
 
+import Accordion from './accordion';
 import AppStoreCredentials from './appStoreCredentials';
 import ItunesCredentials from './itunesCredentials';
 import {AppStoreCredentialsData, ItunesCredentialsData} from './types';
@@ -194,8 +193,12 @@ function AppStoreConnect({
     <Fragment>
       <Body>
         <StyledList symbol="colored-numeric">
-          <ListItem>
-            <ItemTitle>{t('App Store Connect credentials')}</ItemTitle>
+          <Accordion
+            summary={t('App Store Connect credentials')}
+            defaultExpanded={
+              !isUpdating || !!appStoreConnenctContext?.appstoreCredentialsValid
+            }
+          >
             {!!appStoreConnenctContext?.appstoreCredentialsValid && (
               <StyledAlert type="warning" icon={<IconWarning />}>
                 {t(
@@ -203,20 +206,20 @@ function AppStoreConnect({
                 )}
               </StyledAlert>
             )}
-            <ItemContent>
-              <AppStoreCredentials
-                api={api}
-                orgSlug={orgSlug}
-                projectSlug={projectSlug}
-                data={appStoreCredentialsData}
-                onChange={setAppStoreCredentialsData}
-                onReset={() => setAppStoreCredentialsData(appStoreCredentialsInitialData)}
-                isUpdating={isUpdating}
-              />
-            </ItemContent>
-          </ListItem>
-          <ListItem>
-            <ItemTitle>{t('iTunes credentials')}</ItemTitle>
+            <AppStoreCredentials
+              api={api}
+              orgSlug={orgSlug}
+              projectSlug={projectSlug}
+              data={appStoreCredentialsData}
+              onChange={setAppStoreCredentialsData}
+              onReset={() => setAppStoreCredentialsData(appStoreCredentialsInitialData)}
+              isUpdating={isUpdating}
+            />
+          </Accordion>
+          <Accordion
+            summary={t('iTunes credentials')}
+            defaultExpanded={!!appStoreConnenctContext?.itunesSessionValid}
+          >
             {!!appStoreConnenctContext?.itunesSessionValid && (
               <StyledAlert type="warning" icon={<IconWarning />}>
                 {t(
@@ -224,18 +227,16 @@ function AppStoreConnect({
                 )}
               </StyledAlert>
             )}
-            <ItemContent>
-              <ItunesCredentials
-                api={api}
-                orgSlug={orgSlug}
-                projectSlug={projectSlug}
-                data={iTunesCredentialsData}
-                onChange={setItunesCredentialsData}
-                onReset={() => setItunesCredentialsData(iTunesCredentialsInitialData)}
-                isUpdating={isUpdating}
-              />
-            </ItemContent>
-          </ListItem>
+            <ItunesCredentials
+              api={api}
+              orgSlug={orgSlug}
+              projectSlug={projectSlug}
+              data={iTunesCredentialsData}
+              onChange={setItunesCredentialsData}
+              onReset={() => setItunesCredentialsData(iTunesCredentialsInitialData)}
+              isUpdating={isUpdating}
+            />
+          </Accordion>
         </StyledList>
       </Body>
       <Footer>
@@ -257,27 +258,21 @@ function AppStoreConnect({
 export default withApi(AppStoreConnect);
 
 const StyledList = styled(List)`
-  grid-gap: 0;
+  grid-gap: ${space(2)};
   & > li {
     padding-left: 0;
-    display: grid;
-    grid-gap: ${space(1)};
+    :before {
+      z-index: 1;
+      left: 9px;
+      top: ${space(1.5)};
+    }
   }
 `;
 
-const ItemTitle = styled('div')`
-  padding-left: ${space(4)};
-  margin-bottom: ${space(1)};
-`;
-
-const ItemContent = styled(Panel)`
-  padding: ${space(3)} ${space(3)} ${space(2)} ${space(1)};
-`;
-
 const StyledButton = styled(Button)`
   position: relative;
 `;
 
 const StyledAlert = styled(Alert)`
-  margin-bottom: ${space(1)};
+  margin: ${space(1)} 0 ${space(2)} 0;
 `;

+ 1 - 0
static/app/components/modals/debugFileCustomRepository/appStoreConnect/stepper/step.tsx

@@ -55,6 +55,7 @@ const Wrapper = styled('div')<{activeStep: number; isActive: boolean; height?: n
   grid-template-columns: max-content 1fr;
   position: relative;
   color: ${p => p.theme.gray200};
+  margin-left: -${space(1.5)};
 
   :not(:last-child) {
     padding-bottom: ${space(2)};

+ 0 - 161
static/app/data/forms/projectDebugFiles.tsx

@@ -1,161 +0,0 @@
-import {Fragment} from 'react';
-import forEach from 'lodash/forEach';
-import isObject from 'lodash/isObject';
-import set from 'lodash/set';
-
-import {openDebugFileSourceModal} from 'app/actionCreators/modal';
-import Feature from 'app/components/acl/feature';
-import FeatureDisabled from 'app/components/acl/featureDisabled';
-import {DEBUG_SOURCE_TYPES} from 'app/data/debugFileSources';
-import {t} from 'app/locale';
-import {Choices} from 'app/types';
-import {BuiltinSymbolSource} from 'app/types/debugFiles';
-import {Field} from 'app/views/settings/components/forms/type';
-import TextBlock from 'app/views/settings/components/text/textBlock';
-
-type SymbolSourceOptions = {
-  builtinSymbolSources: BuiltinSymbolSource[];
-};
-
-// Export route to make these forms searchable by label/help
-export const route = '/settings/:orgId/projects/:projectId/debug-symbols/';
-
-function flattenKeys(obj: any): Record<string, string> {
-  const result = {};
-  forEach(obj, (value, key) => {
-    if (isObject(value)) {
-      forEach(value, (innerValue, innerKey) => {
-        result[`${key}.${innerKey}`] = innerValue;
-      });
-    } else {
-      result[key] = value;
-    }
-  });
-  return result;
-}
-
-function unflattenKeys(obj: Record<string, string>): Record<string, any> {
-  const result = {};
-  forEach(obj, (value, key) => {
-    set(result, key.split('.'), value);
-  });
-  return result;
-}
-
-export const fields: Record<string, Field> = {
-  builtinSymbolSources: {
-    name: 'builtinSymbolSources',
-    type: 'select',
-    multiple: true,
-    label: t('Built-in Repositories'),
-    help: t(
-      'Configures which built-in repositories Sentry should use to resolve debug files.'
-    ),
-    formatMessageValue: (value, {builtinSymbolSources}: SymbolSourceOptions) => {
-      const rv: string[] = [];
-      value.forEach(key => {
-        builtinSymbolSources.forEach(source => {
-          if (source.sentry_key === key) {
-            rv.push(source.name);
-          }
-        });
-      });
-      return rv.length ? rv.join(', ') : '\u2014';
-    },
-    choices: ({builtinSymbolSources}) => {
-      return (builtinSymbolSources as BuiltinSymbolSource[])
-        ?.filter(source => !source.hidden)
-        .map(source => [source.sentry_key, t(source.name)]) as Choices;
-    },
-    getValue: value => (value === null ? [] : value),
-  },
-  symbolSources: {
-    name: 'symbolSources',
-    type: 'rich_list',
-    label: t('Custom Repositories'),
-    /* eslint-disable-next-line react/prop-types */
-    help: ({organization}) => (
-      <Feature
-        features={['organizations:custom-symbol-sources']}
-        hookName="feature-disabled:custom-symbol-sources"
-        organization={organization}
-        renderDisabled={p => (
-          <FeatureDisabled
-            features={p.features}
-            message={t('Custom repositories are disabled.')}
-            featureName={t('custom repositories')}
-          />
-        )}
-      >
-        {t('Configures custom repositories containing debug files.')}
-      </Feature>
-    ),
-    disabled: ({features}) => !features.has('custom-symbol-sources'),
-    formatMessageValue: false,
-    addButtonText: t('Add Repository'),
-    addDropdown: {
-      items: [
-        {
-          value: 's3',
-          label: t(DEBUG_SOURCE_TYPES.s3),
-          searchKey: t('aws amazon s3 bucket'),
-        },
-        {
-          value: 'gcs',
-          label: t(DEBUG_SOURCE_TYPES.gcs),
-          searchKey: t('gcs google cloud storage bucket'),
-        },
-        {
-          value: 'http',
-          label: t(DEBUG_SOURCE_TYPES.http),
-          searchKey: t('http symbol server ssqp symstore symsrv'),
-        },
-      ],
-    },
-
-    getValue: sources => JSON.stringify(sources.map(unflattenKeys)),
-    setValue: (raw: string) => {
-      if (!raw) {
-        return [];
-      }
-      return (JSON.parse(raw) || []).map(flattenKeys);
-    },
-
-    renderItem(item) {
-      return item.name ? <span>{item.name}</span> : <em>{t('<Unnamed Repository>')}</em>;
-    },
-
-    onAddItem(item, addItem) {
-      openDebugFileSourceModal({
-        sourceType: item.value,
-        onSave: addItem,
-      });
-    },
-
-    onEditItem(item, updateItem) {
-      openDebugFileSourceModal({
-        sourceConfig: item,
-        sourceType: item.type,
-        onSave: updateItem,
-      });
-    },
-
-    removeConfirm: {
-      confirmText: t('Remove Repository'),
-      message: (
-        <Fragment>
-          <TextBlock>
-            <strong>
-              {t('Removing this repository applies instantly to new events.')}
-            </strong>
-          </TextBlock>
-          <TextBlock>
-            {t(
-              'Debug files from this repository will not be used to symbolicate future events. This may create new issues and alert members in your organization.'
-            )}
-          </TextBlock>
-        </Fragment>
-      ),
-    },
-  },
-};

+ 1 - 1
static/app/types/debugFiles.tsx

@@ -34,7 +34,7 @@ export type DebugFile = {
 };
 
 export type AppStoreConnectValidationData = {
-  configured: boolean;
+  id: string;
   appstoreCredentialsValid: boolean;
   itunesSessionValid: boolean;
 };

+ 1 - 0
static/app/views/settings/components/forms/field/fieldControl.tsx

@@ -95,6 +95,7 @@ const FieldControlStyled = styled('div')<{alignRight?: boolean}>`
   flex: 1;
   flex-direction: column;
   position: relative;
+  max-width: 100%;
   ${p => (p.alignRight ? 'align-items: flex-end;' : '')};
 `;
 

+ 124 - 64
static/app/views/settings/components/forms/richListField.tsx

@@ -1,16 +1,19 @@
 import * as React from 'react';
 import styled from '@emotion/styled';
+import omit from 'lodash/omit';
 
 import Button from 'app/components/button';
-import Confirm from 'app/components/confirm';
+import ConfirmDelete from 'app/components/confirmDelete';
 import DropdownAutoComplete from 'app/components/dropdownAutoComplete';
 import {Item as ListItem} from 'app/components/dropdownAutoComplete/types';
 import DropdownButton from 'app/components/dropdownButton';
-import {IconAdd, IconDelete, IconSettings} from 'app/icons';
+import Tooltip from 'app/components/tooltip';
+import {IconAdd, IconDelete, IconSettings, IconWarning} from 'app/icons';
 import {t} from 'app/locale';
+import space from 'app/styles/space';
 import InputField from 'app/views/settings/components/forms/inputField';
 
-type ConfirmProps = Partial<React.ComponentProps<typeof Confirm>>;
+type ConfirmDeleteProps = Partial<React.ComponentProps<typeof ConfirmDelete>>;
 type DropdownProps = Omit<React.ComponentProps<typeof DropdownAutoComplete>, 'children'>;
 
 type UpdatedItem = ListItem | Record<string, string>;
@@ -36,12 +39,6 @@ type DefaultProps = {
   onRemoveItem: RichListCallback;
 };
 
-const defaultProps: DefaultProps = {
-  addButtonText: t('Add item'),
-  onAddItem: (item, addItem) => addItem(item),
-  onRemoveItem: (item, removeItem) => removeItem(item),
-};
-
 /**
  * You can get better typing by specifying the item type
  * when using this component.
@@ -66,9 +63,6 @@ export type RichListProps = {
    */
   value: ListItem[];
 
-  onBlur: InputField['props']['onBlur'];
-  onChange: InputField['props']['onChange'];
-
   /**
    * Configuration for the add item dropdown.
    */
@@ -79,11 +73,14 @@ export type RichListProps = {
    */
   disabled: boolean;
 
+  onBlur?: InputField['props']['onBlur'];
+  onChange?: InputField['props']['onChange'];
+
   /**
-   * Properties for the confirm remove dialog. If missing, the item will be
+   * Properties for the confirm delete dialog. If missing, the item will be
    * removed immediately.
    */
-  removeConfirm?: ConfirmProps;
+  removeConfirm?: ConfirmDeleteProps;
 
   /**
    * Callback invoked when an item is interacted with.
@@ -91,10 +88,14 @@ export type RichListProps = {
    * The callback is expected to call `editItem(item)`
    */
   onEditItem?: RichListCallback;
-} & DefaultProps;
+} & Partial<DefaultProps>;
 
-class RichList extends React.PureComponent<RichListProps> {
-  static defaultProps = defaultProps;
+class RichList extends React.PureComponent<RichListProps, {}> {
+  static defaultProps: DefaultProps = {
+    addButtonText: t('Add item'),
+    onAddItem: (item, addItem) => addItem(item),
+    onRemoveItem: (item, removeItem) => removeItem(item),
+  };
 
   triggerChange = (items: UpdatedItem[]) => {
     if (!this.props.disabled) {
@@ -127,57 +128,87 @@ class RichList extends React.PureComponent<RichListProps> {
   };
 
   onEditItem = (item: ListItem, index: number) => {
-    if (!this.props.disabled && this.props.onEditItem) {
-      this.props.onEditItem(item, data => this.updateItem(data, index));
+    if (!this.props.disabled) {
+      this.props.onEditItem?.(omit(item, 'error') as ListItem, data =>
+        this.updateItem(data, index)
+      );
     }
   };
 
   onRemoveItem = (item: ListItem, index: number) => {
     if (!this.props.disabled) {
-      this.props.onRemoveItem(item, () => this.removeItem(index));
+      this.props.onRemoveItem?.(item, () => this.removeItem(index));
     }
   };
 
   renderItem = (item: ListItem, index: number) => {
-    const {disabled} = this.props;
-
-    const removeIcon = (onClick?: () => void) => (
-      <ItemButton
-        onClick={onClick}
-        disabled={disabled}
-        size="zero"
-        icon={<IconDelete size="xs" />}
-        borderless
-      />
-    );
+    const {disabled, renderItem, onEditItem} = this.props;
 
-    const removeConfirm =
-      this.props.removeConfirm && !disabled ? (
-        <Confirm
-          priority="danger"
-          confirmText={t('Remove')}
-          {...this.props.removeConfirm}
-          onConfirm={() => this.onRemoveItem(item, index)}
-        >
-          {removeIcon()}
-        </Confirm>
-      ) : (
-        removeIcon(() => this.onRemoveItem(item, index))
-      );
+    const error = item.error;
 
     return (
-      <Item disabled={disabled} key={index}>
-        {this.props.renderItem(item)}
-        {this.props.onEditItem && (
-          <ItemButton
-            onClick={() => this.onEditItem(item, index)}
-            disabled={disabled}
-            icon={<IconSettings />}
-            size="zero"
-            borderless
-          />
+      <Item
+        disabled={!!disabled}
+        key={index}
+        onClick={
+          error && onEditItem && !disabled
+            ? () => this.onEditItem(item, index)
+            : undefined
+        }
+      >
+        {renderItem(item)}
+        {error ? (
+          <ErrorIcon>
+            <Tooltip title={error} containerDisplayMode="inline-flex">
+              <IconWarning color="red300" />
+            </Tooltip>
+          </ErrorIcon>
+        ) : (
+          onEditItem && (
+            <SettingsButton
+              onClick={() => this.onEditItem(item, index)}
+              disabled={disabled}
+              icon={<IconSettings />}
+              size="zero"
+              label={t('Edit Item')}
+              borderless
+            />
+          )
         )}
-        {removeConfirm}
+        <DeleteButtonWrapper
+          onClick={event => {
+            event.preventDefault();
+            event.stopPropagation();
+          }}
+        >
+          {this.props.removeConfirm ? (
+            <ConfirmDelete
+              confirmText={t('Remove')}
+              disabled={disabled}
+              {...this.props.removeConfirm}
+              confirmInput={item.name}
+              priority="danger"
+              onConfirm={() => this.onRemoveItem(item, index)}
+            >
+              <DeleteButton
+                disabled={disabled}
+                size="zero"
+                icon={<IconDelete size="xs" />}
+                label={t('Delete Item')}
+                borderless
+              />
+            </ConfirmDelete>
+          ) : (
+            <DeleteButton
+              disabled={disabled}
+              size="zero"
+              icon={<IconDelete size="xs" />}
+              label={t('Delete Item')}
+              onClick={() => this.onRemoveItem(item, index)}
+              borderless
+            />
+          )}
+        </DeleteButtonWrapper>
       </Item>
     );
   };
@@ -249,30 +280,59 @@ const ItemList = styled('ul')`
   padding: 0;
 `;
 
-const Item = styled('li')<{disabled?: boolean}>`
+const Item = styled('li')<{
+  disabled: boolean;
+  onClick?: (event: React.MouseEvent) => void;
+}>`
+  position: relative;
   display: flex;
   align-items: center;
-  background-color: ${p => p.theme.button.default.background};
-  border: 1px solid ${p => p.theme.button.default.border};
   border-radius: ${p => p.theme.button.borderRadius};
-  color: ${p => p.theme.button.default.color};
-  cursor: ${p => (p.disabled ? 'not-allowed' : 'default')};
   font-size: ${p => p.theme.fontSizeSmall};
   font-weight: 600;
   line-height: ${p => p.theme.fontSizeSmall};
   text-transform: none;
   margin: 0 10px 5px 0;
   white-space: nowrap;
-  opacity: ${p => (p.disabled ? 0.65 : null)};
-  padding: 8px 12px;
+  padding: ${space(1)} 36px ${space(1)} ${space(1.5)};
   /* match adjacent elements */
-  height: 30px;
+  height: 32px;
+  overflow: hidden;
+  background-color: ${p => p.theme.button.default.background};
+  border: 1px solid ${p => p.theme.button.default.border};
+  color: ${p => p.theme.button.default.color};
+  opacity: ${p => (p.disabled ? 0.65 : null)};
+  cursor: ${p => (p.disabled ? 'not-allowed' : p.onClick ? 'pointer' : 'default')};
 `;
 
 const ItemButton = styled(Button)`
-  margin-left: 10px;
   color: ${p => p.theme.gray300};
   &:hover {
     color: ${p => (p.disabled ? p.theme.gray300 : p.theme.button.default.color)};
   }
 `;
+
+const SettingsButton = styled(ItemButton)`
+  margin-left: 10px;
+`;
+
+const DeleteButton = styled(ItemButton)`
+  height: 100%;
+  width: 100%;
+`;
+
+const ErrorIcon = styled('div')`
+  margin-left: 10px;
+  display: inline-flex;
+`;
+
+const DeleteButtonWrapper = styled('div')`
+  position: absolute;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1;
+  right: 0;
+  width: 36px;
+  height: 100%;
+`;

+ 5 - 2
static/app/views/settings/project/appStoreConnectContext.tsx

@@ -41,10 +41,13 @@ const Provider = withApi(
       }
 
       try {
-        const response: AppStoreConnectValidationData = await api.requestPromise(
+        const response = await api.requestPromise(
           `/projects/${orgSlug}/${project.slug}/appstoreconnect/validate/${appStoreConnectSymbolSourceId}/`
         );
-        setAppStoreConnectValidationData(response);
+        setAppStoreConnectValidationData({
+          id: appStoreConnectSymbolSourceId,
+          ...response,
+        });
       } catch {
         // do nothing
       }

+ 80 - 0
static/app/views/settings/projectDebugFiles/externalSources/buildInSymbolSources.tsx

@@ -0,0 +1,80 @@
+import {addErrorMessage, addSuccessMessage} from 'app/actionCreators/indicator';
+import ProjectActions from 'app/actions/projectActions';
+import {Client} from 'app/api';
+import {t} from 'app/locale';
+import {Choices, Organization, Project} from 'app/types';
+import {BuiltinSymbolSource} from 'app/types/debugFiles';
+import SelectField from 'app/views/settings/components/forms/selectField';
+
+type Props = {
+  api: Client;
+  organization: Organization;
+  projectSlug: Project['slug'];
+  builtinSymbolSourceOptions: BuiltinSymbolSource[];
+  builtinSymbolSources: string[];
+};
+
+function BuildInSymbolSources({
+  api,
+  organization,
+  builtinSymbolSourceOptions,
+  builtinSymbolSources,
+  projectSlug,
+}: Props) {
+  function getRequestMessages(builtinSymbolSourcesQuantity: number) {
+    if (builtinSymbolSourcesQuantity > builtinSymbolSources.length) {
+      return {
+        successMessage: t('Successfully added built-in repository'),
+        errorMessage: t('An error occurred while adding new built-in repository'),
+      };
+    }
+
+    return {
+      successMessage: t('Successfully removed built-in repository'),
+      errorMessage: t('An error occurred while removing built-in repository'),
+    };
+  }
+
+  async function handleChange(value: string[]) {
+    const {successMessage, errorMessage} = getRequestMessages(value.length);
+
+    try {
+      const updatedProjectDetails: Project = await api.requestPromise(
+        `/projects/${organization.slug}/${projectSlug}/`,
+        {
+          method: 'PUT',
+          data: {
+            builtinSymbolSources: value,
+          },
+        }
+      );
+
+      ProjectActions.updateSuccess(updatedProjectDetails);
+      addSuccessMessage(successMessage);
+    } catch {
+      addErrorMessage(errorMessage);
+    }
+  }
+
+  return (
+    <SelectField
+      name="builtinSymbolSources"
+      label={t('Built-in Repositories')}
+      help={t(
+        'Configures which built-in repositories Sentry should use to resolve debug files.'
+      )}
+      value={builtinSymbolSources}
+      onChange={handleChange}
+      choices={
+        builtinSymbolSourceOptions
+          ?.filter(source => !source.hidden)
+          .map(source => [source.sentry_key, t(source.name)]) as Choices
+      }
+      getValue={value => (value === null ? [] : value)}
+      flexibleControlStateSize
+      multiple
+    />
+  );
+}
+
+export default BuildInSymbolSources;

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