Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 52 additions & 16 deletions dev/mockHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }));
Expand Down
22 changes: 21 additions & 1 deletion src/api/FlagsmithClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -56,7 +62,7 @@ export interface FlagsmithFeature {
first_name?: string;
last_name?: string;
} | null;
tags?: Array<string>;
tags?: Array<number>;
is_server_key_only?: boolean;
type?: string;
default_enabled?: boolean;
Expand Down Expand Up @@ -200,6 +206,20 @@ export class FlagsmithClient {
return await response.json();
}

async getProjectTags(projectId: number): Promise<FlagsmithTag[]> {
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,
Expand Down
9 changes: 6 additions & 3 deletions src/components/FlagsTab/ExpandableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
FlagsmithEnvironment,
FlagsmithFeature,
FlagsmithFeatureDetails,
FlagsmithTag,
} from '../../api/FlagsmithClient';
import { FlagsmithLink } from '../shared';
import { buildFlagUrl } from '../../theme/flagsmithTheme';
Expand Down Expand Up @@ -78,13 +79,14 @@ const TRAILING_COLUMNS_COUNT = 1;
interface ExpandableRowProps {
feature: FlagsmithFeature;
environments: FlagsmithEnvironment[];
tagMap: Map<number, FlagsmithTag>;
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<FlagsmithFeatureDetails | null>(null);
Expand Down Expand Up @@ -165,10 +167,10 @@ export const ExpandableRow = memo(
</TableCell>
<TableCell className={classes.tagsCell}>
<Box className={classes.tagsContainer}>
{displayTags.map((tag, index) => (
{displayTags.map((tagId, index) => (
<Chip
key={index}
label={tag}
label={tagMap.get(tagId)?.label || tagId}
size="small"
variant="outlined"
className={classes.tagChip}
Expand Down Expand Up @@ -239,6 +241,7 @@ export const ExpandableRow = memo(

<FeatureDetailsGrid
feature={feature}
tagMap={tagMap}
liveVersion={liveVersion}
segmentOverrides={segmentOverrides}
scheduledVersion={scheduledVersion}
Expand Down
8 changes: 5 additions & 3 deletions src/components/FlagsTab/FeatureDetailsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { makeStyles } from '@material-ui/core/styles';
import ArchiveIcon from '@material-ui/icons/Archive';
import ScheduleIcon from '@material-ui/icons/Schedule';
import VpnKeyIcon from '@material-ui/icons/VpnKey';
import { FlagsmithFeature, FlagsmithFeatureVersion } from '../../api/FlagsmithClient';
import { FlagsmithFeature, FlagsmithFeatureVersion, FlagsmithTag } from '../../api/FlagsmithClient';
import { flagsmithColors } from '../../theme/flagsmithTheme';
import { detailCardStyle } from '../../theme/sharedStyles';
import { getFlagType, getValueType, isDefined } from '../../utils/flagTypeHelpers';
Expand Down Expand Up @@ -51,6 +51,7 @@ type LiveVersionInfo = FlagsmithFeature['live_version'];

interface FeatureDetailsGridProps {
feature: FlagsmithFeature;
tagMap: Map<number, FlagsmithTag>;
liveVersion: LiveVersionInfo;
segmentOverrides: number;
scheduledVersion?: FlagsmithFeatureVersion | null;
Expand All @@ -69,6 +70,7 @@ const getCreatorDisplayName = (feature: FlagsmithFeature): string => {

export const FeatureDetailsGrid = ({
feature,
tagMap,
liveVersion,
segmentOverrides,
scheduledVersion,
Expand Down Expand Up @@ -207,8 +209,8 @@ export const FeatureDetailsGrid = ({
Tags
</Typography>
<Box className={classes.tagsContainer}>
{feature.tags.map((tag, index) => (
<Chip key={index} label={tag} size="small" variant="outlined" />
{feature.tags.map((tagId, index) => (
<Chip key={index} label={tagMap.get(tagId)?.label || tagId} size="small" variant="outlined" />
))}
</Box>
</Grid>
Expand Down
3 changes: 2 additions & 1 deletion src/components/FlagsTab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -153,6 +153,7 @@ export const FlagsTab = () => {
key={feature.id}
feature={feature}
environments={environments}
tagMap={tagMap}
client={client}
projectId={projectId!}
orgId={project?.organisation || 0}
Expand Down
7 changes: 5 additions & 2 deletions src/hooks/useFlagsmithProject.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand All @@ -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]);
});
});
15 changes: 14 additions & 1 deletion src/hooks/useFlagsmithProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
FlagsmithProject,
FlagsmithEnvironment,
FlagsmithFeature,
FlagsmithTag,
} from '../api/FlagsmithClient';

export interface UseFlagsmithProjectResult {
project: FlagsmithProject | null;
environments: FlagsmithEnvironment[];
features: FlagsmithFeature[];
tagMap: Map<number, FlagsmithTag>;
loading: boolean;
error: string | null;
client: FlagsmithClient;
Expand All @@ -30,6 +32,7 @@ export function useFlagsmithProject(
const [project, setProject] = useState<FlagsmithProject | null>(null);
const [environments, setEnvironments] = useState<FlagsmithEnvironment[]>([]);
const [features, setFeatures] = useState<FlagsmithFeature[]>([]);
const [tags, setTags] = useState<FlagsmithTag[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

Expand All @@ -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 {
Expand All @@ -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<number, FlagsmithTag>();
tags.forEach(tag => map.set(tag.id, tag));
return map;
}, [tags]);

return { project, environments: memoizedEnvironments, features, tagMap, loading, error, client };
}