From 97c20012edf83fd7a26868af0a6d54ff518e45a2 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 6 Feb 2026 12:02:52 -0300 Subject: [PATCH] fix(ui): display tag labels instead of numeric IDs The Flagsmith API returns tags as an array of numeric IDs, but the UI was displaying these IDs directly. This fix fetches tag definitions from the project and creates a lookup map to display labels instead. - Add FlagsmithTag interface and getProjectTags method to FlagsmithClient - Update useFlagsmithProject hook to fetch tags and provide tagMap - Update components to use tagMap for resolving tag labels - Update mock handlers with realistic tag data Co-Authored-By: Claude Opus 4.5 --- dev/mockHandlers.ts | 68 ++++++++++++++----- src/api/FlagsmithClient.ts | 22 +++++- src/components/FlagsTab/ExpandableRow.tsx | 9 ++- .../FlagsTab/FeatureDetailsGrid.tsx | 8 ++- src/components/FlagsTab/index.tsx | 3 +- src/hooks/useFlagsmithProject.test.tsx | 7 +- src/hooks/useFlagsmithProject.ts | 15 +++- 7 files changed, 105 insertions(+), 27 deletions(-) diff --git a/dev/mockHandlers.ts b/dev/mockHandlers.ts index db53d54..11c788c 100644 --- a/dev/mockHandlers.ts +++ b/dev/mockHandlers.ts @@ -32,23 +32,54 @@ const mockEnvironments = [ }, ]; +// Mock tags for the project +const mockTags = [ + { id: 1, label: 'ui', color: '#2196F3' }, + { id: 2, label: 'theme', color: '#9C27B0' }, + { id: 3, label: 'checkout', color: '#4CAF50' }, + { id: 4, label: 'experiment', color: '#FF9800' }, + { id: 5, label: 'api', color: '#F44336' }, + { id: 6, label: 'performance', color: '#00BCD4' }, + { id: 7, label: 'beta', color: '#E91E63' }, + { id: 8, label: 'ops', color: '#795548' }, + { id: 9, label: 'notifications', color: '#607D8B' }, + { id: 10, label: 'v2', color: '#3F51B5' }, + { id: 11, label: 'payments', color: '#8BC34A' }, + { id: 12, label: 'integration', color: '#FFEB3B' }, + { id: 13, label: 'cache', color: '#009688' }, + { id: 14, label: 'analytics', color: '#673AB7' }, + { id: 15, label: 'onboarding', color: '#CDDC39' }, + { id: 16, label: 'ux', color: '#FF5722' }, + { id: 17, label: 'search', color: '#03A9F4' }, + { id: 18, label: 'v3', color: '#FFC107' }, + { id: 19, label: 'ai', color: '#9E9E9E' }, + { id: 20, label: 'recommendations', color: '#00E676' }, + { id: 21, label: 'export', color: '#651FFF' }, + { id: 22, label: 'bulk', color: '#1DE9B6' }, + { id: 23, label: 'admin', color: '#D500F9' }, + { id: 24, label: 'security', color: '#C51162' }, + { id: 25, label: 'audit', color: '#304FFE' }, + { id: 26, label: 'unique', color: '#64DD17' }, + { id: 27, label: 'page2', color: '#FFAB00' }, +]; + // Feature name templates for generating mock data const featureTemplates = [ - { name: 'dark_mode', desc: 'Enable dark mode theme for the application', tags: ['ui', 'theme'], type: 'FLAG' }, - { name: 'new_checkout_flow', desc: 'A/B test for the new checkout experience', tags: ['checkout', 'experiment'], type: 'FLAG' }, - { name: 'api_rate_limit', desc: 'API rate limiting configuration', tags: ['api', 'performance'], type: 'CONFIG' }, - { name: 'beta_features', desc: 'Enable beta features for selected users', tags: ['beta'], type: 'FLAG' }, - { name: 'maintenance_mode', desc: 'Put the application in maintenance mode', tags: ['ops'], type: 'FLAG' }, - { name: 'notifications_v2', desc: 'New notification system', tags: ['notifications', 'v2'], type: 'FLAG' }, - { name: 'payment_gateway', desc: 'Enable new payment gateway integration', tags: ['payments', 'integration'], type: 'FLAG' }, - { name: 'cache_ttl', desc: 'Cache time-to-live configuration', tags: ['cache', 'performance'], type: 'CONFIG' }, - { name: 'feature_analytics', desc: 'Track feature usage analytics', tags: ['analytics'], type: 'FLAG' }, - { name: 'user_onboarding', desc: 'New user onboarding flow', tags: ['onboarding', 'ux'], type: 'FLAG' }, - { name: 'search_v3', desc: 'Enhanced search functionality', tags: ['search', 'v3'], type: 'FLAG' }, - { name: 'recommendation_engine', desc: 'AI-powered recommendations', tags: ['ai', 'recommendations'], type: 'FLAG' }, - { name: 'export_csv', desc: 'Enable CSV export functionality', tags: ['export'], type: 'FLAG' }, - { name: 'bulk_operations', desc: 'Enable bulk edit operations', tags: ['bulk', 'admin'], type: 'FLAG' }, - { name: 'audit_logging', desc: 'Enhanced audit logging', tags: ['security', 'audit'], type: 'FLAG' }, + { name: 'dark_mode', desc: 'Enable dark mode theme for the application', tags: [1, 2], type: 'FLAG' }, + { name: 'new_checkout_flow', desc: 'A/B test for the new checkout experience', tags: [3, 4], type: 'FLAG' }, + { name: 'api_rate_limit', desc: 'API rate limiting configuration', tags: [5, 6], type: 'CONFIG' }, + { name: 'beta_features', desc: 'Enable beta features for selected users', tags: [7], type: 'FLAG' }, + { name: 'maintenance_mode', desc: 'Put the application in maintenance mode', tags: [8], type: 'FLAG' }, + { name: 'notifications_v2', desc: 'New notification system', tags: [9, 10], type: 'FLAG' }, + { name: 'payment_gateway', desc: 'Enable new payment gateway integration', tags: [11, 12], type: 'FLAG' }, + { name: 'cache_ttl', desc: 'Cache time-to-live configuration', tags: [13, 6], type: 'CONFIG' }, + { name: 'feature_analytics', desc: 'Track feature usage analytics', tags: [14], type: 'FLAG' }, + { name: 'user_onboarding', desc: 'New user onboarding flow', tags: [15, 16], type: 'FLAG' }, + { name: 'search_v3', desc: 'Enhanced search functionality', tags: [17, 18], type: 'FLAG' }, + { name: 'recommendation_engine', desc: 'AI-powered recommendations', tags: [19, 20], type: 'FLAG' }, + { name: 'export_csv', desc: 'Enable CSV export functionality', tags: [21], type: 'FLAG' }, + { name: 'bulk_operations', desc: 'Enable bulk edit operations', tags: [22, 23], type: 'FLAG' }, + { name: 'audit_logging', desc: 'Enhanced audit logging', tags: [24, 25], type: 'FLAG' }, ]; // Generate 55 mock features @@ -108,7 +139,7 @@ const generateMockFeatures = () => { type: 'FLAG', is_archived: false, is_server_key_only: false, - tags: ['unique', 'page2'], + tags: [26, 27], owners: [{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }], num_segment_overrides: 1, num_identity_overrides: 3, @@ -302,6 +333,11 @@ export const handlers = [ return res(ctx.json({ results: mockEnvironments })); }), + // Get project tags + rest.get('*/proxy/flagsmith/projects/:projectId/tags/', (req, res, ctx) => { + return res(ctx.json({ results: mockTags })); + }), + // Get project features rest.get('*/proxy/flagsmith/projects/:projectId/features/', (req, res, ctx) => { return res(ctx.json({ results: mockFeatures })); diff --git a/src/api/FlagsmithClient.ts b/src/api/FlagsmithClient.ts index 9585cec..0c8e603 100644 --- a/src/api/FlagsmithClient.ts +++ b/src/api/FlagsmithClient.ts @@ -13,6 +13,12 @@ export interface FlagsmithProject { created_date: string; } +export interface FlagsmithTag { + id: number; + label: string; + color?: string; +} + export interface FlagsmithEnvironment { id: number; name: string; @@ -56,7 +62,7 @@ export interface FlagsmithFeature { first_name?: string; last_name?: string; } | null; - tags?: Array; + tags?: Array; is_server_key_only?: boolean; type?: string; default_enabled?: boolean; @@ -200,6 +206,20 @@ export class FlagsmithClient { return await response.json(); } + async getProjectTags(projectId: number): Promise { + const baseUrl = await this.getBaseUrl(); + const response = await this.fetchApi.fetch( + `${baseUrl}/projects/${projectId}/tags/`, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch project tags: ${response.statusText}`); + } + + const data = await response.json(); + return data.results || data; + } + async getUsageData( orgId: number, projectId?: number, diff --git a/src/components/FlagsTab/ExpandableRow.tsx b/src/components/FlagsTab/ExpandableRow.tsx index 180a850..a7a5545 100644 --- a/src/components/FlagsTab/ExpandableRow.tsx +++ b/src/components/FlagsTab/ExpandableRow.tsx @@ -19,6 +19,7 @@ import { FlagsmithEnvironment, FlagsmithFeature, FlagsmithFeatureDetails, + FlagsmithTag, } from '../../api/FlagsmithClient'; import { FlagsmithLink } from '../shared'; import { buildFlagUrl } from '../../theme/flagsmithTheme'; @@ -78,13 +79,14 @@ const TRAILING_COLUMNS_COUNT = 1; interface ExpandableRowProps { feature: FlagsmithFeature; environments: FlagsmithEnvironment[]; + tagMap: Map; client: FlagsmithClient; projectId: string; orgId: number; } export const ExpandableRow = memo( - ({ feature, environments, client, projectId, orgId }: ExpandableRowProps) => { + ({ feature, environments, tagMap, client, projectId, orgId }: ExpandableRowProps) => { const classes = useStyles(); const [open, setOpen] = useState(false); const [details, setDetails] = useState(null); @@ -165,10 +167,10 @@ export const ExpandableRow = memo( - {displayTags.map((tag, index) => ( + {displayTags.map((tagId, index) => ( ; liveVersion: LiveVersionInfo; segmentOverrides: number; scheduledVersion?: FlagsmithFeatureVersion | null; @@ -69,6 +70,7 @@ const getCreatorDisplayName = (feature: FlagsmithFeature): string => { export const FeatureDetailsGrid = ({ feature, + tagMap, liveVersion, segmentOverrides, scheduledVersion, @@ -207,8 +209,8 @@ export const FeatureDetailsGrid = ({ Tags - {feature.tags.map((tag, index) => ( - + {feature.tags.map((tagId, index) => ( + ))} diff --git a/src/components/FlagsTab/index.tsx b/src/components/FlagsTab/index.tsx index 097edba..c1a072b 100644 --- a/src/components/FlagsTab/index.tsx +++ b/src/components/FlagsTab/index.tsx @@ -50,7 +50,7 @@ export const FlagsTab = () => { const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_ROWS_PER_PAGE); const projectId = entity.metadata.annotations?.['flagsmith.com/project-id']; - const { project, environments, features, loading, error, client } = + const { project, environments, features, tagMap, loading, error, client } = useFlagsmithProject(projectId); const filteredFeatures = useMemo(() => { @@ -153,6 +153,7 @@ export const FlagsTab = () => { key={feature.id} feature={feature} environments={environments} + tagMap={tagMap} client={client} projectId={projectId!} orgId={project?.organisation || 0} diff --git a/src/hooks/useFlagsmithProject.test.tsx b/src/hooks/useFlagsmithProject.test.tsx index 2b47af8..14d8676 100644 --- a/src/hooks/useFlagsmithProject.test.tsx +++ b/src/hooks/useFlagsmithProject.test.tsx @@ -36,12 +36,14 @@ describe('useFlagsmithProject', () => { it('fetches project data successfully', async () => { const mockProject = { id: 123, name: 'Test', organisation: 1 }; const mockEnvs = [{ id: 1, name: 'Dev', api_key: 'key', project: 123 }]; - const mockFeatures = [{ id: 1, name: 'flag', created_date: '2024-01-01', project: 123 }]; + const mockFeatures = [{ id: 1, name: 'flag', created_date: '2024-01-01', project: 123, tags: [1] }]; + const mockTags = [{ id: 1, label: 'ui', color: '#2196F3' }]; mockFetch .mockResolvedValueOnce({ ok: true, json: async () => mockProject }) .mockResolvedValueOnce({ ok: true, json: async () => ({ results: mockEnvs }) }) - .mockResolvedValueOnce({ ok: true, json: async () => ({ results: mockFeatures }) }); + .mockResolvedValueOnce({ ok: true, json: async () => ({ results: mockFeatures }) }) + .mockResolvedValueOnce({ ok: true, json: async () => ({ results: mockTags }) }); const { result } = renderHook(() => useFlagsmithProject('123'), { wrapper }); @@ -51,5 +53,6 @@ describe('useFlagsmithProject', () => { expect(result.current.project).toEqual(mockProject); expect(result.current.environments).toEqual(mockEnvs); expect(result.current.features).toEqual(mockFeatures); + expect(result.current.tagMap.get(1)).toEqual(mockTags[0]); }); }); diff --git a/src/hooks/useFlagsmithProject.ts b/src/hooks/useFlagsmithProject.ts index 0b0f80f..afa5794 100644 --- a/src/hooks/useFlagsmithProject.ts +++ b/src/hooks/useFlagsmithProject.ts @@ -5,12 +5,14 @@ import { FlagsmithProject, FlagsmithEnvironment, FlagsmithFeature, + FlagsmithTag, } from '../api/FlagsmithClient'; export interface UseFlagsmithProjectResult { project: FlagsmithProject | null; environments: FlagsmithEnvironment[]; features: FlagsmithFeature[]; + tagMap: Map; loading: boolean; error: string | null; client: FlagsmithClient; @@ -30,6 +32,7 @@ export function useFlagsmithProject( const [project, setProject] = useState(null); const [environments, setEnvironments] = useState([]); const [features, setFeatures] = useState([]); + const [tags, setTags] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -50,6 +53,9 @@ export function useFlagsmithProject( const projectFeatures = await client.getProjectFeatures(projectId); setFeatures(projectFeatures || []); + + const projectTags = await client.getProjectTags(parseInt(projectId, 10)); + setTags(projectTags || []); } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); } finally { @@ -69,5 +75,12 @@ export function useFlagsmithProject( [envIds], ); - return { project, environments: memoizedEnvironments, features, loading, error, client }; + // Create a map of tag ID to tag object for efficient lookup + const tagMap = useMemo(() => { + const map = new Map(); + tags.forEach(tag => map.set(tag.id, tag)); + return map; + }, [tags]); + + return { project, environments: memoizedEnvironments, features, tagMap, loading, error, client }; }