From ceef94fbb4d9cf65721bb6c180699e76af25bbe2 Mon Sep 17 00:00:00 2001
From: Sjoerd <svink@graphpolaris.com>
Date: Tue, 12 Nov 2024 11:41:49 +0000
Subject: [PATCH] fix: environment variables for feature flags

---
 apps/web/.env                                 | 27 ++++--
 apps/web/.env.development                     | 27 ++++--
 apps/web/.env.example                         | 27 ++++--
 apps/web/.env.production                      | 27 ++++--
 apps/web/src/components/navbar/navbar.tsx     | 34 +++----
 apps/web/src/main.tsx                         |  5 +-
 apps/web/tsconfig.json                        |  7 +-
 libs/config/src/envVariables.ts               | 48 ++++++++++
 libs/config/src/featureFlags.ts               | 32 -------
 libs/config/src/index.ts                      |  2 +-
 .../featureFlags/FeatureEnabled.tsx           |  6 ++
 .../components/featureFlags/featureFlags.ts   | 45 +++++++++
 .../lib/components/featureFlags/index.ts      |  2 +
 libs/shared/lib/data-access/broker/broker.tsx |  3 +-
 .../security/useAuthentication.tsx            |  5 +-
 libs/shared/lib/inspector/InspectorPanel.tsx  |  3 +-
 .../panel/querysidepanel/QueryMLDialog.tsx    | 93 ++++++++++---------
 libs/shared/lib/sidebar/index.tsx             |  8 +-
 .../lib/vis/components/VisualizationPanel.tsx | 20 ++--
 19 files changed, 268 insertions(+), 153 deletions(-)
 create mode 100644 libs/config/src/envVariables.ts
 delete mode 100644 libs/config/src/featureFlags.ts
 create mode 100644 libs/shared/lib/components/featureFlags/FeatureEnabled.tsx
 create mode 100644 libs/shared/lib/components/featureFlags/featureFlags.ts
 create mode 100644 libs/shared/lib/components/featureFlags/index.ts

diff --git a/apps/web/.env b/apps/web/.env
index 2458b1b74..e085c0ec9 100644
--- a/apps/web/.env
+++ b/apps/web/.env
@@ -11,14 +11,21 @@ SENTRY_URL=
 
 GP_AUTH_URL=
 
-WIP_TABLEVIS=false
-WIP_NODELINKVIS=false
-WIP_RAWJSONVIS=false
-WIP_PAOHVIS=true
-WIP_MATRIXVIS=true
-WIP_SEMANTICSUBSTRATESVIS=true
-WIP_MAPVIS=true
+LINK_PREDICTION=true
+CENTRALITY=true
+COMMUNITY_DETECTION=true
+SHORTEST_PATH=true
 
-WIP_INSIGHT_SHARING=true
-WIP_VIEWER_PERMISSIONS=true
-WIP_SHARABLE_EXPLORATION=true
+TABLEVIS=true
+NODELINKVIS=true
+RAWJSONVIS=true
+PAOHVIS=true
+MATRIXVIS=true
+SEMANTICSUBSTRATESVIS=true
+MAPVIS=true
+VIS0D=true
+VIS1D=true
+
+INSIGHT_SHARING=true
+VIEWER_PERMISSIONS=true
+SHARABLE_EXPLORATION=true
diff --git a/apps/web/.env.development b/apps/web/.env.development
index f56103b54..0e5a47287 100644
--- a/apps/web/.env.development
+++ b/apps/web/.env.development
@@ -11,14 +11,21 @@ SENTRY_URL=
 
 GP_AUTH_URL=
 
-WIP_TABLEVIS=false
-WIP_NODELINKVIS=false
-WIP_RAWJSONVIS=false
-WIP_PAOHVIS=true
-WIP_MATRIXVIS=true
-WIP_SEMANTICSUBSTRATESVIS=true
-WIP_MAPVIS=true
+LINK_PREDICTION=true
+CENTRALITY=true
+COMMUNITY_DETECTION=true
+SHORTEST_PATH=true
 
