From 9814ac6cf8d03f8781e7f7053d1f30c5c11f05f6 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Fri, 17 Jan 2025 16:12:42 -0800 Subject: [PATCH 1/4] ref: Convert AddCodeOwnersModal to FC --- .../projectOwnership/addCodeOwnerModal.tsx | 378 +++++++++--------- 1 file changed, 186 insertions(+), 192 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index 54ff8bfc5df361..eb5821b7a820b6 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -1,14 +1,14 @@ -import {Fragment} from 'react'; +import {Fragment, useCallback, useState} from 'react'; import styled from '@emotion/styled'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import {Alert} from 'sentry/components/alert'; import {Button, LinkButton} from 'sentry/components/button'; -import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent'; import SelectField from 'sentry/components/forms/fields/selectField'; import Form from 'sentry/components/forms/form'; import Link from 'sentry/components/links/link'; +import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; @@ -24,215 +24,209 @@ import type { import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {getIntegrationIcon} from 'sentry/utils/integrationUtil'; +import {fetchMutation, useApiQuery, useMutation} from 'sentry/utils/queryClient'; +import type RequestError from 'sentry/utils/requestError/requestError'; +import useApi from 'sentry/utils/useApi'; type Props = { organization: Organization; project: Project; onSave?: (data: CodeOwner) => void; -} & ModalRenderProps & - DeprecatedAsyncComponent['props']; +} & ModalRenderProps; -type State = { - codeMappingId: string | null; - codeMappings: RepositoryProjectPathConfig[]; - codeownersFile: CodeownersFile | null; - error: boolean; - errorJSON: {raw?: string} | null; - integrations: Integration[]; - isLoading: boolean; -} & DeprecatedAsyncComponent['state']; +type TCodeownersPayload = {codeMappingId: string | null; raw: string}; +type TCodeownersData = CodeOwner; +type TCodeownersError = RequestError; +type TCodeownersVariables = [TCodeownersPayload]; +type TCodeownersContext = unknown; -export default class AddCodeOwnerModal extends DeprecatedAsyncComponent { - getDefaultState() { - return { - ...super.getDefaultState(), - codeownersFile: null, - codeMappingId: null, - isLoading: false, - error: false, - errorJSON: null, - }; - } +export default function AddCodeOwnerModal({ + organization, + Header, + Body, + Footer, + project, + onSave, + closeModal, +}: Props) { + const { + data: codeMappings, + isPending: isCodeMappingsPending, + isError: isCodeMappingsError, + } = useApiQuery( + [ + `/organizations/${organization.slug}/code-mappings/`, + {query: {project: project.id}}, + ], + {staleTime: Infinity} + ); - getEndpoints(): ReturnType { - const {organization, project} = this.props; - const endpoints: ReturnType = [ - [ - 'codeMappings', - `/organizations/${organization.slug}/code-mappings/`, - {query: {project: project.id}}, - ], - [ - 'integrations', - `/organizations/${organization.slug}/integrations/`, - {query: {features: ['codeowners']}}, - ], - ]; - return endpoints; - } + const { + data: integrations, + isPending: isIntegrationsPending, + isError: isIntegrationsError, + } = useApiQuery( + [ + `/organizations/${organization.slug}/integrations/`, + {query: {features: ['codeowners']}}, + ], + {staleTime: Infinity} + ); - fetchFile = async (codeMappingId: string) => { - const {organization} = this.props; - this.setState({ - codeMappingId, - codeownersFile: null, - error: false, - errorJSON: null, - isLoading: true, - }); - try { - const data: CodeownersFile = await this.api.requestPromise( - `/organizations/${organization.slug}/code-mappings/${codeMappingId}/codeowners/`, - { - method: 'GET', - } - ); - this.setState({codeownersFile: data, isLoading: false}); - } catch (_err) { - this.setState({isLoading: false}); - } - }; + const [codeMappingId, setCodeMappingId] = useState(null); - addFile = async () => { - const {organization, project, onSave, closeModal} = this.props; - const {codeownersFile, codeMappingId, codeMappings} = this.state; + const { + data: codeownersFile, + isPending: isCodeownersFilePending, + isError: _isCodeownersFileError, + } = useApiQuery( + [`/organizations/${organization.slug}/code-mappings/${codeMappingId}/codeowners/`], + {staleTime: Infinity, enabled: Boolean(codeMappingId)} + ); - if (codeownersFile) { - const postData: { - codeMappingId: string | null; - raw: string; - } = { - codeMappingId, - raw: codeownersFile.raw, - }; + const api = useApi({ + persistInFlight: false, + }); + const mutation = useMutation< + TCodeownersData, + TCodeownersError, + TCodeownersVariables, + TCodeownersContext + >({ + mutationFn: ([payload]: TCodeownersVariables) => { + return fetchMutation(api)([ + 'POST', + `/projects/${organization.slug}/${project.slug}/codeowners/`, + {}, + payload, + ]); + }, + gcTime: 0, + }); - try { - const data = await this.api.requestPromise( - `/projects/${organization.slug}/${project.slug}/codeowners/`, - { - method: 'POST', - data: postData, + const addFile = useCallback(() => { + if (codeownersFile) { + mutation.mutate([{codeMappingId, raw: codeownersFile.raw}], { + onSuccess: d => { + const codeMapping = codeMappings?.find( + mapping => mapping.id === codeMappingId?.toString() + ); + onSave?.({...d, codeMapping}); + closeModal(); + }, + onError: err => { + if (err.responseJSON && !('raw' in err.responseJSON)) { + addErrorMessage( + Object.values(err.responseJSON ?? {}) + .flat() + .join(' ') + ); } - ); - - const codeMapping = codeMappings.find( - mapping => mapping.id === codeMappingId?.toString() - ); - - onSave?.({...data, codeMapping}); - closeModal(); - } catch (err) { - if (err.responseJSON.raw) { - this.setState({error: true, errorJSON: err.responseJSON, isLoading: false}); - } else { - addErrorMessage(Object.values(err.responseJSON).flat().join(' ')); - } - } + }, + }); } - }; + }, [closeModal, codeMappingId, codeMappings, codeownersFile, mutation, onSave]); - renderBody() { - const {organization, Header, Body, Footer} = this.props; - const { - codeownersFile, - error, - errorJSON, - isLoading, - codeMappings, - integrations, - codeMappingId, - } = this.state; - const baseUrl = `/settings/${organization.slug}/integrations/`; + if (isCodeMappingsPending || isIntegrationsPending) { + return ; + } + if (isCodeMappingsError || isIntegrationsError) { + return ; + } - return ( - -
{t('Add Code Owner File')}
- - {!codeMappings.length ? ( - !integrations.length ? ( - -
- {t('Install a GitHub or GitLab integration to use this feature.')} -
- - - Setup Integration - - -
- ) : ( - -
- {t( - "Configure code mapping to add your CODEOWNERS file. Select the integration you'd like to use for mapping:" - )} -
- - {integrations.map(integration => ( - - {getIntegrationIcon(integration.provider.key)} - {integration.name} - - ))} - -
- ) - ) : null} - {codeMappings.length > 0 && ( -
- ({ - value: cm.id, - label: `Repo Name: ${cm.repoName}, Stack Trace Root: ${cm.stackRoot}, Source Code Root: ${cm.sourceRoot}`, - }))} - onChange={this.fetchFile} - required - inline={false} - flexibleControlStateSize - stacked - /> + const baseUrl = `/settings/${organization.slug}/integrations/`; - - {codeownersFile ? ( - - ) : ( - + return ( + +
{t('Add Code Owner File')}
+ + {!codeMappings.length ? ( + !integrations.length ? ( + +
+ {t('Install a GitHub or GitLab integration to use this feature.')} +
+ + + Setup Integration + + +
+ ) : ( + +
+ {t( + "Configure code mapping to add your CODEOWNERS file. Select the integration you'd like to use for mapping:" )} - {error && errorJSON ? ( - - ) : null} - - - )} - -
- -
- - ); - } + ({ + value: cm.id, + label: `Repo Name: ${cm.repoName}, Stack Trace Root: ${cm.stackRoot}, Source Code Root: ${cm.sourceRoot}`, + }))} + onChange={setCodeMappingId} + required + inline={false} + flexibleControlStateSize + stacked + /> + + + {codeownersFile ? ( + + ) : ( + + )} + {mutation.isError && mutation.error.responseJSON?.raw ? ( + + ) : null} + + + )} + +
+ +
+ + ); } function SourceFile({codeownersFile}: {codeownersFile: CodeownersFile}) { From c40792b04b6b89a0b6a09d3aae9cd96a2cef76a8 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 21 Jan 2025 11:01:04 -0800 Subject: [PATCH 2/4] check isCodeownersFileError --- .../settings/project/projectOwnership/addCodeOwnerModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index eb5821b7a820b6..cc56855a150230 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -78,7 +78,7 @@ export default function AddCodeOwnerModal({ const { data: codeownersFile, isPending: isCodeownersFilePending, - isError: _isCodeownersFileError, + isError: isCodeownersFileError, } = useApiQuery( [`/organizations/${organization.slug}/code-mappings/${codeMappingId}/codeowners/`], {staleTime: Infinity, enabled: Boolean(codeMappingId)} @@ -130,7 +130,7 @@ export default function AddCodeOwnerModal({ if (isCodeMappingsPending || isIntegrationsPending) { return ; } - if (isCodeMappingsError || isIntegrationsError) { + if (isCodeMappingsError || isIntegrationsError || isCodeownersFileError) { return ; } From ffe1613a82d57146f0fef237a7ff4af28af98f79 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Wed, 22 Jan 2025 11:01:12 -0800 Subject: [PATCH 3/4] inline mutation callbacks, and extract render functions --- .../projectOwnership/addCodeOwnerModal.tsx | 231 ++++++++++-------- 1 file changed, 135 insertions(+), 96 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index cc56855a150230..e190ce90bddb68 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useState} from 'react'; +import {type Dispatch, Fragment, type SetStateAction, useCallback, useState} from 'react'; import styled from '@emotion/styled'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; @@ -24,7 +24,12 @@ import type { import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {getIntegrationIcon} from 'sentry/utils/integrationUtil'; -import {fetchMutation, useApiQuery, useMutation} from 'sentry/utils/queryClient'; +import { + fetchMutation, + useApiQuery, + useMutation, + type UseMutationResult, +} from 'sentry/utils/queryClient'; import type RequestError from 'sentry/utils/requestError/requestError'; import useApi from 'sentry/utils/useApi'; @@ -101,31 +106,30 @@ export default function AddCodeOwnerModal({ payload, ]); }, + onSuccess: d => { + const codeMapping = codeMappings?.find( + mapping => mapping.id === codeMappingId?.toString() + ); + onSave?.({...d, codeMapping}); + closeModal(); + }, + onError: err => { + if (err.responseJSON && !('raw' in err.responseJSON)) { + addErrorMessage( + Object.values(err.responseJSON ?? {}) + .flat() + .join(' ') + ); + } + }, gcTime: 0, }); const addFile = useCallback(() => { if (codeownersFile) { - mutation.mutate([{codeMappingId, raw: codeownersFile.raw}], { - onSuccess: d => { - const codeMapping = codeMappings?.find( - mapping => mapping.id === codeMappingId?.toString() - ); - onSave?.({...d, codeMapping}); - closeModal(); - }, - onError: err => { - if (err.responseJSON && !('raw' in err.responseJSON)) { - addErrorMessage( - Object.values(err.responseJSON ?? {}) - .flat() - .join(' ') - ); - } - }, - }); + mutation.mutate([{codeMappingId, raw: codeownersFile.raw}]); } - }, [closeModal, codeMappingId, codeMappings, codeownersFile, mutation, onSave]); + }, [codeMappingId, codeownersFile, mutation]); if (isCodeMappingsPending || isIntegrationsPending) { return ; @@ -134,85 +138,22 @@ export default function AddCodeOwnerModal({ return ; } - const baseUrl = `/settings/${organization.slug}/integrations/`; - return (
{t('Add Code Owner File')}
- {!codeMappings.length ? ( - !integrations.length ? ( - -
- {t('Install a GitHub or GitLab integration to use this feature.')} -
- - - Setup Integration - - -
- ) : ( - -
- {t( - "Configure code mapping to add your CODEOWNERS file. Select the integration you'd like to use for mapping:" - )} -
- - {integrations.map(integration => ( - - {getIntegrationIcon(integration.provider.key)} - {integration.name} - - ))} - -
- ) - ) : null} - {codeMappings.length > 0 && ( -
- ({ - value: cm.id, - label: `Repo Name: ${cm.repoName}, Stack Trace Root: ${cm.stackRoot}, Source Code Root: ${cm.sourceRoot}`, - }))} - onChange={setCodeMappingId} - required - inline={false} - flexibleControlStateSize - stacked - /> - - - {codeownersFile ? ( - - ) : ( - - )} - {mutation.isError && mutation.error.responseJSON?.raw ? ( - - ) : null} - - + {codeMappings.length ? ( + + ) : ( + )}