0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): optimize tenant context and hooks

remove the use of useSWR and make the tenant context as the single
source of truth for tenant data. also reorg deduplicate hook logics.
This commit is contained in:
Gao Sun 2023-06-22 15:13:49 +08:00
parent cfe4fce51c
commit c4e13ff525
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
14 changed files with 196 additions and 230 deletions

View file

@ -29,7 +29,7 @@ import TenantsProvider, { TenantsContext } from './contexts/TenantsProvider';
void initI18n();
function Content() {
const { tenants, isSettle, currentTenantId } = useContext(TenantsContext);
const { tenants, isInitComplete, currentTenantId } = useContext(TenantsContext);
const resources = useMemo(
() =>
@ -39,7 +39,7 @@ function Content() {
// access a URL with Tenant ID, adding the ID from the URL here can possibly remove one
// additional redirect.
currentTenantId && getManagementApi(currentTenantId).indicator,
...(tenants ?? []).map(({ id }) => getManagementApi(id).indicator),
...tenants.map(({ id }) => getManagementApi(id).indicator),
isCloud && cloudApi.indicator,
meApi.indicator
)
@ -76,7 +76,11 @@ function Content() {
<AppInsightsBoundary cloudRole="console">
<Helmet titleTemplate={`%s - ${mainTitle}`} defaultTitle={mainTitle} />
<ErrorBoundary>
{!isCloud || isSettle ? (
{/**
* If it's not Cloud (OSS), render the tenant app container directly since only default tenant is available;
* if it's Cloud, render the tenant app container only when init is complete and a tenant ID is available (in a tenant context).
*/}
{!isCloud || (isInitComplete && currentTenantId) ? (
<AppEndpointsProvider>
<AppConfirmModalProvider>
<TenantAppContainer />

View file

@ -1,23 +0,0 @@
import type router from '@logto/cloud/routes';
import { type RouterRoutes } from '@withtyped/client';
import useSWR from 'swr';
import { useCloudApi } from './use-cloud-api';
type GetRoutes = RouterRoutes<typeof router>['get'];
const normalizeError = (error: unknown) => {
if (error === undefined || error === null) {
return;
}
return error instanceof Error ? error : new Error(String(error));
};
export const useCloudSwr = <Key extends keyof GetRoutes>(key: Key) => {
const cloudApi = useCloudApi();
const response = useSWR(key, async () => cloudApi.get(key));
// By default, `useSWR()` uses `any` for the error type which is unexpected under our lint rule set.
return { ...response, error: normalizeError(response.error) };
};

View file

@ -0,0 +1,22 @@
import { useContext, useEffect } from 'react';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import AppLoading from '@/components/AppLoading';
import { TenantsContext } from '@/contexts/TenantsProvider';
export default function AutoCreateTenant() {
const api = useCloudApi();
const { appendTenant, navigateTenant } = useContext(TenantsContext);
useEffect(() => {
const createTenant = async () => {
const newTenant = await api.post('/api/tenants', { body: {} });
appendTenant(newTenant);
navigateTenant(newTenant.id);
};
void createTenant();
}, [api, appendTenant, navigateTenant]);
return <AppLoading />;
}

View file

@ -1,5 +1,4 @@
import { useLogto } from '@logto/react';
import type { TenantInfo } from '@logto/schemas/models';
import { trySafe } from '@silverhand/essentials';
import { useContext, useEffect } from 'react';
import { useHref } from 'react-router-dom';
@ -7,16 +6,12 @@ import { useHref } from 'react-router-dom';
import AppLoading from '@/components/AppLoading';
import { TenantsContext } from '@/contexts/TenantsProvider';
type Props = {
tenants: TenantInfo[];
toTenantId: string;
};
function Redirect({ tenants, toTenantId }: Props) {
function Redirect() {
const { getAccessToken, signIn } = useLogto();
const tenant = tenants.find(({ id }) => id === toTenantId);
const { setIsSettle, navigate } = useContext(TenantsContext);
const href = useHref(toTenantId + '/callback');
const { navigateTenant, tenants, currentTenantId } = useContext(TenantsContext);
const tenant = tenants.find(({ id }) => id === currentTenantId);
const href = useHref(currentTenantId + '/callback');
useEffect(() => {
const validate = async (indicator: string) => {
@ -24,8 +19,7 @@ function Redirect({ tenants, toTenantId }: Props) {
// If failed, it means the user finishes the first auth, ands still needs to auth again to
// fetch the full-scoped (with all available tenants) token.
if (await trySafe(getAccessToken(indicator))) {
setIsSettle(true);
navigate(toTenantId);
navigateTenant(currentTenantId);
} else {
void signIn(new URL(href, window.location.origin).toString());
}
@ -34,12 +28,14 @@ function Redirect({ tenants, toTenantId }: Props) {
if (tenant) {
void validate(tenant.indicator);
}
}, [getAccessToken, href, navigate, setIsSettle, signIn, tenant, toTenantId]);
}, [currentTenantId, getAccessToken, href, navigateTenant, signIn, tenant]);
if (!tenant) {
/** Fallback to another available tenant instead of showing `Forbidden`. */
navigate(tenants[0]?.id ?? '');
}
useEffect(() => {
if (!tenant) {
/** Fallback to another available tenant instead of showing `Forbidden`. */
navigateTenant(tenants[0]?.id ?? '');
}
}, [navigateTenant, tenant, tenants]);
return <AppLoading />;
}

View file

@ -1,13 +1,11 @@
import { Theme } from '@logto/schemas';
import type { TenantInfo } from '@logto/schemas/models';
import classNames from 'classnames';
import { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useContext, useState } from 'react';
import Plus from '@/assets/icons/plus.svg';
import TenantLandingPageImageDark from '@/assets/images/tenant-landing-page-dark.svg';
import TenantLandingPageImage from '@/assets/images/tenant-landing-page.svg';
import { useCloudSwr } from '@/cloud/hooks/use-cloud-swr';
import { TenantsContext } from '@/contexts/TenantsProvider';
import Button from '@/ds-components/Button';
import DynamicT from '@/ds-components/DynamicT';
@ -21,21 +19,12 @@ type Props = {
};
function TenantLandingPageContent({ className }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { tenants, setTenants } = useContext(TenantsContext);
const { data: availableTenants, mutate } = useCloudSwr('/api/tenants');
useEffect(() => {
if (availableTenants) {
setTenants(availableTenants);
}
}, [availableTenants, setTenants]);
const { tenants, appendTenant, navigateTenant } = useContext(TenantsContext);
const theme = useTheme();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
if (tenants?.length) {
if (tenants.length > 0) {
return null;
}
@ -66,8 +55,8 @@ function TenantLandingPageContent({ className }: Props) {
isOpen={isCreateModalOpen}
onClose={async (tenant?: TenantInfo) => {
if (tenant) {
void mutate();
window.location.assign(new URL(`/${tenant.id}`, window.location.origin).toString());
appendTenant(tenant);
navigateTenant(tenant.id);
}
setIsCreateModalOpen(false);
}}

View file

@ -1,93 +1,56 @@
import { useLogto } from '@logto/react';
import { conditional, yes } from '@silverhand/essentials';
import { HTTPError } from 'ky';
import { useContext, useEffect, useState } from 'react';
import { useContext, useEffect } from 'react';
import { useHref, useSearchParams } from 'react-router-dom';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import AppError from '@/components/AppError';
import AppLoading from '@/components/AppLoading';
import SessionExpired from '@/components/SessionExpired';
import { searchKeys } from '@/consts';
import { TenantsContext } from '@/contexts/TenantsProvider';
import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data';
import AutoCreateTenant from './AutoCreateTenant';
import Redirect from './Redirect';
import TenantLandingPage from './TenantLandingPage';
function Protected() {
function Content() {
const api = useCloudApi();
const { tenants, setTenants, currentTenantId, navigate } = useContext(TenantsContext);
const { isOnboarding, isLoaded } = useUserOnboardingData();
const [error, setError] = useState<Error>();
const { tenants, resetTenants, currentTenantId, isInitComplete } = useContext(TenantsContext);
const { isOnboarding, isLoaded: isOnboardingDataLoaded } = useUserOnboardingData();
// Load tenants from the cloud API for the first render.
useEffect(() => {
const loadTenants = async () => {
setError(undefined);
try {
const data = await api.get('/api/tenants');
setTenants(data);
} catch (error: unknown) {
setError(error instanceof Error ? error : new Error(String(error)));
}
const data = await api.get('/api/tenants');
resetTenants(data);
};
if (!tenants) {
if (!isInitComplete) {
void loadTenants();
}
}, [api, setTenants, tenants]);
}, [api, resetTenants, tenants, isInitComplete]);
useEffect(() => {
const createFirstTenant = async () => {
setError(undefined);
try {
const newTenant = await api.post('/api/tenants', { body: {} }); // Use DB default value.
setTenants([newTenant]);
navigate(newTenant.id);
} catch (error: unknown) {
setError(error instanceof Error ? error : new Error(String(error)));
}
};
if (isLoaded && isOnboarding && tenants?.length === 0) {
void createFirstTenant();
}
}, [api, isOnboarding, isLoaded, setTenants, tenants, navigate]);
if (error) {
if (error instanceof HTTPError && error.response.status === 401) {
return <SessionExpired error={error} />;
}
return <AppError errorMessage={error.message} callStack={error.stack} />;
if (!isInitComplete || !isOnboardingDataLoaded) {
return <AppLoading />;
}
if (tenants) {
/**
* Redirect to the first tenant if the current tenant ID is not set or can not be found.
*
* `currentTenantId` can be empty string, so that Boolean is required and `??` is
* not applicable for current case.
*/
// eslint-disable-next-line no-extra-boolean-cast
const toTenantId = Boolean(currentTenantId) ? currentTenantId : tenants[0]?.id;
if (toTenantId) {
return <Redirect tenants={tenants} toTenantId={toTenantId} />;
}
/**
* Will create a new tenant for new users that need go through onboarding process,
* but create tenant takes time, the screen will have a glance of landing page of empty tenant.
*/
if (isLoaded && !isOnboarding) {
return <TenantLandingPage />;
}
/**
* Trigger a redirect when one of the following conditions is met:
*
* - If a current tenant ID has been set in the URL; or
* - If current tenant ID is not set, but there is at least one tenant available.
*/
if (currentTenantId || tenants[0]) {
return <Redirect />;
}
return <AppLoading />;
// A new user has just signed up and has no tenant, needs to create a new tenant.
if (isOnboarding) {
return <AutoCreateTenant />;
}
// If user has completed onboarding and still has no tenant, redirect to a special landing page.
return <TenantLandingPage />;
}
function Main() {
@ -110,7 +73,7 @@ function Main() {
return <AppLoading />;
}
return <Protected />;
return <Content />;
}
export default Main;

View file

@ -112,6 +112,12 @@ $dropdown-item-height: 40px;
}
.createTenantButton {
all: unset;
/**
* `inline-size: stretch` is needed since button will have the used value `inline-size: fit-content` by default.
* @see {@link https://html.spec.whatwg.org/multipage/rendering.html#button-layout}
*/
inline-size: stretch;
display: flex;
align-items: center;
padding: _.unit(2.5) _.unit(3) _.unit(2.5) _.unit(4);

View file

@ -1,16 +1,14 @@
import { adminTenantId } from '@logto/schemas';
import { type TenantInfo } from '@logto/schemas/models';
import classNames from 'classnames';
import { useContext, useRef, useState, useEffect, useMemo } from 'react';
import { useContext, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
import PlusSign from '@/assets/icons/plus.svg';
import Tick from '@/assets/icons/tick.svg';
import { useCloudSwr } from '@/cloud/hooks/use-cloud-swr';
import CreateTenantModal from '@/cloud/pages/Main/TenantLandingPage/TenantLandingPageContent/CreateTenantModal';
import AppError from '@/components/AppError';
import { maxFreeTenantNumbers } from '@/consts/tenants';
import { maxFreeTenantNumbers } from '@/consts';
import { TenantsContext } from '@/contexts/TenantsProvider';
import Divider from '@/ds-components/Divider';
import Dropdown, { DropdownItem } from '@/ds-components/Dropdown';
@ -20,39 +18,26 @@ import { onKeyDownHandler } from '@/utils/a11y';
import TenantEnvTag from './TenantEnvTag';
import * as styles from './index.module.scss';
function TenantSelector() {
export default function TenantSelector() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { tenants, setTenants, currentTenantId } = useContext(TenantsContext);
const { data: availableTenants, mutate, error } = useCloudSwr('/api/tenants');
useEffect(() => {
if (availableTenants) {
setTenants(availableTenants);
}
}, [availableTenants, setTenants]);
const currentTenantInfo = useMemo(() => {
return tenants?.find((tenant) => tenant.id === currentTenantId);
}, [currentTenantId, tenants]);
const {
tenants,
appendTenant,
currentTenant: currentTenantInfo,
currentTenantId,
} = useContext(TenantsContext);
const isCreateButtonDisabled = useMemo(
() =>
/** Should not block admin tenant owners from creating more than three tenants */
tenants &&
!tenants.some(({ id }) => id === adminTenantId) &&
tenants.length >= maxFreeTenantNumbers,
!tenants.some(({ id }) => id === adminTenantId) && tenants.length >= maxFreeTenantNumbers,
[tenants]
);
const anchorRef = useRef<HTMLDivElement>(null);
const [showDropdown, setShowDropdown] = useState(false);
const [showCreateTenantModal, setShowCreateTenantModal] = useState(false);
if (error) {
return <AppError errorMessage={error.message} callStack={error.stack} />;
}
if (!tenants?.length || !currentTenantInfo) {
if (tenants.length === 0 || !currentTenantInfo) {
return null;
}
@ -102,35 +87,29 @@ function TenantSelector() {
))}
</OverlayScrollbar>
<Divider />
<div
role="button"
<button
tabIndex={0}
className={classNames(
isCreateButtonDisabled && styles.disabled,
styles.createTenantButton
)}
disabled={isCreateButtonDisabled}
onClick={() => {
if (isCreateButtonDisabled) {
return;
}
setShowCreateTenantModal(true);
}}
onKeyDown={onKeyDownHandler(() => {
if (isCreateButtonDisabled) {
return;
}
setShowCreateTenantModal(true);
})}
>
<div>{t('cloud.tenant.create_tenant')}</div>
<PlusSign />
</div>
</button>
</Dropdown>
<CreateTenantModal
isOpen={showCreateTenantModal}
onClose={async (tenant?: TenantInfo) => {
if (tenant) {
void mutate();
appendTenant(tenant);
window.location.assign(new URL(`/${tenant.id}`, window.location.origin).toString());
}
setShowCreateTenantModal(false);
@ -139,5 +118,3 @@ function TenantSelector() {
</>
);
}
export default TenantSelector;

View file

@ -3,10 +3,9 @@ import { HTTPError } from 'ky';
import type { ReactNode } from 'react';
import { Component } from 'react';
import AppError from '@/components/AppError';
import SessionExpired from '@/components/SessionExpired';
import AppError from '../../components/AppError';
type Props = {
children: ReactNode;
};
@ -15,6 +14,14 @@ type State = {
error?: Error;
};
/**
* An error boundary that catches errors in its children.
* Note it uses several hooks and contexts:
*
* - Includes `useLogto()` (depending on `<LogtoProvider />`) to provide a "Sign in again" button for session expired errors.
* - Includes `useTranslation()` to provide translations for the error messages.
* - includes `useTheme()` (depending on `<AppThemeProvider />`) to provide the local stored theme.
*/
class ErrorBoundary extends Component<Props, State> {
static getDerivedStateFromError(error: Error): State {
return { error };

View file

@ -1,6 +1,6 @@
import { defaultManagementApi } from '@logto/schemas';
import { type TenantInfo, TenantTag } from '@logto/schemas/models';
import { conditional, noop } from '@silverhand/essentials';
import { conditionalArray, noop } from '@silverhand/essentials';
import type { ReactNode } from 'react';
import { useCallback, useMemo, createContext, useState } from 'react';
import type { NavigateOptions } from 'react-router-dom';
@ -12,68 +12,116 @@ type Props = {
children: ReactNode;
};
/** @see {@link TenantsProvider} for why `useSWR()` is not applicable for this context. */
type Tenants = {
tenants?: TenantInfo[];
isSettle: boolean;
setTenants: (tenants: TenantInfo[]) => void;
setIsSettle: (isSettle: boolean) => void;
tenants: readonly TenantInfo[];
/** Indicates if the tenants data is ready for the first render. */
isInitComplete: boolean;
/** Reset tenants to the given value. It will overwrite the current tenants data and set `isInitComplete` to `true`. */
resetTenants: (tenants: TenantInfo[]) => void;
/** Append a new tenant to the current tenants data. */
appendTenant: (tenant: TenantInfo) => void;
/** Remove a tenant by ID from the current tenants data. */
removeTenant: (tenantId: string) => void;
/** Update a tenant by ID if it exists in the current tenants data. */
updateTenant: (tenantId: string, data: Partial<TenantInfo>) => void;
/**
* The current tenant ID that the URL is pointing to. It is navigated programmatically
* since there's [no easy way](https://stackoverflow.com/questions/34999976/detect-changes-on-the-url)
* to listen to the URL change without polling.
*/
currentTenantId: string;
currentTenant?: TenantInfo;
setCurrentTenantId: (tenantId: string) => void;
navigate: (tenantId: string, options?: NavigateOptions) => void;
navigateTenant: (tenantId: string, options?: NavigateOptions) => void;
};
const { tenantId, indicator } = defaultManagementApi.resource;
const initialTenants = conditional(
!isCloud && [
{ id: tenantId, name: `tenant_${tenantId}`, tag: TenantTag.Development, indicator }, // Make `tag` value to be string type.
]
/**
* - For cloud, the initial tenants data is empty, and it will be fetched from the cloud API.
* - OSS has a fixed tenant with ID `default` and no cloud API to dynamically fetch tenants.
*/
const initialTenants = Object.freeze(
conditionalArray(
!isCloud && { id: tenantId, name: `tenant_${tenantId}`, tag: TenantTag.Development, indicator }
)
);
export const TenantsContext = createContext<Tenants>({
tenants: initialTenants,
setTenants: noop,
isSettle: false,
setIsSettle: noop,
isInitComplete: false,
resetTenants: noop,
appendTenant: noop,
removeTenant: noop,
updateTenant: noop,
currentTenantId: '',
setCurrentTenantId: noop,
navigate: noop,
navigateTenant: noop,
});
/**
* The global tenants context provider for all available tenants of the current users.
* It is used to manage the tenants information, including create, update, and delete;
* also for navigating between tenants.
*
* Note it is not practical to use `useSWR()` for tenants context, since fetching tenants
* requires authentication, and the authentication is managed by the `LogtoProvider` which
* depends and locates inside the `TenantsProvider`. Thus the fetching tenants action should
* be done by a component inside the `LogtoProvider`, which `useSWR()` cannot handle.
*/
function TenantsProvider({ children }: Props) {
const [tenants, setTenants] = useState(initialTenants);
const [isSettle, setIsSettle] = useState(false);
/** @see {@link initialTenants} */
const [isInitComplete, setIsInitComplete] = useState(!isCloud);
const [currentTenantId, setCurrentTenantId] = useState(getUserTenantId());
const navigate = useCallback((tenantId: string, options?: NavigateOptions) => {
if (options?.replace) {
window.history.replaceState(
options.state ?? {},
'',
new URL(`/${tenantId}`, window.location.origin).toString()
);
const navigateTenant = useCallback((tenantId: string, options?: NavigateOptions) => {
const params = [
options?.state ?? {},
'',
new URL(`/${tenantId}`, window.location.origin).toString(),
] satisfies Parameters<typeof window.history.pushState>;
if (options?.replace) {
window.history.replaceState(...params);
return;
}
window.history.pushState(
options?.state ?? {},
'',
new URL(`/${tenantId}`, window.location.origin).toString()
);
window.history.pushState(...params);
setCurrentTenantId(tenantId);
}, []);
const currentTenant = useMemo(
() => tenants.find((tenant) => tenant.id === currentTenantId),
[currentTenantId, tenants]
);
const memorizedContext = useMemo(
() => ({
tenants,
setTenants,
isSettle,
setIsSettle,
resetTenants: (tenants: TenantInfo[]) => {
setTenants(tenants);
setIsInitComplete(true);
},
appendTenant: (tenant: TenantInfo) => {
setTenants((tenants) => [...tenants, tenant]);
},
removeTenant: (tenantId: string) => {
setTenants((tenants) => tenants.filter((tenant) => tenant.id !== tenantId));
},
updateTenant: (tenantId: string, data: Partial<TenantInfo>) => {
setTenants((tenants) =>
tenants.map((tenant) => (tenant.id === tenantId ? { ...tenant, ...data } : tenant))
);
},
isInitComplete,
currentTenantId,
currentTenant,
setCurrentTenantId,
navigate,
navigateTenant,
}),
[currentTenantId, isSettle, navigate, tenants]
[currentTenant, currentTenantId, isInitComplete, navigateTenant, tenants]
);
return <TenantsContext.Provider value={memorizedContext}>{children}</TenantsContext.Provider>;

View file

@ -21,12 +21,12 @@ import * as styles from './index.module.scss';
function Congrats() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { update } = useUserOnboardingData();
const { navigate, currentTenantId } = useContext(TenantsContext);
const { navigateTenant, currentTenantId } = useContext(TenantsContext);
const enterAdminConsole = () => {
void update({ isOnboardingDone: true });
// Note: navigate to the admin console page directly instead of using the router
navigate(currentTenantId);
navigateTenant(currentTenantId);
};
return (

View file

@ -1,4 +1,4 @@
import { adminTenantId, defaultTenantId } from '@logto/schemas';
import { adminTenantId } from '@logto/schemas';
import { useTranslation } from 'react-i18next';
import FormCard from '@/components/FormCard';
@ -25,7 +25,7 @@ function DeleteCard({ currentTenantId, onClick }: Props) {
<Button
type="default"
title="tenants.deletion_card.tenant_deletion_button"
disabled={[adminTenantId, defaultTenantId].includes(currentTenantId)}
disabled={[adminTenantId].includes(currentTenantId)}
onClick={onClick}
/>
</div>

View file

@ -1,14 +1,12 @@
import { type TenantInfo, TenantTag } from '@logto/schemas/models';
import classNames from 'classnames';
import { useEffect, useState, useContext, useMemo } from 'react';
import { useContext, useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { useCloudSwr } from '@/cloud/hooks/use-cloud-swr';
import AppError from '@/components/AppError';
import AppLoading from '@/components/AppLoading';
import PageMeta from '@/components/PageMeta';
import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
@ -29,30 +27,12 @@ const tenantProfileToForm = (tenant?: TenantInfo): TenantSettingsForm => {
function TenantBasicSettings() {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const api = useCloudApi();
const { tenants, setTenants, currentTenantId } = useContext(TenantsContext);
const { data: availableTenants, mutate, error: requestError } = useCloudSwr('/api/tenants');
const isLoading = !availableTenants && !requestError;
useEffect(() => {
if (availableTenants) {
setTenants(availableTenants);
}
}, [availableTenants, setTenants]);
const currentTenant = useMemo(() => {
return tenants?.find((tenant) => tenant.id === currentTenantId);
}, [currentTenantId, tenants]);
const { currentTenant, currentTenantId, tenants, updateTenant, removeTenant } =
useContext(TenantsContext);
const [error, setError] = useState<Error>();
const [isDeletionModalOpen, setIsDeletionModalOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
useEffect(() => {
if (requestError) {
setError(requestError);
}
}, [requestError]);
const methods = useForm<TenantSettingsForm>({
defaultValues: tenantProfileToForm(currentTenant),
});
@ -74,8 +54,8 @@ function TenantBasicSettings() {
body: data,
});
reset({ profile: { name, tag } });
void mutate();
toast.success(t('tenants.settings.tenant_info_saved'));
updateTenant(currentTenantId, data);
} catch (error: unknown) {
setError(
error instanceof Error
@ -109,7 +89,7 @@ function TenantBasicSettings() {
try {
await api.delete(`/api/tenants/:tenantId`, { params: { tenantId: currentTenantId } });
setIsDeletionModalOpen(false);
void mutate();
removeTenant(currentTenantId);
} catch (error: unknown) {
setError(
error instanceof Error
@ -126,7 +106,7 @@ function TenantBasicSettings() {
* Redirect to the first tenant if the current tenant is deleted;
* Redirect to Cloud console landing page if there is no tenant.
*/
if (tenants && !tenants.some(({ id }) => id === currentTenantId)) {
if (!tenants.some(({ id }) => id === currentTenantId)) {
window.location.assign(
tenants[0]?.id
? new URL(`/${tenants[0]?.id}`, window.location.origin).toString()
@ -135,10 +115,6 @@ function TenantBasicSettings() {
}
}, [currentTenantId, tenants]);
if (isLoading) {
return <AppLoading />;
}
if (error) {
return <AppError errorMessage={error.message} callStack={error.stack} />;
}

View file

@ -9,6 +9,7 @@
cursor: pointer;
font: var(--font-label-1);
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
transition: background 0.2s ease-in-out;
user-select: none;