From f4db4058f87d6271550648b8e63e797e6aa7c1ea Mon Sep 17 00:00:00 2001 From: EYHN Date: Thu, 5 Sep 2024 15:11:27 +0000 Subject: [PATCH] feat(core): make permission and invoice offline available (#8123) --- .../server/src/core/permission/service.ts | 8 + .../backend/server/src/core/quota/storage.ts | 3 + .../backend/server/src/core/quota/types.ts | 3 + .../core/workspaces/resolvers/workspace.ts | 15 +- packages/backend/server/src/schema.gql | 1 + .../member-components/pagination.tsx | 7 +- .../new-workspace-setting-detail/members.tsx | 174 ++++++++---------- .../core/src/hooks/affine/use-member-count.ts | 14 -- .../core/src/hooks/affine/use-members.ts | 59 ------ .../src/modules/cloud/services/graphql.ts | 7 +- .../modules/permissions/entities/members.ts | 79 ++++++++ .../core/src/modules/permissions/index.ts | 10 +- .../modules/permissions/services/members.ts | 7 + .../src/modules/permissions/stores/members.ts | 31 ++++ .../frontend/graphql/src/graphql/index.ts | 1 + .../graphql/src/graphql/workspace-quota.gql | 1 + packages/frontend/graphql/src/schema.ts | 2 + 17 files changed, 236 insertions(+), 186 deletions(-) delete mode 100644 packages/frontend/core/src/hooks/affine/use-member-count.ts delete mode 100644 packages/frontend/core/src/hooks/affine/use-members.ts create mode 100644 packages/frontend/core/src/modules/permissions/entities/members.ts create mode 100644 packages/frontend/core/src/modules/permissions/services/members.ts create mode 100644 packages/frontend/core/src/modules/permissions/stores/members.ts diff --git a/packages/backend/server/src/core/permission/service.ts b/packages/backend/server/src/core/permission/service.ts index 27ef6e91af..65e9269499 100644 --- a/packages/backend/server/src/core/permission/service.ts +++ b/packages/backend/server/src/core/permission/service.ts @@ -75,6 +75,14 @@ export class PermissionService { return owner.user; } + async getWorkspaceMemberCount(workspaceId: string) { + return this.prisma.workspaceUserPermission.count({ + where: { + workspaceId, + }, + }); + } + async tryGetWorkspaceOwner(workspaceId: string) { return this.prisma.workspaceUserPermission.findFirst({ where: { diff --git a/packages/backend/server/src/core/quota/storage.ts b/packages/backend/server/src/core/quota/storage.ts index 18a3c9a1c6..ac77ace364 100644 --- a/packages/backend/server/src/core/quota/storage.ts +++ b/packages/backend/server/src/core/quota/storage.ts @@ -113,6 +113,8 @@ export class QuotaManagementService { // quota was apply to owner's account async getWorkspaceUsage(workspaceId: string): Promise { const owner = await this.permissions.getWorkspaceOwner(workspaceId); + const memberCount = + await this.permissions.getWorkspaceMemberCount(workspaceId); const { feature: { name, @@ -145,6 +147,7 @@ export class QuotaManagementService { humanReadable, usedSize, unlimited, + memberCount, }; if (quota.unlimited) { diff --git a/packages/backend/server/src/core/quota/types.ts b/packages/backend/server/src/core/quota/types.ts index ef21f05271..5bfff228a5 100644 --- a/packages/backend/server/src/core/quota/types.ts +++ b/packages/backend/server/src/core/quota/types.ts @@ -87,6 +87,9 @@ export class QuotaQueryType { @Field(() => SafeIntResolver) memberLimit!: number; + @Field(() => SafeIntResolver) + memberCount!: number; + @Field(() => SafeIntResolver) storageQuota!: number; diff --git a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts index 16037f03f3..bfd7e2f680 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts @@ -115,11 +115,7 @@ export class WorkspaceResolver { complexity: 2, }) memberCount(@Parent() workspace: WorkspaceType) { - return this.prisma.workspaceUserPermission.count({ - where: { - workspaceId: workspace.id, - }, - }); + return this.permissions.getWorkspaceMemberCount(workspace.id); } @ResolveField(() => Boolean, { @@ -388,13 +384,8 @@ export class WorkspaceResolver { } // member limit check - const [memberCount, quota] = await Promise.all([ - this.prisma.workspaceUserPermission.count({ - where: { workspaceId }, - }), - this.quota.getWorkspaceUsage(workspaceId), - ]); - if (memberCount >= quota.memberLimit) { + const quota = await this.quota.getWorkspaceUsage(workspaceId); + if (quota.memberCount >= quota.memberLimit) { return new MemberQuotaExceeded(); } diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 32db5c43a0..9e6c508e38 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -603,6 +603,7 @@ type QuotaQueryType { copilotActionLimit: SafeInt historyPeriod: SafeInt! humanReadable: HumanReadableQuotaType! + memberCount: SafeInt! memberLimit: SafeInt! name: String! storageQuota: SafeInt! diff --git a/packages/frontend/component/src/components/member-components/pagination.tsx b/packages/frontend/component/src/components/member-components/pagination.tsx index 136aa413bc..39f79269f1 100644 --- a/packages/frontend/component/src/components/member-components/pagination.tsx +++ b/packages/frontend/component/src/components/member-components/pagination.tsx @@ -6,19 +6,21 @@ import ReactPaginate from 'react-paginate'; import * as styles from './styles.css'; export interface PaginationProps { totalCount: number; + pageNum?: number; countPerPage: number; - onPageChange: (skip: number) => void; + onPageChange: (skip: number, pageNum: number) => void; } export const Pagination = ({ totalCount, countPerPage, + pageNum, onPageChange, }: PaginationProps) => { const handlePageClick = useCallback( (e: { selected: number }) => { const newOffset = (e.selected * countPerPage) % totalCount; - onPageChange(newOffset); + onPageChange(newOffset, e.selected); }, [countPerPage, onPageChange, totalCount] ); @@ -34,6 +36,7 @@ export const Pagination = ({ pageRangeDisplayed={3} marginPagesDisplayed={2} pageCount={pageCount} + forcePage={pageNum} previousLabel={} nextLabel={} pageClassName={styles.pageItem} diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx index a97ed00adb..5634fd4ec1 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx @@ -1,8 +1,5 @@ import { notify } from '@affine/component'; -import type { - InviteModalProps, - PaginationProps, -} from '@affine/component/member-components'; +import type { InviteModalProps } from '@affine/component/member-components'; import { InviteModal, MemberLimitModal, @@ -17,15 +14,16 @@ import { Tooltip } from '@affine/component/ui/tooltip'; import { openSettingModalAtom } from '@affine/core/atoms'; import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary'; import { useInviteMember } from '@affine/core/hooks/affine/use-invite-member'; -import { useMemberCount } from '@affine/core/hooks/affine/use-member-count'; -import type { Member } from '@affine/core/hooks/affine/use-members'; -import { useMembers } from '@affine/core/hooks/affine/use-members'; import { useRevokeMemberPermission } from '@affine/core/hooks/affine/use-revoke-member-permission'; import { track } from '@affine/core/mixpanel'; -import { WorkspacePermissionService } from '@affine/core/modules/permissions'; +import { + type Member, + WorkspaceMembersService, + WorkspacePermissionService, +} from '@affine/core/modules/permissions'; import { WorkspaceQuotaService } from '@affine/core/modules/quota'; import { WorkspaceFlavour } from '@affine/env/workspace'; -import { Permission } from '@affine/graphql'; +import { Permission, UserFriendlyError } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import { MoreVerticalIcon } from '@blocksuite/icons/rc'; import { @@ -34,18 +32,11 @@ import { useService, WorkspaceService, } from '@toeverything/infra'; +import { cssVar } from '@toeverything/theme'; import clsx from 'clsx'; import { useSetAtom } from 'jotai'; import type { ReactElement } from 'react'; -import { - Suspense, - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { type AuthAccountInfo, @@ -55,7 +46,6 @@ import { } from '../../../../../modules/cloud'; import * as style from './style.css'; -const COUNT_PER_PAGE = 8; type OnRevoke = (memberId: string) => void; const MembersPanelLocal = () => { const t = useI18n(); @@ -76,7 +66,6 @@ export const CloudWorkspaceMembersPanel = () => { serverConfig.features$.map(f => f?.payment) ); const workspace = useService(WorkspaceService).workspace; - const memberCount = useMemberCount(workspace.id); const permissionService = useService(WorkspacePermissionService); const isOwner = useLiveData(permissionService.permission.isOwner$); @@ -84,42 +73,32 @@ export const CloudWorkspaceMembersPanel = () => { permissionService.permission.revalidate(); }, [permissionService]); - const checkMemberCountLimit = useCallback( - (memberCount: number, memberLimit?: number) => { - if (memberLimit === undefined) return false; - return memberCount >= memberLimit; - }, - [] - ); - const workspaceQuotaService = useService(WorkspaceQuotaService); useEffect(() => { workspaceQuotaService.quota.revalidate(); }, [workspaceQuotaService]); + const isLoading = useLiveData(workspaceQuotaService.quota.isLoading$); + const error = useLiveData(workspaceQuotaService.quota.error$); const workspaceQuota = useLiveData(workspaceQuotaService.quota.quota$); const subscriptionService = useService(SubscriptionService); const plan = useLiveData( subscriptionService.subscription.pro$.map(s => s?.plan) ); - const isLimited = workspaceQuota - ? checkMemberCountLimit(memberCount, workspaceQuota.memberLimit) - : null; + const isLimited = + workspaceQuota && workspaceQuota.memberLimit + ? workspaceQuota.memberCount >= workspaceQuota.memberLimit + : null; const t = useI18n(); const { invite, isMutating } = useInviteMember(workspace.id); const revokeMemberPermission = useRevokeMemberPermission(workspace.id); const [open, setOpen] = useState(false); - const [memberSkip, setMemberSkip] = useState(0); const openModal = useCallback(() => { setOpen(true); }, []); - const onPageChange = useCallback(offset => { - setMemberSkip(offset); - }, []); - const onInviteConfirm = useCallback( async ({ email, permission }) => { const success = await invite( @@ -151,20 +130,6 @@ export const CloudWorkspaceMembersPanel = () => { }); }, [setSettingModalAtom]); - const listContainerRef = useRef(null); - const [memberListHeight, setMemberListHeight] = useState(null); - - useLayoutEffect(() => { - if ( - memberCount > COUNT_PER_PAGE && - listContainerRef.current && - memberListHeight === null - ) { - const rect = listContainerRef.current.getBoundingClientRect(); - setMemberListHeight(rect.height); - } - }, [listContainerRef, memberCount, memberListHeight]); - const onRevoke = useCallback( async memberId => { const res = await revokeMemberPermission(memberId); @@ -195,14 +160,23 @@ export const CloudWorkspaceMembersPanel = () => { }, [handleUpgradeConfirm, hasPaymentFeature, t, workspaceQuota]); if (workspaceQuota === null) { - // TODO(@eyhn): loading ui - return null; + if (isLoading) { + return ; + } + if (error) { + return ( + + {UserFriendlyError.fromAnyError(error).message} + + ); + } + return; // never reach here } return ( <> @@ -230,27 +204,8 @@ export const CloudWorkspaceMembersPanel = () => { ) : null} -
- }> - - - - {memberCount > COUNT_PER_PAGE && ( - - )} +
+
); @@ -271,12 +226,12 @@ export const MembersPanelFallback = () => { ); }; -const MemberListFallback = ({ memberCount }: { memberCount: number }) => { +const MemberListFallback = ({ memberCount }: { memberCount?: number }) => { // prevent page jitter const height = useMemo(() => { - if (memberCount > COUNT_PER_PAGE) { + if (memberCount) { // height and margin-bottom - return COUNT_PER_PAGE * 58 + (COUNT_PER_PAGE - 1) * 6; + return memberCount * 58 + (memberCount - 1) * 6; } return 'auto'; }, [memberCount]); @@ -296,31 +251,66 @@ const MemberListFallback = ({ memberCount }: { memberCount: number }) => { }; const MemberList = ({ - workspaceId, isOwner, - skip, onRevoke, }: { - workspaceId: string; isOwner: boolean; - skip: number; onRevoke: OnRevoke; }) => { - const members = useMembers(workspaceId, skip, COUNT_PER_PAGE); + const membersService = useService(WorkspaceMembersService); + const memberCount = useLiveData(membersService.members.memberCount$); + const pageNum = useLiveData(membersService.members.pageNum$); + const isLoading = useLiveData(membersService.members.isLoading$); + const pageMembers = useLiveData(membersService.members.pageMembers$); + + useEffect(() => { + membersService.members.revalidate(); + }, [membersService]); + const session = useService(AuthService).session; const account = useEnsureLiveData(session.account$); + const handlePageChange = useCallback( + (_: number, pageNum: number) => { + membersService.members.setPageNum(pageNum); + membersService.members.revalidate(); + }, + [membersService] + ); + return (
- {members.map(member => ( - - ))} + ) : ( + pageMembers?.map(member => ( + + )) + )} + {memberCount !== undefined && + memberCount > membersService.members.PAGE_SIZE && ( + + )}
); }; @@ -409,9 +399,7 @@ export const MembersPanel = (): ReactElement | null => { } return ( - }> - - + ); }; diff --git a/packages/frontend/core/src/hooks/affine/use-member-count.ts b/packages/frontend/core/src/hooks/affine/use-member-count.ts deleted file mode 100644 index 0bfb47d8f2..0000000000 --- a/packages/frontend/core/src/hooks/affine/use-member-count.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getMemberCountByWorkspaceIdQuery } from '@affine/graphql'; - -import { useQuery } from '../use-query'; - -export function useMemberCount(workspaceId: string) { - const { data } = useQuery({ - query: getMemberCountByWorkspaceIdQuery, - variables: { - workspaceId, - }, - }); - - return data.workspace.memberCount; -} diff --git a/packages/frontend/core/src/hooks/affine/use-members.ts b/packages/frontend/core/src/hooks/affine/use-members.ts deleted file mode 100644 index 97238a0a94..0000000000 --- a/packages/frontend/core/src/hooks/affine/use-members.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { GetMembersByWorkspaceIdQuery } from '@affine/graphql'; -import { getMembersByWorkspaceIdQuery, Permission } from '@affine/graphql'; -import { useMemo } from 'react'; - -import { useQuery } from '../use-query'; - -export function calculateWeight(member: Member) { - const permissionWeight = { - [Permission.Owner]: 4, - [Permission.Admin]: 3, - [Permission.Write]: 2, - [Permission.Read]: 1, - }; - - const factors = [ - Number(member.permission === Permission.Owner), // Owner weight is the highest - Number(!member.accepted), // Unaccepted members are before accepted members - permissionWeight[member.permission] || 0, - ]; - - return factors.reduce((ret, factor, index, arr) => { - return ret + factor * Math.pow(10, arr.length - 1 - index); - }, 0); -} - -export type Member = Omit< - GetMembersByWorkspaceIdQuery['workspace']['members'][number], - '__typename' ->; -export function useMembers( - workspaceId: string, - skip: number, - take: number = 8 -) { - const { data } = useQuery({ - query: getMembersByWorkspaceIdQuery, - variables: { - workspaceId, - skip, - take, - }, - }); - - const members = data.workspace.members; - - return useMemo(() => { - // sort members by weight - return members.sort((a, b) => { - const weightDifference = calculateWeight(b) - calculateWeight(a); - if (weightDifference !== 0) { - return weightDifference; - } - // if weight is the same, sort by name - if (a.name === null) return 1; - if (b.name === null) return -1; - return a.name.localeCompare(b.name); - }); - }, [members]); -} diff --git a/packages/frontend/core/src/modules/cloud/services/graphql.ts b/packages/frontend/core/src/modules/cloud/services/graphql.ts index 4ac2faf3b8..16b68259d4 100644 --- a/packages/frontend/core/src/modules/cloud/services/graphql.ts +++ b/packages/frontend/core/src/modules/cloud/services/graphql.ts @@ -3,7 +3,6 @@ import { type GraphQLQuery, type QueryOptions, type QueryResponse, - UserFriendlyError, } from '@affine/graphql'; import { fromPromise, Service } from '@toeverything/infra'; import type { Observable } from 'rxjs'; @@ -39,13 +38,11 @@ export class GraphQLService extends Service { try { return await this.rawGql(options); } catch (err) { - const standardError = UserFriendlyError.fromAnyError(err); - - if (standardError.status === 403) { + if (err instanceof BackendError && err.status === 403) { this.framework.get(AuthService).session.revalidate(); } - throw new BackendError(standardError); + throw err; } }; } diff --git a/packages/frontend/core/src/modules/permissions/entities/members.ts b/packages/frontend/core/src/modules/permissions/entities/members.ts new file mode 100644 index 0000000000..5064fe38e1 --- /dev/null +++ b/packages/frontend/core/src/modules/permissions/entities/members.ts @@ -0,0 +1,79 @@ +import type { GetMembersByWorkspaceIdQuery } from '@affine/graphql'; +import type { WorkspaceService } from '@toeverything/infra'; +import { + backoffRetry, + catchErrorInto, + effect, + Entity, + fromPromise, + LiveData, + onComplete, + onStart, +} from '@toeverything/infra'; +import { distinctUntilChanged, EMPTY, map, mergeMap, switchMap } from 'rxjs'; + +import { isBackendError, isNetworkError } from '../../cloud'; +import type { WorkspaceMembersStore } from '../stores/members'; + +export type Member = + GetMembersByWorkspaceIdQuery['workspace']['members'][number]; + +export class WorkspaceMembers extends Entity { + constructor( + private readonly store: WorkspaceMembersStore, + private readonly workspaceService: WorkspaceService + ) { + super(); + } + + pageNum$ = new LiveData(0); + memberCount$ = new LiveData(undefined); + pageMembers$ = new LiveData(undefined); + + isLoading$ = new LiveData(false); + error$ = new LiveData(null); + + readonly PAGE_SIZE = 8; + + readonly revalidate = effect( + map(() => this.pageNum$.value), + distinctUntilChanged(), + switchMap(pageNum => { + return fromPromise(async signal => { + return this.store.fetchMembers( + this.workspaceService.workspace.id, + pageNum * this.PAGE_SIZE, + this.PAGE_SIZE, + signal + ); + }).pipe( + mergeMap(data => { + this.memberCount$.setValue(data.memberCount); + this.pageMembers$.setValue(data.members); + return EMPTY; + }), + backoffRetry({ + when: isNetworkError, + count: Infinity, + }), + backoffRetry({ + when: isBackendError, + }), + catchErrorInto(this.error$), + onStart(() => { + this.pageMembers$.setValue(undefined); + this.isLoading$.setValue(true); + }), + onComplete(() => this.isLoading$.setValue(false)) + ); + }) + ); + + setPageNum(pageNum: number) { + this.pageNum$.setValue(pageNum); + } + + override dispose(): void { + this.revalidate.unsubscribe(); + } +} diff --git a/packages/frontend/core/src/modules/permissions/index.ts b/packages/frontend/core/src/modules/permissions/index.ts index 667ffc3fcd..17b5b949d3 100644 --- a/packages/frontend/core/src/modules/permissions/index.ts +++ b/packages/frontend/core/src/modules/permissions/index.ts @@ -1,3 +1,5 @@ +export type { Member } from './entities/members'; +export { WorkspaceMembersService } from './services/members'; export { WorkspacePermissionService } from './services/permission'; import { GraphQLService } from '@affine/core/modules/cloud'; @@ -8,8 +10,11 @@ import { WorkspacesService, } from '@toeverything/infra'; +import { WorkspaceMembers } from './entities/members'; import { WorkspacePermission } from './entities/permission'; +import { WorkspaceMembersService } from './services/members'; import { WorkspacePermissionService } from './services/permission'; +import { WorkspaceMembersStore } from './stores/members'; import { WorkspacePermissionStore } from './stores/permission'; export function configurePermissionsModule(framework: Framework) { @@ -21,5 +26,8 @@ export function configurePermissionsModule(framework: Framework) { WorkspacePermissionStore, ]) .store(WorkspacePermissionStore, [GraphQLService]) - .entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore]); + .entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore]) + .service(WorkspaceMembersService) + .store(WorkspaceMembersStore, [GraphQLService]) + .entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService]); } diff --git a/packages/frontend/core/src/modules/permissions/services/members.ts b/packages/frontend/core/src/modules/permissions/services/members.ts new file mode 100644 index 0000000000..1e53a9cb36 --- /dev/null +++ b/packages/frontend/core/src/modules/permissions/services/members.ts @@ -0,0 +1,7 @@ +import { Service } from '@toeverything/infra'; + +import { WorkspaceMembers } from '../entities/members'; + +export class WorkspaceMembersService extends Service { + members = this.framework.createEntity(WorkspaceMembers); +} diff --git a/packages/frontend/core/src/modules/permissions/stores/members.ts b/packages/frontend/core/src/modules/permissions/stores/members.ts new file mode 100644 index 0000000000..37bf2b8144 --- /dev/null +++ b/packages/frontend/core/src/modules/permissions/stores/members.ts @@ -0,0 +1,31 @@ +import { getMembersByWorkspaceIdQuery } from '@affine/graphql'; +import { Store } from '@toeverything/infra'; + +import type { GraphQLService } from '../../cloud'; + +export class WorkspaceMembersStore extends Store { + constructor(private readonly graphqlService: GraphQLService) { + super(); + } + + async fetchMembers( + workspaceId: string, + skip: number, + take: number, + signal?: AbortSignal + ) { + const data = await this.graphqlService.gql({ + query: getMembersByWorkspaceIdQuery, + variables: { + workspaceId, + skip, + take, + }, + context: { + signal, + }, + }); + + return data.workspace; + } +} diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index ef12a94e5d..43206d73bd 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -1297,6 +1297,7 @@ query workspaceQuota($id: String!) { storageQuota historyPeriod memberLimit + memberCount humanReadable { name blobLimit diff --git a/packages/frontend/graphql/src/graphql/workspace-quota.gql b/packages/frontend/graphql/src/graphql/workspace-quota.gql index 5ce5e63195..176e1fc7b5 100644 --- a/packages/frontend/graphql/src/graphql/workspace-quota.gql +++ b/packages/frontend/graphql/src/graphql/workspace-quota.gql @@ -6,6 +6,7 @@ query workspaceQuota($id: String!) { storageQuota historyPeriod memberLimit + memberCount humanReadable { name blobLimit diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index d0dea28ef0..0ffe888349 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -901,6 +901,7 @@ export interface QuotaQueryType { copilotActionLimit: Maybe; historyPeriod: Scalars['SafeInt']['output']; humanReadable: HumanReadableQuotaType; + memberCount: Scalars['SafeInt']['output']; memberLimit: Scalars['SafeInt']['output']; name: Scalars['String']['output']; storageQuota: Scalars['SafeInt']['output']; @@ -2423,6 +2424,7 @@ export type WorkspaceQuotaQuery = { storageQuota: number; historyPeriod: number; memberLimit: number; + memberCount: number; usedSize: number; humanReadable: { __typename?: 'HumanReadableQuotaType';