feat(webapp): Org level feature flags for Private Links (#3287)

This commit is contained in:
Oskar Otwinowski
2026-03-30 11:46:01 +02:00
committed by GitHub
parent 9cb3dcb07c
commit 66b70101e9
6 changed files with 53 additions and 15 deletions
@@ -9,6 +9,7 @@ import {
import { ArrowLeftIcon } from "@heroicons/react/24/solid";
import { SlackIcon } from "@trigger.dev/companyicons";
import { VercelLogo } from "~/components/integrations/VercelLogo";
import { useFeatureFlags } from "~/hooks/useFeatureFlags";
import { useFeatures } from "~/hooks/useFeatures";
import { type MatchedOrganization } from "~/hooks/useOrganizations";
import { cn } from "~/utils/cn";
@@ -48,7 +49,8 @@ export function OrganizationSettingsSideMenu({
organization: MatchedOrganization;
buildInfo: BuildInfo;
}) {
const { isManagedCloud, hasPrivateConnections } = useFeatures();
const { isManagedCloud } = useFeatures();
const featureFlags = useFeatureFlags();
const currentPlan = useCurrentPlan();
const isAdmin = useHasAdminAccess();
const showBuildInfo = isAdmin || !isManagedCloud;
@@ -105,7 +107,7 @@ export function OrganizationSettingsSideMenu({
/>
</>
)}
{hasPrivateConnections && (
{featureFlags.hasPrivateConnections && (
<SideMenuItem
name="Private Connections"
icon={LockClosedIcon}
@@ -10,6 +10,7 @@ import {
} from "./SelectBestEnvironmentPresenter.server";
import { sortEnvironments } from "~/utils/environmentSort";
import { defaultAvatar, parseAvatar } from "~/components/primitives/Avatar";
import { env } from "~/env.server";
import { flags, validatePartialFeatureFlags } from "~/v3/featureFlags.server";
export class OrganizationsPresenter {
@@ -154,8 +155,12 @@ export class OrganizationsPresenter {
},
});
// Get global feature flags (no overrides or defaults)
const globalFlags = await flags();
// Get global feature flags with env-var-based defaults
const globalFlags = await flags({
defaultValues: {
hasPrivateConnections: env.PRIVATE_CONNECTIONS_ENABLED === "1",
},
});
return orgs.map((org) => {
const orgFlagsResult = org.featureFlags
@@ -13,7 +13,7 @@ import { Header2 } from "~/components/primitives/Headers";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { prisma } from "~/db.server";
import { featuresForRequest } from "~/features.server";
import { canAccessPrivateConnections } from "~/v3/canAccessPrivateConnections.server";
import { logger } from "~/services/logger.server";
import { getPrivateLinks } from "~/services/platform.v3.server";
import { requireUserId } from "~/services/session.server";
@@ -44,8 +44,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
const { hasPrivateConnections } = featuresForRequest(request);
if (!hasPrivateConnections) {
const canAccess = await canAccessPrivateConnections({ organizationSlug, userId });
if (!canAccess) {
return redirect(organizationPath({ slug: organizationSlug }));
}
@@ -24,7 +24,7 @@ import { Paragraph } from "~/components/primitives/Paragraph";
import { Select, SelectItem } from "~/components/primitives/Select";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { featuresForRequest } from "~/features.server";
import { canAccessPrivateConnections } from "~/v3/canAccessPrivateConnections.server";
import {
redirectWithErrorMessage,
redirectWithSuccessMessage,
@@ -56,8 +56,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
const { hasPrivateConnections } = featuresForRequest(request);
if (!hasPrivateConnections) {
const canAccess = await canAccessPrivateConnections({ organizationSlug, userId });
if (!canAccess) {
return redirect(organizationPath({ slug: organizationSlug }));
}
@@ -0,0 +1,27 @@
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { FEATURE_FLAG, makeFlag } from "~/v3/featureFlags.server";
export async function canAccessPrivateConnections(options: {
organizationSlug: string;
userId: string;
}): Promise<boolean> {
const { organizationSlug, userId } = options;
const org = await prisma.organization.findFirst({
where: {
slug: organizationSlug,
members: { some: { userId } },
},
select: {
featureFlags: true,
},
});
const flag = makeFlag();
return flag({
key: FEATURE_FLAG.hasPrivateConnections,
defaultValue: env.PRIVATE_CONNECTIONS_ENABLED === "1",
overrides: (org?.featureFlags as Record<string, unknown>) ?? {},
});
}
+9 -5
View File
@@ -10,6 +10,7 @@ export const FEATURE_FLAG = {
hasAiAccess: "hasAiAccess",
hasAiModelsAccess: "hasAiModelsAccess",
hasComputeAccess: "hasComputeAccess",
hasPrivateConnections: "hasPrivateConnections",
} as const;
const FeatureFlagCatalog = {
@@ -21,6 +22,7 @@ const FeatureFlagCatalog = {
[FEATURE_FLAG.hasAiAccess]: z.coerce.boolean(),
[FEATURE_FLAG.hasAiModelsAccess]: z.coerce.boolean(),
[FEATURE_FLAG.hasComputeAccess]: z.coerce.boolean(),
[FEATURE_FLAG.hasPrivateConnections]: z.coerce.boolean(),
};
type FeatureFlagKey = keyof typeof FeatureFlagCatalog;
@@ -49,7 +51,7 @@ export function makeFlag(_prisma: PrismaClientOrTransaction = prisma) {
const flagSchema = FeatureFlagCatalog[opts.key];
if (opts.overrides?.[opts.key]) {
if (opts.overrides?.[opts.key] !== undefined) {
const parsed = flagSchema.safeParse(opts.overrides[opts.key]);
if (parsed.success) {
@@ -57,13 +59,15 @@ export function makeFlag(_prisma: PrismaClientOrTransaction = prisma) {
}
}
const parsed = flagSchema.safeParse(value?.value);
if (value !== null) {
const parsed = flagSchema.safeParse(value.value);
if (!parsed.success) {
return opts.defaultValue;
if (parsed.success) {
return parsed.data;
}
}
return parsed.data;
return opts.defaultValue;
}
return flag;