diff --git a/nuxt/content/handbook/engineering/feature-flags.md b/nuxt/content/handbook/engineering/feature-flags.md index ee6cc7cad6..de225389c6 100644 --- a/nuxt/content/handbook/engineering/feature-flags.md +++ b/nuxt/content/handbook/engineering/feature-flags.md @@ -96,29 +96,5 @@ Finally, **hard refresh** in the environment you just enabled to pull the latest ## Using feature flags in code -The FlowFuse frontend wires PostHog flags through the `account-settings` store (`frontend/src/stores/account-settings.js`). - -**Where to add the flag** — register its key in the `POSTHOG_FLAGS` constant, then expose a boolean check in the `featuresCheck` getter: - -```js -export const POSTHOG_FLAGS = { - EXPERT_COMMS_BETA_ENABLED: 'EXPERT_COMMS_BETA_ENABLED' -} - -// in featuresCheck: -checks.isExpertCommsBetaEnabled = !!state.posthogFlags[POSTHOG_FLAGS.EXPERT_COMMS_BETA_ENABLED] -``` - -**How to use it** — read the check from the resolved `featuresCheck` object wherever you need it. In a component template: - -```html - -``` - -Or in script/logic — gate a method, computed property, route guard, etc: - -```js -if (featuresCheck?.isExpertCommsBetaEnabled) { - // run the gated behaviour -} -``` +Once the flag exists in PostHog, it gets wired into the frontend by adding a +`posthogKey` to the feature's entry in `FEATURE_CONFIGS`. diff --git a/nuxt/content/handbook/engineering/frontend/index.md b/nuxt/content/handbook/engineering/frontend/index.md index e25bc0d848..02244f5fca 100644 --- a/nuxt/content/handbook/engineering/frontend/index.md +++ b/nuxt/content/handbook/engineering/frontend/index.md @@ -10,3 +10,4 @@ A collection of how-to's and best practice guides for FlowFuse's front-end devel - [Services](./services.md) - [Testing](./testing.md) - [Layouts](./layouts.md) +- [UI Conventions and Style Guide](./ui-conventions/) diff --git a/nuxt/content/handbook/engineering/frontend/ui-conventions/.navigation.yml b/nuxt/content/handbook/engineering/frontend/ui-conventions/.navigation.yml new file mode 100644 index 0000000000..aacf216de3 --- /dev/null +++ b/nuxt/content/handbook/engineering/frontend/ui-conventions/.navigation.yml @@ -0,0 +1,3 @@ +title: "UI Conventions and Style Guide" +navigation: + icon: i-lucide-palette diff --git a/nuxt/content/handbook/engineering/frontend/ui-conventions/feature-flags.md b/nuxt/content/handbook/engineering/frontend/ui-conventions/feature-flags.md new file mode 100644 index 0000000000..166179cbaa --- /dev/null +++ b/nuxt/content/handbook/engineering/frontend/ui-conventions/feature-flags.md @@ -0,0 +1,275 @@ +--- +title: "Feature Flags" +--- + +# Feature Flags + +Every gated capability in the platform UI resolves through one place: the +`FEATURE_CONFIGS` array in `frontend/src/composables/FeatureChecks.ts`. You +declare a flag there once, and `buildFeatureChecks()` turns that declaration +into the booleans components read off the `featuresCheck` getter of the +`account-settings` store. + +Declare the flag in `FEATURE_CONFIGS` rather than reading platform settings or +team properties directly in a component. Everything below (combining the +platform and team answers, defaults, dependencies, PostHog) is handled for you +once the entry exists. + +This page covers defining a flag in code. For creating the PostHog flag itself +and rolling it out to teams, see +[Feature Flags in PostHog](/handbook/engineering/feature-flags/). + +## The three checks + +A flag answers two separate questions, so each entry produces up to three +booleans, all named from its `output`: + +| Check | Question it answers | +|---|---| +| `{output}ForPlatform` | Is the feature available on this installation at all? Driven by license and configuration. Only produced when `platformKey` is set. | +| `{output}ForTeam` | Is the feature included in the current team's team type? Only produced when `teamKey` is set. | +| `{output}` | The combined answer. | + +Name the `output` `is{Feature}FeatureEnabled`. + +## Defining a flag + +Add one entry to `FEATURE_CONFIGS`: + +```ts +{ output: 'isTablesFeatureEnabled', platformKey: 'tables', teamKey: 'tables' } +``` + +The available fields: + +| Field | Purpose | +|---|---| +| `output` | Required. Name of the resulting check. | +| `platformKey` | Key to look up in the platform features object. | +| `teamKey` | Key to look up in the team type's `properties.features`. | +| `optOut` | Flips the team check's default from off to on. See [Opt-in and opt-out](#opt-in-and-opt-out). | +| `platformSource` | Set to `'settingsRoot'` to read a top-level settings key instead of a platform feature. | +| `platformDefault` | Value to use for a platform-only check when the key is absent. | +| `dependsOn` | The `output` of another entry that must be enabled. | +| `dependsOnPlatform` | A platform key that must be enabled. | +| `dependsOnTeam` | A team key that must be enabled. | +| `dependsOnTeamOptOut` | Applies `optOut` semantics to the `dependsOnTeam` check. | +| `posthogKey` | Hands the decision to a PostHog flag. See [PostHog flags](#posthog-flags). | + +At least one of `platformKey` or `teamKey` is required. + +## Where the keys come from + +`platformKey` and `teamKey` are two independent registries that both live in the +backend. The names do not have to match, and sometimes they don't: +`isHTTPBearerTokensFeatureEnabled` uses the platform key `httpBearerTokens` and +the team key `teamHttpSecurity`. + +### Platform keys + +Register the key on the backend with `app.config.features.register(name, value, isPublic)`. +Registrations live under `forge/ee/lib/`, behind the relevant license check. +Many sit together in `forge/ee/lib/index.js`; features with a module of their own +register there instead, as Tables does in `forge/ee/lib/tables/index.js`: + +```js +app.config.features.register('tables', true, true) +``` + +The registry itself is `forge/config/features.js`, and `GET /api/v1/settings` +returns the result as the `features` object that the frontend reads. + +### Team keys + +Team keys are properties of a team type. To add one, put the key in +`featureList` and a human-readable label in `featureNames`, both in +`forge/lib/features.js`: + +```js +const featureList = [ + // ... + 'tables' +] + +const featureNames = { + // ... + tables: 'Tables' +} +``` + +That file is imported directly by the admin Team Type edit dialog +(`frontend/src/pages/admin/TeamTypes/dialogs/TeamTypeEditDialog.vue`), which +renders a checkbox for each entry available on this installation. A team key +that isn't in `featureList` won't appear there, so an admin will have no way to +toggle it. + +## How the checks combine + +The base rule follows from which keys you set: + +- `platformKey` and `teamKey` both set: platform **AND** team +- `platformKey` only: the platform answer +- `teamKey` only: the team answer + +### Opt-in and opt-out + +The team check defaults to off. The team type has to explicitly enable the +feature, or be marked `enableAllFeatures: true`: + +```ts +{ output: 'isTablesFeatureEnabled', platformKey: 'tables', teamKey: 'tables' } +``` + +Setting `optOut: true` reverses that default. The feature counts as enabled +unless the team type explicitly sets it to `false`, so a team type that has +never heard of the key still gets the feature: + +```ts +{ output: 'isSharedLibraryFeatureEnabled', platformKey: 'shared-library', teamKey: 'shared-library', optOut: true } +``` + +Use `optOut` for capabilities every team should have unless deliberately taken +away. Leave it off for anything sold as part of a specific tier. + +### Platform defaults + +For a platform-only check, `platformDefault` covers the case where the key is +missing altogether, which is the mirror image of `optOut`: + +```ts +{ output: 'isRemoteInstanceFeatureEnabled', platformKey: 'remoteInstances', platformDefault: true } +``` + +### Reading from the settings root + +A few flags are plain settings values rather than registered features. Point +`platformSource` at `'settingsRoot'` to read them from the top level of the +settings response: + +```ts +{ output: 'isTelemetryEnabled', platformKey: 'telemetry:enabled', platformSource: 'settingsRoot' } +``` + +### Dependencies + +When a feature only makes sense alongside another, gate it with a `dependsOn*` +field. Any failing dependency forces the combined check to `false`. + +```ts +{ + output: 'isExpertAssistantFeatureEnabled', + platformKey: 'expertAssistant', + teamKey: 'expertAssistant', + optOut: true, + dependsOnPlatform: 'ai', + dependsOnTeam: 'ai', + dependsOnTeamOptOut: true +} +``` + +`dependsOnPlatform` and `dependsOnTeam` take raw keys, so the feature they point +at needs no entry of its own. `dependsOn` takes another entry's `output`, and +that entry **must appear earlier in the array** so it has already been computed. + +## PostHog flags + +Adding a `posthogKey` ties the check to a PostHog feature flag: + +```ts +{ + output: 'isMcpThirdPartyFeatureEnabled', + platformKey: 'mcpThirdParty', + teamKey: 'mcpThirdParty', + optOut: true, + posthogKey: 'MCP_THIRD_PARTY' +} +``` + +Once PostHog is loaded, its flag decides the check. It replaces the platform and +team answer rather than adding a condition on top of it, so reach for +`posthogKey` when you want PostHog to own the rollout, which is the usual case +for a beta you're enabling team by team. When a flag has a `posthogKey`, gate +your UI on the combined `{output}` so the PostHog decision is respected. + +Flag keys are `ALL_CAPS`. Create the flag and copy it to every PostHog project +before you rely on it, as described in +[Feature Flags in PostHog](/handbook/engineering/feature-flags/). + +## Reading a check in the UI + +Map the getter in and read the check off it. In a component: + +```js +import { mapState } from 'pinia' + +import { useAccountSettingsStore } from '@/stores/account-settings.js' + +export default { + computed: { + ...mapState(useAccountSettingsStore, ['featuresCheck']) + } +} +``` + +```html + +``` + +Outside a component, such as in a route guard or another store, read the getter +off the store directly: + +```js +const features = useAccountSettingsStore().featuresCheck + +if (features.isTablesFeatureEnabled) { + // ... +} +``` + +### Telling the user why something is unavailable + +Don't just hide a gated feature. Use the split checks to show the right banner, +so the user knows what to do about it: + +```html + +``` + +`FeatureUnavailable` (`components/banners/FeatureUnavailable.vue`) points at the +upgrade documentation. `FeatureUnavailableToTeam` +(`components/banners/FeatureUnavailableToTeam.vue`) links to the team's +change-type page. Both accept a custom message. + +## Testing a flag + +Add a case to `test/unit/frontend/composables/FeatureChecks.spec.js`. It calls +`buildFeatureChecks()` directly with a plain state and team object, so there's +no store to set up: + +```js +const checks = buildFeatureChecks( + { features: { tables: true }, settings: { features: {} }, posthogFlags: {} }, + { type: { properties: { features: { tables: true }, enableAllFeatures: false } } } +) + +expect(checks.isTablesFeatureEnabled).toBe(true) +expect(checks.isTablesFeatureEnabledForPlatform).toBe(true) +expect(checks.isTablesFeatureEnabledForTeam).toBe(true) +``` + +Cover the interesting combination for your flag: both sides on, each side off, +and any default or dependency you declared. + +## Adding a flag, end to end + +1. Register the `platformKey` in `forge/`, if the feature is license or config gated. +2. Add the `teamKey` to `featureList` and `featureNames` in `forge/lib/features.js`, if it varies by team type. +3. Add the `FEATURE_CONFIGS` entry in `FeatureChecks.ts`. +4. If PostHog owns the rollout, create the flag, copy it to every project, and add `posthogKey`. +5. Gate the UI on the combined check, and add the unavailable banners. +6. Add a case to `FeatureChecks.spec.js`. diff --git a/nuxt/content/handbook/engineering/frontend/ui-conventions/index.md b/nuxt/content/handbook/engineering/frontend/ui-conventions/index.md new file mode 100644 index 0000000000..90543e6320 --- /dev/null +++ b/nuxt/content/handbook/engineering/frontend/ui-conventions/index.md @@ -0,0 +1,14 @@ +--- +title: "UI Conventions and Style Guide" +--- + +# UI Conventions and Style Guide + +The conventions we follow when building the FlowFuse platform UI. Each entry +covers one recurring decision, says what the agreed approach is, and shows the +code that implements it. + +These pages describe how we want new work to look. Where you find older code +that does something different, prefer the convention documented here. + +- [Feature Flags](./feature-flags.md) - how to define a flag and gate UI on it diff --git a/nuxt/content/handbook/engineering/index.md b/nuxt/content/handbook/engineering/index.md index b99e00e83b..2470e15922 100644 --- a/nuxt/content/handbook/engineering/index.md +++ b/nuxt/content/handbook/engineering/index.md @@ -49,9 +49,11 @@ These pages help you setup additional infrastructure for local development. A collection of how-tos and best practice guides for FlowFuse's front-end development. +- [UI Conventions and Style Guide](./frontend/ui-conventions/) - the conventions we follow when building platform UI - [Data Attributes](./frontend/data-attributes.md) - [Services](./frontend/services.md) - [Testing](./frontend/testing.md) +- [Layouts](./frontend/layouts.md) ## Operations