Skip to content

Commit bc93a2e

Browse files
committed
docs: add UI Conventions and updated Feature Flags guides to handbook
1 parent 29790c5 commit bc93a2e

6 files changed

Lines changed: 295 additions & 26 deletions

File tree

nuxt/content/handbook/engineering/feature-flags.md

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -96,29 +96,5 @@ Finally, **hard refresh** in the environment you just enabled to pull the latest
9696

9797
## Using feature flags in code
9898

99-
The FlowFuse frontend wires PostHog flags through the `account-settings` store (`frontend/src/stores/account-settings.js`).
100-
101-
**Where to add the flag** — register its key in the `POSTHOG_FLAGS` constant, then expose a boolean check in the `featuresCheck` getter:
102-
103-
```js
104-
export const POSTHOG_FLAGS = {
105-
EXPERT_COMMS_BETA_ENABLED: 'EXPERT_COMMS_BETA_ENABLED'
106-
}
107-
108-
// in featuresCheck:
109-
checks.isExpertCommsBetaEnabled = !!state.posthogFlags[POSTHOG_FLAGS.EXPERT_COMMS_BETA_ENABLED]
110-
```
111-
112-
**How to use it** — read the check from the resolved `featuresCheck` object wherever you need it. In a component template:
113-
114-
```html
115-
<template v-if="featuresCheck?.isExpertCommsBetaEnabled"> … </template>
116-
```
117-
118-
Or in script/logic — gate a method, computed property, route guard, etc:
119-
120-
```js
121-
if (featuresCheck?.isExpertCommsBetaEnabled) {
122-
// run the gated behaviour
123-
}
124-
```
99+
Once the flag exists in PostHog, it gets wired into the frontend by adding a
100+
`posthogKey` to the feature's entry in `FEATURE_CONFIGS`.

