feat(webapp): Org level feature flags for Private Links (#3287)
This commit is contained in:
@@ -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
|
||||
|
||||
+3
-3
@@ -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 }));
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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>) ?? {},
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user