perf: Server Fetching for platform members and org members pages (#21342)

* remove members page

* refactor to server fetch

* same for /members

* use server fetches for all queires

* fix

* add skeletons

* refactor

* fix loading page

* fix

* fix type checks

* fix details

* refactor platform members page
This commit is contained in:
Benny Joo
2025-05-20 16:36:34 -04:00
committed by GitHub
parent 6258ca14d0
commit bb98143e2e
14 changed files with 231 additions and 130 deletions
@@ -0,0 +1,19 @@
import { getTranslate } from "app/_utils";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import Shell from "@calcom/features/shell/Shell";
export default async function Layout({ children }: { children: React.ReactNode }) {
const t = await getTranslate();
return (
<Shell
withoutMain={false}
heading={t("organization_members")}
subtitle={t("organization_description")}
headerClassName="hidden md:block"
actions={<div className={`mb-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
{children}
</Shell>
);
}
@@ -0,0 +1,5 @@
import { UserListTableSkeleton } from "@calcom/features/users/components/UserTable/UserListTableSkeleton";
export default function Loading() {
return <UserListTableSkeleton />;
}
@@ -1,6 +1,10 @@
import { createRouterCaller } from "app/_trpc/context";
import { _generateMetadata } from "app/_utils";
import MembersPage from "~/members/members-view";
import { attributesRouter } from "@calcom/trpc/server/routers/viewer/attributes/_router";
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
import { MembersView } from "~/members/members-view";
export const generateMetadata = async ({ params }: { params: Promise<{ id: string }> }) =>
await _generateMetadata(
@@ -11,8 +15,20 @@ export const generateMetadata = async ({ params }: { params: Promise<{ id: strin
`/settings/organizations/${(await params).id}/members`
);
const ServerPageWrapper = () => {
return <MembersPage />;
const ServerPageWrapper = async () => {
const [orgCaller, attributesCaller] = await Promise.all([
createRouterCaller(viewerOrganizationsRouter),
createRouterCaller(attributesRouter),
]);
const [org, teams, facetedTeamValues, attributes] = await Promise.all([
orgCaller.listCurrent(),
orgCaller.getTeams(),
orgCaller.getFacetedValues(),
attributesCaller.list(),
]);
return (
<MembersView org={org} teams={teams} facetedTeamValues={facetedTeamValues} attributes={attributes} />
);
};
export default ServerPageWrapper;
@@ -0,0 +1,20 @@
import SettingsLayoutAppDirClient from "app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient";
import { getTranslate } from "app/_utils";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
export default async function Layout({ children }: { children: React.ReactNode }) {
const t = await getTranslate();
return (
<SettingsLayoutAppDirClient containerClassName="lg:max-w-screen-2xl">
<SettingsHeader
title={t("organization_members")}
description={t("organization_description")}
ctaClassName={CTA_CONTAINER_CLASS_NAME}>
{children}
</SettingsHeader>
</SettingsLayoutAppDirClient>
);
}
@@ -0,0 +1,5 @@
import { UserListTableSkeleton } from "@calcom/features/users/components/UserTable/UserListTableSkeleton";
export default function Loading() {
return <UserListTableSkeleton />;
}
@@ -1,9 +1,10 @@
import SettingsLayoutAppDir from "app/(use-page-wrapper)/settings/(settings-layout)/layout";
import { _generateMetadata, getTranslate } from "app/_utils";
import { createRouterCaller } from "app/_trpc/context";
import { _generateMetadata } from "app/_utils";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import LegacyPage from "@calcom/features/ee/organizations/pages/members";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { attributesRouter } from "@calcom/trpc/server/routers/viewer/attributes/_router";
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
import { MembersView } from "~/members/members-view";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -15,17 +16,20 @@ export const generateMetadata = async () =>
);
const Page = async () => {
const t = await getTranslate();
const [orgCaller, attributesCaller] = await Promise.all([
createRouterCaller(viewerOrganizationsRouter),
createRouterCaller(attributesRouter),
]);
const [org, teams, facetedTeamValues, attributes] = await Promise.all([
orgCaller.listCurrent(),
orgCaller.getTeams(),
orgCaller.getFacetedValues(),
attributesCaller.list(),
]);
const children = (
<SettingsHeader
title={t("organization_members")}
description={t("organization_description")}
ctaClassName={CTA_CONTAINER_CLASS_NAME}>
<LegacyPage />
</SettingsHeader>
return (
<MembersView org={org} teams={teams} facetedTeamValues={facetedTeamValues} attributes={attributes} />
);
return await SettingsLayoutAppDir({ children, containerClassName: "lg:max-w-screen-2xl" });
};
export default Page;
@@ -0,0 +1,33 @@
import { getTranslate } from "app/_utils";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import Shell from "@calcom/features/shell/Shell";
import { Button } from "@calcom/ui/components/button";
export default async function Layout({ children }: { children: React.ReactNode }) {
const t = await getTranslate();
return (
<Shell
heading={
<div className="flex">
<h1>Member management</h1>
<Button
tooltip="Only teammates invited as admins can create OAuth clients while teammates invited as members have read only access"
tooltipSide="right"
className="mx-2 hover:bg-transparent"
color="minimal"
variant="icon"
StartIcon="info"
/>
</div>
}
title={t("platform_members")}
subtitle={t("platform_members_description")}
withoutMain={false}
isPlatformUser={true}
actions={<div className={CTA_CONTAINER_CLASS_NAME} />}>
{children}
</Shell>
);
}
@@ -0,0 +1,5 @@
import { UserListTableSkeleton } from "@calcom/features/users/components/UserTable/UserListTableSkeleton";
export default function Loading() {
return <UserListTableSkeleton />;
}
@@ -1,6 +1,9 @@
import { createRouterCaller } from "app/_trpc/context";
import { _generateMetadata } from "app/_utils";
import PlatformMembersView from "@calcom/features/ee/platform/pages/settings/members";
import { attributesRouter } from "@calcom/trpc/server/routers/viewer/attributes/_router";
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
export const generateMetadata = async () =>
await _generateMetadata(
@@ -11,8 +14,26 @@ export const generateMetadata = async () =>
"/settings/platform/members"
);
const ServerPageWrapper = () => {
return <PlatformMembersView />;
const ServerPageWrapper = async () => {
const [orgCaller, attributesCaller] = await Promise.all([
createRouterCaller(viewerOrganizationsRouter),
createRouterCaller(attributesRouter),
]);
const [org, teams, facetedTeamValues, attributes] = await Promise.all([
orgCaller.listCurrent(),
orgCaller.getTeams(),
orgCaller.getFacetedValues(),
attributesCaller.list(),
]);
return (
<PlatformMembersView
org={org}
teams={teams}
facetedTeamValues={facetedTeamValues}
attributes={attributes}
/>
);
};
export default ServerPageWrapper;
+17 -16
View File
@@ -1,24 +1,25 @@
"use client";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import MembersView from "@calcom/features/ee/organizations/pages/members";
import Shell from "@calcom/features/shell/Shell";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { UserListTable } from "@calcom/features/users/components/UserTable/UserListTable";
import type { UserListTableProps } from "@calcom/features/users/components/UserTable/UserListTable";
import { useLocale } from "@calcom/lib/hooks/useLocale";
const MembersPage: React.FC = () => {
export const MembersView = (props: UserListTableProps) => {
const { t } = useLocale();
const isOrgAdminOrOwner = props.org && checkAdminOrOwner(props.org.user.role);
const canLoggedInUserSeeMembers =
(props.org?.isPrivate && isOrgAdminOrOwner) || isOrgAdminOrOwner || !props.org?.isPrivate;
return (
<Shell
withoutMain={false}
title={t("organization_members")}
description={t("organization_description")}
heading={t("organization_members")}
subtitle={t("organization_description")}
headerClassName="hidden md:block"
actions={<div className={`mb-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
<MembersView />
</Shell>
<LicenseRequired>
<div>{canLoggedInUserSeeMembers && <UserListTable {...props} />}</div>
{!canLoggedInUserSeeMembers && (
<div className="border-subtle rounded-xl border p-6" data-testid="members-privacy-warning">
<h2 className="text-default">{t("only_admin_can_see_members_of_org")}</h2>
</div>
)}
</LicenseRequired>
);
};
export default MembersPage;
@@ -1,30 +0,0 @@
"use client";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { UserListTable } from "@calcom/features/users/components/UserTable/UserListTable";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
const MembersView = () => {
const { t } = useLocale();
const { data: currentOrg, isPending } = trpc.viewer.organizations.listCurrent.useQuery();
const isOrgAdminOrOwner = currentOrg && checkAdminOrOwner(currentOrg.user.role);
const canLoggedInUserSeeMembers =
(currentOrg?.isPrivate && isOrgAdminOrOwner) || isOrgAdminOrOwner || !currentOrg?.isPrivate;
return (
<LicenseRequired>
<div>{!isPending && canLoggedInUserSeeMembers && <UserListTable />}</div>
{!canLoggedInUserSeeMembers && (
<div className="border-subtle rounded-xl border p-6" data-testid="members-privacy-warning">
<h2 className="text-default">{t("only_admin_can_see_members_of_org")}</h2>
</div>
)}
</LicenseRequired>
);
};
export default MembersView;
@@ -1,29 +1,24 @@
"use client";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import Shell from "@calcom/features/shell/Shell";
import { UserListTable } from "@calcom/features/users/components/UserTable/UserListTable";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import type { UserListTableProps } from "@calcom/features/users/components/UserTable/UserListTable";
import { UserListTableSkeleton } from "@calcom/features/users/components/UserTable/UserListTableSkeleton";
import NoPlatformPlan from "@calcom/web/components/settings/platform/dashboard/NoPlatformPlan";
import { useGetUserAttributes } from "@calcom/web/components/settings/platform/hooks/useGetUserAttributes";
import { PlatformPricing } from "@calcom/web/components/settings/platform/pricing/platform-pricing/index";
const PlatformMembersView = () => {
const { t } = useLocale();
const PlatformMembersView = (props: UserListTableProps) => {
const { isUserLoading, isUserBillingDataLoading, isPlatformUser, isPaidUser, userBillingData, userOrgId } =
useGetUserAttributes();
const { data: currentOrg, isPending } = trpc.viewer.organizations.listCurrent.useQuery();
const currentOrg = props.org;
const isOrgAdminOrOwner = currentOrg && checkAdminOrOwner(currentOrg.user.role);
const canLoggedInUserSeeMembers =
(currentOrg?.isPrivate && isOrgAdminOrOwner) || isOrgAdminOrOwner || !currentOrg?.isPrivate;
if (isUserLoading || (isUserBillingDataLoading && !userBillingData)) {
return <div className="m-5">Loading...</div>;
return <UserListTableSkeleton />;
}
if (isPlatformUser && !isPaidUser)
@@ -38,37 +33,10 @@ const PlatformMembersView = () => {
/>
);
if (!isPlatformUser)
return (
<div>
<Shell isPlatformUser={true} withoutMain={false} SidebarContainer={<></>}>
<NoPlatformPlan />
</Shell>
</div>
);
return (
<Shell
heading={
<div className="flex">
<h1>Member management</h1>
<Button
tooltip="Only teammates invited as admins can create OAuth clients while teammates invited as members have read only access"
tooltipSide="right"
className="mx-2 hover:bg-transparent"
color="minimal"
variant="icon"
StartIcon="info"
/>
</div>
}
title={t("platform_members")}
subtitle={t("platform_members_description")}
withoutMain={false}
isPlatformUser={true}
actions={<div className={CTA_CONTAINER_CLASS_NAME} />}>
<div>{!isPending && canLoggedInUserSeeMembers && <UserListTable />}</div>
</Shell>
return !isPlatformUser ? (
<div>{canLoggedInUserSeeMembers && <UserListTable {...props} />}</div>
) : (
<NoPlatformPlan />
);
};
@@ -31,6 +31,7 @@ import {
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import type { RouterOutputs } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Avatar } from "@calcom/ui/components/avatar";
import { Badge } from "@calcom/ui/components/badge";
@@ -105,15 +106,22 @@ function reducer(state: UserTableState, action: UserTableAction): UserTableState
}
}
export function UserListTable() {
export type UserListTableProps = {
org: RouterOutputs["viewer"]["organizations"]["listCurrent"];
teams: RouterOutputs["viewer"]["organizations"]["getTeams"];
facetedTeamValues: RouterOutputs["viewer"]["organizations"]["getFacetedValues"];
attributes: RouterOutputs["viewer"]["attributes"]["list"];
};
export function UserListTable(props: UserListTableProps) {
return (
<DataTableProvider useSegments={useSegments} defaultPageSize={25}>
<UserListTableContent />
<UserListTableContent {...props} />
</DataTableProvider>
);
}
function UserListTableContent() {
function UserListTableContent({ org, attributes, teams, facetedTeamValues }: UserListTableProps) {
const [dynamicLinkVisible, setDynamicLinkVisible] = useQueryState("dynamicLink", parseAsBoolean);
const orgBranding = useOrgBranding();
const domain = orgBranding?.fullDomain ?? WEBAPP_URL;
@@ -121,22 +129,6 @@ function UserListTableContent() {
const { data: session } = useSession();
const { isPlatformUser } = useGetUserAttributes();
const { data: org } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const { data: attributes, isSuccess: isSuccessAttributes } = trpc.viewer.attributes.list.useQuery(
undefined,
{
refetchOnWindowFocus: false,
}
);
const { data: teams } = trpc.viewer.organizations.getTeams.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const { data: facetedTeamValues } = trpc.viewer.organizations.getFacetedValues.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const [state, dispatch] = useReducer(reducer, initialState);
const [isDownloading, setIsDownloading] = useState(false);
const [rowSelection, setRowSelection] = useState({});
@@ -561,11 +553,6 @@ function UserListTableContent() {
}
};
if (!isPlatformUser && !isSuccessAttributes) {
// do not render the table until the attributes are fetched
return null;
}
return (
<>
<DataTableWrapper<UserTableUser>
@@ -0,0 +1,47 @@
"use client";
import { DataTableSkeleton } from "@calcom/features/data-table";
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
export function UserListTableSkeleton() {
return (
<SkeletonContainer>
{/* Toolbar with search, filters, etc. */}
<div className="grid w-full items-center gap-2 pb-4">
<div className="flex w-full flex-col gap-2">
<div className="flex w-full flex-wrap justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
{/* Search bar */}
<div className="border-default bg-default flex h-9 w-52 items-center gap-1 rounded-md border px-3 py-2">
<div className="flex items-center justify-center">
<SkeletonText className="h-4 w-4" />
</div>
<SkeletonText className="h-4 w-40" />
</div>
{/* Filters */}
<SkeletonButton className="h-9 w-28" />
</div>
<div className="flex flex-wrap items-center gap-2">
<SkeletonButton className="h-9 w-20" />
<SkeletonButton className="h-9 w-24" />
</div>
</div>
<div>
<SkeletonButton className="h-9 w-24" />
</div>
</div>
</div>
{/* DataTable */}
<DataTableSkeleton columns={7} rows={10} columnWidths={[30, 200, 100, 140, 120, 120, 80]} />
{/* CTA actions at the bottom */}
<div className={`mt-4 flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`}>
<SkeletonButton className="h-9 w-32" />
<SkeletonButton className="h-9 w-24" />
</div>
</SkeletonContainer>
);
}