Skip to content
Closed
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,19 @@ 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
```

> **Note:** The organization ID is automatically derived from the project data.

## 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
4. Find your **Project ID** in the URL or project settings

## Development

Expand Down
23 changes: 22 additions & 1 deletion dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PropsWithChildren } from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
import { Box, Grid } from '@material-ui/core';
import { setupWorker } from 'msw';
import { FlagsTab } from '../src/components/FlagsTab';
import { FlagsmithOverviewCard } from '../src/components/FlagsmithOverviewCard';
Expand All @@ -23,7 +24,6 @@ const mockEntity: Entity = {
description: 'A demo service with Flagsmith feature flags integration',
annotations: {
'flagsmith.com/project-id': '31465',
'flagsmith.com/org-id': '24242',
},
},
spec: {
Expand Down Expand Up @@ -64,4 +64,25 @@ createDevApp()
title: 'Overview Cards',
path: '/flagsmith-cards',
})
.addPage({
element: (
<EntityWrapper>
<Box p={3}>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<FlagsmithOverviewCard />
</Grid>
<Grid item xs={12} md={6}>
<FlagsmithUsageCard />
</Grid>
</Grid>
<Box mt={3}>
<FlagsTab />
</Box>
</Box>
</EntityWrapper>
),
title: 'Complete View',
path: '/flagsmith-complete',
})
.render();
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: string): 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
17 changes: 7 additions & 10 deletions src/components/FlagsTab/ExpandableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import {
FlagsmithEnvironment,
FlagsmithFeature,
FlagsmithFeatureDetails,
FlagsmithTag,
} from '../../api/FlagsmithClient';
import { FlagsmithLink } from '../shared';
import { FlagsmithLink, TagChip } from '../shared';
import { buildFlagUrl } from '../../theme/flagsmithTheme';
import { switchOnStyle } from '../../theme/sharedStyles';
import {
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,14 +167,8 @@ export const ExpandableRow = memo(
</TableCell>
<TableCell className={classes.tagsCell}>
<Box className={classes.tagsContainer}>
{displayTags.map((tag, index) => (
<Chip
key={index}
label={tag}
size="small"
variant="outlined"
className={classes.tagChip}
/>
{displayTags.map((tagId, index) => (
<TagChip key={index} tagId={tagId} tagMap={tagMap} />
))}
{remainingTagsCount > 0 && (
<Chip
Expand Down Expand Up @@ -239,6 +235,7 @@ export const ExpandableRow = memo(

<FeatureDetailsGrid
feature={feature}
tagMap={tagMap}
liveVersion={liveVersion}
segmentOverrides={segmentOverrides}
scheduledVersion={scheduledVersion}
Expand Down
9 changes: 6 additions & 3 deletions src/components/FlagsTab/FeatureDetailsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ 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 { TagChip } from '../shared';
import { flagsmithColors } from '../../theme/flagsmithTheme';
import { detailCardStyle } from '../../theme/sharedStyles';
import { getFlagType, getValueType, isDefined } from '../../utils/flagTypeHelpers';
Expand Down Expand Up @@ -51,6 +52,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 +71,7 @@ const getCreatorDisplayName = (feature: FlagsmithFeature): string => {

export const FeatureDetailsGrid = ({
feature,
tagMap,
liveVersion,
segmentOverrides,
scheduledVersion,
Expand Down Expand Up @@ -207,8 +210,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) => (
<TagChip key={index} tagId={tagId} tagMap={tagMap} />
))}
</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
17 changes: 6 additions & 11 deletions src/components/FlagsmithUsageCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ export const FlagsmithUsageCard = () => {
const { entity } = useEntity();

const projectId = entity.metadata.annotations?.['flagsmith.com/project-id'];
const orgId = entity.metadata.annotations?.['flagsmith.com/org-id'];

const { project, usageData, totalFlags, loading, error } = useFlagsmithUsage(
projectId,
orgId,
);
const { project, usageData, totalFlags, loading, error } = useFlagsmithUsage(projectId);

const usageUrl = `${FLAGSMITH_DASHBOARD_URL}/organisation/${orgId}/usage`;
// Derive organization ID from project data for the dashboard link
const orgId = project?.organisation;
const usageUrl = orgId ? `${FLAGSMITH_DASHBOARD_URL}/organisation/${orgId}/usage` : undefined;

if (loading) {
return (
Expand All @@ -40,10 +38,7 @@ export const FlagsmithUsageCard = () => {
if (error) {
return (
<InfoCard title="Flags Usage Data (30 Days)">
<ErrorState
message={error}
hint={!orgId ? 'Add a flagsmith.com/organization-id annotation to this entity.' : undefined}
/>
<ErrorState message={error} />
</InfoCard>
);
}
Expand All @@ -57,7 +52,7 @@ export const FlagsmithUsageCard = () => {
title="Flags Usage Data (30 Days)"
subheader={subheader}
action={
orgId && (
usageUrl && (
<Box className={classes.headerActions}>
<FlagsmithLink href={usageUrl} iconOnly tooltip="View Usage Analytics" />
</Box>
Expand Down
37 changes: 37 additions & 0 deletions src/components/shared/TagChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Chip } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { FlagsmithTag } from '../../api/FlagsmithClient';
import { getTagChipStyle } from '../../utils/colorUtils';

const useStyles = makeStyles(() => ({
root: {
fontSize: '0.7rem',
height: 20,
},
}));

interface TagChipProps {
tagId: number;
tagMap: Map<number, FlagsmithTag>;
}

export const TagChip = ({ tagId, tagMap }: TagChipProps) => {
const classes = useStyles();
const tag = tagMap.get(tagId);

if (!tag) {
return null;
}

const tagStyle = getTagChipStyle(tag.color);

return (
<Chip
label={tag.label}
size="small"
variant="outlined"
className={classes.root}
style={tagStyle}
/>
);
};
1 change: 1 addition & 0 deletions src/components/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { LoadingState } from './LoadingState';
export { EmptyState } from './EmptyState';
export { ErrorState } from './ErrorState';
export { ChartTooltip, ChartTooltipText } from './ChartTooltip';
export { TagChip } from './TagChip';
Loading