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 }; }