From 25c8e3a2ed179af7588349066aab0a2c4c677042 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Sat, 12 Jul 2025 14:18:53 -0700 Subject: [PATCH 1/2] ref(gsAdmin): Rewrite `` as a functional component --- static/gsAdmin/components/forkCustomer.tsx | 116 ++++++++++----------- 1 file changed, 56 insertions(+), 60 deletions(-) diff --git a/static/gsAdmin/components/forkCustomer.tsx b/static/gsAdmin/components/forkCustomer.tsx index b946357a77ef06..a7d57303781093 100644 --- a/static/gsAdmin/components/forkCustomer.tsx +++ b/static/gsAdmin/components/forkCustomer.tsx @@ -1,14 +1,16 @@ -import {Component, Fragment} from 'react'; +import {Fragment, useEffect, useState} from 'react'; -import {Client} from 'sentry/api'; import SelectField from 'sentry/components/forms/fields/selectField'; import type {Organization} from 'sentry/types/organization'; -import {browserHistory} from 'sentry/utils/browserHistory'; +import {useMutation} from 'sentry/utils/queryClient'; import { getRegionChoices, getRegionDataFromOrganization, getRegions, } from 'sentry/utils/regions'; +import type RequestError from 'sentry/utils/requestError/requestError'; +import useApi from 'sentry/utils/useApi'; +import {useNavigate} from 'sentry/utils/useNavigate'; import type { AdminConfirmParams, @@ -19,69 +21,63 @@ type Props = AdminConfirmRenderProps & { organization: Organization; }; -type State = { - regionUrl: string; -}; - /** * Rendered as part of a openAdminConfirmModal call */ -class ForkCustomerAction extends Component { - state: State = { - regionUrl: '', - }; - - componentDidMount() { - this.props.setConfirmCallback(this.handleConfirm); - } +export default function ForkCustomerAction({ + organization, + onConfirm, + setConfirmCallback, +}: Props) { + const [regionUrl, setRegionUrl] = useState(''); + const api = useApi({persistInFlight: true}); + const navigate = useNavigate(); - handleConfirm = async (params: AdminConfirmParams) => { - const api = new Client({headers: {Accept: 'application/json; charset=utf-8'}}); - const {organization} = this.props; - const {regionUrl} = this.state; - const regions = getRegions(); - const region = regions.find(r => r.url === regionUrl); - - try { - const response = await api.requestPromise( - `/organizations/${organization.slug}/fork/`, - { - method: 'POST', - host: region?.url, - } - ); + const {mutate} = useMutation({ + mutationFn: () => { + const regions = getRegions(); + const region = regions.find(r => r.url === regionUrl); - browserHistory.push(`/_admin/relocations/${region?.name}/${response.uuid}/`); - this.props.onConfirm?.({regionUrl, ...params}); - } catch (error) { + return api.requestPromise(`/organizations/${organization.slug}/fork/`, { + method: 'POST', + host: region?.url, + }); + }, + onSuccess: (response, params) => { + const regions = getRegions(); + const region = regions.find(r => r.url === regionUrl); + navigate(`/_admin/relocations/${region?.name}/${response.uuid}/`); + onConfirm?.({regionUrl, ...params}); + }, + onError: (error: RequestError, _params) => { if (error.responseJSON) { - this.props.onConfirm?.({error}); + onConfirm?.({error}); } - } - }; + }, + }); - render() { - const {organization} = this.props; - const currentRegionData = getRegionDataFromOrganization(organization); - const regionChoices = getRegionChoices(currentRegionData ? [currentRegionData] : []); - return ( - - SAAS relocation job, but the source organization will not be affected." - } - choices={regionChoices} - inline={false} - stacked - required - value={this.state.regionUrl} - onChange={(val: any) => this.setState({regionUrl: val})} - /> - - ); - } -} + useEffect(() => { + setConfirmCallback(mutate); + }, [mutate, setConfirmCallback]); + + const currentRegionData = getRegionDataFromOrganization(organization); + const regionChoices = getRegionChoices(currentRegionData ? [currentRegionData] : []); -export default ForkCustomerAction; + return ( + + SAAS relocation job, but the source organization will not be affected." + } + choices={regionChoices} + inline={false} + stacked + required + value={regionUrl} + onChange={(val: any) => setRegionUrl(val)} + /> + + ); +} From 63a1c4f04ef141ab89e6a56b5d598c996710a407 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 14 Jul 2025 09:55:43 -0700 Subject: [PATCH 2/2] account for unstable setConfirmCallback --- static/gsAdmin/components/forkCustomer.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/static/gsAdmin/components/forkCustomer.tsx b/static/gsAdmin/components/forkCustomer.tsx index a7d57303781093..3bae9e85f284d3 100644 --- a/static/gsAdmin/components/forkCustomer.tsx +++ b/static/gsAdmin/components/forkCustomer.tsx @@ -1,4 +1,4 @@ -import {Fragment, useEffect, useState} from 'react'; +import {Fragment, useEffect, useRef, useState} from 'react'; import SelectField from 'sentry/components/forms/fields/selectField'; import type {Organization} from 'sentry/types/organization'; @@ -29,6 +29,11 @@ export default function ForkCustomerAction({ onConfirm, setConfirmCallback, }: Props) { + // TODO: We should make sure that `setConfirmCallback` is a stable function + // before passing it in here. But because it's not memoized right now, we + // need to store it in a ref between renders. + const onConfirmRef = useRef(setConfirmCallback); + const [regionUrl, setRegionUrl] = useState(''); const api = useApi({persistInFlight: true}); const navigate = useNavigate(); @@ -57,8 +62,8 @@ export default function ForkCustomerAction({ }); useEffect(() => { - setConfirmCallback(mutate); - }, [mutate, setConfirmCallback]); + onConfirmRef.current(mutate); + }, [mutate]); const currentRegionData = getRegionDataFromOrganization(organization); const regionChoices = getRegionChoices(currentRegionData ? [currentRegionData] : []); @@ -76,7 +81,7 @@ export default function ForkCustomerAction({ stacked required value={regionUrl} - onChange={(val: any) => setRegionUrl(val)} + onChange={setRegionUrl} /> );