-WIP_INSIGHT_SHARING=true
-WIP_VIEWER_PERMISSIONS=true
-WIP_SHARABLE_EXPLORATION=true
\ No newline at end of file
+TABLEVIS=true
+NODELINKVIS=true
+RAWJSONVIS=true
+PAOHVIS=true
+MATRIXVIS=true
+SEMANTICSUBSTRATESVIS=true
+MAPVIS=true
+VIS0D=true
+VIS1D=true
+
+INSIGHT_SHARING=true
+VIEWER_PERMISSIONS=true
+SHARABLE_EXPLORATION=true
diff --git a/apps/web/.env.example b/apps/web/.env.example
index 10dba96b2..8a53e6ecc 100644
--- a/apps/web/.env.example
+++ b/apps/web/.env.example
@@ -10,14 +10,21 @@ SENTRY_URL=
 
 GP_AUTH_URL=
 
-WIP_TABLEVIS=false
-WIP_NODELINKVIS=false
-WIP_RAWJSONVIS=false
-WIP_PAOHVIS=true
-WIP_MATRIXVIS=true
-WIP_SEMANTICSUBSTRATESVIS=true
-WIP_MAPVIS=true
+LINK_PREDICTION=true
+CENTRALITY=true
+COMMUNITY_DETECTION=true
+SHORTEST_PATH=true
 
-WIP_INSIGHT_SHARING=true
-WIP_VIEWER_PERMISSIONS=true
-WIP_SHARABLE_EXPLORATION=true
\ No newline at end of file
+TABLEVIS=true
+NODELINKVIS=true
+RAWJSONVIS=true
+PAOHVIS=true
+MATRIXVIS=true
+SEMANTICSUBSTRATESVIS=true
+MAPVIS=true
+VIS0D=true
+VIS1D=true
+
+INSIGHT_SHARING=true
+VIEWER_PERMISSIONS=true
+SHARABLE_EXPLORATION=true
diff --git a/apps/web/.env.production b/apps/web/.env.production
index b848c9cca..fc1f37a8c 100644
--- a/apps/web/.env.production
+++ b/apps/web/.env.production
@@ -12,14 +12,21 @@ SENTRY_URL=
 
 GP_AUTH_URL=https://auth.staging.graphpolaris.com/
 
-WIP_TABLEVIS=false
-WIP_NODELINKVIS=false
-WIP_RAWJSONVIS=false
-WIP_PAOHVIS=true
-WIP_MATRIXVIS=true
-WIP_SEMANTICSUBSTRATESVIS=true
-WIP_MAPVIS=true
+LINK_PREDICTION=true
+CENTRALITY=true
+COMMUNITY_DETECTION=true
+SHORTEST_PATH=true
 
-WIP_INSIGHT_SHARING=true
-WIP_VIEWER_PERMISSIONS=true
-WIP_SHARABLE_EXPLORATION=true
\ No newline at end of file
+TABLEVIS=true
+NODELINKVIS=true
+RAWJSONVIS=true
+PAOHVIS=false
+MATRIXVIS=false
+SEMANTICSUBSTRATESVIS=false
+MAPVIS=false
+VIS0D=false
+VIS1D=false
+
+INSIGHT_SHARING=false
+VIEWER_PERMISSIONS=false
+SHARABLE_EXPLORATION=false
diff --git a/apps/web/src/components/navbar/navbar.tsx b/apps/web/src/components/navbar/navbar.tsx
index 5cb0e428e..a93578f0e 100644
--- a/apps/web/src/components/navbar/navbar.tsx
+++ b/apps/web/src/components/navbar/navbar.tsx
@@ -13,11 +13,10 @@ import { useAuthCache, useAuthentication } from '@graphpolaris/shared/lib/data-a
 import { DropdownItem } from '@graphpolaris/shared/lib/components/dropdowns';
 import GpLogo from './gp-logo';
 import { Popover, PopoverContent, PopoverTrigger } from '@graphpolaris/shared/lib/components/layout/Popover';
-import { showSharableExploration } from 'config';
+import { FeatureEnabled } from '@graphpolaris/shared/lib/components/featureFlags';
 import { Button, useActiveSaveStateAuthorization } from '@graphpolaris/shared';
 import { ManagementTrigger, ManagementViews } from '@graphpolaris/shared/lib/management';
