From bf1ba719483c0fdd8c4c8a6f9a5d38308e809e09 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 17 Dec 2025 08:25:03 -0300 Subject: [PATCH 01/11] Migrate to proxy architecture with lazy loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove backend plugin (src/service/) in favor of Backstage proxy - Update FlagsmithClient to use proxy endpoint (/proxy/flagsmith) - Add lazy loading for feature details (versions, states) on accordion expand - Reduce initial API calls from 35 to 1 for improved performance - Update README with proxy configuration instructions - Remove backend dependencies from package.json Closes Flagsmith/flagsmith#6420 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitignore | 1 + README.md | 164 +++++- package.json | 5 - src/api/FlagsmithClient.ts | 170 +++++-- src/components/FlagsTab.tsx | 608 ++++++++++++++--------- src/components/FlagsmithOverviewCard.tsx | 66 ++- src/index.ts | 3 +- src/service/index.ts | 1 - src/service/plugin.ts | 32 -- src/service/router.ts | 217 -------- 10 files changed, 696 insertions(+), 571 deletions(-) delete mode 100644 src/service/index.ts delete mode 100644 src/service/plugin.ts delete mode 100644 src/service/router.ts diff --git a/.gitignore b/.gitignore index 5e91c8b..c025b43 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ node_modules/ # Misc .DS_Store .env +app-config.local.yaml .env.local .env.development.local .env.test.local diff --git a/README.md b/README.md index 1d5df18..a69f164 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,161 @@ -# flagsmith +# Flagsmith Plugin for Backstage -Welcome to the Flagsmith plugin! +Integrate [Flagsmith](https://flagsmith.com) feature flags into your Backstage instance. -This plugins: +## Features -- Adds a 'Feature Flags' tab on component pages. -- Provides 2 Cards that can be added to component Overview pages. +- **Feature Flags Tab** - View all feature flags for a service directly in the entity page +- **Overview Card** - Quick summary of flags and their states +- **Usage Card** - Display Flagsmith usage metrics -## Getting started +## Installation -Currently, it is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. +### 1. Install the plugin -Add the following annotations to a component to link it to a flagsmith project, replacing with your Project and Organization IDs: +```bash +# From your Backstage root directory +yarn --cwd packages/app add @flagsmith/backstage-plugin +``` + +### 2. Configure the Backstage proxy + +Add to your `app-config.yaml` (or `app-config.local.yaml` for local development): + +```yaml +proxy: + endpoints: + '/flagsmith': + target: 'https://api.flagsmith.com/api/v1' + headers: + Authorization: Api-Key ${FLAGSMITH_API_TOKEN} +``` +> **Note:** Use an environment variable for the API token in production. Never commit tokens to version control. + +For self-hosted Flagsmith, change the target URL: + +```yaml +proxy: + endpoints: + '/flagsmith': + target: 'https://your-flagsmith-instance.com/api/v1' + headers: + Authorization: Api-Key ${FLAGSMITH_API_TOKEN} ``` -annotations: - flagsmith.com/project-id: "00000" - flagsmith.com/org-id: "00000" # Optional, defaults to first org + +### 3. Add the Feature Flags tab to entity pages + +In `packages/app/src/components/catalog/EntityPage.tsx`: + +```typescript +import { FlagsTab } from '@flagsmith/backstage-plugin'; + +// Add to your entity page layout (e.g., serviceEntityPage) + + + +``` + +### 4. (Optional) Add cards to the Overview page + +```typescript +import { + FlagsmithOverviewCard, + FlagsmithUsageCard, +} from '@flagsmith/backstage-plugin'; + +// Add to your entity overview page + + + + + + +``` + +### 5. Annotate your entities + +Add Flagsmith annotations to your `catalog-info.yaml`: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: my-service + annotations: + flagsmith.com/project-id: '12345' + flagsmith.com/org-id: '67890' # Optional - defaults to first organization +spec: + type: service + owner: team-a ``` -Configure your credentials by adding the following to app-config.yaml (or your local override app-config.local.yaml): +## Getting your Flagsmith credentials + +1. Log in to your [Flagsmith dashboard](https://app.flagsmith.com) +2. Go to **Organisation Settings** > **API Keys** +3. Create or copy your **Admin API Key** +4. Find your **Project ID** and **Organisation ID** in the URL or project settings + +## Development + +### Prerequisites + +- Node.js 22+ (Node 24 has known ESM compatibility issues with Backstage) +- Yarn +- A Backstage application for testing + +### Local Development Setup + +1. Clone the repository: + + ```bash + git clone https://github.com/Flagsmith/flagsmith-backstage-plugin.git + cd flagsmith-backstage-plugin + ``` + +2. Install dependencies: + + ```bash + yarn install + ``` + +3. To test in a Backstage app, copy or link the plugin to your Backstage workspace's `plugins/` directory and add it as a workspace dependency. + +4. Create `app-config.local.yaml` with your Flagsmith credentials (this file is gitignored). + +5. Run the Backstage app: + ```bash + yarn start + ``` + +### Available Scripts + +| Command | Description | +| ------------ | ---------------------------- | +| `yarn start` | Start the development server | +| `yarn build` | Build for production | +| `yarn test` | Run tests | +| `yarn lint` | Lint the codebase | + +### Project Structure ``` -# Backstage override configuration for your local development environment -flagsmith: - apiUrl: https://api.flagsmith.com - apiToken: yourApiToken +src/ +├── components/ # React components +│ ├── FlagsTab.tsx +│ ├── FlagsmithOverviewCard.tsx +│ └── FlagsmithUsageCard.tsx +├── api/ # API client (uses Backstage proxy) +│ └── FlagsmithClient.ts +├── plugin.ts # Frontend plugin definition +└── index.ts # Package exports ``` + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request. + +## License + +Apache-2.0 diff --git a/package.json b/package.json index 5323669..4099501 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,6 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { - "@backstage/backend-defaults": "^0.2.0", - "@backstage/backend-plugin-api": "^0.6.0", - "@backstage/config": "^1.1.0", "@backstage/core-components": "^0.18.2", "@backstage/core-plugin-api": "^1.11.1", "@backstage/plugin-catalog-react": "^1.13.3", @@ -35,8 +32,6 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.61", - "express": "^4.17.1", - "express-promise-router": "^4.1.0", "react-use": "^17.2.4", "recharts": "^2.5.0" }, diff --git a/src/api/FlagsmithClient.ts b/src/api/FlagsmithClient.ts index a84e8e8..b88826b 100644 --- a/src/api/FlagsmithClient.ts +++ b/src/api/FlagsmithClient.ts @@ -4,7 +4,6 @@ export interface FlagsmithOrganization { id: number; name: string; created_date: string; - // Add more fields as needed } export interface FlagsmithProject { @@ -12,7 +11,6 @@ export interface FlagsmithProject { name: string; organisation: number; created_date: string; - // Add more fields as needed } export interface FlagsmithEnvironment { @@ -28,18 +26,20 @@ export interface FlagsmithFeature { description?: string; created_date: string; project: number; - environment_state: Array<{ + environment_state?: Array<{ id: number; enabled: boolean; - }>; + feature_segment?: number | null; + }> | null; num_segment_overrides?: number | null; num_identity_overrides?: number | null; - live_version: { + live_version?: { is_live: boolean; live_from?: string | null; published: boolean; published_by?: string | null; - }; + uuid?: string; + } | null; owners?: Array<{ id: number; name: string; @@ -52,6 +52,27 @@ export interface FlagsmithFeature { is_archived?: boolean; } +export interface FlagsmithFeatureVersion { + uuid: string; + is_live: boolean; + live_from?: string | null; + published: boolean; + published_by?: string | null; +} + +export interface FlagsmithFeatureState { + id: number; + enabled: boolean; + feature_segment?: number | null; + feature_state_value?: string | null; +} + +export interface FlagsmithFeatureDetails { + liveVersion: FlagsmithFeatureVersion | null; + featureState: FlagsmithFeatureState[] | null; + segmentOverrides: number; +} + export interface FlagsmithUsageData { flags: number | null; identities: number; @@ -72,72 +93,80 @@ export class FlagsmithClient { ) {} private async getBaseUrl(): Promise { - return await this.discoveryApi.getBaseUrl('flagsmith'); + const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); + return `${proxyUrl}/flagsmith`; } async getOrganizations(): Promise { const baseUrl = await this.getBaseUrl(); - const response = await this.fetchApi.fetch(`${baseUrl}/organizations`); - + const response = await this.fetchApi.fetch(`${baseUrl}/organisations/`); + if (!response.ok) { throw new Error(`Failed to fetch organizations: ${response.statusText}`); } - + const data = await response.json(); - return data.results || data; // Handle paginated vs non-paginated responses + return data.results || data; } async getProjectsInOrg(orgId: number): Promise { const baseUrl = await this.getBaseUrl(); - const response = await this.fetchApi.fetch(`${baseUrl}/organizations/${orgId}/projects`); - + const response = await this.fetchApi.fetch( + `${baseUrl}/organisations/${orgId}/projects/`, + ); + if (!response.ok) { throw new Error(`Failed to fetch projects: ${response.statusText}`); } - + const data = await response.json(); return data.results || data; } async getProjectFeatures(projectId: string): Promise { const baseUrl = await this.getBaseUrl(); - const response = await this.fetchApi.fetch(`${baseUrl}/projects/${projectId}/features`); - + const response = await this.fetchApi.fetch( + `${baseUrl}/projects/${projectId}/features/`, + ); + if (!response.ok) { throw new Error(`Failed to fetch features: ${response.statusText}`); } - + const data = await response.json(); return data.results || data; } - async getEnvironmentFeatures(environmentId: number, projectId: string): Promise { - const baseUrl = await this.getBaseUrl(); - const response = await this.fetchApi.fetch(`${baseUrl}/projects/${projectId}/environments/${environmentId}/features`); - - if (!response.ok) { - throw new Error(`Failed to fetch environment features: ${response.statusText}`); - } - - const data = await response.json(); - return data.results || data; + async getEnvironmentFeatures( + _environmentId: number, + projectId: string, + ): Promise { + // With proxy approach, we just get project features + // Details are loaded lazily on accordion expand + return this.getProjectFeatures(projectId); } - async getProjectEnvironments(projectId: number): Promise { + async getProjectEnvironments( + projectId: number, + ): Promise { const baseUrl = await this.getBaseUrl(); - const response = await this.fetchApi.fetch(`${baseUrl}/projects/${projectId}/environments`); - + const response = await this.fetchApi.fetch( + `${baseUrl}/projects/${projectId}/environments/`, + ); + if (!response.ok) { throw new Error(`Failed to fetch environments: ${response.statusText}`); } - + const data = await response.json(); return data.results || data; } async getProject(projectId: number): Promise { const baseUrl = await this.getBaseUrl(); - const response = await this.fetchApi.fetch(`${baseUrl}/projects/${projectId}`); + const response = await this.fetchApi.fetch( + `${baseUrl}/projects/${projectId}/`, + ); if (!response.ok) { throw new Error(`Failed to fetch project: ${response.statusText}`); @@ -146,11 +175,14 @@ export class FlagsmithClient { return await response.json(); } - async getUsageData(orgId: number, projectId?: number, period: string = '30_day_period'): Promise { + async getUsageData( + orgId: number, + projectId?: number, + ): Promise { const baseUrl = await this.getBaseUrl(); - let url = `${baseUrl}/organizations/${orgId}/usage-data?period=${period}`; + let url = `${baseUrl}/organisations/${orgId}/usage-data/`; if (projectId) { - url += `&project_id=${projectId}`; + url += `?project_id=${projectId}`; } const response = await this.fetchApi.fetch(url); @@ -161,4 +193,70 @@ export class FlagsmithClient { return await response.json(); } -} \ No newline at end of file + + // Lazy loading methods for feature details + async getFeatureVersions( + environmentId: number, + featureId: number, + ): Promise { + const baseUrl = await this.getBaseUrl(); + const response = await this.fetchApi.fetch( + `${baseUrl}/environments/${environmentId}/features/${featureId}/versions/`, + ); + + if (!response.ok) { + throw new Error( + `Failed to fetch feature versions: ${response.statusText}`, + ); + } + + const data = await response.json(); + return data.results || data; + } + + async getFeatureStates( + environmentId: number, + featureId: number, + versionUuid: string, + ): Promise { + const baseUrl = await this.getBaseUrl(); + const response = await this.fetchApi.fetch( + `${baseUrl}/environments/${environmentId}/features/${featureId}/versions/${versionUuid}/featurestates/`, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch feature states: ${response.statusText}`); + } + + return await response.json(); + } + + // Helper to load full feature details (called on accordion expand) + async getFeatureDetails( + environmentId: number, + featureId: number, + ): Promise { + const versions = await this.getFeatureVersions(environmentId, featureId); + const liveVersion = versions.find(v => v.is_live) || null; + + let featureState: FlagsmithFeatureState[] | null = null; + let segmentOverrides = 0; + + if (liveVersion) { + featureState = await this.getFeatureStates( + environmentId, + featureId, + liveVersion.uuid, + ); + segmentOverrides = (featureState || []).filter( + s => s.feature_segment != null, + ).length; + } + + return { + liveVersion, + featureState, + segmentOverrides, + }; + } +} diff --git a/src/components/FlagsTab.tsx b/src/components/FlagsTab.tsx index 22934ee..370e44b 100644 --- a/src/components/FlagsTab.tsx +++ b/src/components/FlagsTab.tsx @@ -18,28 +18,73 @@ import { IconButton, Collapse, Chip, - Badge, } from '@material-ui/core'; import { KeyboardArrowDown, KeyboardArrowRight } from '@material-ui/icons'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { useApi, discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api'; -import { FlagsmithClient, FlagsmithEnvironment, FlagsmithFeature } from '../api/FlagsmithClient'; +import { + useApi, + discoveryApiRef, + fetchApiRef, +} from '@backstage/core-plugin-api'; +import { + FlagsmithClient, + FlagsmithEnvironment, + FlagsmithFeature, + FlagsmithFeatureDetails, +} from '../api/FlagsmithClient'; interface ExpandableRowProps { feature: FlagsmithFeature; + client: FlagsmithClient; + environmentId: number; } -const ExpandableRow = ({ feature }: ExpandableRowProps) => { +const ExpandableRow = ({ + feature, + client, + environmentId, +}: ExpandableRowProps) => { const [open, setOpen] = useState(false); const [envStatesOpen, setEnvStatesOpen] = useState(false); + const [details, setDetails] = useState(null); + const [loadingDetails, setLoadingDetails] = useState(false); + const [detailsError, setDetailsError] = useState(null); + + const handleToggle = async () => { + const newOpen = !open; + setOpen(newOpen); + + // Load details on first expand + if (newOpen && !details && !loadingDetails) { + setLoadingDetails(true); + setDetailsError(null); + try { + const featureDetails = await client.getFeatureDetails( + environmentId, + feature.id, + ); + setDetails(featureDetails); + } catch (err) { + setDetailsError( + err instanceof Error ? err.message : 'Failed to load details', + ); + } finally { + setLoadingDetails(false); + } + } + }; - console.log('Rendering feature row for:', feature); + // Use details if loaded, otherwise fall back to feature data + const liveVersion = details?.liveVersion || feature.live_version; + const environmentState = details?.featureState || feature.environment_state; + const segmentOverrides = + details?.segmentOverrides ?? feature.num_segment_overrides ?? 0; return ( <> - setOpen(!open)}> + {open ? : } @@ -54,20 +99,14 @@ const ExpandableRow = ({ feature }: ExpandableRowProps) => { - 0 ? feature.num_segment_overrides : null} - color="secondary" - overlap="rectangular" - > - - + - - {/* Placeholder for value */} + - @@ -79,200 +118,311 @@ const ExpandableRow = ({ feature }: ExpandableRowProps) => { - {/* Main Info Row - 4 Columns */} - - {/* Column 1: Active Version */} - {feature.live_version && ( - - - Active Version - - - - Status:{' '} - {feature.live_version.is_live ? 'Active' : 'Inactive'} - - - Published: {feature.live_version.published ? 'Yes' : 'No'} - - {feature.live_version.live_from && ( - - Active From: {new Date(feature.live_version.live_from).toLocaleString()} - - )} - - Published By: User ID {feature.live_version.published_by} - - - - )} - - {/* Column 2: Overview */} - - - Overview + {loadingDetails ? ( + + + + Loading feature details... - - - ID: {feature.id} - - - Type: {feature.type} - - - Default Enabled: {feature.default_enabled ? 'Yes' : 'No'} - - - Archived: {feature.is_archived ? 'Yes' : 'No'} - - {feature.is_server_key_only && ( - - - - )} - - - - {/* Column 3: Owners */} - {feature.owners && feature.owners.length > 0 && ( - - - Owners - - - {feature.owners.map((owner: any) => ( - - - {owner.first_name} {owner.last_name} + + ) : detailsError ? ( + + {detailsError} + + ) : ( + <> + {/* Main Info Row - 4 Columns */} + + {/* Column 1: Active Version */} + {liveVersion && ( + + + Active Version + + + + Status:{' '} + {liveVersion.is_live ? 'Active' : 'Inactive'} - - {owner.email} + + Published:{' '} + {liveVersion.published ? 'Yes' : 'No'} - - Last login: {new Date(owner.last_login).toLocaleString()} + {liveVersion.live_from && ( + + Active From:{' '} + {new Date(liveVersion.live_from).toLocaleString()} + + )} + + Published By: User ID{' '} + {liveVersion.published_by} - ))} - - - )} - - {/* Column 4: Overrides */} - - - Overrides - - - - Segment Overrides: {feature.num_segment_overrides || 0} - - {feature.num_identity_overrides !== null && ( - - Identity Overrides: {feature.num_identity_overrides} - + )} - - - {/* Tags Row (if exists) */} - {feature.tags && feature.tags.length > 0 && ( - - - Tags - - - {feature.tags.map((tag: any, index: number) => ( - - ))} - - - )} - + {/* Column 2: Overview */} + + + Overview + + + + ID: {feature.id} + + + Type: {feature.type} + + + Default Enabled:{' '} + {feature.default_enabled ? 'Yes' : 'No'} + + + Archived:{' '} + {feature.is_archived ? 'Yes' : 'No'} + + {feature.is_server_key_only && ( + + + + )} + + - {/* Environment States - Collapsible Section */} - {feature.environment_state && feature.environment_state.length > 0 && ( - - setEnvStatesOpen(!envStatesOpen)} style={{ cursor: 'pointer' }}> - - {envStatesOpen ? : } - - - Environment States ({feature.environment_state.length}) - - - - - {feature.environment_state.map((state: any) => ( - 0 && ( + + - - - - {state.feature_segment && ( - - )} + Owners + + + {feature.owners.map((owner: any) => ( + - Env ID: {state.environment} + + {owner.first_name} {owner.last_name} + - - - Updated: {new Date(state.updated_at).toLocaleString()} - - - - {/* Feature State Value - Only if not null */} - {state.feature_state_value && ( - state.feature_state_value.string_value !== null || - state.feature_state_value.integer_value !== null || - state.feature_state_value.boolean_value !== null - ) && ( - - {state.feature_state_value.string_value !== null && ( - - Value: {state.feature_state_value.string_value} - - )} - {state.feature_state_value.integer_value !== null && ( - - Value: {state.feature_state_value.integer_value} - - )} - {state.feature_state_value.boolean_value !== null && ( - - Value: {state.feature_state_value.boolean_value.toString()} + + {owner.email} + + {owner.last_login && ( + + Last login:{' '} + {new Date(owner.last_login).toLocaleString()} )} + ))} + + + )} + + {/* Column 4: Overrides */} + + + Overrides + + + + Segment Overrides: {segmentOverrides} + + {feature.num_identity_overrides !== null && + feature.num_identity_overrides !== undefined && ( + + Identity Overrides:{' '} + {feature.num_identity_overrides} + )} + + - {/* Segment Information */} - {state.feature_segment && ( - - - Segment ID: {state.feature_segment.segment} | Priority: {state.feature_segment.priority} - - + {/* Tags Row (if exists) */} + {feature.tags && feature.tags.length > 0 && ( + + + Tags + + + {feature.tags.map((tag: any, index: number) => ( + + ))} + + + )} + + + {/* Environment States - Collapsible Section */} + {environmentState && environmentState.length > 0 && ( + + setEnvStatesOpen(!envStatesOpen)} + style={{ cursor: 'pointer' }} + > + + {envStatesOpen ? ( + + ) : ( + )} + + + Environment States ({environmentState.length}) + + + + + {environmentState.map((state: any) => ( + + + + + {state.feature_segment && ( + + )} + {state.environment && ( + + Env ID: {state.environment} + + )} + + {state.updated_at && ( + + Updated:{' '} + {new Date(state.updated_at).toLocaleString()} + + )} + + + {/* Feature State Value */} + {state.feature_state_value && ( + + {state.feature_state_value.string_value !== + null && ( + + Value:{' '} + {state.feature_state_value.string_value} + + )} + {state.feature_state_value.integer_value !== + null && ( + + Value:{' '} + {state.feature_state_value.integer_value} + + )} + {state.feature_state_value.boolean_value !== + null && ( + + Value:{' '} + {state.feature_state_value.boolean_value.toString()} + + )} + + )} + + {/* Segment Information */} + {state.feature_segment && ( + + + Segment ID:{' '} + {state.feature_segment.segment} |{' '} + Priority:{' '} + {state.feature_segment.priority} + + + )} + + ))} - ))} + - - + )} + )} @@ -286,15 +436,19 @@ export const FlagsTab = () => { const { entity } = useEntity(); const discoveryApi = useApi(discoveryApiRef); const fetchApi = useApi(fetchApiRef); - + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [projectInfo, setProjectInfo] = useState(null); const [environments, setEnvironments] = useState([]); - const [selectedEnvironment, setSelectedEnvironment] = useState(null); + const [selectedEnvironment, setSelectedEnvironment] = useState( + null, + ); const [features, setFeatures] = useState([]); const [featuresLoading, setFeaturesLoading] = useState(false); + const client = new FlagsmithClient(discoveryApi, fetchApi); + // Get project ID from entity annotations const projectId = entity.metadata.annotations?.['flagsmith.com/project-id']; @@ -307,22 +461,18 @@ export const FlagsTab = () => { const fetchData = async () => { try { - const client = new FlagsmithClient(discoveryApi, fetchApi); - // Fetch project info const project = await client.getProject(parseInt(projectId)); setProjectInfo(project); - + // Fetch environments const envs = await client.getProjectEnvironments(parseInt(projectId)); setEnvironments(envs); - - // Select first environment by default and fetch its features + + // Select first environment by default if (envs.length > 0) { setSelectedEnvironment(envs[0].id); - // We'll fetch features for this environment in the effect below } - } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); } finally { @@ -331,44 +481,31 @@ export const FlagsTab = () => { }; fetchData(); - }, [projectId, discoveryApi, fetchApi]); + }, [projectId]); - // Separate effect to fetch features when environment changes + // Fetch features when environment changes useEffect(() => { if (!selectedEnvironment || !projectId) return; const fetchFeaturesForEnvironment = async () => { setFeaturesLoading(true); try { - const client = new FlagsmithClient(discoveryApi, fetchApi); - - // Fetch features for the selected environment - // We'll need to add this method to the client - const envFeatures = await client.getEnvironmentFeatures(selectedEnvironment, projectId); - setFeatures(envFeatures); - + // Just get project features - details loaded lazily on expand + const projectFeatures = await client.getProjectFeatures(projectId); + setFeatures(projectFeatures); } catch (err) { - //console.error('Failed to fetch environment features:', err); - // For now, fall back to project features - try { - const client = new FlagsmithClient(discoveryApi, fetchApi); - const projectFeatures = await client.getProjectFeatures(projectId); - setFeatures(projectFeatures); - } catch (fallbackErr) { - setError('Failed to fetch features'); - } + setError('Failed to fetch features'); } finally { setFeaturesLoading(false); } }; fetchFeaturesForEnvironment(); - }, [selectedEnvironment, projectId, discoveryApi, fetchApi]); + }, [selectedEnvironment, projectId]); // Handle environment selection change const handleEnvironmentChange = (envId: number) => { setSelectedEnvironment(envId); - // Features will be fetched by the useEffect above }; if (loading) { @@ -382,12 +519,11 @@ export const FlagsTab = () => { if (error) { return ( - - Error: {error} - + Error: {error} {!projectId && ( - Add a flagsmith.com/project-id annotation to this entity to view feature flags. + Add a flagsmith.com/project-id annotation to this + entity to view feature flags. )} @@ -398,23 +534,21 @@ export const FlagsTab = () => { - - Feature Flags - + Feature Flags {projectInfo?.name} ({features.length} flags) - + Environment