From d261b91a659bece60d7169ca66c4a9e910c03287 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 24 Apr 2025 12:32:26 +0100 Subject: [PATCH 1/7] New endpoint to set feature flag overrides. --- packages/backend-core/src/constants/misc.ts | 1 + packages/backend-core/src/context/mainContext.ts | 11 +++++++++++ packages/backend-core/src/context/types.ts | 1 + .../src/middleware/featureFlagCookie.ts | 13 +++++++++++++ packages/backend-core/src/middleware/index.ts | 1 + packages/server/src/api/controllers/features | 7 +++++++ packages/server/src/api/index.ts | 1 + packages/server/src/api/routes/features.ts | 8 ++++++++ packages/types/src/api/web/app/features.ts | 3 +++ packages/types/src/api/web/app/index.ts | 1 + packages/types/src/api/web/cookies.ts | 4 ++++ packages/types/src/sdk/featureFlag.ts | 3 +++ 12 files changed, 54 insertions(+) create mode 100644 packages/backend-core/src/middleware/featureFlagCookie.ts create mode 100644 packages/server/src/api/controllers/features create mode 100644 packages/server/src/api/routes/features.ts create mode 100644 packages/types/src/api/web/app/features.ts diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index e2fd975e409..1fa8d6e9269 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -9,6 +9,7 @@ export enum Cookie { ACCOUNT_RETURN_URL = "budibase:account:returnurl", DatasourceAuth = "budibase:datasourceauth", OIDC_CONFIG = "budibase:oidc:config", + FeatureFlags = "budibase:featureflags", } export { Header } from "@budibase/shared-core" diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 22b937db88f..c04be33dcb5 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -460,6 +460,17 @@ export function setFeatureFlags(key: string, value: Record) { context.featureFlagCache[key] = value } +export function getFeatureFlagOverrides(): Record { + return getCurrentContext()?.featureFlagOverrides || {} +} + +export function doInFeatureFlagOverrideContext( + value: Record, + callback: () => Promise +) { + return newContext({ featureFlagOverrides: value }, callback) +} + export function getTableForView(viewId: string): Table | undefined { const context = getCurrentContext() if (!context) { diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts index c2cb966731c..b34ef8c1b76 100644 --- a/packages/backend-core/src/context/types.ts +++ b/packages/backend-core/src/context/types.ts @@ -24,5 +24,6 @@ export type ContextMap = { featureFlagCache?: { [key: string]: Record } + featureFlagOverrides?: Record viewToTableCache?: Record } diff --git a/packages/backend-core/src/middleware/featureFlagCookie.ts b/packages/backend-core/src/middleware/featureFlagCookie.ts new file mode 100644 index 00000000000..070e2161af2 --- /dev/null +++ b/packages/backend-core/src/middleware/featureFlagCookie.ts @@ -0,0 +1,13 @@ +import { Ctx, FeatureFlagCookie } from "@budibase/types" +import { Middleware, Next } from "koa" +import { getCookie } from "../utils" +import { Cookie } from "../constants" +import { doInFeatureFlagOverrideContext } from "../context" + +export default (async (ctx: Ctx, next: Next) => { + const cookie = getCookie(ctx, Cookie.FeatureFlags) + const flags = cookie?.flags || {} + await doInFeatureFlagOverrideContext(flags, async () => { + await next() + }) +}) as Middleware diff --git a/packages/backend-core/src/middleware/index.ts b/packages/backend-core/src/middleware/index.ts index 9ee51db45bc..0d16ae52e3a 100644 --- a/packages/backend-core/src/middleware/index.ts +++ b/packages/backend-core/src/middleware/index.ts @@ -20,5 +20,6 @@ export { default as correlation } from "../logging/correlation/middleware" export { default as errorHandling } from "./errorHandling" export { default as querystringToBody } from "./querystringToBody" export { default as csp } from "./contentSecurityPolicy" +export { default as featureFlagCookie } from "./featureFlagCookie" export * as joiValidator from "./joi-validator" export { default as ip } from "./ip" diff --git a/packages/server/src/api/controllers/features b/packages/server/src/api/controllers/features new file mode 100644 index 00000000000..7760970bd6d --- /dev/null +++ b/packages/server/src/api/controllers/features @@ -0,0 +1,7 @@ +import { UserCtx, OverrideFeatureFlagRequest } from "@budibase/types" +import { Cookie, utils } from "@budibase/backend-core" + +export async function override(ctx: UserCtx) { + const { flags = {} } = ctx.request.body + utils.setCookie(ctx, flags, Cookie.FeatureFlags) +} diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts index 4f88b771477..c73a48de47d 100644 --- a/packages/server/src/api/index.ts +++ b/packages/server/src/api/index.ts @@ -71,6 +71,7 @@ if (apiEnabled()) { ) .use(pro.licensing()) .use(currentApp) + .use(middleware.featureFlagCookie) // Add CSP as soon as possible - depends on licensing and currentApp if (!coreEnv.DISABLE_CONTENT_SECURITY_POLICY) { diff --git a/packages/server/src/api/routes/features.ts b/packages/server/src/api/routes/features.ts new file mode 100644 index 00000000000..aaf9835ac0e --- /dev/null +++ b/packages/server/src/api/routes/features.ts @@ -0,0 +1,8 @@ +import Router from "@koa/router" +import * as controller from "../controllers/features" + +const router: Router = new Router() + +router.patch("/api/features", controller.override) + +export default router diff --git a/packages/types/src/api/web/app/features.ts b/packages/types/src/api/web/app/features.ts new file mode 100644 index 00000000000..018251a71e3 --- /dev/null +++ b/packages/types/src/api/web/app/features.ts @@ -0,0 +1,3 @@ +export interface OverrideFeatureFlagRequest { + flags: Record +} diff --git a/packages/types/src/api/web/app/index.ts b/packages/types/src/api/web/app/index.ts index b2243452c54..612f4bbc327 100644 --- a/packages/types/src/api/web/app/index.ts +++ b/packages/types/src/api/web/app/index.ts @@ -5,6 +5,7 @@ export * from "./backup" export * from "./component" export * from "./datasource" export * from "./deployment" +export * from "./features" export * from "./integration" export * from "./layout" export * from "./metadata" diff --git a/packages/types/src/api/web/cookies.ts b/packages/types/src/api/web/cookies.ts index 27954a36a10..b189863e601 100644 --- a/packages/types/src/api/web/cookies.ts +++ b/packages/types/src/api/web/cookies.ts @@ -7,3 +7,7 @@ export interface SessionCookie { sessionId: string userId: string } + +export interface FeatureFlagCookie { + flags: Record +} diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts index 83376e1d6a4..8be6b532c0f 100644 --- a/packages/types/src/sdk/featureFlag.ts +++ b/packages/types/src/sdk/featureFlag.ts @@ -1,4 +1,5 @@ export enum FeatureFlag { + DEBUG_UI = "DEBUG_UI", USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR", AI_JS_GENERATION = "AI_JS_GENERATION", AI_TABLE_GENERATION = "AI_TABLE_GENERATION", @@ -14,6 +15,8 @@ export const FeatureFlagDefaults: Record = { // Account-portal [FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false, + + [FeatureFlag.DEBUG_UI]: false, } export type FeatureFlags = typeof FeatureFlagDefaults From eb2acdf99e3cd14e25625fc767f0acc4e8f747dd Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 24 Apr 2025 17:49:13 +0100 Subject: [PATCH 2/7] Feature flag overriding UI. --- .../backend-core/src/context/mainContext.ts | 4 +- .../backend-core/src/features/features.ts | 15 +++++ .../src/components/debug/DebugUI.svelte | 56 +++++++++++++++++++ .../src/pages/builder/portal/_layout.svelte | 29 +++++++++- packages/frontend-core/src/api/features.ts | 19 +++++++ packages/frontend-core/src/api/index.ts | 2 + packages/frontend-core/src/api/types.ts | 2 + packages/server/src/api/controllers/features | 7 --- .../server/src/api/controllers/features.ts | 26 +++++++++ packages/server/src/api/index.ts | 3 +- packages/server/src/api/routes/features.ts | 8 ++- packages/server/src/api/routes/index.ts | 2 + packages/worker/src/api/index.ts | 1 + 13 files changed, 159 insertions(+), 15 deletions(-) create mode 100644 packages/builder/src/components/debug/DebugUI.svelte create mode 100644 packages/frontend-core/src/api/features.ts delete mode 100644 packages/server/src/api/controllers/features create mode 100644 packages/server/src/api/controllers/features.ts diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index c04be33dcb5..9bea94de29f 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -464,11 +464,11 @@ export function getFeatureFlagOverrides(): Record { return getCurrentContext()?.featureFlagOverrides || {} } -export function doInFeatureFlagOverrideContext( +export async function doInFeatureFlagOverrideContext( value: Record, callback: () => Promise ) { - return newContext({ featureFlagOverrides: value }, callback) + return await newContext({ featureFlagOverrides: value }, callback) } export function getTableForView(viewId: string): Table | undefined { diff --git a/packages/backend-core/src/features/features.ts b/packages/backend-core/src/features/features.ts index 71067770848..a76ffe8a7ba 100644 --- a/packages/backend-core/src/features/features.ts +++ b/packages/backend-core/src/features/features.ts @@ -175,6 +175,21 @@ export class FlagSet { } } + const overrides = context.getFeatureFlagOverrides() + for (const [key, value] of Object.entries(overrides)) { + if (!this.isFlagName(key)) { + continue + } + + if (typeof value !== "boolean") { + continue + } + + // @ts-expect-error - TS does not like you writing into a generic type. + flagValues[key] = value + tags[`flags.${key}.source`] = "override" + } + context.setFeatureFlags(this.setId, flagValues) for (const [key, value] of Object.entries(flagValues)) { tags[`flags.${key}.value`] = value diff --git a/packages/builder/src/components/debug/DebugUI.svelte b/packages/builder/src/components/debug/DebugUI.svelte new file mode 100644 index 00000000000..8df7e9c1b02 --- /dev/null +++ b/packages/builder/src/components/debug/DebugUI.svelte @@ -0,0 +1,56 @@ + + + +
+ {#each Object.entries($featureFlags) as [key, value]} +
+ setFlag(key, value.detail)} + /> +
+ {/each} +
+
+ + diff --git a/packages/builder/src/pages/builder/portal/_layout.svelte b/packages/builder/src/pages/builder/portal/_layout.svelte index 323b2310371..2fe4312da1d 100644 --- a/packages/builder/src/pages/builder/portal/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/_layout.svelte @@ -1,6 +1,6 @@ - - -
- {#each Object.entries($featureFlags) as [key, value]} -
- setFlag(key, value.detail)} - /> -
- {/each} -
-
- - diff --git a/packages/builder/src/pages/builder/app/[application]/data/new.svelte b/packages/builder/src/pages/builder/app/[application]/data/new.svelte index 5b1209046e8..f79a292d6c7 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/new.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/new.svelte @@ -16,8 +16,7 @@ import CreationPage from "@/components/common/CreationPage.svelte" import ICONS from "@/components/backend/DatasourceNavigator/icons/index.js" import AiTableGeneration from "./_components/AITableGeneration.svelte" - import { featureFlag } from "@/helpers" - import { FeatureFlag } from "@budibase/types" + import { featureFlags } from "@/stores/portal" let internalTableModal: CreateInternalTableModal let externalDatasourceModal: CreateExternalDatasourceModal @@ -26,10 +25,7 @@ let externalDatasourceLoading = false $: disabled = sampleDataLoading || externalDatasourceLoading - - $: aiTableGenerationEnabled = featureFlag.isEnabled( - FeatureFlag.AI_TABLE_GENERATION - ) + $: aiTableGenerationEnabled = $featureFlags.AI_TABLE_GENERATION const createSampleData = async () => { sampleDataLoading = true diff --git a/packages/builder/src/pages/builder/portal/_layout.svelte b/packages/builder/src/pages/builder/portal/_layout.svelte index 2fe4312da1d..e49dc8b3f34 100644 --- a/packages/builder/src/pages/builder/portal/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/_layout.svelte @@ -1,6 +1,6 @@ + + + + + {#if loaded} {/if} diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index a799553aca1..668aca657ee 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -14,7 +14,6 @@ Tabs, Tab, Heading, - Modal, notifications, TooltipPosition, } from "@budibase/bbui" @@ -24,7 +23,6 @@ import { capitalise } from "@/helpers" import { onMount, onDestroy } from "svelte" import VerificationPromptBanner from "@/components/common/VerificationPromptBanner.svelte" - import CommandPalette from "@/components/commandPalette/CommandPalette.svelte" import TourWrap from "@/components/portal/onboarding/TourWrap.svelte" import TourPopover from "@/components/portal/onboarding/TourPopover.svelte" import BuilderSidePanel from "./_components/BuilderSidePanel.svelte" @@ -38,7 +36,6 @@ let promise = getPackage() let hasSynced = false - let commandPaletteModal let loaded = false $: loaded && initTour() @@ -79,14 +76,6 @@ $goto($builderStore.previousTopNavPath[path] || path) } - // Event handler for the command palette - const handleKeyDown = e => { - if (e.key === "k" && (e.ctrlKey || e.metaKey)) { - e.preventDefault() - commandPaletteModal.toggle() - } - } - const initTour = async () => { // Check if onboarding is enabled. if (!$auth.user?.onboardedAt) { @@ -184,11 +173,6 @@ {/if} - - - - -