-
-const AuthURL = import.meta.env.GP_AUTH_URL;
+import { getEnvVariable } from 'config';
 
 export const Navbar = () => {
   const dropdownRef = useRef<HTMLDivElement>(null);
@@ -25,7 +24,7 @@ export const Navbar = () => {
   const authCache = useAuthCache();
   const authorization = useActiveSaveStateAuthorization();
   const [menuOpen, setMenuOpen] = useState(false);
-  const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION;
+  const buildInfo = getEnvVariable('GRAPHPOLARIS_VERSION');
   const [managementOpen, setManagementOpen] = useState<boolean>(false);
   const [current, setCurrent] = useState<ManagementViews>('overview');
 
@@ -67,28 +66,30 @@ export const Navbar = () => {
               </div>
               {authCache.authentication?.authenticated ? (
                 <>
-                  {showSharableExploration() && (
+                  <FeatureEnabled featureFlag="SHARABLE_EXPLORATION">
                     <DropdownItem
                       value="Share"
                       onClick={() => {
                         auth.newShareRoom();
                       }}
                     />
-                  )}
-                  {authCache.authorization?.savestate?.W && authorization.database.W && (
-                    <DropdownItem
-                      value="Viewer Permissions"
-                      onClick={() => {
-                        setManagementOpen(true);
-                        setCurrent('members');
-                      }}
-                    />
-                  )}
+                  </FeatureEnabled>
+                  <FeatureEnabled featureFlag="VIEWER_PERMISSIONS">
+                    {authCache.authorization?.savestate?.W && authorization.database.W && (
+                      <DropdownItem
+                        value="Viewer Permissions"
+                        onClick={() => {
+                          setManagementOpen(true);
+                          setCurrent('members');
+                        }}
+                      />
+                    )}
+                  </FeatureEnabled>
                   <DropdownItem value="Settings" onClick={() => {}} />
                   <DropdownItem
                     value="Log out"
                     onClick={() => {
-                      location.replace(`${AuthURL}/outpost.goauthentik.io/sign_out`);
+                      location.replace(`${getEnvVariable('GP_AUTH_URL')}/outpost.goauthentik.io/sign_out`);
                     }}
                   />
                 </>
@@ -97,6 +98,7 @@ export const Navbar = () => {
                   <DropdownItem value="Login" onClick={() => {}} />
                 </>
               )}
+
               {authCache.authentication?.roomID && (
                 <div className="p-2 border-b">
                   <h3 className="text-xs break-words">Share ID: {authCache.authentication?.roomID}</h3>
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index e25807e6d..cdd0079ed 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -7,10 +7,11 @@ import App from './app/App';
 import { createRoot } from 'react-dom/client';
 import './main.css';
 import { ErrorBoundary } from '@graphpolaris/shared/lib/components/errorBoundary';
+import { getEnvVariable } from 'config';
 
-if (import.meta.env.SENTRY_ENABLED) {
+if (getEnvVariable('SENTRY_ENABLED') === 'true') {
   Sentry.init({
-    dsn: import.meta.env.SENTRY_URL,
+    dsn: getEnvVariable('SENTRY_URL'),
     integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
     tracesSampleRate: 1.0,
     replaysSessionSampleRate: 0.1,
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index b5d6c7282..263a48be6 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -45,7 +45,12 @@
     "postcss.config.js", // excludes PostCSS configuration file
     "tsconfig.tsbuildinfo" // excludes TypeScript build info file
   ],
-  "include": ["vite.config.ts", "src/**/*", "../../libs/shared/lib/error-boundary"],
+  "include": [
+    "vite.config.ts",
+    "src/**/*",
+    "../../libs/shared/lib/error-boundary",
+    "../../libs/shared/lib/components/featureFlags/featureFlags.ts"
+  ],
   "files": ["vite.config.ts"],
   "references": []
 }
diff --git a/libs/config/src/envVariables.ts b/libs/config/src/envVariables.ts
new file mode 100644
index 000000000..cc8b8bb21
--- /dev/null
+++ b/libs/config/src/envVariables.ts
@@ -0,0 +1,48 @@
+/// <reference types="vite/client" />
+
+export const envVar = {
+  GRAPHPOLARIS_VERSION: import.meta.env.GRAPHPOLARIS_VERSION,
+  BACKEND_URL: import.meta.env.BACKEND_URL,
+  BACKEND_WSS_URL: import.meta.env.BACKEND_WSS_URL,
+  STAGING: import.meta.env.STAGING,
+  SKIP_LOGIN: import.meta.env.SKIP_LOGIN,
+  BACKEND_USER: import.meta.env.BACKEND_USER,
+
+  SENTRY_ENABLED: import.meta.env.SENTRY_ENABLED,
+  SENTRY_URL: import.meta.env.SENTRY_URL,
+
+  GP_AUTH_URL: import.meta.env.GP_AUTH_URL,
+
+  LINK_PREDICTION: import.meta.env.LINK_PREDICTION,
+  CENTRALITY: import.meta.env.CENTRALITY,
+  COMMUNITY_DETECTION: import.meta.env.COMMUNITY_DETECTION,
+  SHORTEST_PATH: import.meta.env.SHORTEST_PATH,
+
+  TABLEVIS: import.meta.env.TABLEVIS,
+  NODELINKVIS: import.meta.env.NODELINKVIS,
+  RAWJSONVIS: import.meta.env.RAWJSONVIS,
+  PAOHVIS: import.meta.env.PAOHVIS,
+  MATRIXVIS: import.meta.env.MATRIXVIS,
+  SEMANTICSUBSTRATESVIS: import.meta.env.SEMANTICSUBSTRATESVIS,
+  MAPVIS: import.meta.env.MAPVIS,
+  VIS0D: import.meta.env.VIS0D,
+  VIS1D: import.meta.env.VIS1D,
+
+  INSIGHT_SHARING: import.meta.env.INSIGHT_SHARING,
+  VIEWER_PERMISSIONS: import.meta.env.VIEWER_PERMISSIONS,
+  SHARABLE_EXPLORATION: import.meta.env.SHARABLE_EXPLORATION,
+};
+
+type EnvVarKey = keyof typeof envVar;
+
+function getEnvVariable(key: EnvVarKey): string | undefined {
+  const value = envVar[key];
+  if (value === undefined) {
+    console.error(`Environment variable ${key} is not defined`);
+    return;
+  }
+  return value;
+}
+
+export { getEnvVariable };
+export type { EnvVarKey };
diff --git a/libs/config/src/featureFlags.ts b/libs/config/src/featureFlags.ts
deleted file mode 100644
index b6364d037..000000000
--- a/libs/config/src/featureFlags.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-// Safely retrieve environment variable values with a default fallback
-const getEnvVariable = (key: string, defaultValue: string = 'false'): string => {
-  return import.meta.env[key] ?? defaultValue;
-};
-
-// Check if the environment is production
-const isProduction = (): boolean => {
-  return getEnvVariable('GRAPHPOLARIS_VERSION', 'dev') === 'prod';
-};
-
-// Check if the Manage Permissions feature is enabled
-const showManagePermissions = (): boolean => {
-  return !isProduction() || (isProduction() && getEnvVariable('WIP_VIEWER_PERMISSIONS') === 'false');
-};
-
-// Check if the Insight Sharing feature is enabled
-const showInsightSharing = (): boolean => {
-  return !isProduction() || (isProduction() && getEnvVariable('WIP_INSIGHT_SHARING') === 'false');
-};
-
-// Check if the Insight Sharing feature is enabled
-const showSharableExploration = (): boolean => {
-  return !isProduction() || (isProduction() && getEnvVariable('WIP_SHARABLE_EXPLORATION') === 'false');
-};
-
-// Utility to check if a specific visualization is released based on environment variables
-const isVisualizationReleased = (visualizationName: string): boolean => {
-  const visualizationFlag = getEnvVariable(`WIP_${visualizationName.toUpperCase()}`, 'false');
-  return !isProduction() || (isProduction() && visualizationFlag === 'false');
-};
-
-export { isProduction, showManagePermissions, showInsightSharing, showSharableExploration, isVisualizationReleased };
diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts
index caecba356..867698fe2 100644
--- a/libs/config/src/index.ts
+++ b/libs/config/src/index.ts
@@ -1,2 +1,2 @@
 export * from './colors';
-export * from './featureFlags';
+export * from './envVariables';
diff --git a/libs/shared/lib/components/featureFlags/FeatureEnabled.tsx b/libs/shared/lib/components/featureFlags/FeatureEnabled.tsx
new file mode 100644
index 000000000..75ba6e0b2
--- /dev/null
+++ b/libs/shared/lib/components/featureFlags/FeatureEnabled.tsx
@@ -0,0 +1,6 @@
+import { canViewFeature, FeatureFlagName } from '.';
+import { ReactNode } from 'react';
+
+export function FeatureEnabled({ featureFlag, children }: { featureFlag: FeatureFlagName; children: ReactNode }) {
+  return canViewFeature(featureFlag) ? children : null;
+}
diff --git a/libs/shared/lib/components/featureFlags/featureFlags.ts b/libs/shared/lib/components/featureFlags/featureFlags.ts
new file mode 100644
index 000000000..1f98e9b85
--- /dev/null
+++ b/libs/shared/lib/components/featureFlags/featureFlags.ts
@@ -0,0 +1,45 @@
+import { getEnvVariable, EnvVarKey } from 'config';
+
+const getFeatureVariable = (flagId: EnvVarKey): boolean => {
+  // TODO: move feature flag storage to database instead of env variables
+  const value = getEnvVariable(flagId);
+  if (value === undefined) {
+    return false;
+  }
+  return String(value) === 'true';
+};
+
+export const ML_FLAGS = {
+  LINK_PREDICTION: getFeatureVariable('LINK_PREDICTION'),
+  CENTRALITY: getFeatureVariable('CENTRALITY'),
+  COMMUNITY_DETECTION: getFeatureVariable('COMMUNITY_DETECTION'),
+  SHORTEST_PATH: getFeatureVariable('SHORTEST_PATH'),
+};
+
+export const VISUALIZATIONS_FLAGS = {
+  RAWJSONVIS: getFeatureVariable('RAWJSONVIS'),
+  NODELINKVIS: getFeatureVariable('NODELINKVIS'),
+  TABLEVIS: getFeatureVariable('TABLEVIS'),
+  PAOHVIS: getFeatureVariable('PAOHVIS'),
+  MATRIXVIS: getFeatureVariable('MATRIXVIS'),
+  SEMANTICSUBSTRATESVIS: getFeatureVariable('SEMANTICSUBSTRATESVIS'),
+  MAPVIS: getFeatureVariable('MAPVIS'),
+  VIS0D: getFeatureVariable('VIS0D'),
+  VIS1D: getFeatureVariable('VIS1D'),
+};
+
+export const FEATURE_FLAGS = {
+  ...ML_FLAGS,
+  ...VISUALIZATIONS_FLAGS,
+  INSIGHT_SHARING: getFeatureVariable('INSIGHT_SHARING'),
+  VIEWER_PERMISSIONS: getFeatureVariable('VIEWER_PERMISSIONS'),
+  SHARABLE_EXPLORATION: getFeatureVariable('SHARABLE_EXPLORATION'),
+} as const satisfies Record<string, boolean>;
+
+export type FeatureFlagName = keyof typeof FEATURE_FLAGS;
+
+const canViewFeature = (flagId: FeatureFlagName): boolean => {
+  return FEATURE_FLAGS[flagId];
+};
+
+export { canViewFeature };
diff --git a/libs/shared/lib/components/featureFlags/index.ts b/libs/shared/lib/components/featureFlags/index.ts
new file mode 100644
index 000000000..20f816cf1
--- /dev/null
+++ b/libs/shared/lib/components/featureFlags/index.ts
@@ -0,0 +1,2 @@
+export * from './featureFlags';
+export * from './FeatureEnabled';
diff --git a/libs/shared/lib/data-access/broker/broker.tsx b/libs/shared/lib/data-access/broker/broker.tsx
index f915a2f24..1f577c33f 100644
--- a/libs/shared/lib/data-access/broker/broker.tsx
+++ b/libs/shared/lib/data-access/broker/broker.tsx
@@ -4,6 +4,7 @@
  * © Copyright Utrecht University (Department of Information and Computing Sciences)
  */
 
+import { getEnvVariable } from 'config';
 import { UseIsAuthorizedState } from '../store/authSlice';
 import { ReceiveMessageI, SendMessageI, SendMessageWithSessionI } from './types';
 
@@ -38,7 +39,7 @@ export class Broker {
 
   /** Get the singleton instance of the Broker. */
   public static instance(): Broker {
-    if (!this.singletonInstance) this.singletonInstance = new Broker(import.meta.env.BACKEND_WSS_URL);
+    if (!this.singletonInstance) this.singletonInstance = new Broker(String(getEnvVariable('BACKEND_WSS_URL')));
     return this.singletonInstance;
   }
 
diff --git a/libs/shared/lib/data-access/security/useAuthentication.tsx b/libs/shared/lib/data-access/security/useAuthentication.tsx
index a14824d96..0da759abb 100644
--- a/libs/shared/lib/data-access/security/useAuthentication.tsx
+++ b/libs/shared/lib/data-access/security/useAuthentication.tsx
@@ -1,9 +1,10 @@
+import { getEnvVariable } from 'config';
 import { useAppDispatch, useAuthCache } from '../store';
 import { authenticated, changeRoom, UserAuthenticationHeader } from '../store/authSlice';
 import { addInfo, addError } from '../store/configSlice';
 
-const domain = import.meta.env.BACKEND_URL;
-const userURI = import.meta.env.BACKEND_USER;
+const domain = getEnvVariable('BACKEND_URL');
+const userURI = getEnvVariable('BACKEND_USER');
 
 export const fetchSettings: RequestInit = {
   method: 'GET',
diff --git a/libs/shared/lib/inspector/InspectorPanel.tsx b/libs/shared/lib/inspector/InspectorPanel.tsx
index 074ad172e..66a88f64b 100644
--- a/libs/shared/lib/inspector/InspectorPanel.tsx
+++ b/libs/shared/lib/inspector/InspectorPanel.tsx
@@ -9,9 +9,10 @@ import { SelectionConfig } from '../vis/components/config/SelectionConfig';
 import { SchemaSettings } from '../schema/panel/SchemaSettings';
 import { QuerySettings } from '../querybuilder/panel/querysidepanel/QuerySettings';
 import { useActiveVisualization } from '@graphpolaris/shared/lib/data-access';
+import { getEnvVariable } from 'config';
 
 export function InspectorPanel(props: { children?: React.ReactNode }) {
-  const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION;
+  const buildInfo = getEnvVariable('GRAPHPOLARIS_VERSION') === 'prod';
   const selection = useSelection();
   const focus = useFocus();
   const dispatch = useDispatch();
diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/QueryMLDialog.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/QueryMLDialog.tsx
index 1d2860ced..54f65c205 100644
--- a/libs/shared/lib/querybuilder/panel/querysidepanel/QueryMLDialog.tsx
+++ b/libs/shared/lib/querybuilder/panel/querysidepanel/QueryMLDialog.tsx
@@ -8,6 +8,7 @@ import {
 } from '@graphpolaris/shared/lib/data-access/store/mlSlice';
 import { FormCard, FormBody, FormTitle, FormHBar } from '@graphpolaris/shared/lib/components/forms';
 import { Input } from '@graphpolaris/shared/lib/components/inputs';
+import { FeatureEnabled } from '@graphpolaris/shared/lib/components/featureFlags';
 
 export const QueryMLDialog = () => {
   const dispatch = useAppDispatch();
@@ -25,51 +26,59 @@ export const QueryMLDialog = () => {
           <FormHBar />
 
           <div className="px-5">
-            <Input
-              type="boolean"
-              label="Link Prediction"
-              value={ml.linkPrediction.enabled}
-              onChange={() => dispatch(setLinkPredictionEnabled(!ml.linkPrediction.enabled))}
-            />
-            {ml.linkPrediction.enabled && ml.linkPrediction.result && <span># of predictions: {ml.linkPrediction.result.length}</span>}
-            {ml.linkPrediction.enabled && !ml.linkPrediction.result && <span>Loading...</span>}
+            <FeatureEnabled featureFlag="LINK_PREDICTION">
+              <Input
+                type="boolean"
+                label="Link Prediction"
+                value={ml.linkPrediction.enabled}
+                onChange={() => dispatch(setLinkPredictionEnabled(!ml.linkPrediction.enabled))}
+              />
+              {ml.linkPrediction.enabled && ml.linkPrediction.result && <span># of predictions: {ml.linkPrediction.result.length}</span>}
+              {ml.linkPrediction.enabled && !ml.linkPrediction.result && <span>Loading...</span>}
+            </FeatureEnabled>
 
-            <Input
-              type="boolean"
-              label="Centrality"
-              value={ml.centrality.enabled}
-              onChange={() => dispatch(setCentralityEnabled(!ml.centrality.enabled))}
-            />
-            {ml.centrality.enabled && Object.values(ml.centrality.result).length > 0 && (
-              <span>
-                sum of centers:
-                {Object.values(ml.centrality.result)
-                  .reduce((a, b) => b + a)
-                  .toFixed(2)}
-              </span>
-            )}
-            {ml.centrality.enabled && Object.values(ml.centrality.result).length === 0 && <span>No Centers Found</span>}
+            <FeatureEnabled featureFlag="CENTRALITY">
+              <Input
+                type="boolean"
+                label="Centrality"
+                value={ml.centrality.enabled}
+                onChange={() => dispatch(setCentralityEnabled(!ml.centrality.enabled))}
+              />
+              {ml.centrality.enabled && Object.values(ml.centrality.result).length > 0 && (
+                <span>
+                  sum of centers:
+                  {Object.values(ml.centrality.result)
+                    .reduce((a, b) => b + a)
+                    .toFixed(2)}
+                </span>
+              )}
+              {ml.centrality.enabled && Object.values(ml.centrality.result).length === 0 && <span>No Centers Found</span>}
+            </FeatureEnabled>
 
-            <Input
-              type="boolean"
-              label="Community detection"
-              value={ml.communityDetection.enabled}
-              onChange={() => dispatch(setCommunityDetectionEnabled(!ml.communityDetection.enabled))}
-            />
-            {ml.communityDetection.enabled && ml.communityDetection.result && (
-              <span># of communities: {ml.communityDetection.result.length}</span>
-            )}
-            {ml.communityDetection.enabled && !ml.communityDetection.result && <span>Loading...</span>}
+            <FeatureEnabled featureFlag="COMMUNITY_DETECTION">
+              <Input
+                type="boolean"
+                label="Community detection"
+                value={ml.communityDetection.enabled}
+                onChange={() => dispatch(setCommunityDetectionEnabled(!ml.communityDetection.enabled))}
+              />
+              {ml.communityDetection.enabled && ml.communityDetection.result && (
+                <span># of communities: {ml.communityDetection.result.length}</span>
+              )}
+              {ml.communityDetection.enabled && !ml.communityDetection.result && <span>Loading...</span>}
+            </FeatureEnabled>
 
-            <Input
-              type="boolean"
-              label="Shortest path"
-              value={ml.shortestPath.enabled}
-              onChange={() => dispatch(setShortestPathEnabled(!ml.shortestPath.enabled))}
-            />
-            {ml.shortestPath.enabled && ml.shortestPath.result?.length > 0 && <span># of hops: {ml.shortestPath.result.length}</span>}
-            {ml.shortestPath.enabled && !ml.shortestPath.srcNode && <span>Please select source node</span>}
-            {ml.shortestPath.enabled && ml.shortestPath.srcNode && !ml.shortestPath.trtNode && <span>Please select target node</span>}
+            <FeatureEnabled featureFlag="SHORTEST_PATH">
+              <Input
+                type="boolean"
+                label="Shortest path"
+                value={ml.shortestPath.enabled}
+                onChange={() => dispatch(setShortestPathEnabled(!ml.shortestPath.enabled))}
+              />
+              {ml.shortestPath.enabled && ml.shortestPath.result?.length > 0 && <span># of hops: {ml.shortestPath.result.length}</span>}
+              {ml.shortestPath.enabled && !ml.shortestPath.srcNode && <span>Please select source node</span>}
+              {ml.shortestPath.enabled && ml.shortestPath.srcNode && !ml.shortestPath.trtNode && <span>Please select target node</span>}
+            </FeatureEnabled>
           </div>
         </FormBody>
       </FormCard>
diff --git a/libs/shared/lib/sidebar/index.tsx b/libs/shared/lib/sidebar/index.tsx
index 7c2f42419..c4f160cf1 100644
--- a/libs/shared/lib/sidebar/index.tsx
+++ b/libs/shared/lib/sidebar/index.tsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../components';
 import ColorMode from '../components/color-mode';
+import { FeatureEnabled } from '@graphpolaris/shared/lib/components/featureFlags';
 
 export type SideNavTab = 'Schema' | 'Search' | undefined;
 
@@ -29,9 +30,6 @@ export function Sidebar({
   tab: SideNavTab;
   openMonitoringDialog: () => void;
 }) {
-  const isProd = import.meta.env.GRAPHPOLARIS_VERSION === 'prod';
-  const isInsightSharingWIP = import.meta.env.WIP_INSIGHT_SHARING === 'true';
-
   return (
     <div className="side-bar w-fit h-full flex shrink">
       <TooltipProvider delayDuration={100}>
@@ -58,7 +56,7 @@ export function Sidebar({
             </Tooltip>
           ))}
 
-          {(!isProd || (isProd && !isInsightSharingWIP)) && (
+          <FeatureEnabled featureFlag="INSIGHT_SHARING">
             <Tooltip placement={'right'}>
               <TooltipTrigger asChild>
                 <Button
@@ -71,7 +69,7 @@ export function Sidebar({
               </TooltipTrigger>
               <TooltipContent>Insight Sharing</TooltipContent>
             </Tooltip>
-          )}
+          </FeatureEnabled>
 
           <div className="mt-auto mb-2">
             <ColorMode />
diff --git a/libs/shared/lib/vis/components/VisualizationPanel.tsx b/libs/shared/lib/vis/components/VisualizationPanel.tsx
index 506a38c79..21f04168e 100644
--- a/libs/shared/lib/vis/components/VisualizationPanel.tsx
+++ b/libs/shared/lib/vis/components/VisualizationPanel.tsx
@@ -17,21 +17,21 @@ import { updateVisualization, addVisualization } from '../../data-access/store/v
 import { VisualizationPropTypes, VISComponentType } from '../common';
 import { ErrorBoundary } from '../../components/errorBoundary';
 import { addError } from '../../data-access/store/configSlice';
-import { isVisualizationReleased } from 'config';
+import { canViewFeature } from '@graphpolaris/shared/lib/components/featureFlags/featureFlags';
 
 type PromiseFunc = () => Promise<{ default: VISComponentType<any> }>;
 export const Visualizations: Record<string, PromiseFunc> = {
-  ...(isVisualizationReleased('TableVis') && { TableVis: () => import('../visualizations/tablevis/tablevis') }),
-  ...(isVisualizationReleased('PaohVis') && { PaohVis: () => import('../visualizations/paohvis/paohvis') }),
-  ...(isVisualizationReleased('RawJSONVis') && { RawJSONVis: () => import('../visualizations/rawjsonvis/rawjsonvis') }),
-  ...(isVisualizationReleased('NodeLinkVis') && { NodeLinkVis: () => import('../visualizations/nodelinkvis/nodelinkvis') }),
-  ...(isVisualizationReleased('MatrixVis') && { MatrixVis: () => import('../visualizations/matrixvis/matrixvis') }),
-  ...(isVisualizationReleased('SemanticSubstratesVis') && {
+  ...(canViewFeature('TABLEVIS') && { TableVis: () => import('../visualizations/tablevis/tablevis') }),
+  ...(canViewFeature('PAOHVIS') && { PaohVis: () => import('../visualizations/paohvis/paohvis') }),
+  ...(canViewFeature('RAWJSONVIS') && { RawJSONVis: () => import('../visualizations/rawjsonvis/rawjsonvis') }),
+  ...(canViewFeature('NODELINKVIS') && { NodeLinkVis: () => import('../visualizations/nodelinkvis/nodelinkvis') }),
+  ...(canViewFeature('MATRIXVIS') && { MatrixVis: () => import('../visualizations/matrixvis/matrixvis') }),
+  ...(canViewFeature('SEMANTICSUBSTRATESVIS') && {
     SemanticSubstratesVis: () => import('../visualizations/semanticsubstratesvis/semanticsubstratesvis'),
   }),
-  ...(isVisualizationReleased('MapVis') && { MapVis: () => import('../visualizations/mapvis/mapvis') }),
-  ...(isVisualizationReleased('Vis0D') && { Vis0D: () => import('../visualizations/vis0D/Vis0D') }),
-  ...(isVisualizationReleased('Vis1D') && { Vis1D: () => import('../visualizations/vis1D/Vis1D') }),
+  ...(canViewFeature('MAPVIS') && { MapVis: () => import('../visualizations/mapvis/mapvis') }),
+  ...(canViewFeature('VIS0D') && { Vis0D: () => import('../visualizations/Vis0D/Vis0D') }),
+  ...(canViewFeature('VIS1D') && { Vis1D: () => import('../visualizations/vis1D/Vis1D') }),
 };
 
 export const VISUALIZATION_TYPES: string[] = Object.keys(Visualizations);
-- 
GitLab