mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): should limit only the number of free tenant (#4205)
* fix(console): update free tenant limit * fix(console): use TenantResponse type * chore(console): update dependency on @logto/cloud * chore: remove import as
This commit is contained in:
parent
3ef8c06d4a
commit
350d070ef7
14 changed files with 71 additions and 70 deletions
|
@ -26,7 +26,7 @@
|
||||||
"@fontsource/roboto-mono": "^5.0.0",
|
"@fontsource/roboto-mono": "^5.0.0",
|
||||||
"@jest/types": "^29.5.0",
|
"@jest/types": "^29.5.0",
|
||||||
"@logto/app-insights": "workspace:^1.3.1",
|
"@logto/app-insights": "workspace:^1.3.1",
|
||||||
"@logto/cloud": "0.2.5-2087c06",
|
"@logto/cloud": "0.2.5-70aa370",
|
||||||
"@logto/connector-kit": "workspace:^1.1.1",
|
"@logto/connector-kit": "workspace:^1.1.1",
|
||||||
"@logto/core-kit": "workspace:^2.0.1",
|
"@logto/core-kit": "workspace:^2.0.1",
|
||||||
"@logto/language-kit": "workspace:^1.0.0",
|
"@logto/language-kit": "workspace:^1.0.0",
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
"@types/react-helmet": "^6.1.6",
|
"@types/react-helmet": "^6.1.6",
|
||||||
"@types/react-modal": "^3.13.1",
|
"@types/react-modal": "^3.13.1",
|
||||||
"@types/react-syntax-highlighter": "^15.5.1",
|
"@types/react-syntax-highlighter": "^15.5.1",
|
||||||
"@withtyped/client": "^0.7.19",
|
"@withtyped/client": "^0.7.21",
|
||||||
"buffer": "^5.7.1",
|
"buffer": "^5.7.1",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"clean-deep": "^3.4.0",
|
"clean-deep": "^3.4.0",
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Theme } from '@logto/schemas';
|
import { Theme } from '@logto/schemas';
|
||||||
import type { TenantInfo } from '@logto/schemas/models';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
|
|
||||||
import Plus from '@/assets/icons/plus.svg';
|
import Plus from '@/assets/icons/plus.svg';
|
||||||
import TenantLandingPageImageDark from '@/assets/images/tenant-landing-page-dark.svg';
|
import TenantLandingPageImageDark from '@/assets/images/tenant-landing-page-dark.svg';
|
||||||
import TenantLandingPageImage from '@/assets/images/tenant-landing-page.svg';
|
import TenantLandingPageImage from '@/assets/images/tenant-landing-page.svg';
|
||||||
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import CreateTenantModal from '@/components/CreateTenantModal';
|
import CreateTenantModal from '@/components/CreateTenantModal';
|
||||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||||
import Button from '@/ds-components/Button';
|
import Button from '@/ds-components/Button';
|
||||||
|
@ -54,7 +54,7 @@ function TenantLandingPageContent({ className }: Props) {
|
||||||
<CreateTenantModal
|
<CreateTenantModal
|
||||||
skipPlanSelection
|
skipPlanSelection
|
||||||
isOpen={isCreateModalOpen}
|
isOpen={isCreateModalOpen}
|
||||||
onClose={async (tenant?: TenantInfo) => {
|
onClose={async (tenant?: TenantResponse) => {
|
||||||
if (tenant) {
|
if (tenant) {
|
||||||
prependTenant(tenant);
|
prependTenant(tenant);
|
||||||
navigateTenant(tenant.id);
|
navigateTenant(tenant.id);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import type router from '@logto/cloud/routes';
|
||||||
import { type GuardedResponse, type RouterRoutes } from '@withtyped/client';
|
import { type GuardedResponse, type RouterRoutes } from '@withtyped/client';
|
||||||
|
|
||||||
type GetRoutes = RouterRoutes<typeof router>['get'];
|
type GetRoutes = RouterRoutes<typeof router>['get'];
|
||||||
|
type GetArrayElementType<T> = T extends Array<infer U> ? U : never;
|
||||||
|
|
||||||
export type SubscriptionPlanResponse = GuardedResponse<
|
export type SubscriptionPlanResponse = GuardedResponse<
|
||||||
GetRoutes['/api/subscription-plans']
|
GetRoutes['/api/subscription-plans']
|
||||||
|
@ -12,3 +13,6 @@ export type Subscription = GuardedResponse<GetRoutes['/api/tenants/:tenantId/sub
|
||||||
export type SubscriptionUsage = GuardedResponse<GetRoutes['/api/tenants/:tenantId/usage']>;
|
export type SubscriptionUsage = GuardedResponse<GetRoutes['/api/tenants/:tenantId/usage']>;
|
||||||
|
|
||||||
export type InvoicesResponse = GuardedResponse<GetRoutes['/api/tenants/:tenantId/invoices']>;
|
export type InvoicesResponse = GuardedResponse<GetRoutes['/api/tenants/:tenantId/invoices']>;
|
||||||
|
|
||||||
|
// The response of GET /api/tenants is TenantResponse[].
|
||||||
|
export type TenantResponse = GetArrayElementType<GuardedResponse<GetRoutes['/api/tenants']>>;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { maxFreeTenantLimit } from '@logto/schemas';
|
import { maxFreeTenantLimit, adminTenantId } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useContext, useMemo } from 'react';
|
import { useContext, useMemo } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
@ -51,8 +51,13 @@ function PlanCardItem({ plan, onSelect }: Props) {
|
||||||
|
|
||||||
const isFreePlan = planId === ReservedPlanId.free;
|
const isFreePlan = planId === ReservedPlanId.free;
|
||||||
|
|
||||||
// Todo: @xiaoyijun filter our all free tenants
|
const isFreeTenantExceeded = useMemo(
|
||||||
const isFreeTenantExceeded = tenants.length >= maxFreeTenantLimit;
|
() =>
|
||||||
|
/** Should not block admin tenant owners from creating more than three tenants */
|
||||||
|
!tenants.some(({ id }) => id === adminTenantId) &&
|
||||||
|
tenants.filter(({ planId }) => planId === ReservedPlanId.free).length >= maxFreeTenantLimit,
|
||||||
|
[tenants]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { type TenantInfo } from '@logto/schemas/models';
|
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import Modal from 'react-modal';
|
import Modal from 'react-modal';
|
||||||
|
|
||||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||||
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import { ReservedPlanId } from '@/consts/subscriptions';
|
import { ReservedPlanId } from '@/consts/subscriptions';
|
||||||
import DangerousRaw from '@/ds-components/DangerousRaw';
|
import DangerousRaw from '@/ds-components/DangerousRaw';
|
||||||
import ModalLayout from '@/ds-components/ModalLayout';
|
import ModalLayout from '@/ds-components/ModalLayout';
|
||||||
|
@ -20,7 +20,7 @@ import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tenantData?: CreateTenantData;
|
tenantData?: CreateTenantData;
|
||||||
onClose: (tenant?: TenantInfo) => void;
|
onClose: (tenant?: TenantResponse) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function SelectTenantPlanModal({ tenantData, onClose }: Props) {
|
function SelectTenantPlanModal({ tenantData, onClose }: Props) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { AdminConsoleKey } from '@logto/phrases';
|
import type { AdminConsoleKey } from '@logto/phrases';
|
||||||
import { Theme } from '@logto/schemas';
|
import { Theme } from '@logto/schemas';
|
||||||
import { TenantTag, type TenantInfo } from '@logto/schemas/models';
|
import { TenantTag } from '@logto/schemas/models';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
@ -10,6 +10,7 @@ import Modal from 'react-modal';
|
||||||
import CreateTenantHeaderIconDark from '@/assets/icons/create-tenant-header-dark.svg';
|
import CreateTenantHeaderIconDark from '@/assets/icons/create-tenant-header-dark.svg';
|
||||||
import CreateTenantHeaderIcon from '@/assets/icons/create-tenant-header.svg';
|
import CreateTenantHeaderIcon from '@/assets/icons/create-tenant-header.svg';
|
||||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||||
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import { isProduction } from '@/consts/env';
|
import { isProduction } from '@/consts/env';
|
||||||
import Button from '@/ds-components/Button';
|
import Button from '@/ds-components/Button';
|
||||||
import FormField from '@/ds-components/FormField';
|
import FormField from '@/ds-components/FormField';
|
||||||
|
@ -25,7 +26,7 @@ import { type CreateTenantData } from './type';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: (tenant?: TenantInfo) => void;
|
onClose: (tenant?: TenantResponse) => void;
|
||||||
// eslint-disable-next-line react/boolean-prop-naming
|
// eslint-disable-next-line react/boolean-prop-naming
|
||||||
skipPlanSelection?: boolean;
|
skipPlanSelection?: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { type TenantInfo } from '@logto/schemas/models';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import Tick from '@/assets/icons/tick.svg';
|
import Tick from '@/assets/icons/tick.svg';
|
||||||
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import PlanName from '@/components/PlanName';
|
import PlanName from '@/components/PlanName';
|
||||||
import { isProduction } from '@/consts/env';
|
import { isProduction } from '@/consts/env';
|
||||||
import { DropdownItem } from '@/ds-components/Dropdown';
|
import { DropdownItem } from '@/ds-components/Dropdown';
|
||||||
|
@ -14,7 +14,7 @@ import TenantStatusTag from './TenantStatusTag';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tenantData: TenantInfo;
|
tenantData: TenantResponse;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
|
@ -101,19 +101,4 @@ $dropdown-item-height: 40px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
color: var(--color-neutral-50);
|
color: var(--color-neutral-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
&:hover {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:disabled) {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div,
|
|
||||||
> svg {
|
|
||||||
color: var(--color-placeholder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { adminTenantId, maxFreeTenantLimit } from '@logto/schemas';
|
import { useContext, useRef, useState } from 'react';
|
||||||
import { type TenantInfo } from '@logto/schemas/models';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useContext, useMemo, useRef, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
|
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
|
||||||
import PlusSign from '@/assets/icons/plus.svg';
|
import PlusSign from '@/assets/icons/plus.svg';
|
||||||
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import CreateTenantModal from '@/components/CreateTenantModal';
|
import CreateTenantModal from '@/components/CreateTenantModal';
|
||||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||||
import Divider from '@/ds-components/Divider';
|
import Divider from '@/ds-components/Divider';
|
||||||
|
@ -27,12 +25,6 @@ export default function TenantSelector() {
|
||||||
navigateTenant,
|
navigateTenant,
|
||||||
} = useContext(TenantsContext);
|
} = useContext(TenantsContext);
|
||||||
|
|
||||||
const isCreateButtonDisabled = useMemo(
|
|
||||||
() =>
|
|
||||||
/** Should not block admin tenant owners from creating more than three tenants */
|
|
||||||
!tenants.some(({ id }) => id === adminTenantId) && tenants.length >= maxFreeTenantLimit,
|
|
||||||
[tenants]
|
|
||||||
);
|
|
||||||
const anchorRef = useRef<HTMLDivElement>(null);
|
const anchorRef = useRef<HTMLDivElement>(null);
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
const [showCreateTenantModal, setShowCreateTenantModal] = useState(false);
|
const [showCreateTenantModal, setShowCreateTenantModal] = useState(false);
|
||||||
|
@ -85,11 +77,7 @@ export default function TenantSelector() {
|
||||||
<Divider />
|
<Divider />
|
||||||
<button
|
<button
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={classNames(
|
className={styles.createTenantButton}
|
||||||
isCreateButtonDisabled && styles.disabled,
|
|
||||||
styles.createTenantButton
|
|
||||||
)}
|
|
||||||
disabled={isCreateButtonDisabled}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCreateTenantModal(true);
|
setShowCreateTenantModal(true);
|
||||||
}}
|
}}
|
||||||
|
@ -103,7 +91,7 @@ export default function TenantSelector() {
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<CreateTenantModal
|
<CreateTenantModal
|
||||||
isOpen={showCreateTenantModal}
|
isOpen={showCreateTenantModal}
|
||||||
onClose={async (tenant?: TenantInfo) => {
|
onClose={async (tenant?: TenantResponse) => {
|
||||||
if (tenant) {
|
if (tenant) {
|
||||||
prependTenant(tenant);
|
prependTenant(tenant);
|
||||||
navigateTenant(tenant.id);
|
navigateTenant(tenant.id);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useLogto } from '@logto/react';
|
import { useLogto } from '@logto/react';
|
||||||
import { type TenantInfo } from '@logto/schemas/lib/models/tenants.js';
|
|
||||||
import { trySafe } from '@silverhand/essentials';
|
import { trySafe } from '@silverhand/essentials';
|
||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { useSWRConfig } from 'swr';
|
import { useSWRConfig } from 'swr';
|
||||||
|
|
||||||
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import AppLoading from '@/components/AppLoading';
|
import AppLoading from '@/components/AppLoading';
|
||||||
// Used in the docs
|
// Used in the docs
|
||||||
// eslint-disable-next-line unused-imports/no-unused-imports
|
// eslint-disable-next-line unused-imports/no-unused-imports
|
||||||
|
@ -63,7 +63,7 @@ export default function TenantAccess() {
|
||||||
}, [mutate, currentTenantId]);
|
}, [mutate, currentTenantId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validate = async ({ indicator }: TenantInfo) => {
|
const validate = async ({ indicator }: TenantResponse) => {
|
||||||
// Test fetching an access token for the current Tenant ID.
|
// Test fetching an access token for the current Tenant ID.
|
||||||
// If failed, it means the user finishes the first auth, ands still needs to auth again to
|
// 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.
|
// fetch the full-scoped (with all available tenants) token.
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { defaultManagementApi, defaultTenantId } from '@logto/schemas';
|
import { defaultManagementApi, defaultTenantId } from '@logto/schemas';
|
||||||
import { type TenantInfo, TenantTag } from '@logto/schemas/models';
|
import { TenantTag } from '@logto/schemas/models';
|
||||||
import { conditionalArray, noop } from '@silverhand/essentials';
|
import { conditionalArray, noop } from '@silverhand/essentials';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback, useMemo, createContext, useState } from 'react';
|
import { useCallback, useMemo, createContext, useState } from 'react';
|
||||||
import { useMatch, useNavigate } from 'react-router-dom';
|
import { useMatch, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import { isCloud } from '@/consts/env';
|
import { isCloud } from '@/consts/env';
|
||||||
|
import { ReservedPlanId } from '@/consts/subscriptions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The routes don't start with a tenant ID.
|
* The routes don't start with a tenant ID.
|
||||||
|
@ -32,20 +34,20 @@ type CurrentTenantStatus = 'pending' | 'validating' | 'validated';
|
||||||
|
|
||||||
/** @see {@link TenantsProvider} for why `useSWR()` is not applicable for this context. */
|
/** @see {@link TenantsProvider} for why `useSWR()` is not applicable for this context. */
|
||||||
type Tenants = {
|
type Tenants = {
|
||||||
tenants: readonly TenantInfo[];
|
tenants: readonly TenantResponse[];
|
||||||
/** Indicates if the tenants data is ready for the first render. */
|
/** Indicates if the tenants data is ready for the first render. */
|
||||||
isInitComplete: boolean;
|
isInitComplete: boolean;
|
||||||
/** Reset tenants to the given value. It will overwrite the current tenants data and set `isInitComplete` to `true`. */
|
/** Reset tenants to the given value. It will overwrite the current tenants data and set `isInitComplete` to `true`. */
|
||||||
resetTenants: (tenants: TenantInfo[]) => void;
|
resetTenants: (tenants: TenantResponse[]) => void;
|
||||||
/** Prepend a new tenant to the current tenants data. */
|
/** Prepend a new tenant to the current tenants data. */
|
||||||
prependTenant: (tenant: TenantInfo) => void;
|
prependTenant: (tenant: TenantResponse) => void;
|
||||||
/** Remove a tenant by ID from the current tenants data. */
|
/** Remove a tenant by ID from the current tenants data. */
|
||||||
removeTenant: (tenantId: string) => void;
|
removeTenant: (tenantId: string) => void;
|
||||||
/** Update a tenant by ID if it exists in the current tenants data. */
|
/** Update a tenant by ID if it exists in the current tenants data. */
|
||||||
updateTenant: (tenantId: string, data: Partial<TenantInfo>) => void;
|
updateTenant: (tenantId: string, data: Partial<TenantResponse>) => void;
|
||||||
/** The current tenant ID parsed from the URL. */
|
/** The current tenant ID parsed from the URL. */
|
||||||
currentTenantId: string;
|
currentTenantId: string;
|
||||||
currentTenant?: TenantInfo;
|
currentTenant?: TenantResponse;
|
||||||
/**
|
/**
|
||||||
* Indicates if the Access Token has been validated for use. Will be reset to `pending` when the current tenant changes.
|
* Indicates if the Access Token has been validated for use. Will be reset to `pending` when the current tenant changes.
|
||||||
*
|
*
|
||||||
|
@ -65,7 +67,13 @@ const { tenantId, indicator } = defaultManagementApi.resource;
|
||||||
*/
|
*/
|
||||||
const initialTenants = Object.freeze(
|
const initialTenants = Object.freeze(
|
||||||
conditionalArray(
|
conditionalArray(
|
||||||
!isCloud && { id: tenantId, name: `tenant_${tenantId}`, tag: TenantTag.Development, indicator }
|
!isCloud && {
|
||||||
|
id: tenantId,
|
||||||
|
name: `tenant_${tenantId}`,
|
||||||
|
tag: TenantTag.Development,
|
||||||
|
indicator,
|
||||||
|
planId: `${ReservedPlanId.free}`, // `planId` is string type.
|
||||||
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -132,18 +140,18 @@ function TenantsProvider({ children }: Props) {
|
||||||
const memorizedContext = useMemo(
|
const memorizedContext = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
tenants,
|
tenants,
|
||||||
resetTenants: (tenants: TenantInfo[]) => {
|
resetTenants: (tenants: TenantResponse[]) => {
|
||||||
setTenants(tenants);
|
setTenants(tenants);
|
||||||
setCurrentTenantStatus('pending');
|
setCurrentTenantStatus('pending');
|
||||||
setIsInitComplete(true);
|
setIsInitComplete(true);
|
||||||
},
|
},
|
||||||
prependTenant: (tenant: TenantInfo) => {
|
prependTenant: (tenant: TenantResponse) => {
|
||||||
setTenants((tenants) => [tenant, ...tenants]);
|
setTenants((tenants) => [tenant, ...tenants]);
|
||||||
},
|
},
|
||||||
removeTenant: (tenantId: string) => {
|
removeTenant: (tenantId: string) => {
|
||||||
setTenants((tenants) => tenants.filter((tenant) => tenant.id !== tenantId));
|
setTenants((tenants) => tenants.filter((tenant) => tenant.id !== tenantId));
|
||||||
},
|
},
|
||||||
updateTenant: (tenantId: string, data: Partial<TenantInfo>) => {
|
updateTenant: (tenantId: string, data: Partial<TenantResponse>) => {
|
||||||
setTenants((tenants) =>
|
setTenants((tenants) =>
|
||||||
tenants.map((tenant) => (tenant.id === tenantId ? { ...tenant, ...data } : tenant))
|
tenants.map((tenant) => (tenant.id === tenantId ? { ...tenant, ...data } : tenant))
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { type TenantInfo } from '@logto/schemas/models';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useTranslation, Trans } from 'react-i18next';
|
import { useTranslation, Trans } from 'react-i18next';
|
||||||
|
|
||||||
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import { contactEmailLink } from '@/consts';
|
import { contactEmailLink } from '@/consts';
|
||||||
import { tenantTagMap } from '@/containers/AppContent/components/Topbar/TenantSelector/TenantEnvTag';
|
import { tenantTagMap } from '@/containers/AppContent/components/Topbar/TenantSelector/TenantEnvTag';
|
||||||
import DeleteConfirmModal from '@/ds-components/DeleteConfirmModal';
|
import DeleteConfirmModal from '@/ds-components/DeleteConfirmModal';
|
||||||
|
@ -14,7 +14,7 @@ type Props = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
tenant: Pick<TenantInfo, 'name' | 'tag'>;
|
tenant: Pick<TenantResponse, 'name' | 'tag'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function DeleteModal({ isOpen, isLoading, onClose, onDelete, tenant }: Props) {
|
function DeleteModal({ isOpen, isLoading, onClose, onDelete, tenant }: Props) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type TenantInfo, TenantTag } from '@logto/schemas/models';
|
import { TenantTag } from '@logto/schemas/models';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
@ -6,6 +6,7 @@ import { toast } from 'react-hot-toast';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||||
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import AppError from '@/components/AppError';
|
import AppError from '@/components/AppError';
|
||||||
import PageMeta from '@/components/PageMeta';
|
import PageMeta from '@/components/PageMeta';
|
||||||
import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar';
|
import SubmitFormChangesActionBar from '@/components/SubmitFormChangesActionBar';
|
||||||
|
@ -18,7 +19,7 @@ import ProfileForm from './ProfileForm';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
import { type TenantSettingsForm } from './types.js';
|
import { type TenantSettingsForm } from './types.js';
|
||||||
|
|
||||||
const tenantProfileToForm = (tenant?: TenantInfo): TenantSettingsForm => {
|
const tenantProfileToForm = (tenant?: TenantResponse): TenantSettingsForm => {
|
||||||
return {
|
return {
|
||||||
profile: { name: tenant?.name ?? 'My project', tag: tenant?.tag ?? TenantTag.Development },
|
profile: { name: tenant?.name ?? 'My project', tag: tenant?.tag ?? TenantTag.Development },
|
||||||
};
|
};
|
||||||
|
|
|
@ -2755,8 +2755,8 @@ importers:
|
||||||
specifier: workspace:^1.3.1
|
specifier: workspace:^1.3.1
|
||||||
version: link:../app-insights
|
version: link:../app-insights
|
||||||
'@logto/cloud':
|
'@logto/cloud':
|
||||||
specifier: 0.2.5-2087c06
|
specifier: 0.2.5-70aa370
|
||||||
version: 0.2.5-2087c06(zod@3.20.2)
|
version: 0.2.5-70aa370(zod@3.20.2)
|
||||||
'@logto/connector-kit':
|
'@logto/connector-kit':
|
||||||
specifier: workspace:^1.1.1
|
specifier: workspace:^1.1.1
|
||||||
version: link:../toolkit/connector-kit
|
version: link:../toolkit/connector-kit
|
||||||
|
@ -2857,8 +2857,8 @@ importers:
|
||||||
specifier: ^15.5.1
|
specifier: ^15.5.1
|
||||||
version: 15.5.1
|
version: 15.5.1
|
||||||
'@withtyped/client':
|
'@withtyped/client':
|
||||||
specifier: ^0.7.19
|
specifier: ^0.7.21
|
||||||
version: 0.7.19(zod@3.20.2)
|
version: 0.7.21(zod@3.20.2)
|
||||||
buffer:
|
buffer:
|
||||||
specifier: ^5.7.1
|
specifier: ^5.7.1
|
||||||
version: 5.7.1
|
version: 5.7.1
|
||||||
|
@ -7210,8 +7210,8 @@ packages:
|
||||||
jose: 4.14.4
|
jose: 4.14.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@logto/cloud@0.2.5-2087c06(zod@3.20.2):
|
/@logto/cloud@0.2.5-4d5e389(zod@3.20.2):
|
||||||
resolution: {integrity: sha512-v/zisil/t8XrlFxlVic+0O6T2J3FUzB5FDC1w3OYNzhXNbSpmbqlt5vyN9RIwXUGgAKjdhKQjoACpV7HpPFZcQ==}
|
resolution: {integrity: sha512-vRJZGc0WvjE1rFJ0DNLaOHkhpe4TMdui/pvcTwGb/bDKzw/NM+4HtUoZj1a1DZ8Qqn24ex1WkTjxY8XGd3EruQ==}
|
||||||
engines: {node: ^18.12.0}
|
engines: {node: ^18.12.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@silverhand/essentials': 2.7.0
|
'@silverhand/essentials': 2.7.0
|
||||||
|
@ -7220,12 +7220,12 @@ packages:
|
||||||
- zod
|
- zod
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@logto/cloud@0.2.5-4d5e389(zod@3.20.2):
|
/@logto/cloud@0.2.5-70aa370(zod@3.20.2):
|
||||||
resolution: {integrity: sha512-vRJZGc0WvjE1rFJ0DNLaOHkhpe4TMdui/pvcTwGb/bDKzw/NM+4HtUoZj1a1DZ8Qqn24ex1WkTjxY8XGd3EruQ==}
|
resolution: {integrity: sha512-V5fIsbotJ8+L6Q+R9PnLjspvuccDKpukpLz/uHRUv4SYDo8U5MAcC680T2TGiROtMjWqnb0vd6WHuqXZ9XWTcw==}
|
||||||
engines: {node: ^18.12.0}
|
engines: {node: ^18.12.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@silverhand/essentials': 2.7.0
|
'@silverhand/essentials': 2.7.0
|
||||||
'@withtyped/server': 0.12.7(zod@3.20.2)
|
'@withtyped/server': 0.12.8(zod@3.20.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- zod
|
- zod
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -9855,6 +9855,16 @@ packages:
|
||||||
'@withtyped/shared': 0.2.2
|
'@withtyped/shared': 0.2.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- zod
|
- zod
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@withtyped/client@0.7.21(zod@3.20.2):
|
||||||
|
resolution: {integrity: sha512-N9dvH5nqIwaT7YxaIm83RRQf9AEjxwJ4ugJviZJSxtWy8zLul2/odEMc6epieylFVa6CcLg82yJmRSlqPtJiTw==}
|
||||||
|
dependencies:
|
||||||
|
'@withtyped/server': 0.12.8(zod@3.20.2)
|
||||||
|
'@withtyped/shared': 0.2.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- zod
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@withtyped/server@0.12.7(zod@3.20.2):
|
/@withtyped/server@0.12.7(zod@3.20.2):
|
||||||
resolution: {integrity: sha512-NNT78ZZmSZiEosxI3iW/kVx1KEG5vetvpEXNl0Gy58OlOnI8l/7h8Q//JZJ268xWOKyaNI4KrngTRtL5uvZu9Q==}
|
resolution: {integrity: sha512-NNT78ZZmSZiEosxI3iW/kVx1KEG5vetvpEXNl0Gy58OlOnI8l/7h8Q//JZJ268xWOKyaNI4KrngTRtL5uvZu9Q==}
|
||||||
|
@ -9873,7 +9883,6 @@ packages:
|
||||||
'@silverhand/essentials': 2.7.0
|
'@silverhand/essentials': 2.7.0
|
||||||
'@withtyped/shared': 0.2.2
|
'@withtyped/shared': 0.2.2
|
||||||
zod: 3.20.2
|
zod: 3.20.2
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@withtyped/shared@0.2.2:
|
/@withtyped/shared@0.2.2:
|
||||||
resolution: {integrity: sha512-Vpcj12NqaoZ8M5Z/1kffheI9FBZEm9goed0THmgTcMKXLHjXSRbMZMp0olVxovEgaTIAydshqJOQUXKZMctIZw==}
|
resolution: {integrity: sha512-Vpcj12NqaoZ8M5Z/1kffheI9FBZEm9goed0THmgTcMKXLHjXSRbMZMp0olVxovEgaTIAydshqJOQUXKZMctIZw==}
|
||||||
|
|
Loading…
Reference in a new issue