diff --git a/.github/workflows/core-web.unit.yaml b/.github/workflows/core-web.unit.yaml
index 9a30a0adc6..312d8dce20 100644
--- a/.github/workflows/core-web.unit.yaml
+++ b/.github/workflows/core-web.unit.yaml
@@ -60,6 +60,8 @@ jobs:
CYPRESS_KEYCLOAK_CLIENT_ID: ${{ secrets.CYPRESS_KEYCLOAK_CLIENT_ID }}
CYPRESS_KEYCLOAK_IDP_HINT: ${{ secrets.CYPRESS_KEYCLOAK_IDP_HINT }}
CYPRESS_KEYCLOAK_RESOURCE: ${{ secrets.CYPRESS_KEYCLOAK_RESOURCE }}
+ CYPRESS_FLAGSMITH_URL: ${{ secrets.CYPRESS_FLAGSMITH_URL }}
+ CYPRESS_FLAGSMITH_KEY: ${{ secrets.CYPRESS_FLAGSMITH_KEY }}
- name: Upload cypress video
uses: actions/upload-artifact@v3
if: always()
diff --git a/.github/workflows/minespace.unit.yaml b/.github/workflows/minespace.unit.yaml
index 8b34dbaddd..57a56c2405 100644
--- a/.github/workflows/minespace.unit.yaml
+++ b/.github/workflows/minespace.unit.yaml
@@ -57,6 +57,8 @@ jobs:
CYPRESS_KEYCLOAK_CLIENT_ID: ${{ secrets.CYPRESS_KEYCLOAK_CLIENT_ID }}
CYPRESS_KEYCLOAK_IDP_HINT: ${{ secrets.CYPRESS_KEYCLOAK_IDP_HINT }}
CYPRESS_KEYCLOAK_RESOURCE: ${{ secrets.CYPRESS_KEYCLOAK_RESOURCE }}
+ CYPRESS_FLAGSMITH_URL: ${{ secrets.CYPRESS_FLAGSMITH_URL }}
+ CYPRESS_FLAGSMITH_KEY: ${{ secrets.CYPRESS_FLAGSMITH_KEY }}
- name: Upload cypress video
uses: actions/upload-artifact@v3
diff --git a/docs/feature_flags.mds b/docs/feature_flags.mds
new file mode 100644
index 0000000000..9d0aafd375
--- /dev/null
+++ b/docs/feature_flags.mds
@@ -0,0 +1,120 @@
+# Feature Flags
+
+MDS Supports feature flags both for our frontend and backend apps using [Flagsmith](https://docs.flagsmith.com).
+
+## Frontend
+
+A couple of options exists to check for a feature flag in core-web and minespace-web.
+
+### Defining feature flag
+
+Feature flags for use in core / minespace have to be defined in the `Feature` enum located in `featureFlag.ts` in the common package.
+The values of this enum, must match the name of a feature flag defined in Flagsmith.
+
+```typescript
+export enum Feature {
+ MAJOR_PROJECT_ARCHIVE_FILE = "major_project_archive_file",
+ DOCUMENTS_REPLACE_FILE = "major_project_replace_file",
+ MAJOR_PROJECT_ALL_DOCUMENTS = "major_project_all_documents",
+ MAJOR_PROJECT_DECISION_PACKAGE = "major_project_decision_package",
+ FLAGSMITH = "flagsmith",
+ TSF_V2 = "tsf_v2",
+}
+```
+
+### Using `useFeatureFlag` hook
+
+Preferred method if using feature flag in a functional React component.
+
+```typescript
+import { useFeatureFlag } from "@common/providers/featureFlags/useFeatureFlag";
+import { Feature } from "@mds/common";
+
+const ThisIsAReactComponent = () => {
+ const { isFeatureEnabled } = useFeatureFlag();
+
+ return (
+ {isFeatureEnabled(Feature.TSF_V2)?
SuperSecretFeature
:
Sorry, you can't access this
}
+ );
+};
+```
+
+### Using `withFeatureFlag` HOC
+
+Alternative method if using feature flag in a React component and you cannot use hooks (for example in class components).
+
+```typescript
+import withFeatureFlag from "@common/providers/featureFlags/withFeatureFlag";
+import { Feature } from "@mds/common";
+
+class ThisIsAReactComponent {
+ render() {
+ return (
+ {props.isFeatureEnabled(Feature.TSF_V2)?
SuperSecretFeature
:
Sorry, you can't access this
}
+ );
+ }
+};
+
+export default withFeatureFlag(ThisIsAReactComponent);
+```
+
+### Using FeatureFlagGuard
+Need to restrict a route based on a feature flag?
+
+You can use the `FeatureFlagGuard` and pass along the feature you want to check for.
+If it's not enabled, you get a nice little "you don't have access" notice.
+
+```typescript
+import { Feature } from "@mds/common";
+import FeatureFlagGuard from "@/components/common/featureFlag.guard";
+
+
+const DamsPage: React.FC = (props) => {
+ return (
+
+ ALL THE DAMS
+
+ );
+};
+
+const mapStateToProps = (state: RootState) => ({
+ initialValues: getDam(state),
+ tsf: getTsf(state),
+});
+
+const mapDispatchToProps = (dispatch) =>
+ bindActionCreators(
+ { createDam, updateDam, fetchMineRecordById, storeTsf, storeDam, submit },
+ dispatch
+ );
+
+export default compose(
+ connect(mapStateToProps, mapDispatchToProps),
+)(withRouter(FeatureFlagGuard(Feature.TSF_V2)(DamsPage)));
+```
+
+### Using FeatureFlag directly (discouraged)
+
+If you need access to a feature flag outside of a react context you can use `featureFlag.ts` directly.
+Please use the other methods above as far as you can.
+
+```typescript
+import { isFeatureEnabled } from @mds/common;
+import { Feature } from "@mds/common";
+
+console.log(isFeatureEnabled(Feature.TSF_V2));
+
+```
+
+## Core API
+
+How to use:
+1. Define a feature flag in the `Feature` enum (feature_flag.py). The value of the enum must match a feature flag defined in Flagsmith.
+2. You can use the `is_feature_enabled` function to check if the given flag is enabled.
+
+```python
+from app.api.utils.feature_flag import is_feature_enabled, Feature
+
+if is_feature_enabled(Feature.TSF_V2):
+ # Do something if TSF_V2 feature is enabled
+```
diff --git a/services/common/package.json b/services/common/package.json
index 7829c63797..2592484eea 100644
--- a/services/common/package.json
+++ b/services/common/package.json
@@ -13,10 +13,14 @@
"dependencies": {
"@babel/runtime": "7.15.3",
"@babel/runtime-corejs3": "7.15.3",
+ "flagsmith": "3.19.1",
"query-string": "5.1.1",
- "react": "16.13.1",
"ts-loader": "8.4.0"
},
+ "peerDependencies": {
+ "react": "16.13.1",
+ "react-dom": "16.13.1"
+ },
"devDependencies": {
"@babel/core": "7.15.0",
"@babel/plugin-proposal-class-properties": "7.14.5",
diff --git a/services/common/src/constants/environment.ts b/services/common/src/constants/environment.ts
index 30f6444be0..d1db04928d 100644
--- a/services/common/src/constants/environment.ts
+++ b/services/common/src/constants/environment.ts
@@ -11,6 +11,8 @@ export const DEFAULT_ENVIRONMENT = {
keycloak_clientId: "mines-digital-services-mds-public-client-4414",
keycloak_idpHint: "test",
keycloak_url: "https://test.loginproxy.gov.bc.ca/auth",
+ flagsmithKey: "4Eu9eEMDmWVEHKDaKoeWY7",
+ flagsmithUrl: "https://mds-flags-dev.apps.silver.devops.gov.bc.ca/api/v1/",
};
export const ENVIRONMENT = {
@@ -19,6 +21,9 @@ export const ENVIRONMENT = {
matomoUrl: "",
filesystemProviderUrl: "",
environment: "",
+ flagsmithKey: "",
+ flagsmithUrl: "",
+ _loaded: false,
};
export const KEYCLOAK = {
@@ -43,7 +48,15 @@ export const KEYCLOAK = {
userInfoURL: "",
};
-export function setupEnvironment(apiUrl, docManUrl, filesystemProviderUrl, matomoUrl, environment) {
+export function setupEnvironment(
+ apiUrl,
+ docManUrl,
+ filesystemProviderUrl,
+ matomoUrl,
+ environment,
+ flagsmithKey,
+ flagsmithUrl
+) {
if (!apiUrl) {
throw new Error("apiUrl Is Mandatory");
}
@@ -63,12 +76,22 @@ export function setupEnvironment(apiUrl, docManUrl, filesystemProviderUrl, matom
if (!environment) {
throw new Error("environment Is Mandatory");
}
+ if (!flagsmithKey) {
+ throw new Error("flagsmithKey Is Mandatory");
+ }
+ if (!flagsmithUrl) {
+ throw new Error("flagsmithUrl Is Mandatory");
+ }
ENVIRONMENT.apiUrl = apiUrl;
ENVIRONMENT.docManUrl = docManUrl;
ENVIRONMENT.filesystemProviderUrl = filesystemProviderUrl;
ENVIRONMENT.matomoUrl = matomoUrl;
ENVIRONMENT.environment = environment || "development";
+ ENVIRONMENT.flagsmithKey = flagsmithKey;
+ ENVIRONMENT.flagsmithUrl = flagsmithUrl;
+
+ ENVIRONMENT._loaded = true;
}
export function setupKeycloak(
diff --git a/services/common/src/utils/featureFlag.ts b/services/common/src/utils/featureFlag.ts
index 864981fceb..3613b0d255 100644
--- a/services/common/src/utils/featureFlag.ts
+++ b/services/common/src/utils/featureFlag.ts
@@ -1,13 +1,22 @@
-import { detectProdEnvironment as IN_PROD } from "./environmentUtils";
+import flagsmith from "flagsmith";
+// Name of feature flags. These correspond to feature flags defined in flagsmith.
export enum Feature {
- MAJOR_PROJECT_ARCHIVE_FILE,
- DOCUMENTS_REPLACE_FILE,
+ MAJOR_PROJECT_ARCHIVE_FILE = "major_project_archive_file",
+ DOCUMENTS_REPLACE_FILE = "major_project_replace_file",
+ MAJOR_PROJECT_ALL_DOCUMENTS = "major_project_all_documents",
+ MAJOR_PROJECT_DECISION_PACKAGE = "major_project_decision_package",
+ FLAGSMITH = "flagsmith",
+ TSF_V2 = "tsf_v2",
}
-const Flags = {
- [Feature.MAJOR_PROJECT_ARCHIVE_FILE]: !IN_PROD(),
- [Feature.DOCUMENTS_REPLACE_FILE]: !IN_PROD(),
+export const initializeFlagsmith = async (flagsmithUrl, flagsmithKey) => {
+ await flagsmith.init({
+ api: flagsmithUrl,
+ environmentID: flagsmithKey,
+ cacheFlags: true,
+ enableAnalytics: true,
+ });
};
/**
@@ -16,9 +25,5 @@ const Flags = {
* @returns true if the given feature is enabled
*/
export const isFeatureEnabled = (feature: Feature) => {
- if (feature in Flags) {
- return Flags[feature];
- }
-
- return false;
+ return flagsmith.hasFeature(feature);
};
diff --git a/services/common/webpack.config.ts b/services/common/webpack.config.ts
index ecb61cff42..8bc68df837 100755
--- a/services/common/webpack.config.ts
+++ b/services/common/webpack.config.ts
@@ -9,6 +9,7 @@ module.exports = (opts) => {
const mode = opts.development ? "development" : "production";
console.log(`\n============= Webpack Mode : ${mode} =============\n`);
return {
+ watch: mode === "development",
mode,
entry: "./src/index.ts",
output: {
@@ -23,7 +24,7 @@ module.exports = (opts) => {
// Increase file change poll interval to reduce
// CPU usage on some operating systems.
poll: 2500,
- ignored: /node_modules/,
+ ignored: /node_modules|dist/,
},
module: {
rules: [
diff --git a/services/core-api/.env-example b/services/core-api/.env-example
index e1a4300490..b0768a6ff0 100644
--- a/services/core-api/.env-example
+++ b/services/core-api/.env-example
@@ -32,6 +32,10 @@ VFCBC_TOKEN_URL=
VFCBC_CLIENT_ID=mms_srv1
VFCBC_CLIENT_SECRET=
+FLAGSMITH_URL=https://mds-flags-dev.apps.silver.devops.gov.bc.ca/api/v1/
+FLAGSMITH_KEY=4Eu9eEMDmWVEHKDaKoeWY7
+FLAGSMITH_ENABLE_LOCAL_EVALUTION=false
+
DOCUMENT_MANAGER_URL=http://document_manager_backend:5001
CACHE_REDIS_HOST=redis
diff --git a/services/core-api/app/api/parties/party_appt/resources/mine_party_appt_resource.py b/services/core-api/app/api/parties/party_appt/resources/mine_party_appt_resource.py
index 951b3a6d57..ee91cd549c 100644
--- a/services/core-api/app/api/parties/party_appt/resources/mine_party_appt_resource.py
+++ b/services/core-api/app/api/parties/party_appt/resources/mine_party_appt_resource.py
@@ -23,6 +23,8 @@
from app.config import Config
from app.api.activity.utils import trigger_notification
+from app.api.utils.feature_flag import is_feature_enabled, Feature
+
class MinePartyApptResource(Resource, UserMixin):
parser = CustomReqparser()
@@ -202,8 +204,7 @@ def post(self, mine_party_appt_guid=None):
data.get('mine_party_appt_type_code')).description
raise BadRequest(f'Date ranges for {mpa_type_name} must not overlap')
- if Config.ENVIRONMENT_NAME != 'prod':
- # TODO: Remove this once TSF functionality is ready to go live
+ if is_feature_enabled(Feature.TSF_V2):
if mine_party_appt_type_code == "EOR":
trigger_notification(
diff --git a/services/core-api/app/api/projects/project_summary/resources/project_summary_document_upload.py b/services/core-api/app/api/projects/project_summary/resources/project_summary_document_upload.py
index 3bd336fff7..cbe3ebb70e 100644
--- a/services/core-api/app/api/projects/project_summary/resources/project_summary_document_upload.py
+++ b/services/core-api/app/api/projects/project_summary/resources/project_summary_document_upload.py
@@ -8,6 +8,7 @@
from app.api.mines.mine.models.mine import Mine
from app.api.services.document_manager_service import DocumentManagerService
from app.config import Config
+from app.api.utils.feature_flag import is_feature_enabled, Feature
class ProjectSummaryDocumentUploadResource(Resource, UserMixin):
@api.doc(
@@ -26,9 +27,7 @@ def post(self, project_guid, project_summary_guid):
if not mine:
raise NotFound('Mine not found')
- # FEATURE FLAG: DOCUMENTS_REPLACE_FILE
- if Config.ENVIRONMENT_NAME != 'prod':
- # TODO: Remove the ENV check and else part when 5273 is ready to go live
+ if is_feature_enabled(Feature.MAJOR_PROJECT_REPLACE_FILE):
return DocumentManagerService.validateFileNameAndInitializeFileUploadWithDocumentManager(
request, mine, project_guid, 'project_summaries')
else:
diff --git a/services/core-api/app/api/utils/feature_flag.py b/services/core-api/app/api/utils/feature_flag.py
new file mode 100644
index 0000000000..10b77f208f
--- /dev/null
+++ b/services/core-api/app/api/utils/feature_flag.py
@@ -0,0 +1,24 @@
+from enum import Enum
+from flagsmith import Flagsmith
+from app.config import Config
+from flask import current_app
+
+
+class Feature(Enum):
+ TSF_V2='tsf_v2'
+ MAJOR_PROJECT_REPLACE_FILE='major_project_replace_file'
+
+ def __str__(self):
+ return self.value
+
+flagsmith = Flagsmith(
+ environment_id = Config.FLAGSMITH_KEY,
+ api = Config.FLAGSMITH_URL,
+)
+
+def is_feature_enabled(feature):
+ try:
+ return flagsmith.has_feature(feature) and flagsmith.feature_enabled(feature)
+ except Exception as e:
+ current_app.logger.error(f'Failed to look up feature flag for: {feature}. ' + str(e))
+ return False
diff --git a/services/core-api/app/config.py b/services/core-api/app/config.py
index b0065cb4c3..902663c460 100644
--- a/services/core-api/app/config.py
+++ b/services/core-api/app/config.py
@@ -105,6 +105,7 @@ def JWT_ROLE_CALLBACK(jwt_dict):
NROS_NOW_CLIENT_ID = os.environ.get('NROS_NOW_CLIENT_ID', None)
NROS_NOW_TOKEN_URL = os.environ.get('NROS_NOW_TOKEN_URL', None)
NROS_NOW_CLIENT_SECRET = os.environ.get('NROS_NOW_CLIENT_SECRET', None)
+
# Cache settings
CACHE_TYPE = os.environ.get('CACHE_TYPE', 'redis')
@@ -122,6 +123,13 @@ def JWT_ROLE_CALLBACK(jwt_dict):
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ENGINE_OPTIONS = {'pool_timeout': 300, 'max_overflow': 20}
+ # Flagsmith
+ FLAGSMITH_URL=os.environ.get('FLAGSMITH_URL', 'https://mds-flags-dev.apps.silver.devops.gov.bc.ca/api/v1/')
+ FLAGSMITH_KEY=os.environ.get('FLAGSMITH_KEY', '4Eu9eEMDmWVEHKDaKoeWY7')
+
+ # Enable flag caching and evalutation. If set to True, FLAGSMITH_KEY must be set to a server side FLAGSMITH_KEY
+ FLAGSMITH_ENABLE_LOCAL_EVALUTION=os.environ.get('FLAGSMITH_ENABLE_LOCAL_EVALUTION', 'false') == 'true'
+
# NROS
NROS_CLIENT_SECRET = os.environ.get('NROS_CLIENT_SECRET', None)
NROS_CLIENT_ID = os.environ.get('NROS_CLIENT_ID', None)
diff --git a/services/core-api/requirements.txt b/services/core-api/requirements.txt
index a1a28a99f9..9ff27fc404 100644
--- a/services/core-api/requirements.txt
+++ b/services/core-api/requirements.txt
@@ -1,5 +1,6 @@
cached-property==1.5.1
factory-boy==2.12.0
+flagsmith==2.0.1
Flask==1.1.2
Flask-Caching==1.8.0
Flask-Cors==3.0.9
diff --git a/services/core-web/.env-example b/services/core-web/.env-example
index b9f292cfa9..6f7b2c76b2 100644
--- a/services/core-web/.env-example
+++ b/services/core-web/.env-example
@@ -2,6 +2,9 @@ API_URL=http://localhost:5000
DOCUMENT_MANAGER_URL=http://localhost:5001
MATOMO_URL=http://localhost:5001
FILESYSTEM_PROVIDER_URL=http://localhost:62870/file-api/AmazonS3Provider/
+FLAGSMITH_URL=https://mds-flags-dev.apps.silver.devops.gov.bc.ca/api/v1/
+FLAGSMITH_KEY=4Eu9eEMDmWVEHKDaKoeWY7
+
NODE_ENV=development
KEYCLOAK_CLIENT_ID=mines-digital-services-mds-public-client-4414
@@ -12,7 +15,7 @@ KEYCLOAK_IDP_HINT=idir
BASE_PATH=
ASSET_PATH=/
-NODE_OPTIONS=--max_old_space_size=2048
+NODE_OPTIONS=--max_old_space_size=8096
CYPRESS_TEST_USER=cypress
CYPRESS_TEST_PASSWORD=cypress
@@ -26,4 +29,6 @@ CYPRESS_MATOMO_URL=https://matomo-4c2ba9-test.apps.silver.devops.gov.bc.ca/
CYPRESS_KEYCLOAK_CLIENT_ID=mines-digital-services-mds-public-client-4414
CYPRESS_KEYCLOAK_RESOURCE=mines-digital-services-mds-public-client-4414
CYPRESS_KEYCLOAK_IDP_HINT=idir
-CYPRESS_FILE_SYSTEM_PROVIDER_URL=https://mds-dev.apps.silver.devops.gov.bc.ca/file-api/AmazonS3Provider/
\ No newline at end of file
+CYPRESS_FILE_SYSTEM_PROVIDER_URL=https://mds-dev.apps.silver.devops.gov.bc.ca/file-api/AmazonS3Provider/
+CYPRESS_FLAGSMITH_URL=https://mds-flags-dev.apps.silver.devops.gov.bc.ca/api/v1/
+CYPRESS_FLAGSMITH_KEY=4Eu9eEMDmWVEHKDaKoeWY7
diff --git a/services/core-web/README.md b/services/core-web/README.md
index 4938883c2f..4193b97210 100644
--- a/services/core-web/README.md
+++ b/services/core-web/README.md
@@ -68,6 +68,8 @@ CYPRESS_KEYCLOAK_URL
CYPRESS_ENVIRONMENT
CYPRESS_DOC_MAN_URL
CYPRESS_MATOMO_URL
+CYPRESS_FLAGSMITH_URL
+CYPRESS_FLAGSMITH_KEY
CYPRESS_KEYCLOAK_CLIENT_ID
CYPRESS_KEYCLOAK_RESOURCE
CYPRESS_KEYCLOAK_IDP_HINT
diff --git a/services/core-web/common/components/documents/ArchivedDocumentsSection.tsx b/services/core-web/common/components/documents/ArchivedDocumentsSection.tsx
index f8acfb78b5..b42400eaab 100644
--- a/services/core-web/common/components/documents/ArchivedDocumentsSection.tsx
+++ b/services/core-web/common/components/documents/ArchivedDocumentsSection.tsx
@@ -2,9 +2,10 @@ import React from "react";
import DocumentTable from "@/components/common/DocumentTable";
import { Typography } from "antd";
import { DeleteOutlined } from "@ant-design/icons";
-import { Feature, isFeatureEnabled } from "@mds/common";
+import { Feature } from "@mds/common";
import { MineDocument } from "@common/models/documents/document";
import { ColumnType } from "antd/es/table";
+import { useFeatureFlag } from "@common/providers/featureFlags/useFeatureFlag";
interface ArchivedDocumentsSectionProps {
documents: MineDocument[];
@@ -14,6 +15,8 @@ interface ArchivedDocumentsSectionProps {
}
const ArchivedDocumentsSection = (props: ArchivedDocumentsSectionProps) => {
+ const { isFeatureEnabled } = useFeatureFlag();
+
if (!isFeatureEnabled(Feature.MAJOR_PROJECT_ARCHIVE_FILE)) {
return <>>;
}
diff --git a/services/core-web/common/components/tailings/TailingsSummaryPage.tsx b/services/core-web/common/components/tailings/TailingsSummaryPage.tsx
index c280281e25..15b7699632 100644
--- a/services/core-web/common/components/tailings/TailingsSummaryPage.tsx
+++ b/services/core-web/common/components/tailings/TailingsSummaryPage.tsx
@@ -43,9 +43,9 @@ import {
getEngineersOfRecordOptions,
getQualifiedPersons,
} from "@common/selectors/partiesSelectors";
-import AuthorizationGuard from "@/HOC/AuthorizationGuard";
-import * as Permission from "@/constants/permissions";
import { ICreateTSF, IMine, ActionCreator } from "@mds/common";
+import { Feature } from "@mds/common";
+import FeatureFlagGuard from "@/components/common/featureFlag.guard";
interface TailingsSummaryPageProps {
form: string;
@@ -330,7 +330,6 @@ export default compose(
destroyOnUnmount: true,
onSubmit: () => {},
})
- // FEATURE FLAG: TSF
-)(
- withRouter(AuthorizationGuard(Permission.IN_TESTING)(TailingsSummaryPage)) as any
-) as FC;
+)(withRouter(FeatureFlagGuard(Feature.TSF_V2)(TailingsSummaryPage)) as any) as FC<
+ TailingsSummaryPageProps
+>;
diff --git a/services/core-web/common/components/tailings/dam/DamsPage.tsx b/services/core-web/common/components/tailings/dam/DamsPage.tsx
index 7524160cb7..baf6047c8a 100644
--- a/services/core-web/common/components/tailings/dam/DamsPage.tsx
+++ b/services/core-web/common/components/tailings/dam/DamsPage.tsx
@@ -17,12 +17,12 @@ import { storeDam } from "@common/actions/damActions";
import { storeTsf } from "@common/actions/tailingsActions";
import { EDIT_TAILINGS_STORAGE_FACILITY } from "@/constants/routes";
import DamForm from "./DamForm";
-import AuthorizationGuard from "@/HOC/AuthorizationGuard";
import { ADD_EDIT_DAM } from "@/constants/forms";
-import * as Permission from "@/constants/permissions";
import { ICreateDam, ITailingsStorageFacility } from "@mds/common";
import { ActionCreator } from "@/interfaces/actionCreator";
import { RootState } from "@/App";
+import { Feature } from "@mds/common";
+import FeatureFlagGuard from "@/components/common/featureFlag.guard";
interface DamsPageProps {
tsf: ITailingsStorageFacility;
@@ -165,5 +165,4 @@ export default compose(
resetForm(ADD_EDIT_DAM);
},
})
- // FEATURE FLAG: TSF
-)(withRouter(AuthorizationGuard(Permission.IN_TESTING)(DamsPage)) as any) as FC;
+)(withRouter(FeatureFlagGuard(Feature.TSF_V2)(DamsPage)) as any) as FC;
diff --git a/services/core-web/common/constants/environment.js b/services/core-web/common/constants/environment.js
index 3cfadfb945..dc144cb675 100644
--- a/services/core-web/common/constants/environment.js
+++ b/services/core-web/common/constants/environment.js
@@ -8,6 +8,8 @@ export const DEFAULT_ENVIRONMENT = {
matomoUrl: "https://matomo-4c2ba9-test.apps.silver.devops.gov.bc.ca/",
environment: "development",
filesystemProviderUrl: "http://localhost:62870/file-api/AmazonS3Provider/",
+ flagsmithKey: "4Eu9eEMDmWVEHKDaKoeWY7",
+ flagsmithUrl: "https://mds-flags-dev.apps.silver.devops.gov.bc.ca/api/v1/",
keycloak_resource: "mines-digital-services-mds-public-client-4414",
keycloak_clientId: "mines-digital-services-mds-public-client-4414",
keycloak_idpHint: "test",
@@ -20,6 +22,8 @@ export const ENVIRONMENT = {
matomoUrl: "",
filesystemProviderUrl: "",
environment: "",
+ flagsmithKey: "",
+ flagsmithUrl: "",
};
export const KEYCLOAK = {
diff --git a/services/core-web/common/providers/featureFlags/featureFlag.context.tsx b/services/core-web/common/providers/featureFlags/featureFlag.context.tsx
new file mode 100644
index 0000000000..8f7dea0dec
--- /dev/null
+++ b/services/core-web/common/providers/featureFlags/featureFlag.context.tsx
@@ -0,0 +1,5 @@
+import { createContext } from "react";
+
+const FeatureFlagContext = createContext(null);
+
+export default FeatureFlagContext;
diff --git a/services/core-web/common/providers/featureFlags/featureFlag.provider.tsx b/services/core-web/common/providers/featureFlags/featureFlag.provider.tsx
new file mode 100644
index 0000000000..84cd7cea6d
--- /dev/null
+++ b/services/core-web/common/providers/featureFlags/featureFlag.provider.tsx
@@ -0,0 +1,32 @@
+import React, { useCallback, useEffect, useState } from "react";
+import FeatureFlagContext from "./featureFlag.context";
+import { initializeFlagsmith, isFeatureEnabled } from "@mds/common";
+import { ENVIRONMENT } from "@mds/common";
+import flagsmith from "flagsmith";
+
+const FeatureFlagProvider = ({ children }) => {
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ setIsLoading(true);
+ await initializeFlagsmith(ENVIRONMENT.flagsmithUrl, ENVIRONMENT.flagsmithKey);
+ setIsLoading(false);
+ })();
+ }, [ENVIRONMENT._loaded]);
+
+ const checkFeatureFlag = useCallback((feature) => isFeatureEnabled(feature), [flagsmith]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default FeatureFlagProvider;
diff --git a/services/core-web/common/providers/featureFlags/useFeatureFlag.tsx b/services/core-web/common/providers/featureFlags/useFeatureFlag.tsx
new file mode 100644
index 0000000000..465cc8886a
--- /dev/null
+++ b/services/core-web/common/providers/featureFlags/useFeatureFlag.tsx
@@ -0,0 +1,10 @@
+import { useContext } from "react";
+import FeatureFlagContext from "./featureFlag.context";
+
+const useFeatureFlag = () => {
+ const context = useContext(FeatureFlagContext);
+
+ return context;
+};
+
+export { useFeatureFlag };
diff --git a/services/core-web/common/providers/featureFlags/withFeatureFlag.tsx b/services/core-web/common/providers/featureFlags/withFeatureFlag.tsx
new file mode 100644
index 0000000000..8c0548ad47
--- /dev/null
+++ b/services/core-web/common/providers/featureFlags/withFeatureFlag.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+import { useFeatureFlag } from "./useFeatureFlag";
+const withFeatureFlag = (Component) => {
+ return function WrappedComponent(props) {
+ const { isFeatureEnabled } = useFeatureFlag();
+
+ return ;
+ };
+};
+
+export default withFeatureFlag;
diff --git a/services/core-web/cypress/support/commands.ts b/services/core-web/cypress/support/commands.ts
index 6b8a542732..7f6e0b0505 100644
--- a/services/core-web/cypress/support/commands.ts
+++ b/services/core-web/cypress/support/commands.ts
@@ -40,6 +40,8 @@ Cypress.Commands.add("login", () => {
keycloak_url: Cypress.env("CYPRESS_KEYCLOAK_URL"),
keycloak_idpHint: Cypress.env("CYPRESS_KEYCLOAK_IDP_HINT"),
environment: Cypress.env("CYPRESS_ENVIRONMENT"),
+ flagsmithUrl: Cypress.env("CYPRESS_FLAGSMITH_URL"),
+ flagsmithKey: Cypress.env("CYPRESS_FLAGSMITH_KEY"),
};
cy.intercept("GET", environmentUrl, (req) => {
diff --git a/services/core-web/runner/server.js b/services/core-web/runner/server.js
index a82ba66640..952beeb160 100755
--- a/services/core-web/runner/server.js
+++ b/services/core-web/runner/server.js
@@ -47,6 +47,8 @@ app.get(`${BASE_PATH}/env`, (req, res) => {
keycloak_url: process.env.KEYCLOAK_URL,
keycloak_idpHint: process.env.KEYCLOAK_IDP_HINT,
environment: process.env.NODE_ENV,
+ flagsmithKey: process.env.FLAGSMITH_KEY,
+ flagsmithUrl: process.env.FLAGSMITH_URL,
});
});
diff --git a/services/core-web/src/components/common/DocumentTable.tsx b/services/core-web/src/components/common/DocumentTable.tsx
index 5ccfd5a518..ee9e508849 100644
--- a/services/core-web/src/components/common/DocumentTable.tsx
+++ b/services/core-web/src/components/common/DocumentTable.tsx
@@ -14,7 +14,7 @@ import { archiveMineDocuments } from "@common/actionCreators/mineActionCreator";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { modalConfig } from "@/components/modalContent/config";
-import { Feature, isFeatureEnabled } from "@mds/common";
+import { Feature } from "@mds/common";
import { SizeType } from "antd/lib/config-provider/SizeContext";
import { ColumnType, ColumnsType } from "antd/es/table";
import { FileOperations, MineDocument } from "@common/models/documents/document";
@@ -30,6 +30,7 @@ import { downloadFileFromDocumentManager } from "@common/utils/actionlessNetwork
import { getUserAccessData } from "@common/selectors/authenticationSelectors";
import { Dropdown, Button, MenuProps } from "antd";
import { DownOutlined } from "@ant-design/icons";
+import { useFeatureFlag } from "@common/providers/featureFlags/useFeatureFlag";
interface DocumentTableProps {
documents: MineDocument[];
@@ -85,6 +86,8 @@ export const DocumentTable = ({
const [documentTypeCode, setDocumentTypeCode] = useState("");
const [documentsCanBulkDropDown, setDocumentsCanBulkDropDown] = useState(false);
+ const { isFeatureEnabled } = useFeatureFlag();
+
const allowedTableActions = {
[FileOperations.View]: true,
[FileOperations.Download]: true,
diff --git a/services/core-web/src/components/common/featureFlag.guard.tsx b/services/core-web/src/components/common/featureFlag.guard.tsx
new file mode 100644
index 0000000000..c3190d1888
--- /dev/null
+++ b/services/core-web/src/components/common/featureFlag.guard.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import hoistNonReactStatics from "hoist-non-react-statics";
+import { Feature } from "@mds/common";
+import { useFeatureFlag } from "@common/providers/featureFlags/useFeatureFlag";
+import NullScreen from "./NullScreen";
+
+/**
+ * @constant FeatureFlagGuard - Higher Order Component that provides "feature flagging", in order to hide routes that
+ * should not be accessible based on a feature flag
+ */
+export const FeatureFlagGuard = (feature: Feature) => (WrappedComponent) => {
+ const featureFlagGuard = (props) => {
+ const { isFeatureEnabled } = useFeatureFlag();
+
+ if (isFeatureEnabled(feature)) {
+ return ;
+ }
+
+ return ;
+ };
+
+ hoistNonReactStatics(featureFlagGuard, WrappedComponent);
+
+ return featureFlagGuard;
+};
+
+export default FeatureFlagGuard;
diff --git a/services/core-web/src/components/mine/Projects/DecisionPackageTab.js b/services/core-web/src/components/mine/Projects/DecisionPackageTab.js
index 9d592260d5..3a20d4a26b 100644
--- a/services/core-web/src/components/mine/Projects/DecisionPackageTab.js
+++ b/services/core-web/src/components/mine/Projects/DecisionPackageTab.js
@@ -26,10 +26,11 @@ import * as FORM from "@/constants/forms";
import { fetchMineDocuments } from "@common/actionCreators/mineActionCreator";
import { getMineDocuments } from "@common/selectors/mineSelectors";
import ArchivedDocumentsSection from "@common/components/documents/ArchivedDocumentsSection";
-import { Feature, isFeatureEnabled } from "@mds/common";
+import { Feature } from "@mds/common";
import { renderCategoryColumn } from "@/components/common/CoreTableCommonColumns";
import * as Strings from "@common/constants/strings";
import { MajorMineApplicationDocument } from "@common/models/documents/document";
+import withFeatureFlag from "@common/providers/featureFlags/withFeatureFlag";
const propTypes = {
match: PropTypes.shape({
@@ -43,6 +44,7 @@ const propTypes = {
fetchMineDocuments: PropTypes.func.isRequired,
openModal: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired,
+ isFeatureEnabled: PropTypes.func.isRequired,
updateProjectDecisionPackage: PropTypes.func.isRequired,
createProjectDecisionPackage: PropTypes.func.isRequired,
removeDocumentFromProjectDecisionPackage: PropTypes.func.isRequired,
@@ -129,15 +131,13 @@ export class DecisionPackageTab extends Component {
return (
0
- ? archivedDocuments.map((doc) => new MajorMineApplicationDocument(doc)) : []}
+ documents={
+ archivedDocuments && archivedDocuments.length > 0
+ ? archivedDocuments.map((doc) => new MajorMineApplicationDocument(doc))
+ : []
+ }
/>
);
};
@@ -215,7 +215,7 @@ export class DecisionPackageTab extends Component {
modalType,
closeModal: this.props.closeModal,
handleSubmit: submitHandler,
- afterClose: () => { },
+ afterClose: () => {},
optionalProps,
},
content,
@@ -253,7 +253,7 @@ export class DecisionPackageTab extends Component {
Boolean(projectDecisionPackage?.project_decision_package_guid) &&
projectDecisionPackage?.status_code !== "NTS";
- const canArchiveDocuments = isFeatureEnabled(Feature.MAJOR_PROJECT_ARCHIVE_FILE);
+ const canArchiveDocuments = this.props.isFeatureEnabled(Feature.MAJOR_PROJECT_ARCHIVE_FILE);
return (
<>
@@ -415,4 +415,4 @@ DecisionPackageTab.propTypes = propTypes;
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
-)(DecisionPackageTab);
+)(withFeatureFlag(DecisionPackageTab));
diff --git a/services/core-web/src/components/mine/Projects/MajorMineApplicationTab.js b/services/core-web/src/components/mine/Projects/MajorMineApplicationTab.js
index 59e6510a2f..debd8f0a1c 100644
--- a/services/core-web/src/components/mine/Projects/MajorMineApplicationTab.js
+++ b/services/core-web/src/components/mine/Projects/MajorMineApplicationTab.js
@@ -22,12 +22,13 @@ import { fetchMineDocuments } from "@common/actionCreators/mineActionCreator";
import { getMineDocuments } from "@common/selectors/mineSelectors";
import ArchivedDocumentsSection from "@common/components/documents/ArchivedDocumentsSection";
import DocumentCompression from "@/components/common/DocumentCompression";
-import { Feature, isFeatureEnabled } from "@mds/common";
+import { Feature } from "@mds/common";
import { MajorMineApplicationDocument } from "@common/models/documents/document";
import { renderCategoryColumn } from "@/components/common/CoreTableCommonColumns";
import { DownloadOutlined } from "@ant-design/icons";
import { Button } from "antd";
+import withFeatureFlag from "@common/providers/featureFlags/withFeatureFlag";
const propTypes = {
project: CustomPropTypes.project.isRequired,
@@ -40,37 +41,9 @@ const propTypes = {
majorMineAppStatusCodesHash: PropTypes.objectOf(PropTypes.string).isRequired,
updateMajorMineApplication: PropTypes.func.isRequired,
fetchProjectById: PropTypes.func.isRequired,
+ isFeatureEnabled: PropTypes.func.isRequired,
};
-const canArchiveDocuments = isFeatureEnabled(Feature.MAJOR_PROJECT_ARCHIVE_FILE);
-
-const menuOptions = [
- {
- href: "basic-information",
- title: "Basic Information",
- },
- {
- href: "primary-documents",
- title: "Primary Documents",
- },
- {
- href: "spatial-components",
- title: "Spatial Components",
- },
- {
- href: "supporting-documents",
- title: "Supporting Documents",
- },
- {
- href: "ministry-decision-documents",
- title: "Ministry Decision Documents",
- },
- canArchiveDocuments && {
- href: "archived-documents",
- title: "Archived Documents",
- },
-].filter(Boolean);
-
export class MajorMineApplicationTab extends Component {
state = {
fixedTop: false,
@@ -136,12 +109,13 @@ export class MajorMineApplicationTab extends Component {
) : (
{sectionTitle}
);
+
return (