perf: use app dir to fetch team feature flags in settings (#21433)

* use app dir to fetch team feature flags

* guard against type error if teamId does not exist in teamfeatures

* remove redundant router
This commit is contained in:
sean-brydon
2025-05-21 12:41:45 +00:00
committed by GitHub
parent 4ff366cd99
commit 1dca160db0
5 changed files with 79 additions and 19 deletions
@@ -10,6 +10,8 @@ import React, { useEffect, useState, useMemo } from "react";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import type { OrganizationBranding } from "@calcom/features/ee/organizations/context/provider";
import type { AppFlags } from "@calcom/features/flags/config";
import { useIsFeatureEnabledForTeam } from "@calcom/features/flags/hooks/useIsFeatureEnabledForTeam";
import Shell from "@calcom/features/shell/Shell";
import { HOSTED_CAL_FEATURES, IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
@@ -27,6 +29,8 @@ import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
import { VerticalTabItem } from "@calcom/ui/components/navigation";
import { Skeleton } from "@calcom/ui/components/skeleton";
type TeamFeatures = Record<number, Record<keyof AppFlags, boolean>> | null;
const getTabs = (orgBranding: OrganizationBranding | null) => {
const tabs: VerticalTabItemProps[] = [
{
@@ -272,6 +276,7 @@ interface SettingsSidebarContainerProps {
className?: string;
navigationIsOpenedOnMobile?: boolean;
bannersHeight?: number;
teamFeatures?: TeamFeatures;
}
const TeamListCollapsible = () => {
@@ -438,6 +443,7 @@ const SettingsSidebarContainer = ({
className = "",
navigationIsOpenedOnMobile,
bannersHeight,
teamFeatures,
}: SettingsSidebarContainerProps) => {
const searchParams = useCompatSearchParams();
const orgBranding = useOrgBranding();
@@ -449,11 +455,15 @@ const SettingsSidebarContainer = ({
}[]
>();
const session = useSession();
const { data } = trpc.viewer.delegationCredential.check.useQuery(undefined, {
enabled: !!session.data?.user?.org,
const isDelegationCredentialEnabled = useIsFeatureEnabledForTeam({
teamFeatures,
teamId: session.data?.user?.org?.id,
feature: "delegation-credential",
});
const tabsWithPermissions = useTabs({
isDelegationCredentialEnabled: data?.hasDelegationCredential ?? false,
isDelegationCredentialEnabled,
});
const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(undefined, {
@@ -727,9 +737,10 @@ const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) =>
export type SettingsLayoutProps = {
children: React.ReactNode;
containerClassName?: string;
teamFeatures?: TeamFeatures;
} & ComponentProps<typeof Shell>;
export default function SettingsLayoutAppDirClient({ children, ...rest }: SettingsLayoutProps) {
export default function SettingsLayoutAppDirClient({ children, teamFeatures, ...rest }: SettingsLayoutProps) {
const pathname = usePathname();
const state = useState(false);
const [sideContainerOpen, setSideContainerOpen] = state;
@@ -761,6 +772,7 @@ export default function SettingsLayoutAppDirClient({ children, ...rest }: Settin
<SidebarContainerElement
sideContainerOpen={sideContainerOpen}
setSideContainerOpen={setSideContainerOpen}
teamFeatures={teamFeatures}
/>
}
drawerState={state}
@@ -778,10 +790,18 @@ export default function SettingsLayoutAppDirClient({ children, ...rest }: Settin
);
}
type SidebarContainerElementProps = {
sideContainerOpen: boolean;
bannersHeight?: number;
setSideContainerOpen: React.Dispatch<React.SetStateAction<boolean>>;
teamFeatures?: TeamFeatures;
};
const SidebarContainerElement = ({
sideContainerOpen,
bannersHeight,
setSideContainerOpen,
teamFeatures,
}: SidebarContainerElementProps) => {
const { t } = useLocale();
return (
@@ -797,13 +817,8 @@ const SidebarContainerElement = ({
<SettingsSidebarContainer
navigationIsOpenedOnMobile={sideContainerOpen}
bannersHeight={bannersHeight}
teamFeatures={teamFeatures}
/>
</>
);
};
type SidebarContainerElementProps = {
sideContainerOpen: boolean;
bannersHeight?: number;
setSideContainerOpen: React.Dispatch<React.SetStateAction<boolean>>;
};
@@ -3,6 +3,8 @@ import { redirect } from "next/navigation";
import React from "react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { AppFlags } from "@calcom/features/flags/config";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -16,9 +18,22 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
return redirect("/auth/login");
}
let teamFeatures: Record<number, Record<keyof AppFlags, boolean>> | null = null;
// For now we only grab organization features but it would be nice to fetch these on the server side for specific team feature flags
if (session?.user.org) {
const featuresRepository = new FeaturesRepository();
const features = await featuresRepository.getTeamFeatures(session.user.org.id);
if (features) {
teamFeatures = {
[session.user.org.id]: features,
};
}
}
return (
<>
<SettingsLayoutAppDirClient {...props} />
<SettingsLayoutAppDirClient {...props} teamFeatures={teamFeatures} />
</>
);
}
@@ -1,6 +1,7 @@
import { Prisma } from "@prisma/client";
import { captureException } from "@sentry/nextjs";
import kysely from "@calcom/kysely";
import db from "@calcom/prisma";
import type { AppFlags } from "./config";
@@ -58,6 +59,27 @@ export class FeaturesRepository implements IFeaturesRepository {
}, {} as AppFlags);
}
/**
* Gets all features enabled for a specific team in a map format.
* @param teamId - The ID of the team to get features for
* @returns Promise<{ [slug: string]: boolean } | null>
*/
public async getTeamFeatures(teamId: number) {
const result = await kysely
.selectFrom("TeamFeatures")
.innerJoin("Feature", "Feature.slug", "TeamFeatures.featureId")
.select(["Feature.slug", "Feature.enabled"])
.where("TeamFeatures.teamId", "=", teamId)
.execute();
if (!result.length) return null;
return result.reduce<Record<keyof AppFlags, boolean>>((acc, feature) => {
acc[feature.slug as keyof AppFlags] = true;
return acc;
}, {} as Record<keyof AppFlags, boolean>);
}
/**
* Checks if a feature is enabled globally in the application.
* @param slug - The feature flag identifier to check
@@ -0,0 +1,16 @@
import type { AppFlags } from "../config";
type TeamFeatures = Record<number, Record<keyof AppFlags, boolean>> | null;
export const useIsFeatureEnabledForTeam = ({
teamFeatures,
teamId,
feature,
}: {
teamFeatures?: TeamFeatures;
teamId?: number;
feature: keyof AppFlags;
}) => {
if (!teamId || !teamFeatures?.[teamId]) return false;
return teamFeatures[teamId][feature];
};
@@ -44,14 +44,6 @@ const checkDelegationCredentialFeature = async ({
};
export const delegationCredentialRouter = router({
check: authedOrgAdminProcedure.query(async (opts) => {
return await checkDelegationCredentialFeature({
ctx: opts.ctx,
next: async () => ({
hasDelegationCredential: true,
}),
});
}),
list: authedOrgAdminProcedure.use(checkDelegationCredentialFeature).query(async (opts) => {
const handler = await import("./list.handler").then((mod) => mod.default);
return handler(opts);