Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions frontend/.claude/context/testing-against-pr-backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Testing the Frontend Against an Unmerged Backend PR

When a backend PR adds or changes API behaviour, you can run that exact backend
locally — without checking out or building the API — and point your local
frontend dev server at it. CI publishes a Docker image for every PR.

## Where the images live

Every PR's CI builds and **pushes** multi-arch (amd64 + arm64) images to GHCR,
tagged `pr-<number>`. They are **public** — no `docker login` needed.

| Image | Tag | Contents |
| --- | --- | --- |
| `ghcr.io/flagsmith/flagsmith` | `pr-<number>` | Unified image — API + bundled frontend, serves on `:8000` |
| `ghcr.io/flagsmith/flagsmith-api` | `pr-<number>` | API only |

Use the **unified** image with the repo's root `docker-compose.yml`, which is
already wired for it (Postgres, migrations, API, task processor).

Confirm an image exists before relying on it:

```bash
docker manifest inspect ghcr.io/flagsmith/flagsmith:pr-<number> >/dev/null && echo pullable
```

## Run a PR backend + local frontend

From the repo root, create a one-off compose override that swaps the three
`flagsmith` services onto the PR image (the root compose hardcodes the image, so
an override file is the clean way to repoint it):

```bash
cat > docker-compose.pr.yml <<'EOF'
services:
migrate-db:
image: ghcr.io/flagsmith/flagsmith:pr-<number>
flagsmith:
image: ghcr.io/flagsmith/flagsmith:pr-<number>
flagsmith-task-processor:
image: ghcr.io/flagsmith/flagsmith:pr-<number>
EOF

docker compose -f docker-compose.yml -f docker-compose.pr.yml pull
docker compose -f docker-compose.yml -f docker-compose.pr.yml up
```

The API comes up on `localhost:8000`. Then run the frontend against it:

```bash
cd frontend
ENV=local npm run dev # dev server on :3000, talks to the API on :8000
```

`docker-compose.pr.yml` is throwaway — delete it (or keep it git-ignored) when
done. To switch PRs, change the tag and re-run `pull` + `up`.

## Notes / gotchas

- **Flagsmith-on-Flagsmith gates.** Backend features are often gated by an
OpenFeature flag (e.g. `feature_lifecycle`). The flag's default for a
self-hosted/local run comes from the baked-in
`api/integrations/flagsmith/data/environment.json`. Check it's enabled there;
if not, the gated endpoints/fields won't appear.
- **Data, not just code.** Endpoints may need seeded data to return anything
meaningful (e.g. code references, stale tags, usage). The image gives you the
behaviour; you still have to create the data through the UI/API.
- **Inspect what the PR actually built.** Job logs show the pushed tag:
`gh run view --job <id> --log | grep -i 'tags:'`. The `pr-<number>` convention
is stable, but the logs are the source of truth.
- **Contract-checking without running.** To verify the frontend matches a
backend PR's contract, read its diff directly instead of (or before) running:
`gh api repos/Flagsmith/flagsmith/contents/<path>?ref=<pr-head-branch> --jq .content | base64 -d`.
1 change: 1 addition & 0 deletions frontend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ For detailed guidance on specific topics:
- **Quick Start**: `.claude/context/quick-reference.md` - Common tasks, commands, patterns
- **API Integration**: `.claude/context/api-integration.md` - Adding endpoints, RTK Query (required reading for API work)
- **Backend**: `.claude/context/backend-integration.md` - Finding endpoints, backend structure
- **Testing vs PR backend**: `.claude/context/testing-against-pr-backend.md` - Run an unmerged backend PR's Docker image locally and point the dev server at it
- **UI Patterns**: `.claude/context/ui-patterns.md` - Tables, tabs, modals, confirmations (required reading for UI work)
- **Feature Flags**: `.claude/context/feature-flags/` - Using Flagsmith flags (optional, only when requested)
- **Code Patterns**: `.claude/context/patterns/` - Complete examples, best practices
Expand Down
26 changes: 26 additions & 0 deletions frontend/common/lifecycleEnvironmentSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

