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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user