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 (
{titleElement} this.fetchData()} additionalColumnProps={[{ key: "document_name", colProps: { width: "80%" } }]} isLoaded={this.state.isLoaded} @@ -185,6 +159,33 @@ export class MajorMineApplicationTab extends Component { }) ); + 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", + }, + this.props.isFeatureEnabled(Feature.MAJOR_PROJECT_ARCHIVE_FILE) && { + href: "archived-documents", + title: "Archived Documents", + }, + ].filter(Boolean); + return ( <>
@@ -345,4 +346,6 @@ const mapDispatchToProps = (dispatch) => MajorMineApplicationTab.propTypes = propTypes; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MajorMineApplicationTab)); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(withFeatureFlag(MajorMineApplicationTab)) +); diff --git a/services/core-web/src/components/mine/Projects/Project.js b/services/core-web/src/components/mine/Projects/Project.js index 18dc773bfc..21c2471bb9 100644 --- a/services/core-web/src/components/mine/Projects/Project.js +++ b/services/core-web/src/components/mine/Projects/Project.js @@ -7,7 +7,7 @@ import { getProject } from "@common/selectors/projectSelectors"; import { fetchProjectById } from "@common/actionCreators/projectActionCreator"; import { Link } from "react-router-dom"; import { ArrowLeftOutlined, EnvironmentOutlined } from "@ant-design/icons"; -import { detectProdEnvironment as IN_PROD } from "@mds/common"; +import { Feature } from "@mds/common"; import CustomPropTypes from "@/customPropTypes"; import * as routes from "@/constants/routes"; import LoadingWrapper from "@/components/common/wrappers/LoadingWrapper"; @@ -17,6 +17,7 @@ import MajorMineApplicationTab from "@/components/mine/Projects/MajorMineApplica import NullScreen from "@/components/common/NullScreen"; import DecisionPackageTab from "@/components/mine/Projects/DecisionPackageTab"; import ProjectDocumentsTab from "./ProjectDocumentsTab"; +import withFeatureFlag from "@common/providers/featureFlags/withFeatureFlag"; const propTypes = { project: CustomPropTypes.project.isRequired, @@ -29,6 +30,7 @@ const propTypes = { replace: PropTypes.func, }).isRequired, fetchProjectById: PropTypes.func.isRequired, + isFeatureEnabled: PropTypes.func.isRequired, }; export class Project extends Component { @@ -182,24 +184,24 @@ export class Project extends Component {
- {/* FEATURE FLAG: PROJECTS */} - {!IN_PROD() && ( - <> - - -
- -
-
-
- - -
- -
-
-
- + {this.props.isFeatureEnabled(Feature.MAJOR_PROJECT_DECISION_PACKAGE) && ( + + +
+ +
+
+
+ )} + + {this.props.isFeatureEnabled(Feature.MAJOR_PROJECT_ALL_DOCUMENTS) && ( + + +
+ +
+
+
)}
@@ -223,4 +225,4 @@ const mapDispatchToProps = (dispatch) => dispatch ); -export default connect(mapStateToProps, mapDispatchToProps)(Project); +export default connect(mapStateToProps, mapDispatchToProps)(withFeatureFlag(Project)); diff --git a/services/core-web/src/components/mine/Projects/ProjectDocumentsTab.js b/services/core-web/src/components/mine/Projects/ProjectDocumentsTab.js index e7ddd38bfa..b126b57f8d 100644 --- a/services/core-web/src/components/mine/Projects/ProjectDocumentsTab.js +++ b/services/core-web/src/components/mine/Projects/ProjectDocumentsTab.js @@ -18,10 +18,11 @@ import ScrollSideMenu from "@/components/common/ScrollSideMenu"; 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 { MajorMineApplicationDocument } from "@common/models/documents/document"; import { renderCategoryColumn } from "@/components/common/CoreTableCommonColumns"; import * as Strings from "@common/constants/strings"; +import withFeatureFlag from "@common/providers/featureFlags/withFeatureFlag"; const propTypes = { match: PropTypes.shape({ @@ -164,15 +165,13 @@ export class ProjectDocumentsTab extends Component { return ( 0 - ? archivedDocuments.map((doc) => new MajorMineApplicationDocument(doc)) : []} + documents={ + archivedDocuments && archivedDocuments.length > 0 + ? archivedDocuments.map((doc) => new MajorMineApplicationDocument(doc)) + : [] + } /> ); }; @@ -192,7 +191,7 @@ export class ProjectDocumentsTab extends Component { { href: "project-description", title: "Project Description" }, { href: "irt", title: "IRT" }, { href: "major-mine-application", title: "Major Mine Application" }, - isFeatureEnabled(Feature.MAJOR_PROJECT_ARCHIVE_FILE) && { + this.props.isFeatureEnabled(Feature.MAJOR_PROJECT_ARCHIVE_FILE) && { href: "archived-documents", title: "Archived Documents", }, @@ -274,4 +273,6 @@ const mapDispatchToProps = (dispatch) => ProjectDocumentsTab.propTypes = propTypes; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ProjectDocumentsTab)); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(withFeatureFlag(ProjectDocumentsTab)) +); diff --git a/services/core-web/src/components/mine/Projects/ProjectSummary.js b/services/core-web/src/components/mine/Projects/ProjectSummary.js index 6e0ba6ff2f..8609c30161 100644 --- a/services/core-web/src/components/mine/Projects/ProjectSummary.js +++ b/services/core-web/src/components/mine/Projects/ProjectSummary.js @@ -41,7 +41,8 @@ import Loading from "@/components/common/Loading"; import ProjectSummaryForm from "@/components/Forms/projectSummaries/ProjectSummaryForm"; import NullScreen from "@/components/common/NullScreen"; import ScrollSideMenu from "@/components/common/ScrollSideMenu"; -import { Feature, isFeatureEnabled } from "@mds/common"; +import { Feature } from "@mds/common"; +import withFeatureFlag from "@common/providers/featureFlags/withFeatureFlag"; const propTypes = { formattedProjectSummary: PropTypes.objectOf( @@ -76,6 +77,7 @@ const propTypes = { removeDocumentFromProjectSummary: PropTypes.func.isRequired, location: PropTypes.shape({ pathname: PropTypes.string }).isRequired, mineDocuments: PropTypes.arrayOf(CustomPropTypes.mineDocument), + isFeatureEnabled: PropTypes.func.isRequired, }; const defaultProps = { @@ -126,7 +128,12 @@ export class ProjectSummary extends Component { return this.props .fetchProjectById(projectGuid) .then((res) => { - this.setState({ isLoaded: true, isValid: true, isNewProject: false, uploadedDocuments: res.project_summary.documents }); + this.setState({ + isLoaded: true, + isValid: true, + isNewProject: false, + uploadedDocuments: res.project_summary.documents, + }); this.props.fetchMineDocuments(res.mine_guid, { project_summary_guid: projectSummaryGuid, is_archived: true, @@ -169,13 +176,13 @@ export class ProjectSummary extends Component { removeUploadedDocument = (payload, uploadedDocuments) => { if (Array.isArray(payload.documents)) { - const uploadedGUIDs = new Set(uploadedDocuments.map(doc => doc.document_manager_guid)); - payload.documents = payload.documents.filter(doc => - !uploadedGUIDs.has(doc.document_manager_guid) + const uploadedGUIDs = new Set(uploadedDocuments.map((doc) => doc.document_manager_guid)); + payload.documents = payload.documents.filter( + (doc) => !uploadedGUIDs.has(doc.document_manager_guid) ); } return payload; - } + }; handleTransformPayload = (payload) => { let values = this.removeUploadedDocument(payload, this.state.uploadedDocuments); @@ -255,9 +262,8 @@ export class ProjectSummary extends Component { { mrc_review_required, contacts, project_lead_party_guid }, "Successfully updated project.", false - ) - } - ) + ); + }) .then(() => { this.handleFetchData(this.props.match.params); this.setState((prevState) => ({ @@ -328,10 +334,11 @@ export class ProjectSummary extends Component { : "New Project Description"} { ), projectSummaryPermitTypesHash: getProjectSummaryPermitTypesHash(state), projectSummaryStatusCodes: getDropdownProjectSummaryStatusCodes(state), - onSubmit: () => { }, + onSubmit: () => {}, }; }; @@ -491,4 +498,7 @@ const mapDispatchToProps = (dispatch) => dispatch ); -export default connect(mapStateToProps, mapDispatchToProps)(withRouter(ProjectSummary)); +export default connect( + mapStateToProps, + mapDispatchToProps +)(withRouter(withFeatureFlag(ProjectSummary))); diff --git a/services/core-web/src/components/mine/Tailings/MineTailingsTable.tsx b/services/core-web/src/components/mine/Tailings/MineTailingsTable.tsx index 48c9e89348..9f1a4363e8 100644 --- a/services/core-web/src/components/mine/Tailings/MineTailingsTable.tsx +++ b/services/core-web/src/components/mine/Tailings/MineTailingsTable.tsx @@ -13,7 +13,6 @@ import { getITRBExemptionStatusCodeOptionsHash, getTSFOperatingStatusCodeOptionsHash, } from "@common/selectors/staticContentSelectors"; -import { detectProdEnvironment as IN_PROD } from "@mds/common"; import { bindActionCreators } from "redux"; import { storeDam } from "@common/actions/damActions"; import { storeTsf } from "@common/actions/tailingsActions"; @@ -26,6 +25,8 @@ import { IDam, ITailingsStorageFacility } from "@mds/common"; import { ColumnsType } from "antd/lib/table"; import { FixedType } from "rc-table/lib/interface"; import { renderCategoryColumn, renderTextColumn } from "@/components/common/CoreTableCommonColumns"; +import { Feature } from "@mds/common"; +import { useFeatureFlag } from "@common/providers/featureFlags/useFeatureFlag"; interface MineTailingsTableProps { tailings: ITailingsStorageFacility[]; @@ -53,6 +54,10 @@ const MineTailingsTable: FC = (pro tailings, } = props; + const { isFeatureEnabled } = useFeatureFlag(); + + const tsfV2Enabled = isFeatureEnabled(Feature.TSF_V2); + const transformRowData = (items: ITailingsStorageFacility[]) => { return items?.map((tailing) => { return { @@ -151,8 +156,7 @@ const MineTailingsTable: FC = (pro > Edit TSF - {/* FEATURE FLAG: TSF */} - {!IN_PROD() && ( + {tsfV2Enabled && ( - - - - -

-
-

- - - These files are visible to the proponent. - - Upload Ministry Decision documentation. - -

- - -
-
-

- - Additional Government Documents - -

-
-

- - - These files are visible to the proponent. - - Upload Supplemental Ministry documentation. - -

- -
-
- - - - -  Confidential Internal Documents (Ministry Visible Only) - -
- -
-
-

- - - - Internal Ministry Documentation - - - - - - -

-
-

- - - These files are for internal staff only and will not be shown to proponents. - - - Upload internal documents that are created durring the review process. - -

- -
- - - + `; diff --git a/services/core-web/src/tests/components/mine/Projects/__snapshots__/MajorMineApplicationTab.spec.js.snap b/services/core-web/src/tests/components/mine/Projects/__snapshots__/MajorMineApplicationTab.spec.js.snap index fa96bdf2bd..c0f3db14db 100644 --- a/services/core-web/src/tests/components/mine/Projects/__snapshots__/MajorMineApplicationTab.spec.js.snap +++ b/services/core-web/src/tests/components/mine/Projects/__snapshots__/MajorMineApplicationTab.spec.js.snap @@ -1,377 +1,41 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ProjectDocumentsTab renders properly 1`] = ` - -
- -
-
- - - - - - - - - Basic Information - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - Application Files - -
- - Primary Documents - - -
-
-
- - Spatial Components - - -
-
-
- - Supporting Documents - - -
-
-
- - Ministry Decision Documents - - -
-
- -
-
+ `; diff --git a/services/core-web/src/tests/components/mine/Projects/__snapshots__/Project.spec.js.snap b/services/core-web/src/tests/components/mine/Projects/__snapshots__/Project.spec.js.snap index 326b6752d1..1e2ed99df6 100644 --- a/services/core-web/src/tests/components/mine/Projects/__snapshots__/Project.spec.js.snap +++ b/services/core-web/src/tests/components/mine/Projects/__snapshots__/Project.spec.js.snap @@ -1,148 +1,32 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Project renders properly 1`] = ` -
-
-

- Test Project Title - - - - - Sample Mine - - - -

- - - Back to: - Sample Mine - Major projects - -
- - - -
- -
-
-
- - -
- -
-
-
- - -
- -
-
-
- - -
- -
-
-
- - -
- -
-
-
-
-
+ } +/> `; diff --git a/services/core-web/src/tests/components/mine/Projects/__snapshots__/ProjectDocumentsTab.spec.js.snap b/services/core-web/src/tests/components/mine/Projects/__snapshots__/ProjectDocumentsTab.spec.js.snap index 26885338ea..2027fd56cc 100644 --- a/services/core-web/src/tests/components/mine/Projects/__snapshots__/ProjectDocumentsTab.spec.js.snap +++ b/services/core-web/src/tests/components/mine/Projects/__snapshots__/ProjectDocumentsTab.spec.js.snap @@ -1,237 +1,32 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ProjectDocumentsTab renders properly 1`] = ` - - - - - All Project Documents - - - -
-
- -
-
-
- - Project Description - - -
-
-
- - IRT - - -
-
- - Application Files - -
- - Primary Documents - - -
-
-
- - Spatial Components - - -
-
-
- - Supporting Documents - - -
-
- -
-
+ `; diff --git a/services/core-web/src/tests/models/document.spec.ts b/services/core-web/src/tests/models/document.spec.ts index 7c52506c80..4253e44345 100644 --- a/services/core-web/src/tests/models/document.spec.ts +++ b/services/core-web/src/tests/models/document.spec.ts @@ -4,6 +4,7 @@ import { MajorMineApplicationDocument, MineDocument, } from "@common/models/documents/document"; +import * as common from "@mds/common"; // Document model testing @@ -46,6 +47,11 @@ const mockDocumentData = { ], }; +jest.mock("@mds/common", () => ({ + ...jest.requireActual("@mds/common"), + isFeatureEnabled: () => true, +})); + describe("MineDocument model", () => { it("Base document model versions", () => { const mineDocumentRecord = new MineDocument(mockDocumentData); diff --git a/services/core-web/src/tests/routes/__snapshots__/DashboardRoutes.spec.js.snap b/services/core-web/src/tests/routes/__snapshots__/DashboardRoutes.spec.js.snap index de422bafeb..e26eedb734 100644 --- a/services/core-web/src/tests/routes/__snapshots__/DashboardRoutes.spec.js.snap +++ b/services/core-web/src/tests/routes/__snapshots__/DashboardRoutes.spec.js.snap @@ -258,7 +258,7 @@ exports[`DashboardRoutes renders properly 1`] = ` "$$typeof": Symbol(react.memo), "WrappedComponent": [Function], "compare": null, - "displayName": "Connect(withRouter(ProjectSummary))", + "displayName": "Connect(withRouter(WrappedComponent))", "type": [Function], } } @@ -272,7 +272,7 @@ exports[`DashboardRoutes renders properly 1`] = ` "$$typeof": Symbol(react.memo), "WrappedComponent": [Function], "compare": null, - "displayName": "Connect(withRouter(ProjectSummary))", + "displayName": "Connect(withRouter(WrappedComponent))", "type": [Function], } } @@ -286,7 +286,7 @@ exports[`DashboardRoutes renders properly 1`] = ` "$$typeof": Symbol(react.memo), "WrappedComponent": [Function], "compare": null, - "displayName": "Connect(Project)", + "displayName": "Connect(WrappedComponent)", "type": [Function], } } diff --git a/services/minespace-web/.env-example b/services/minespace-web/.env-example index 1081c89033..7590a28ffe 100644 --- a/services/minespace-web/.env-example +++ b/services/minespace-web/.env-example @@ -4,6 +4,9 @@ FILESYSTEM_PROVIDER_URL=http://localhost:62870/file-api/AmazonS3Provider/ MATOMO_URL=http://localhost:5001 NODE_ENV=development +FLAGSMITH_URL=https://mds-flags-dev.apps.silver.devops.gov.bc.ca/api/v1/ +FLAGSMITH_KEY=4Eu9eEMDmWVEHKDaKoeWY7 + KEYCLOAK_CLIENT_ID=mines-digital-services-mds-public-client-4414 KEYCLOAK_RESOURCE=mines-digital-services-mds-public-client-4414 KEYCLOAK_URL=https://test.loginproxy.gov.bc.ca/auth @@ -18,7 +21,7 @@ VCAUTHN_PRES_REQ_CONF_ID=minespace-access-0.1-dev 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 @@ -33,3 +36,5 @@ 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/ +CYPRESS_FLAGSMITH_URL=https://mds-flags-dev.apps.silver.devops.gov.bc.ca/api/v1/ +CYPRESS_FLAGSMITH_KEY=4Eu9eEMDmWVEHKDaKoeWY7 diff --git a/services/minespace-web/common/components/documents/ArchivedDocumentsSection.tsx b/services/minespace-web/common/components/documents/ArchivedDocumentsSection.tsx index f8acfb78b5..b42400eaab 100644 --- a/services/minespace-web/common/components/documents/ArchivedDocumentsSection.tsx +++ b/services/minespace-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/minespace-web/common/components/tailings/TailingsSummaryPage.tsx b/services/minespace-web/common/components/tailings/TailingsSummaryPage.tsx index c280281e25..15b7699632 100644 --- a/services/minespace-web/common/components/tailings/TailingsSummaryPage.tsx +++ b/services/minespace-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/minespace-web/common/components/tailings/dam/DamsPage.tsx b/services/minespace-web/common/components/tailings/dam/DamsPage.tsx index 7524160cb7..baf6047c8a 100644 --- a/services/minespace-web/common/components/tailings/dam/DamsPage.tsx +++ b/services/minespace-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/minespace-web/common/constants/environment.js b/services/minespace-web/common/constants/environment.js index 3cfadfb945..dc144cb675 100644 --- a/services/minespace-web/common/constants/environment.js +++ b/services/minespace-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/minespace-web/common/providers/featureFlags/featureFlag.context.tsx b/services/minespace-web/common/providers/featureFlags/featureFlag.context.tsx new file mode 100644 index 0000000000..8f7dea0dec --- /dev/null +++ b/services/minespace-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/minespace-web/common/providers/featureFlags/featureFlag.provider.tsx b/services/minespace-web/common/providers/featureFlags/featureFlag.provider.tsx new file mode 100644 index 0000000000..84cd7cea6d --- /dev/null +++ b/services/minespace-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/minespace-web/common/providers/featureFlags/useFeatureFlag.tsx b/services/minespace-web/common/providers/featureFlags/useFeatureFlag.tsx new file mode 100644 index 0000000000..465cc8886a --- /dev/null +++ b/services/minespace-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/minespace-web/common/providers/featureFlags/withFeatureFlag.tsx b/services/minespace-web/common/providers/featureFlags/withFeatureFlag.tsx new file mode 100644 index 0000000000..8c0548ad47 --- /dev/null +++ b/services/minespace-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/minespace-web/cypress/support/commands.ts b/services/minespace-web/cypress/support/commands.ts index 918614c84a..6be9d2ba95 100644 --- a/services/minespace-web/cypress/support/commands.ts +++ b/services/minespace-web/cypress/support/commands.ts @@ -9,6 +9,8 @@ Cypress.Commands.add("login", () => { docManUrl: Cypress.env("CYPRESS_DOC_MAN_URL"), matomoUrl: Cypress.env("CYPRESS_MATOMO_URL"), filesystemProviderUrl: Cypress.env("CYPRESS_FILE_SYSTEM_PROVIDER_URL"), + flagsmithUrl: Cypress.env("CYPRESS_FLAGSMITH_URL"), + flagsmithKey: Cypress.env("CYPRESS_FLAGSMITH_KEY"), keycloak_clientId: Cypress.env("CYPRESS_KEYCLOAK_CLIENT_ID"), keycloak_resource: Cypress.env("CYPRESS_KEYCLOAK_RESOURCE"), keycloak_url: Cypress.env("CYPRESS_KEYCLOAK_URL"), diff --git a/services/minespace-web/runner/server.js b/services/minespace-web/runner/server.js index 22938023e4..66b94d9786 100755 --- a/services/minespace-web/runner/server.js +++ b/services/minespace-web/runner/server.js @@ -45,6 +45,8 @@ app.get(`${BASE_PATH}/env`, (req, res) => { siteminder_url: process.env.SITEMINDER_URL, environment: process.env.NODE_ENV, vcauthn_pres_req_conf_id: process.env.VCAUTHN_PRES_REQ_CONF_ID, + flagsmithKey: process.env.FLAGSMITH_KEY, + flagsmithUrl: process.env.FLAGSMITH_URL, }); }); diff --git a/services/minespace-web/src/components/common/DocumentTable.tsx b/services/minespace-web/src/components/common/DocumentTable.tsx index d5be4efa27..cdcf874750 100644 --- a/services/minespace-web/src/components/common/DocumentTable.tsx +++ b/services/minespace-web/src/components/common/DocumentTable.tsx @@ -13,7 +13,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"; @@ -27,6 +27,7 @@ import { import { openDocument } from "../syncfusion/DocumentViewer"; import { downloadFileFromDocumentManager } from "@common/utils/actionlessNetworkCalls"; import { getUserInfo } from "@common/selectors/authenticationSelectors"; +import { useFeatureFlag } from "@common/providers/featureFlags/useFeatureFlag"; interface DocumentTableProps { documents: MineDocument[]; @@ -73,6 +74,8 @@ export const DocumentTable = ({ ...props }: DocumentTableProps) => { + const { isFeatureEnabled } = useFeatureFlag(); + const allowedTableActions = { [FileOperations.View]: true, [FileOperations.Download]: true, diff --git a/services/minespace-web/src/components/common/featureFlag.guard.tsx b/services/minespace-web/src/components/common/featureFlag.guard.tsx new file mode 100644 index 0000000000..7ebbcde2a4 --- /dev/null +++ b/services/minespace-web/src/components/common/featureFlag.guard.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import hoistNonReactStatics from "hoist-non-react-statics"; +import UnauthenticatedNotice from "@/components/common/UnauthenticatedNotice"; +import { Feature } from "@mds/common"; +import { useFeatureFlag } from "../../../common/providers/featureFlags/useFeatureFlag"; + +/** + * @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/minespace-web/src/components/dashboard/mine/tailings/Tailings.js b/services/minespace-web/src/components/dashboard/mine/tailings/Tailings.js index 4e829f0205..38e3cb0400 100644 --- a/services/minespace-web/src/components/dashboard/mine/tailings/Tailings.js +++ b/services/minespace-web/src/components/dashboard/mine/tailings/Tailings.js @@ -14,10 +14,11 @@ import { resetForm } from "@common/utils/helpers"; import { storeTsf, clearTsf } from "@common/actions/tailingsActions"; import CustomPropTypes from "@/customPropTypes"; import { modalConfig } from "@/components/modalContent/config"; -import { detectProdEnvironment as IN_PROD } from "@mds/common"; import { EDIT_TAILINGS_STORAGE_FACILITY, ADD_TAILINGS_STORAGE_FACILITY } from "@/constants/routes"; import * as FORM from "@/constants/forms"; import TailingsTable from "./TailingsTable"; +import { Feature } from "@mds/common"; +import { useFeatureFlag } from "@common/providers/featureFlags/useFeatureFlag"; const { Paragraph, Title, Text } = Typography; @@ -35,6 +36,11 @@ const propTypes = { export const Tailings = (props) => { const history = useHistory(); const { mine } = props; + + const { isFeatureEnabled } = useFeatureFlag(); + + const tsfV2Enabled = isFeatureEnabled(Feature.TSF_V2); + const handleEditTailings = (values) => { return props .updateTailingsStorageFacility( @@ -80,8 +86,7 @@ export const Tailings = (props) => { return ( - {/* FEATURE FLAG: TSF */} - + Tailings Storage Facilities @@ -93,8 +98,7 @@ export const Tailings = (props) => {
- {/* FEATURE FLAG: TSF */} - {!IN_PROD() && ( + {tsfV2Enabled && ( @@ -222,7 +226,7 @@ export const TailingsTable = (props) => { dataSource={tailings} // FEATURE FLAG: TSF expandProps={ - !IN_PROD() + tsfV2Enabled ? { recordDescription: "associated dams", getDataSource: (record) => record.dams, diff --git a/services/minespace-web/src/components/pages/Project/DocumentsTab.js b/services/minespace-web/src/components/pages/Project/DocumentsTab.js index 0621570fe0..d8d8c30259 100644 --- a/services/minespace-web/src/components/pages/Project/DocumentsTab.js +++ b/services/minespace-web/src/components/pages/Project/DocumentsTab.js @@ -12,10 +12,11 @@ import DocumentsPage from "./DocumentsPage"; import { getMineDocuments } from "@common/selectors/mineSelectors"; import ArchivedDocumentsSection from "@common/components/documents/ArchivedDocumentsSection"; import { documentNameColumn, uploadDateColumn } from "@/components/common/DocumentColumns"; -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 * as Strings from "@common/constants/strings"; +import withFeatureFlag from "@common/providers/featureFlags/withFeatureFlag"; const propTypes = { match: PropTypes.shape({ @@ -28,20 +29,14 @@ const propTypes = { }).isRequired, project: customPropTypes.project.isRequired, fetchProjectById: PropTypes.func.isRequired, + isFeatureEnabled: PropTypes.func.isFeatureEnabled, refreshData: PropTypes.func.isRequired, mineDocuments: PropTypes.arrayOf(customPropTypes.mineDocument), }; -const tabs = [ - "project-description", - "information-requirements-table", - "major-mine-application", - isFeatureEnabled(Feature.MAJOR_PROJECT_ARCHIVE_FILE) && "archived-documents", -].filter(Boolean); - export class DocumentsTab extends Component { state = { - activeTab: tabs[0], + activeTab: "project-description", }; allDocuments = []; @@ -70,6 +65,13 @@ export class DocumentsTab extends Component { }; render() { + const tabs = [ + "project-description", + "information-requirements-table", + "major-mine-application", + this.props.isFeatureEnabled(Feature.MAJOR_PROJECT_ARCHIVE_FILE) && "archived-documents", + ].filter(Boolean); + const documentColumns = [documentNameColumn(), uploadDateColumn()]; const renderAllDocuments = (docs) => ( @@ -99,16 +101,14 @@ export class DocumentsTab extends Component { 0 - ? this.props.mineDocuments.map((doc) => new MajorMineApplicationDocument(doc)) : []} + documents={ + this.props.mineDocuments && this.props.mineDocuments.length > 0 + ? this.props.mineDocuments.map((doc) => new MajorMineApplicationDocument(doc)) + : [] + } /> @@ -160,4 +160,6 @@ const mapDispatchToProps = (dispatch) => DocumentsTab.propTypes = propTypes; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(DocumentsTab)); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(withFeatureFlag(DocumentsTab)) +); diff --git a/services/minespace-web/src/components/pages/Project/ProjectPage.js b/services/minespace-web/src/components/pages/Project/ProjectPage.js index 6b30bf58ee..c46dff64d1 100644 --- a/services/minespace-web/src/components/pages/Project/ProjectPage.js +++ b/services/minespace-web/src/components/pages/Project/ProjectPage.js @@ -5,7 +5,7 @@ import { Link } from "react-router-dom"; import { Row, Col, Typography, Tabs } from "antd"; import { ArrowLeftOutlined } from "@ant-design/icons"; import PropTypes from "prop-types"; -import { detectProdEnvironment as IN_PROD } from "@mds/common"; +import { Feature } from "@mds/common"; import { getMines } from "@common/selectors/mineSelectors"; import { getProject } from "@common/selectors/projectSelectors"; import { fetchProjectById } from "@common/actionCreators/projectActionCreator"; @@ -20,6 +20,7 @@ import InformationRequirementsTableEntryTab from "./InformationRequirementsTable import MajorMineApplicationEntryTab from "./MajorMineApplicationEntryTab"; import DocumentsTab from "./DocumentsTab"; import { MAJOR_MINE_APPLICATION_SUBMISSION_STATUSES } from "./MajorMineApplicationPage"; +import withFeatureFlag from "@common/providers/featureFlags/withFeatureFlag"; const propTypes = { mines: PropTypes.arrayOf(CustomPropTypes.mine).isRequired, @@ -28,6 +29,7 @@ const propTypes = { fetchMineRecordById: PropTypes.func.isRequired, fetchEMLIContactsByRegion: PropTypes.func.isRequired, fetchMineDocuments: PropTypes.func.isRequired, + isFeatureEnabled: PropTypes.func.isRequired, match: PropTypes.shape({ params: PropTypes.shape({ mineGuid: PropTypes.string, @@ -237,8 +239,7 @@ export class ProjectPage extends Component { {majorMineApplicationTabContent} - {/* FEATURE FLAG: PROJECTS */} - {!IN_PROD() && ( + {this.props.isFeatureEnabled(Feature.MAJOR_PROJECT_ALL_DOCUMENTS) && ( ProjectPage.propTypes = propTypes; -export default connect(mapStateToProps, mapDispatchToProps)(ProjectPage); +export default connect(mapStateToProps, mapDispatchToProps)(withFeatureFlag(ProjectPage)); diff --git a/services/minespace-web/src/constants/environment.js b/services/minespace-web/src/constants/environment.js index 60a86875bb..bf6d5649b0 100644 --- a/services/minespace-web/src/constants/environment.js +++ b/services/minespace-web/src/constants/environment.js @@ -4,6 +4,8 @@ export const DEFAULT_ENVIRONMENT = { docManUrl: "http://localhost:5001", matomoUrl: "https://matomo-4c2ba9-test.apps.silver.devops.gov.bc.ca/", filesystemProviderUrl: "http://localhost:62870/file-api/AmazonS3Provider/", + flagsmithKey: "4Eu9eEMDmWVEHKDaKoeWY7", + flagsmithUrl: "https://mds-flags-dev.apps.silver.devops.gov.bc.ca/api/v1/", environment: "development", keycloak_resource: "mines-digital-services-mds-public-client-4414", keycloak_clientId: "mines-digital-services-mds-public-client-4414", diff --git a/services/minespace-web/src/fetchEnv.js b/services/minespace-web/src/fetchEnv.js index 0fe2a93e1b..e5e1e7827d 100644 --- a/services/minespace-web/src/fetchEnv.js +++ b/services/minespace-web/src/fetchEnv.js @@ -20,7 +20,9 @@ export default function fetchEnv() { env.docManUrl, env.filesystemProviderUrl, env.matomoUrl, - env.environment + env.environment, + env.flagsmithKey, + env.flagsmithUrl ); setupKeycloak( diff --git a/services/minespace-web/src/index.js b/services/minespace-web/src/index.js index 3280f81b0d..e5618a7039 100755 --- a/services/minespace-web/src/index.js +++ b/services/minespace-web/src/index.js @@ -14,6 +14,7 @@ import fetchEnv from "./fetchEnv"; import configureStore from "./store/configureStore"; import keycloak, { keycloakInitConfig } from "./keycloak"; import { unAuthenticateUser } from "./actionCreators/authenticationActionCreator"; +import FeatureFlagProvider from "@common/providers/featureFlags/featureFlag.provider"; // eslint-disable-next-line import/prefer-default-export export const store = configureStore(); @@ -83,29 +84,31 @@ const Index = () => { }; return environment ? ( - { - handleUpdateToken(); - }} - onTokenExpired={() => { - if (!isIdle()) { - keycloak.updateToken(); - } - }} - onReady={() => { - handleUpdateToken(); - }} - onAuthLogout={(err = "") => handleAuthErrors(err)} - onAuthError={(err = "") => handleAuthErrors(err)} - onAuthRefreshError={(err = "") => handleAuthErrors(err)} - onInitError={(err = "") => handleAuthErrors(err)} - > - - - - + + { + handleUpdateToken(); + }} + onTokenExpired={() => { + if (!isIdle()) { + keycloak.updateToken(); + } + }} + onReady={() => { + handleUpdateToken(); + }} + onAuthLogout={(err = "") => handleAuthErrors(err)} + onAuthError={(err = "") => handleAuthErrors(err)} + onAuthRefreshError={(err = "") => handleAuthErrors(err)} + onInitError={(err = "") => handleAuthErrors(err)} + > + + + + + ) : (
); diff --git a/services/minespace-web/src/tests/components/common/DocumentTable.spec.js b/services/minespace-web/src/tests/components/common/DocumentTable.spec.js index 651e479893..fd0faa7eea 100644 --- a/services/minespace-web/src/tests/components/common/DocumentTable.spec.js +++ b/services/minespace-web/src/tests/components/common/DocumentTable.spec.js @@ -2,6 +2,7 @@ import React from "react"; import { shallow } from "enzyme"; import { DocumentTable } from "@/components/common/DocumentTable"; import * as MOCK from "@/tests/mocks/dataMocks"; +import FeatureFlagContext from "@common/providers/featureFlags/featureFlag.context"; const props = {}; const dispatchProps = {}; @@ -24,7 +25,15 @@ beforeEach(() => { describe("DocumentTable", () => { it("renders properly", () => { - const component = shallow(); + const component = shallow( + true, + }} + > + + + ); expect(component).toMatchSnapshot(); }); }); diff --git a/services/minespace-web/src/tests/components/common/__snapshots__/DocumentTable.spec.js.snap b/services/minespace-web/src/tests/components/common/__snapshots__/DocumentTable.spec.js.snap index 70378dad98..bcd38d1139 100644 --- a/services/minespace-web/src/tests/components/common/__snapshots__/DocumentTable.spec.js.snap +++ b/services/minespace-web/src/tests/components/common/__snapshots__/DocumentTable.spec.js.snap @@ -1,36 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DocumentTable renders properly 1`] = ` - `; diff --git a/services/minespace-web/src/tests/components/dashboard/mine/tailings/Tailings.spec.js b/services/minespace-web/src/tests/components/dashboard/mine/tailings/Tailings.spec.js index 37b2f64f95..9856837c80 100644 --- a/services/minespace-web/src/tests/components/dashboard/mine/tailings/Tailings.spec.js +++ b/services/minespace-web/src/tests/components/dashboard/mine/tailings/Tailings.spec.js @@ -2,6 +2,7 @@ import React from "react"; import { shallow } from "enzyme"; import { Tailings } from "@/components/dashboard/mine/tailings/Tailings"; import * as MOCK from "@/tests/mocks/dataMocks"; +import FeatureFlagContext from "@common/providers/featureFlags/featureFlag.context"; jest.mock("react-router-dom", () => ({ ...jest.requireActual("react-router-dom"), @@ -34,7 +35,15 @@ beforeEach(() => { describe("Tailings", () => { it("renders properly", () => { - const component = shallow(); + const component = shallow( + true, + }} + > + + + ); expect(component).toMatchSnapshot(); }); }); diff --git a/services/minespace-web/src/tests/components/dashboard/mine/tailings/__snapshots__/Tailings.spec.js.snap b/services/minespace-web/src/tests/components/dashboard/mine/tailings/__snapshots__/Tailings.spec.js.snap index 5225059851..e6669bd372 100644 --- a/services/minespace-web/src/tests/components/dashboard/mine/tailings/__snapshots__/Tailings.spec.js.snap +++ b/services/minespace-web/src/tests/components/dashboard/mine/tailings/__snapshots__/Tailings.spec.js.snap @@ -1,84 +1,68 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Tailings renders properly 1`] = ` - - - - - - Tailings Storage Facilities - - - This table shows  - - Tailings Storage Facilities - -  for your mine. - -
- - - - -
- - - - - - -
+ `; diff --git a/services/minespace-web/src/tests/components/dashboard/mine/variances/__snapshots__/VarianceDetails.spec.js.snap b/services/minespace-web/src/tests/components/dashboard/mine/variances/__snapshots__/VarianceDetails.spec.js.snap index 3e855a867a..d3bd512355 100644 --- a/services/minespace-web/src/tests/components/dashboard/mine/variances/__snapshots__/VarianceDetails.spec.js.snap +++ b/services/minespace-web/src/tests/components/dashboard/mine/variances/__snapshots__/VarianceDetails.spec.js.snap @@ -60,7 +60,6 @@ exports[`VarianceDetails renders properly 1`] = ` "allowed_actions": Array [ "Open in document viewer", "Download file", - "Replace file", "Archive file", "Delete", ], diff --git a/services/minespace-web/src/tests/components/project/DocumentsTab/DocumentsTab.spec.js b/services/minespace-web/src/tests/components/project/DocumentsTab/DocumentsTab.spec.js index ceaac7c428..3ad91cfbce 100644 --- a/services/minespace-web/src/tests/components/project/DocumentsTab/DocumentsTab.spec.js +++ b/services/minespace-web/src/tests/components/project/DocumentsTab/DocumentsTab.spec.js @@ -2,6 +2,7 @@ import React from "react"; import { shallow } from "enzyme"; import { DocumentsTab } from "@/components/pages/Project/DocumentsTab"; import * as MOCK from "@/tests/mocks/dataMocks"; +import FeatureFlagContext from "@common/providers/featureFlags/featureFlag.context"; const props = {}; const dispatchProps = {}; @@ -23,7 +24,15 @@ beforeEach(() => { describe("DocumentsTab", () => { it("renders properly", () => { - const component = shallow(); + const component = shallow( + true, + }} + > + + + ); expect(component).toMatchSnapshot(); }); }); diff --git a/services/minespace-web/src/tests/components/project/DocumentsTab/__snapshots__/DocumentsTab.spec.js.snap b/services/minespace-web/src/tests/components/project/DocumentsTab/__snapshots__/DocumentsTab.spec.js.snap index 7ca8526c70..60b98e8527 100644 --- a/services/minespace-web/src/tests/components/project/DocumentsTab/__snapshots__/DocumentsTab.spec.js.snap +++ b/services/minespace-web/src/tests/components/project/DocumentsTab/__snapshots__/DocumentsTab.spec.js.snap @@ -1,293 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DocumentsTab renders properly 1`] = ` - - - - All Project Documents - - - - - - - -
- -
-
- -
-
- -
- - -
-
- - - -
- -
-
- -
-
- -
- - -
-
- - - -
- -
-
- -
-
- -
- - -
-
- - - -
- -
-
- -
-
- -
- - -
-
-
- -
+ `; diff --git a/yarn.lock b/yarn.lock index 14a3f43579..94b713289c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2163,6 +2163,7 @@ __metadata: babel-preset-stage-2: 6.24.1 clean-webpack-plugin: 4.0.0 copy-webpack-plugin: 10.1.0 + flagsmith: 3.19.1 hard-source-webpack-plugin: 0.13.1 html-webpack-plugin: 5.5.3 image-webpack-loader: 8.1.0 @@ -2170,7 +2171,6 @@ __metadata: postcss-loader: 7.3.3 prettier: 1.19.1 query-string: 5.1.1 - react: 16.13.1 react-hot-loader: 4.12.21 style-loader: 3.3.3 terser-webpack-plugin: 5.3.9 @@ -2182,6 +2182,9 @@ __metadata: webpack-manifest-plugin: 5.0.0 webpack-merge: 5.9.0 webpack-node-externals: 3.0.0 + peerDependencies: + react: 16.13.1 + react-dom: 16.13.1 languageName: unknown linkType: soft @@ -10349,6 +10352,13 @@ __metadata: languageName: node linkType: hard +"flagsmith@npm:3.19.1": + version: 3.19.1 + resolution: "flagsmith@npm:3.19.1" + checksum: df96b650a73354eb1fcba44ff94fe28c69dbb5b58061fa8782b69b8e959817a9f821070c211bbcb807bda7c9317fc954811ba055718845fb2da7cbd0028da07b + languageName: node + linkType: hard + "flat-cache@npm:^2.0.1": version: 2.0.1 resolution: "flat-cache@npm:2.0.1"