type LifecycleEnvironmentState = {
// Maps a project id to the environment id selected for lifecycle analysis.
byProject: Record<number, number>
}

const initialState: LifecycleEnvironmentState = {
byProject: {},
}

const lifecycleEnvironmentSlice = createSlice({
initialState,
name: 'lifecycleEnvironment',
reducers: {
setLifecycleEnvironment(
state,
action: PayloadAction<{ projectId: number; environmentId: number }>,
) {
state.byProject[action.payload.projectId] = action.payload.environmentId
},
},
})

export const { setLifecycleEnvironment } = lifecycleEnvironmentSlice.actions
export default lifecycleEnvironmentSlice.reducer
13 changes: 13 additions & 0 deletions frontend/common/services/useProjectFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@ export const projectFlagService = service
}),
}),

getLifecycleStatusCounts: builder.query<
Res['lifecycleStatusCounts'],
Req['getLifecycleStatusCounts']
>({
providesTags: (_res, _meta, req) => [
{ id: req?.environment, type: 'ProjectFlag' },
],
query: ({ environment }) => ({
url: `environments/${environment}/feature-lifecycle-counts/`,
}),
}),

getProjectFlag: builder.query<Res['projectFlag'], Req['getProjectFlag']>({
providesTags: (res) => [{ id: res?.id, type: 'ProjectFlag' }],
query: (query: Req['getProjectFlag']) => ({
Expand Down Expand Up @@ -284,6 +296,7 @@ export const {
useAddFlagOwnersMutation,
useCreateProjectFlagMutation,
useGetFeatureListQuery,
useGetLifecycleStatusCountsQuery,
useGetProjectFlagQuery,
useGetProjectFlagsQuery,
useRemoveFlagGroupOwnersMutation,
Expand Down
2 changes: 2 additions & 0 deletions frontend/common/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import storage from 'redux-persist/lib/storage'
import { Persistor } from 'redux-persist/es/types'
import { service } from './service'
import selectedOrganisationReducer from './selectedOrganisationSlice'
import lifecycleEnvironmentReducer from './lifecycleEnvironmentSlice'
// END OF IMPORTS
const createStore = () => {
const reducer = combineReducers({
[service.reducerPath]: service.reducer,
lifecycleEnvironment: lifecycleEnvironmentReducer,
selectedOrganisation: selectedOrganisationReducer,
// END OF REDUCERS
})
Expand Down
5 changes: 5 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
FlagsmithValue,
TagStrategy,
FeatureType,
LifecycleStage,
} from './responses'
import { UtmsType } from './utms'

Expand Down Expand Up @@ -395,6 +396,10 @@ export type Req = {
group_owners?: number[]
sort_field?: string
sort_direction?: SortOrder
lifecycle_stage?: LifecycleStage
}
getLifecycleStatusCounts: {
environment: number
}
getProjectFlag: { project: number; id: number }
getRolesPermissionUsers: { organisation_id: number; role_id: number }
Expand Down
12 changes: 12 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,8 +741,19 @@ export type ProjectFlag = {
last_successful_repository_scanned_at: string
last_feature_found_at: string
}[]
lifecycle_stage?: LifecycleStage | null
}

export type LifecycleStage =
| 'new'
| 'live'
| 'permanent'
| 'stale'
| 'needs_monitoring'
| 'to_remove'

export type LifecycleStatusCounts = Record<LifecycleStage, number>

export type FeatureListProviderData = {
projectFlags: ProjectFlag[] | null
environmentFlags: Record<number, FeatureState> | undefined
Expand Down Expand Up @@ -1269,6 +1280,7 @@ export type Res = {
rolePermission: PagedResponse<RolePermission>
projectFlags: PagedResponse<ProjectFlag>
projectFlag: ProjectFlag
lifecycleStatusCounts: LifecycleStatusCounts
identityFeatureStatesAll: IdentityFeatureState[]
createRolesPermissionUsers: RolePermissionUser
rolesPermissionUsers: PagedResponse<RolePermissionUser>
Expand Down
Loading
Loading