123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- import {useCallback, useContext, useEffect} from 'react';
- import type {InjectedRouter} from 'react-router';
- import styled from '@emotion/styled';
- import type {Location} from 'history';
- import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
- import {openDebugFileSourceModal} from 'sentry/actionCreators/modal';
- import type {Client} from 'sentry/api';
- import Access from 'sentry/components/acl/access';
- import Feature from 'sentry/components/acl/feature';
- import DropdownAutoComplete from 'sentry/components/dropdownAutoComplete';
- import DropdownButton from 'sentry/components/dropdownButton';
- import EmptyStateWarning from 'sentry/components/emptyStateWarning';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import MenuItem from 'sentry/components/menuItem';
- import Panel from 'sentry/components/panels/panel';
- import PanelBody from 'sentry/components/panels/panelBody';
- import PanelHeader from 'sentry/components/panels/panelHeader';
- import AppStoreConnectContext from 'sentry/components/projects/appStoreConnectContext';
- import {Tooltip} from 'sentry/components/tooltip';
- import {t} from 'sentry/locale';
- import ProjectsStore from 'sentry/stores/projectsStore';
- import type {Organization, Project} from 'sentry/types';
- import type {CustomRepo} from 'sentry/types/debugFiles';
- import {CustomRepoType} from 'sentry/types/debugFiles';
- import {defined} from 'sentry/utils';
- import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
- import Repository from './repository';
- import {dropDownItems, expandKeys, getRequestMessages} from './utils';
- const SECTION_TITLE = t('Custom Repositories');
- type Props = {
- api: Client;
- customRepositories: CustomRepo[];
- isLoading: boolean;
- location: Location;
- organization: Organization;
- project: Project;
- router: InjectedRouter;
- };
- function CustomRepositories({
- api,
- organization,
- customRepositories: repositories,
- project,
- router,
- location,
- isLoading,
- }: Props) {
- const appStoreConnectContext = useContext(AppStoreConnectContext);
- const orgSlug = organization.slug;
- const appStoreConnectSourcesQuantity = repositories.filter(
- repository => repository.type === CustomRepoType.APP_STORE_CONNECT
- ).length;
- const persistData = useCallback(
- ({
- updatedItems,
- updatedItem,
- index,
- refresh,
- }: {
- index?: number;
- refresh?: boolean;
- updatedItem?: CustomRepo;
- updatedItems?: CustomRepo[];
- }) => {
- let items = updatedItems ?? [];
- if (updatedItem && defined(index)) {
- items = [...repositories];
- items.splice(index, 1, updatedItem);
- }
- const {successMessage, errorMessage} = getRequestMessages(
- items.length,
- repositories.length
- );
- const symbolSources = JSON.stringify(items.map(expandKeys));
- const promise: Promise<any> = api.requestPromise(
- `/projects/${orgSlug}/${project.slug}/`,
- {
- method: 'PUT',
- data: {symbolSources},
- }
- );
- promise.catch(() => {
- addErrorMessage(errorMessage);
- });
- promise.then(result => {
- ProjectsStore.onUpdateSuccess(result);
- addSuccessMessage(successMessage);
- if (refresh) {
- window.location.reload();
- }
- });
- return promise;
- },
- [api, orgSlug, project.slug, repositories]
- );
- const handleCloseModal = useCallback(() => {
- router.push({
- ...location,
- query: {
- ...location.query,
- customRepository: undefined,
- },
- });
- }, [location, router]);
- const openDebugFileSourceDialog = useCallback(() => {
- const {customRepository} = location.query;
- if (!customRepository) {
- return;
- }
- const itemIndex = repositories.findIndex(
- repository => repository.id === customRepository
- );
- const item = repositories[itemIndex];
- if (!item) {
- return;
- }
- openDebugFileSourceModal({
- organization,
- sourceConfig: item,
- sourceType: item.type,
- appStoreConnectSourcesQuantity,
- appStoreConnectStatusData: appStoreConnectContext?.[item.id],
- onSave: updatedItem =>
- persistData({updatedItem: updatedItem as CustomRepo, index: itemIndex}),
- onClose: handleCloseModal,
- });
- }, [
- appStoreConnectContext,
- appStoreConnectSourcesQuantity,
- handleCloseModal,
- location.query,
- organization,
- persistData,
- repositories,
- ]);
- useEffect(() => {
- openDebugFileSourceDialog();
- }, [location.query, appStoreConnectContext, openDebugFileSourceDialog]);
- function handleAddRepository(repoType: CustomRepoType) {
- openDebugFileSourceModal({
- organization,
- appStoreConnectSourcesQuantity,
- sourceType: repoType,
- onSave: updatedData =>
- persistData({updatedItems: [...repositories, updatedData] as CustomRepo[]}),
- });
- }
- function handleDeleteRepository(repoId: CustomRepo['id']) {
- const newRepositories = [...repositories];
- const index = newRepositories.findIndex(item => item.id === repoId);
- newRepositories.splice(index, 1);
- persistData({
- updatedItems: newRepositories as CustomRepo[],
- refresh: repositories[index].type === CustomRepoType.APP_STORE_CONNECT,
- });
- }
- function handleEditRepository(repoId: CustomRepo['id']) {
- router.push({
- ...location,
- query: {
- ...location.query,
- customRepository: repoId,
- },
- });
- }
- async function handleSyncRepositoryNow(repoId: CustomRepo['id']) {
- try {
- await api.requestPromise(
- `/projects/${orgSlug}/${project.slug}/appstoreconnect/${repoId}/refresh/`,
- {
- method: 'POST',
- }
- );
- addSuccessMessage(t('Repository sync started.'));
- } catch (error) {
- const errorMessage = t(
- 'Rate limit for refreshing repository exceeded. Try again in a few minutes.'
- );
- addErrorMessage(errorMessage);
- handleXhrErrorResponse(errorMessage, error);
- }
- }
- return (
- <Feature features="custom-symbol-sources" organization={organization}>
- {({hasFeature}) => (
- <Access access={['project:write']} project={project}>
- {({hasAccess}) => {
- const addRepositoryButtonDisabled = !hasAccess || isLoading;
- return (
- <Panel>
- <PanelHeader hasButtons>
- {SECTION_TITLE}
- <Tooltip
- title={
- !hasAccess
- ? t('You do not have permission to add custom repositories.')
- : undefined
- }
- >
- <DropdownAutoComplete
- alignMenu="right"
- disabled={addRepositoryButtonDisabled}
- onSelect={item => handleAddRepository(item.value)}
- items={dropDownItems.map(dropDownItem => ({
- ...dropDownItem,
- label: (
- <DropDownLabel
- aria-label={t(
- 'Open %s custom repository modal',
- dropDownItem.label
- )}
- >
- {dropDownItem.label}
- </DropDownLabel>
- ),
- }))}
- >
- {({isOpen}) => (
- <DropdownButton
- isOpen={isOpen}
- disabled={addRepositoryButtonDisabled}
- size="xs"
- aria-label={t('Add Repository')}
- >
- {t('Add Repository')}
- </DropdownButton>
- )}
- </DropdownAutoComplete>
- </Tooltip>
- </PanelHeader>
- <PanelBody>
- {isLoading ? (
- <LoadingIndicator />
- ) : !repositories.length ? (
- <EmptyStateWarning>
- <p>{t('No custom repositories configured')}</p>
- </EmptyStateWarning>
- ) : (
- repositories.map((repository, index) => (
- <Repository
- key={index}
- repository={
- repository.type === CustomRepoType.APP_STORE_CONNECT
- ? {
- ...repository,
- details: appStoreConnectContext?.[repository.id],
- }
- : repository
- }
- hasFeature={
- repository.type === CustomRepoType.APP_STORE_CONNECT
- ? hasFeature || appStoreConnectSourcesQuantity === 1
- : hasFeature
- }
- hasAccess={hasAccess}
- onDelete={handleDeleteRepository}
- onEdit={handleEditRepository}
- onSyncNow={handleSyncRepositoryNow}
- />
- ))
- )}
- </PanelBody>
- </Panel>
- );
- }}
- </Access>
- )}
- </Feature>
- );
- }
- export default CustomRepositories;
- const DropDownLabel = styled(MenuItem)`
- color: ${p => p.theme.textColor};
- font-size: ${p => p.theme.fontSizeMedium};
- font-weight: 400;
- text-transform: none;
- span {
- padding: 0;
- }
- `;
|