Skip to content

Commit cdb1ba7

Browse files
committed
feat(webapp): Org level feature flags for Private Links
1 parent 2366b21 commit cdb1ba7

File tree

6 files changed

+53
-15
lines changed

6 files changed

+53
-15
lines changed

apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { ArrowLeftIcon } from "@heroicons/react/24/solid";
1010
import { SlackIcon } from "@trigger.dev/companyicons";
1111
import { VercelLogo } from "~/components/integrations/VercelLogo";
12+
import { useFeatureFlags } from "~/hooks/useFeatureFlags";
1213
import { useFeatures } from "~/hooks/useFeatures";
1314
import { type MatchedOrganization } from "~/hooks/useOrganizations";
1415
import { cn } from "~/utils/cn";
@@ -48,7 +49,8 @@ export function OrganizationSettingsSideMenu({
4849
organization: MatchedOrganization;
4950
buildInfo: BuildInfo;
5051
}) {
51-
const { isManagedCloud, hasPrivateConnections } = useFeatures();
52+
const { isManagedCloud } = useFeatures();
53+
const featureFlags = useFeatureFlags();
5254
const currentPlan = useCurrentPlan();
5355
const isAdmin = useHasAdminAccess();
5456
const showBuildInfo = isAdmin || !isManagedCloud;
@@ -105,7 +107,7 @@ export function OrganizationSettingsSideMenu({
105107
/>
106108
</>
107109
)}
108-
{hasPrivateConnections && (
110+
{featureFlags.hasPrivateConnections && (
109111
<SideMenuItem
110112
name="Private Connections"
111113
icon={LockClosedIcon}

apps/webapp/app/presenters/OrganizationsPresenter.server.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "./SelectBestEnvironmentPresenter.server";
1111
import { sortEnvironments } from "~/utils/environmentSort";
1212
import { defaultAvatar, parseAvatar } from "~/components/primitives/Avatar";
13+
import { env } from "~/env.server";
1314
import { flags, validatePartialFeatureFlags } from "~/v3/featureFlags.server";
1415

1516
export class OrganizationsPresenter {
@@ -154,8 +155,12 @@ export class OrganizationsPresenter {
154155
},
155156
});
156157

157-
// Get global feature flags (no overrides or defaults)
158-
const globalFlags = await flags();
158+
// Get global feature flags with env-var-based defaults
159+
const globalFlags = await flags({
160+
defaultValues: {
161+
hasPrivateConnections: env.PRIVATE_CONNECTIONS_ENABLED === "1",
162+
},
163+
});
159164

160165
return orgs.map((org) => {
161166
const orgFlagsResult = org.featureFlags

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Header2 } from "~/components/primitives/Headers";
1313
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
1414
import { Paragraph } from "~/components/primitives/Paragraph";
1515
import { prisma } from "~/db.server";
16-
import { featuresForRequest } from "~/features.server";
16+
import { canAccessPrivateConnections } from "~/v3/canAccessPrivateConnections.server";
1717
import { logger } from "~/services/logger.server";
1818
import { getPrivateLinks } from "~/services/platform.v3.server";
1919
import { requireUserId } from "~/services/session.server";
@@ -44,8 +44,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
4444
const userId = await requireUserId(request);
4545
const { organizationSlug } = OrganizationParamsSchema.parse(params);
4646

47-
const { hasPrivateConnections } = featuresForRequest(request);
48-
if (!hasPrivateConnections) {
47+
const canAccess = await canAccessPrivateConnections({ organizationSlug, userId });
48+
if (!canAccess) {
4949
return redirect(organizationPath({ slug: organizationSlug }));
5050
}
5151

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { Paragraph } from "~/components/primitives/Paragraph";
2424
import { Select, SelectItem } from "~/components/primitives/Select";
2525
import { prisma } from "~/db.server";
2626
import { env } from "~/env.server";
27-
import { featuresForRequest } from "~/features.server";
27+
import { canAccessPrivateConnections } from "~/v3/canAccessPrivateConnections.server";
2828
import {
2929
redirectWithErrorMessage,
3030
redirectWithSuccessMessage,
@@ -56,8 +56,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
5656
const userId = await requireUserId(request);
5757
const { organizationSlug } = OrganizationParamsSchema.parse(params);
5858

59-
const { hasPrivateConnections } = featuresForRequest(request);
60-
if (!hasPrivateConnections) {
59+
const canAccess = await canAccessPrivateConnections({ organizationSlug, userId });
60+
if (!canAccess) {
6161
return redirect(organizationPath({ slug: organizationSlug }));
6262
}
6363

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { prisma } from "~/db.server";
2+
import { env } from "~/env.server";
3+
import { FEATURE_FLAG, makeFlag } from "~/v3/featureFlags.server";
4+
5+
export async function canAccessPrivateConnections(options: {
6+
organizationSlug: string;
7+
userId: string;
8+
}): Promise<boolean> {
9+
const { organizationSlug, userId } = options;
10+
11+
const org = await prisma.organization.findFirst({
12+
where: {
13+
slug: organizationSlug,
14+
members: { some: { userId } },
15+
},
16+
select: {
17+
featureFlags: true,
18+
},
19+
});
20+
21+
const flag = makeFlag();
22+
return flag({
23+
key: FEATURE_FLAG.hasPrivateConnections,
24+
defaultValue: env.PRIVATE_CONNECTIONS_ENABLED === "1",
25+
overrides: (org?.featureFlags as Record<string, unknown>) ?? {},
26+
});
27+
}

apps/webapp/app/v3/featureFlags.server.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const FEATURE_FLAG = {
99
hasLogsPageAccess: "hasLogsPageAccess",
1010
hasAiAccess: "hasAiAccess",
1111
hasAiModelsAccess: "hasAiModelsAccess",
12+
hasPrivateConnections: "hasPrivateConnections",
1213
} as const;
1314

1415
const FeatureFlagCatalog = {
@@ -19,6 +20,7 @@ const FeatureFlagCatalog = {
1920
[FEATURE_FLAG.hasLogsPageAccess]: z.coerce.boolean(),
2021
[FEATURE_FLAG.hasAiAccess]: z.coerce.boolean(),
2122
[FEATURE_FLAG.hasAiModelsAccess]: z.coerce.boolean(),
23+
[FEATURE_FLAG.hasPrivateConnections]: z.coerce.boolean(),
2224
};
2325

2426
type FeatureFlagKey = keyof typeof FeatureFlagCatalog;
@@ -47,21 +49,23 @@ export function makeFlag(_prisma: PrismaClientOrTransaction = prisma) {
4749

4850
const flagSchema = FeatureFlagCatalog[opts.key];
4951

50-
if (opts.overrides?.[opts.key]) {
52+
if (opts.overrides?.[opts.key] !== undefined) {
5153
const parsed = flagSchema.safeParse(opts.overrides[opts.key]);
5254

5355
if (parsed.success) {
5456
return parsed.data;
5557
}
5658
}
5759

58-
const parsed = flagSchema.safeParse(value?.value);
60+
if (value !== null) {
61+
const parsed = flagSchema.safeParse(value.value);
5962

60-
if (!parsed.success) {
61-
return opts.defaultValue;
63+
if (parsed.success) {
64+
return parsed.data;
65+
}
6266
}
6367

64-
return parsed.data;
68+
return opts.defaultValue;
6569
}
6670

6771
return flag;

0 commit comments

Comments
 (0)