mirror of
https://github.com/f-eld-ch/sitrep.git
synced 2026-07-16 16:47:05 +02:00
feat(feature-flags): use flipt as feature flag service (#486)
* feat(feature-flags): create basic support for feature flags * feat(feature-flags): add github actions for feature flags * feat(feature-flags): make feature flag evaluation context easier * fix(triage): properly hide tasks behind feature flag for triage editor * docs(feature-flags): update development docs for feature flags * fix: properly close the provider
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
version: "1.0"
|
||||
include:
|
||||
- "ui/features.yaml"
|
||||
@@ -11,6 +11,23 @@ on:
|
||||
- "main"
|
||||
- "develop"
|
||||
jobs:
|
||||
featureflags:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Validate & Push Feature Flags"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: flipt-io/setup-action@v0.1.0
|
||||
- run: flipt validate
|
||||
- name: Build and Push bundle
|
||||
# only build and push feature flags from the develop branch
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
env:
|
||||
FLIPT_STORAGE_OCI_AUTHENTICATION_USERNAME: ${{ github.actor }}
|
||||
FLIPT_STORAGE_OCI_AUTHENTICATION_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
flipt bundle build sitrep-featureflags:latest
|
||||
flipt bundle push sitrep-featureflags:latest ghcr.io/f-eld-ch/sitrep/featureflags:develop
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Test and Build"
|
||||
|
||||
@@ -9,11 +9,20 @@ We intend to implement and use Feature Flags to enable continuous development an
|
||||
|
||||
## Sitrep Feature Flags Policy:
|
||||
1. Responsibility for each Feature Flag and adherence to this policy lies with the developer implementing it.
|
||||
2. Feature Flags shall only be implemented in frontend UI-elements, to enable or disable access to certain functionalities.
|
||||
2. Feature Flags shall only be implemented in frontend UI-elements, to enable or disable access to certain functionalities.
|
||||
3. No Feature Flags must be implemented in back-end code. However, this does not apply to backend API endpoints needed for frontend Feature Flags.
|
||||
4. A list of all prior and currently active Feature Flags shall be kept (specifics tbd) and include at least the following information: intended Use Case, intended life-span, date implemented, date removed, @f-eld-ch/core-development member name.
|
||||
5. The intended life-span of every Feature Flag must not exceed 3 months as of integration of the Feature Flag. Furthermore, it must be appropriately documented in the code using line comments and in the list (point 4).
|
||||
5. The intended life-span of every Feature Flag must not exceed 3 months as of integration of the Feature Flag. Furthermore, it must be appropriately documented in the code using line comments and in the list (point 4).
|
||||
6. In case an extension of the life-span is warranted (at least 2 members of the development-team concur), the life-span may be prolonged once by a maximum of 3 months. The extension must be documented in the list (point 4). After this, no further extensions must be made.
|
||||
7. If a Feature Flag is used for feature testing, the organization/user who requested the feature shall be the early-adopter for said feature. If no request was made, early-adopters are chosen by the development team.
|
||||
8. When a Feature is released to all users, the corresponding Feature Flag shall be removed within the next 7 days, regardless if the intended life-span is up or not.
|
||||
9. All members of @f-eld-ch/core-development must at all times have access to all features under Feature Flags irrespective of their status.
|
||||
|
||||
|
||||
## How to add new Feature Flags
|
||||
|
||||
1. Update the feature flag file in the corresponding directory, e.g. ui/features.yaml according to the flipt documentation.
|
||||
2. Create a new PR and the syntax will be checked by the CI/CD pipeline.
|
||||
3. After the PR is merged, the feature flag container (ghcr.io/f-eld-ch/sitrep/featureflags:develop) will be build and automatically pulled by the flipt service.
|
||||
|
||||
This means that if you want to use a new feature flag, create it first, make a PR and only then start the development works that depend on the feature flag.
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
version: "1.2"
|
||||
namespace: sitrep-ui
|
||||
flags:
|
||||
# nimdanitro: flag for rolling out tasks Module, remove after tasks modules is GA
|
||||
- key: show-tasks
|
||||
name: Show Tasks
|
||||
type: BOOLEAN_FLAG_TYPE
|
||||
description: Show the task module (Pendenzen)
|
||||
enabled: false
|
||||
rollouts:
|
||||
- segment:
|
||||
key: Development
|
||||
value: true
|
||||
# nimdanitro: flag for rolling out Resources Module, remove after resources modules is GA
|
||||
- key: show-resources
|
||||
name: Show Resources
|
||||
type: BOOLEAN_FLAG_TYPE
|
||||
description: Show the resources module (Mittel)
|
||||
enabled: false
|
||||
rollouts:
|
||||
- segment:
|
||||
key: Development
|
||||
value: true
|
||||
segments:
|
||||
- key: Development
|
||||
name: Development
|
||||
constraints:
|
||||
- type: STRING_COMPARISON_TYPE
|
||||
property: domain
|
||||
operator: isoneof
|
||||
value: '["dev.sitrep.ch","localhost","127.0.0.1"]'
|
||||
match_type: ALL_MATCH_TYPE
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; style-src-attr 'self'; child-src 'none'; worker-src 'self' blob:; script-src-elem 'self' https://api.mapbox.com; connect-src 'self' ws://* wss://* https://*;"
|
||||
content="default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; style-src-attr 'self'; child-src 'none'; worker-src 'self' blob:; script-src 'self' 'wasm-unsafe-eval'; script-src-elem 'self' https://api.mapbox.com; connect-src 'self' ws://* wss://* https://*;"
|
||||
/>
|
||||
<meta name="description" content="SitRep" />
|
||||
<link rel="icon" href="/favicon-48x48.ico" sizes="48x48" />
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@mapbox/mapbox-gl-draw": "~1.4.3",
|
||||
"@openfeature/core": "^1.4.0",
|
||||
"@openfeature/flipt-web-provider": "^0.1.0",
|
||||
"@openfeature/react-sdk": "^0.4.6",
|
||||
"@openfeature/web-sdk": "^1.2.4",
|
||||
"@turf/bearing": "^7.1.0",
|
||||
"@turf/center": "^7.1.0",
|
||||
"@turf/helpers": "^7.1.0",
|
||||
|
||||
+91
-88
@@ -23,7 +23,8 @@ import { UserState } from "types";
|
||||
import { UserContext } from "utils";
|
||||
import MessageSheet from "views/journal/MessageSheet";
|
||||
import { Layout, LayoutMarginLess } from "views/Layout";
|
||||
import { default as client } from "./client";
|
||||
import { default as client } from "client";
|
||||
import { Provider as FeatureFlagProvider } from "FeatureFlags";
|
||||
|
||||
const Map = lazy(() => import("views/map"));
|
||||
|
||||
@@ -65,126 +66,128 @@ function App() {
|
||||
return (
|
||||
<UserContext.Provider value={userState}>
|
||||
<ApolloProvider client={client}>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/incident">
|
||||
<Route
|
||||
path="list"
|
||||
element={
|
||||
<Layout>
|
||||
<IncidentList />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="new"
|
||||
element={
|
||||
<Layout>
|
||||
<IncidentNew />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path=":incidentId">
|
||||
<FeatureFlagProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/incident">
|
||||
<Route
|
||||
path="edit"
|
||||
path="list"
|
||||
element={
|
||||
<Layout>
|
||||
<IncidentEditor />
|
||||
<IncidentList />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="new"
|
||||
element={
|
||||
<Layout>
|
||||
<IncidentNew />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="journal">
|
||||
<Route path=":incidentId">
|
||||
<Route
|
||||
path="view"
|
||||
path="edit"
|
||||
element={
|
||||
<Layout>
|
||||
<JournalOverview />
|
||||
<IncidentEditor />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="journal">
|
||||
<Route
|
||||
path="view"
|
||||
element={
|
||||
<Layout>
|
||||
<JournalOverview />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="new"
|
||||
element={
|
||||
<Layout>
|
||||
<JournalNew />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=":journalId/edit"
|
||||
element={
|
||||
<Layout>
|
||||
<JournalEditor />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=":journalId"
|
||||
element={
|
||||
<Layout>
|
||||
<JournalMessageList showControls={false} autoScroll={true} />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=":journalId/messages/:messageId"
|
||||
element={
|
||||
<Layout>
|
||||
<MessageSheet />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="resources"
|
||||
element={
|
||||
<Layout>
|
||||
<ResourcesList />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="new"
|
||||
path="map"
|
||||
element={
|
||||
<LayoutMarginLess>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Map />
|
||||
</Suspense>
|
||||
</LayoutMarginLess>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="tasks"
|
||||
element={
|
||||
<Layout>
|
||||
<JournalNew />
|
||||
<TaskList />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=":journalId/edit"
|
||||
path="requests"
|
||||
element={
|
||||
<Layout>
|
||||
<JournalEditor />
|
||||
<RequestList />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=":journalId"
|
||||
path="soma"
|
||||
element={
|
||||
<Layout>
|
||||
<JournalMessageList showControls={false} autoScroll={true} />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path=":journalId/messages/:messageId"
|
||||
element={
|
||||
<Layout>
|
||||
<MessageSheet />
|
||||
<ImmediateMeasuresList />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="resources"
|
||||
element={
|
||||
<Layout>
|
||||
<ResourcesList />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="map"
|
||||
element={
|
||||
<LayoutMarginLess>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Map />
|
||||
</Suspense>
|
||||
</LayoutMarginLess>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="tasks"
|
||||
element={
|
||||
<Layout>
|
||||
<TaskList />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="requests"
|
||||
element={
|
||||
<Layout>
|
||||
<RequestList />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="soma"
|
||||
element={
|
||||
<Layout>
|
||||
<ImmediateMeasuresList />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="/" element={<Navigate to="/incident/list" />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<Route path="/" element={<Navigate to="/incident/list" />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</FeatureFlagProvider>
|
||||
</ApolloProvider>
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { EvaluationContext, OpenFeatureProvider, OpenFeature, InMemoryProvider } from "@openfeature/react-sdk";
|
||||
import { FliptWebProvider } from "@openfeature/flipt-web-provider";
|
||||
import { PropsWithChildren, useEffect, useContext } from "react";
|
||||
|
||||
import { UserContext } from "utils";
|
||||
|
||||
const localFlagConfig = {
|
||||
"new-message": {
|
||||
disabled: false,
|
||||
variants: {
|
||||
on: true,
|
||||
off: false,
|
||||
},
|
||||
defaultVariant: "on",
|
||||
contextEvaluator: (context: EvaluationContext) => {
|
||||
if (context.silly) {
|
||||
return "on";
|
||||
}
|
||||
return "off";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Provider = (props: PropsWithChildren) => {
|
||||
const { children } = props;
|
||||
const userState = useContext(UserContext);
|
||||
|
||||
useEffect(() => {
|
||||
const fliptProvider = new FliptWebProvider("sitrep-ui", { url: "https://flipt.sitrep.ch" });
|
||||
OpenFeature.setProvider("local", new InMemoryProvider(localFlagConfig));
|
||||
OpenFeature.setProvider(fliptProvider);
|
||||
}, []);
|
||||
|
||||
// sync the evaulation context here, so far only depends on domain and UserContext state
|
||||
useEffect(() => {
|
||||
const context = {
|
||||
domain: document.location.host.split(":")[0],
|
||||
email: userState.email,
|
||||
};
|
||||
OpenFeature.setContext(context);
|
||||
|
||||
return () => {
|
||||
console.log("closing openfeature provider");
|
||||
OpenFeature.close();
|
||||
};
|
||||
}, [userState]);
|
||||
|
||||
return <OpenFeatureProvider>{children}</OpenFeatureProvider>;
|
||||
};
|
||||
|
||||
export { Provider };
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import classNames from "classnames";
|
||||
import { FunctionComponent, useContext, useEffect, useState } from "react";
|
||||
import { useBooleanFlagValue } from "@openfeature/react-sdk";
|
||||
|
||||
import { faCalendar, faClock } from "@fortawesome/free-regular-svg-icons";
|
||||
import logo from "assets/logo.svg";
|
||||
@@ -39,6 +40,9 @@ const Navbar: FunctionComponent<{ isActive?: boolean }> = ({ isActive = false })
|
||||
"is-active": isMenuActive,
|
||||
});
|
||||
|
||||
const showResources = useBooleanFlagValue("show-resources", false);
|
||||
const showTasks = useBooleanFlagValue("show-tasks", false);
|
||||
|
||||
return (
|
||||
<nav className="navbar is-fixed-top is-hidden-print">
|
||||
<div className="navbar-brand">
|
||||
@@ -90,8 +94,8 @@ const Navbar: FunctionComponent<{ isActive?: boolean }> = ({ isActive = false })
|
||||
</div>
|
||||
</div>
|
||||
<JournalNavBar />
|
||||
{/* <ResourcesNavBar />
|
||||
<TasksNavBar /> */}
|
||||
{showResources ? <ResourcesNavBar /> : <></>}
|
||||
{showTasks ? <TasksNavBar /> : <></>}
|
||||
<MapNavBar />
|
||||
</div>
|
||||
<div className="navbar-end">
|
||||
|
||||
@@ -13,11 +13,13 @@ import { Message, MessageDivision, SaveMessageTriageData, SaveMessageTriageVars
|
||||
import { NewForm as TaskNew } from "../measures/tasks";
|
||||
import { default as JournalMessage } from "./Message";
|
||||
import { GetJournalMessages, GetMessageForTriage, SaveMessageTriage } from "./graphql";
|
||||
import { useBooleanFlagValue } from "@openfeature/react-sdk";
|
||||
|
||||
function Triage(props: { message: Message | undefined; setMessage: (message: Message | undefined) => void }) {
|
||||
const { message, setMessage } = props;
|
||||
const { journalId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
const showTasks = useBooleanFlagValue("show-tasks", false);
|
||||
|
||||
const [loadMessage, { loading, error, data }] = useLazyQuery<TriageMessageData, TriageMessageVars>(
|
||||
GetMessageForTriage,
|
||||
@@ -175,10 +177,12 @@ function Triage(props: { message: Message | undefined; setMessage: (message: Mes
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column">
|
||||
<h3 className="title is-size-5">{t("createNewTask")}</h3>
|
||||
<TaskNew />
|
||||
</div>
|
||||
{showTasks && (
|
||||
<div className="column">
|
||||
<h3 className="title is-size-5">{t("createNewTask")}</h3>
|
||||
<TaskNew />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2280,6 +2280,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@flipt-io/flipt-client-browser@npm:^0.0.17":
|
||||
version: 0.0.17
|
||||
resolution: "@flipt-io/flipt-client-browser@npm:0.0.17"
|
||||
checksum: 10/284e5924b3756efd63ae5a3902d54534cd1266463557015b13b00822cf6fc10ee058d6634966519982326ec13e7423f08e65bdd61ed9a009a3bff75eef208d83
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fortawesome/fontawesome-common-types@npm:6.6.0":
|
||||
version: 6.6.0
|
||||
resolution: "@fortawesome/fontawesome-common-types@npm:6.6.0"
|
||||
@@ -3235,6 +3242,44 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@openfeature/core@npm:^1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "@openfeature/core@npm:1.4.0"
|
||||
checksum: 10/7a253920b05659e4590c4b1c07f6fced25f5ff08ff9092ad6608f5c7f42075e44d0add94f95690414a1405e54c73e511db2963bfcc2e78f26f3f7861f755993d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@openfeature/flipt-web-provider@npm:^0.1.0":
|
||||
version: 0.1.0
|
||||
resolution: "@openfeature/flipt-web-provider@npm:0.1.0"
|
||||
dependencies:
|
||||
"@flipt-io/flipt-client-browser": "npm:^0.0.17"
|
||||
tslib: "npm:^2.3.0"
|
||||
peerDependencies:
|
||||
"@openfeature/web-sdk": ^1.0.0
|
||||
checksum: 10/d9d80e2e343f77898639b29aee6bdcf2e65ef82cb4b7d350122481b31e30c668cb175e779edbcf51d70b78add960eb7399c976b52584bebf904e0e5863eb7fe1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@openfeature/react-sdk@npm:^0.4.6":
|
||||
version: 0.4.6
|
||||
resolution: "@openfeature/react-sdk@npm:0.4.6"
|
||||
peerDependencies:
|
||||
"@openfeature/web-sdk": ^1.2.2
|
||||
react: ">=16.8.0"
|
||||
checksum: 10/bac78bc4e201739ae2903f7790195629566d5ace054cc30404d691838d4806b474ae98ae8bb8b87bbfdd015efa1ef6f9e8ffdeee9160b197d278536da3ffe7c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@openfeature/web-sdk@npm:^1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "@openfeature/web-sdk@npm:1.2.4"
|
||||
peerDependencies:
|
||||
"@openfeature/core": 1.4.0
|
||||
checksum: 10/3dcef55af73050638c8c15fccbb23fb20d5efd9619b822d86b926c11b2c042191e0e3a98b5412fc0fe66ef9506941d6dbf2a28caa29847e54b683386fc7d0dc0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@parcel/watcher-android-arm64@npm:2.4.1":
|
||||
version: 2.4.1
|
||||
resolution: "@parcel/watcher-android-arm64@npm:2.4.1"
|
||||
@@ -12647,6 +12692,10 @@ __metadata:
|
||||
"@fortawesome/free-solid-svg-icons": "npm:^6.6.0"
|
||||
"@fortawesome/react-fontawesome": "npm:^0.2.2"
|
||||
"@mapbox/mapbox-gl-draw": "npm:~1.4.3"
|
||||
"@openfeature/core": "npm:^1.4.0"
|
||||
"@openfeature/flipt-web-provider": "npm:^0.1.0"
|
||||
"@openfeature/react-sdk": "npm:^0.4.6"
|
||||
"@openfeature/web-sdk": "npm:^1.2.4"
|
||||
"@testing-library/dom": "npm:^10.4.0"
|
||||
"@testing-library/jest-dom": "npm:^6.6.2"
|
||||
"@testing-library/react": "npm:^16.0.1"
|
||||
|
||||
Reference in New Issue
Block a user