nuxt/content/handbook/engineering/frontend/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ A collection of how-to's and best practice guides for FlowFuse's front-end devel
1010
- [Services](./services.md)
1111
- [Testing](./testing.md)
1212
- [Layouts](./layouts.md)
13+
- [UI Conventions and Style Guide](./ui-conventions/)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
title: "UI Conventions and Style Guide"
2+
navigation:
3+
icon: i-lucide-palette
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
---
2+
title: "Feature Flags"
3+
---
4+
5+
# Feature Flags
6+
7+
Every gated capability in the platform UI resolves through one place: the
8+
`FEATURE_CONFIGS` array in `frontend/src/composables/FeatureChecks.ts`. You
9+
declare a flag there once, and `buildFeatureChecks()` turns that declaration
10+
into the booleans components read off the `featuresCheck` getter of the
11+
`account-settings` store.
12+
13+
Declare the flag in `FEATURE_CONFIGS` rather than reading platform settings or
14+
team properties directly in a component. Everything below (combining the
15+
platform and team answers, defaults, dependencies, PostHog) is handled for you
16+
once the entry exists.
17+
18+
This page covers defining a flag in code. For creating the PostHog flag itself
19+
and rolling it out to teams, see
20+
[Feature Flags in PostHog](/handbook/engineering/feature-flags/).
21+
22+
## The three checks
23+
24+
A flag answers two separate questions, so each entry produces up to three
25+
booleans, all named from its `output`:
26+
27+
| Check | Question it answers |
28+
|---|---|
29+
| `{output}ForPlatform` | Is the feature available on this installation at all? Driven by license and configuration. Only produced when `platformKey` is set. |
30+
| `{output}ForTeam` | Is the feature included in the current team's team type? Only produced when `teamKey` is set. |
31+
| `{output}` | The combined answer. |
32+
33+
Name the `output` `is{Feature}FeatureEnabled`.
34+
35+
## Defining a flag
36+
37+
Add one entry to `FEATURE_CONFIGS`:
38+
39+
```ts
40+
{ output: 'isTablesFeatureEnabled', platformKey: 'tables', teamKey: 'tables' }
41+
```
42+
43+
The available fields:
44+
45+
| Field | Purpose |
46+
|---|---|
47+
| `output` | Required. Name of the resulting check. |
48+
| `platformKey` | Key to look up in the platform features object. |
49+
| `teamKey` | Key to look up in the team type's `properties.features`. |
50+
| `optOut` | Flips the team check's default from off to on. See [Opt-in and opt-out](#opt-in-and-opt-out). |
51+
| `platformSource` | Set to `'settingsRoot'` to read a top-level settings key instead of a platform feature. |
52+
| `platformDefault` | Value to use for a platform-only check when the key is absent. |
53+
| `dependsOn` | The `output` of another entry that must be enabled. |
54+
| `dependsOnPlatform` | A platform key that must be enabled. |
55+
| `dependsOnTeam` | A team key that must be enabled. |
56+
| `dependsOnTeamOptOut` | Applies `optOut` semantics to the `dependsOnTeam` check. |
57+
| `posthogKey` | Hands the decision to a PostHog flag. See [PostHog flags](#posthog-flags). |
58+
59+
At least one of `platformKey` or `teamKey` is required.
60+
61+
## Where the keys come from
62+
63+
`platformKey` and `teamKey` are two independent registries that both live in the
64+
backend. The names do not have to match, and sometimes they don't:
65+
`isHTTPBearerTokensFeatureEnabled` uses the platform key `httpBearerTokens` and
66+
the team key `teamHttpSecurity`.
67+
68+
### Platform keys
69+
70+
Register the key on the backend with `app.config.features.register(name, value, isPublic)`.
71+
Most platform features are registered in `forge/ee/lib/index.js` behind the
72+
relevant license check:
73+
74+
```js
75+
app.config.features.register('tables', true, true)
76+
```
77+
78+
The registry itself is `forge/config/features.js`, and `GET /api/v1/settings`
79+
returns the result as the `features` object that the frontend reads.
80+
81+
### Team keys
82+
83+
Team keys are properties of a team type. To add one, put the key in
84+
`featureList` and a human-readable label in `featureNames`, both in
85+
`forge/lib/features.js`:
86+
87+
```js
88+
const featureList = [
89+
// ...
90+
'tables'
91+
]
92+
93+
const featureNames = {
94+
// ...
95+
tables: 'Tables'
96+
}
97+
```
98+
99+
That file is imported directly by the admin Team Type edit dialog
100+
(`frontend/src/pages/admin/TeamTypes/dialogs/TeamTypeEditDialog.vue`), which
101+
renders one checkbox per entry. A team key that isn't in `featureList` won't
102+
appear there, so an admin will have no way to toggle it.
103+
104+
## How the checks combine
105+
106+
The base rule follows from which keys you set:
107+
108+
- `platformKey` and `teamKey` both set: platform **AND** team
109+
- `platformKey` only: the platform answer
110+
- `teamKey` only: the team answer
111+
112+
### Opt-in and opt-out
113+
114+
The team check defaults to off. The team type has to explicitly enable the
115+
feature, or be marked `enableAllFeatures: true`:
116+
117+
```ts
118+
{ output: 'isTablesFeatureEnabled', platformKey: 'tables', teamKey: 'tables' }
119+
```
120+
121+
Setting `optOut: true` reverses that default. The feature counts as enabled
122+
unless the team type explicitly sets it to `false`, so a team type that has
123+
never heard of the key still gets the feature:
124+
125+
```ts
126+
{ output: 'isSharedLibraryFeatureEnabled', platformKey: 'shared-library', teamKey: 'shared-library', optOut: true }
127+
```
128+
129+
Use `optOut` for capabilities every team should have unless deliberately taken
130+
away. Leave it off for anything sold as part of a specific tier.
131+
132+
### Platform defaults
133+
134+
For a platform-only check, `platformDefault` covers the case where the key is
135+
missing altogether, which is the mirror image of `optOut`:
136+
137+
```ts
138+
{ output: 'isRemoteInstanceFeatureEnabled', platformKey: 'remoteInstances', platformDefault: true }
139+
```
140+
141+
### Reading from the settings root
142+
143+
A few flags are plain settings values rather than registered features. Point
144+
`platformSource` at `'settingsRoot'` to read them from the top level of the
145+
settings response:
146+
147+
```ts
148+
{ output: 'isTelemetryEnabled', platformKey: 'telemetry:enabled', platformSource: 'settingsRoot' }
149+
```
150+
151+
### Dependencies
152+
153+
When a feature only makes sense alongside another, gate it with a `dependsOn*`
154+
field. Any failing dependency forces the combined check to `false`.
155+
156+
```ts
157+
{
158+
output: 'isExpertAssistantFeatureEnabled',
159+
platformKey: 'expertAssistant',
160+
teamKey: 'expertAssistant',
161+
optOut: true,
162+
dependsOnPlatform: 'ai',
163+
dependsOnTeam: 'ai',
164+
dependsOnTeamOptOut: true
165+
}
166+
```
167+
168+
`dependsOnPlatform` and `dependsOnTeam` take raw keys, so the feature they point
169+
at needs no entry of its own. `dependsOn` takes another entry's `output`, and
170+
that entry **must appear earlier in the array** so it has already been computed.
171+
172+
## PostHog flags
173+
174+
Adding a `posthogKey` ties the check to a PostHog feature flag:
175+
176+
```ts
177+
{
178+
output: 'isMcpThirdPartyFeatureEnabled',
179+
platformKey: 'mcpThirdParty',
180+
teamKey: 'mcpThirdParty',
181+
optOut: true,
182+
posthogKey: 'MCP_THIRD_PARTY'
183+
}
184+
```
185+
186+
Once PostHog is loaded, its flag decides the check. It replaces the platform and
187+
team answer rather than adding a condition on top of it, so reach for
188+
`posthogKey` when you want PostHog to own the rollout, which is the usual case
189+
for a beta you're enabling team by team. When a flag has a `posthogKey`, gate
190+
your UI on the combined `{output}` so the PostHog decision is respected.
191+
192+
Flag keys are `ALL_CAPS`. Create the flag and copy it to every PostHog project
193+
before you rely on it, as described in
194+
[Feature Flags in PostHog](/handbook/engineering/feature-flags/).
195+
196+
## Reading a check in the UI
197+
198+
Map the getter in and read the check off it. In a component:
199+
200+
```js
201+
import { mapState } from 'pinia'
202+
203+
import { useAccountSettingsStore } from '@/stores/account-settings.js'
204+
205+
export default {
206+
computed: {
207+
...mapState(useAccountSettingsStore, ['featuresCheck'])
208+
}
209+
}
210+
```
211+
212+
```html
213+
<template v-if="featuresCheck.isTablesFeatureEnabled">
214+
...
215+
</template>
216+
```
217+
218+
Outside a component, such as in a route guard or another store, read the getter
219+
off the store directly:
220+
221+
```js
222+
const features = useAccountSettingsStore().featuresCheck
223+
224+
if (features.isTablesFeatureEnabled) {
225+
// ...
226+
}
227+
```
228+
229+
### Telling the user why something is unavailable
230+
231+
Don't just hide a gated feature. Use the split checks to show the right banner,
232+
so the user knows what to do about it:
233+
234+
```html
235+
<template v-if="!featuresCheck.isTablesFeatureEnabled">
236+
<FeatureUnavailable v-if="!featuresCheck.isTablesFeatureEnabledForPlatform" />
237+
<FeatureUnavailableToTeam v-else-if="!featuresCheck.isTablesFeatureEnabledForTeam" />
238+
</template>
239+
```
240+
241+
`FeatureUnavailable` (`components/banners/FeatureUnavailable.vue`) points at the
242+
upgrade documentation. `FeatureUnavailableToTeam`
243+
(`components/banners/FeatureUnavailableToTeam.vue`) links to the team's
244+
change-type page. Both accept a custom message.
245+
246+
## Testing a flag
247+
248+
Add a case to `test/unit/frontend/composables/FeatureChecks.spec.js`. It calls
249+
`buildFeatureChecks()` directly with a plain state and team object, so there's
250+
no store to set up:
251+
252+
```js
253+
const checks = buildFeatureChecks(
254+
{ features: { tables: true }, settings: { features: {} }, posthogFlags: {} },
255+
{ type: { properties: { features: { tables: true }, enableAllFeatures: false } } }
256+
)
257+
258+
expect(checks.isTablesFeatureEnabled).toBe(true)
259+
expect(checks.isTablesFeatureEnabledForPlatform).toBe(true)
260+
expect(checks.isTablesFeatureEnabledForTeam).toBe(true)
261+
```
262+
263+
Cover the interesting combination for your flag: both sides on, each side off,
264+
and any default or dependency you declared.
265+
266+
## Adding a flag, end to end
267+
268+
1. Register the `platformKey` in `forge/`, if the feature is license or config gated.
269+
2. Add the `teamKey` to `featureList` and `featureNames` in `forge/lib/features.js`, if it varies by team type.
270+
3. Add the `FEATURE_CONFIGS` entry in `FeatureChecks.ts`.
271+
4. If PostHog owns the rollout, create the flag, copy it to every project, and add `posthogKey`.
272+
5. Gate the UI on the combined check, and add the unavailable banners.
273+
6. Add a case to `FeatureChecks.spec.js`.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: "UI Conventions and Style Guide"
3+
---
4+
5+
# UI Conventions and Style Guide
6+
7+
The conventions we follow when building the FlowFuse platform UI. Each entry
8+
covers one recurring decision, says what the agreed approach is, and shows the
9+
code that implements it.
10+
11+
These pages describe how we want new work to look. Where you find older code
12+
that does something different, prefer the convention documented here.
13+
14+
- [Feature Flags](./feature-flags.md) - how to define a flag and gate UI on it

nuxt/content/handbook/engineering/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ These pages help you setup additional infrastructure for local development.
4949

5050
A collection of how-tos and best practice guides for FlowFuse's front-end development.
5151

52+
- [UI Conventions and Style Guide](./frontend/ui-conventions/) - the conventions we follow when building platform UI
5253
- [Data Attributes](./frontend/data-attributes.md)
5354
- [Services](./frontend/services.md)
5455
- [Testing](./frontend/testing.md)
56+
- [Layouts](./frontend/layouts.md)
5557

5658
## Operations
5759

0 commit comments

Comments
 (0)