feat(frontend/feature-flags): Add LaunchDarkly feature flagging UI (#8847)
This PR allows us to feature flag on the frontend, this means we can rollout features in stages, hide features, do AB testing etc. ### Changes 🏗️ Added a LaunchDarkly Provider Added a withFeatureFlag component Added two env vars for: - enabling LD - specifying the _public_ client side key Usage: ``` 'use client' import { useFlags } from 'launchdarkly-react-client-sdk' import { withFeatureFlag } from '@/components/feature-flag/with-feature-flag' function TestFlagPage() { const flags = useFlags() return ( <div className="p-4"> <h1>If you can see this, the feature flag is ON</h1> <pre>Current flag value: {JSON.stringify(flags, null, 2)}</pre> </div> ) } export default withFeatureFlag(TestFlagPage, 'test-flag') ``` ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: <!-- Put your test plan here: --> - [ ] ... <details> <summary>Test plan</summary> - Set LD to false - Navigate to a test page, should not be visible - Set LD to true - Navigate to same test page, should be visible </details> #### For configuration changes: - [x] `.env.example` is updated or already compatible with my changes - [x] I have included a list of my configuration changes in the PR description (under **Changes**) - [x] I have updated infra repo <details> <summary>Examples of configuration changes</summary> - Changing ports - Adding new services that need to communicate with each other - Secrets or environment variable changes - New or infrastructure changes such as databases </details> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Bently <tomnoon9@gmail.com> Co-authored-by: SerchioSD <69461657+serchiosd@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Abhimanyu Yadav <122007096+Abhi1992002@users.noreply.github.com> Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co> Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co> Co-authored-by: Reinier van der Leer <pwuts@agpt.co> Co-authored-by: Toran Bruce Richards <toran.richards@gmail.com>
This commit is contained in:
parent
ea6c9a1152
commit
d7c9742d7e
|
@ -2,6 +2,8 @@ NEXT_PUBLIC_AUTH_CALLBACK_URL=http://localhost:8006/auth/callback
|
|||
NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8006/api
|
||||
NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws
|
||||
NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8015/api/v1/market
|
||||
NEXT_PUBLIC_LAUNCHDARKLY_ENABLED=false
|
||||
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID=
|
||||
NEXT_PUBLIC_APP_ENV=dev
|
||||
|
||||
## Supabase credentials
|
||||
|
|
|
@ -46,23 +46,24 @@
|
|||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@sentry/nextjs": "^8",
|
||||
"@supabase/ssr": "^0.5.2",
|
||||
"@supabase/supabase-js": "^2.46.2",
|
||||
"@supabase/supabase-js": "^2.46.1",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@xyflow/react": "^12.3.5",
|
||||
"ajv": "^8.17.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"cookie": "1.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"elliptic": "6.6.1",
|
||||
"launchdarkly-react-client-sdk": "^3.6.0",
|
||||
"lucide-react": "^0.462.0",
|
||||
"moment": "^2.30.1",
|
||||
"next": "^14.2.13",
|
||||
"next-themes": "^0.4.3",
|
||||
"react": "^18",
|
||||
"react-day-picker": "^9.4.1",
|
||||
"react-day-picker": "^9.4.0",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-icons": "^5.3.0",
|
||||
|
|
|
@ -7,6 +7,7 @@ import { BackendAPIProvider } from "@/lib/autogpt-server-api";
|
|||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import SupabaseProvider from "@/components/SupabaseProvider";
|
||||
import CredentialsProvider from "@/components/integrations/credentials-provider";
|
||||
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";
|
||||
|
||||
export function Providers({ children, ...props }: ThemeProviderProps) {
|
||||
return (
|
||||
|
@ -14,7 +15,9 @@ export function Providers({ children, ...props }: ThemeProviderProps) {
|
|||
<SupabaseProvider>
|
||||
<BackendAPIProvider>
|
||||
<CredentialsProvider>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
<LaunchDarklyProvider>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</LaunchDarklyProvider>
|
||||
</CredentialsProvider>
|
||||
</BackendAPIProvider>
|
||||
</SupabaseProvider>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { LDProvider } from "launchdarkly-react-client-sdk";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export function LaunchDarklyProvider({ children }: { children: ReactNode }) {
|
||||
if (
|
||||
process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === true &&
|
||||
!process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID
|
||||
) {
|
||||
throw new Error("NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID is not defined");
|
||||
}
|
||||
|
||||
return (
|
||||
<LDProvider clientSideID={process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID}>
|
||||
{children}
|
||||
</LDProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
"use client";
|
||||
|
||||
import { useFlags } from "launchdarkly-react-client-sdk";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function withFeatureFlag<P extends object>(
|
||||
WrappedComponent: React.ComponentType<P>,
|
||||
flagKey: string,
|
||||
) {
|
||||
return function FeatureFlaggedComponent(props: P) {
|
||||
const flags = useFlags();
|
||||
const router = useRouter();
|
||||
const [hasFlagLoaded, setHasFlagLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Only proceed if flags received
|
||||
if (flags && flagKey in flags) {
|
||||
setHasFlagLoaded(true);
|
||||
}
|
||||
}, [flags, flagKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasFlagLoaded && !flags[flagKey]) {
|
||||
router.push("/404");
|
||||
}
|
||||
}, [hasFlagLoaded, flags, flagKey, router]);
|
||||
|
||||
// Show loading state until flags loaded
|
||||
if (!hasFlagLoaded) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If flag is loaded but false, return null (will redirect)
|
||||
if (!flags[flagKey]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Flag is loaded and true, show component
|
||||
return <WrappedComponent {...props} />;
|
||||
};
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue