Skip to content

Commit e9907b0

Browse files
committed
Allow retrieval of more than API limit 3 envs for launchdarkly, add option to hide desc or labels
1 parent 24d4003 commit e9907b0

File tree

3 files changed

+131
-84
lines changed

3 files changed

+131
-84
lines changed

.changeset/fine-lies-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@roadiehq/backstage-plugin-launchdarkly': minor
3+
---
4+
5+
Allow retrieval of more than API limit 3 envs for launchdarkly, add option to hide desc or labels

plugins/frontend/backstage-plugin-launchdarkly/src/components/EntityLaunchdarklyCard/EntityLaunchdarklyCard.tsx

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export type EntityLaunchdarklyCardProps = {
6969
title?: string;
7070
enableSearch?: boolean;
7171
envs?: string[];
72+
showDescription?: boolean;
73+
showLabels?: boolean;
7274
};
7375

7476
const TableHeader = ({ title }: { title?: string }) => {
@@ -85,7 +87,13 @@ const TableHeader = ({ title }: { title?: string }) => {
8587
};
8688

8789
export const EntityLaunchdarklyCard = (props: EntityLaunchdarklyCardProps) => {
88-
const { title, enableSearch = false, envs = ['production'] } = props;
90+
const {
91+
title,
92+
enableSearch = false,
93+
envs = ['production'],
94+
showDescription = true,
95+
showLabels = true,
96+
} = props;
8997
const classes = useStyles();
9098
const { entity } = useEntity();
9199

@@ -101,7 +109,7 @@ export const EntityLaunchdarklyCard = (props: EntityLaunchdarklyCardProps) => {
101109
{
102110
title: 'Name',
103111
field: 'name',
104-
render: row => (
112+
render: (row: MultiEnvironmentFlag) => (
105113
<Link
106114
to={Object.values(row.environments)[0]?.link || '#'}
107115
target="_blank"
@@ -114,39 +122,43 @@ export const EntityLaunchdarklyCard = (props: EntityLaunchdarklyCardProps) => {
114122
title: 'Key',
115123
field: 'key',
116124
},
117-
{
118-
title: 'Description',
119-
field: 'description',
120-
render: row => (
121-
<span style={{ fontSize: '0.875rem' }}>
122-
{row.description || 'No description'}
123-
</span>
124-
),
125-
},
126-
{
127-
title: 'Labels',
128-
field: 'tags',
129-
render: row => {
130-
if (!row.tags || row.tags.length === 0) {
131-
return <span className={classes.noTags}>No labels</span>;
125+
showDescription
126+
? {
127+
title: 'Description',
128+
field: 'description',
129+
render: (row: MultiEnvironmentFlag) => (
130+
<span style={{ fontSize: '0.875rem' }}>
131+
{row.description || 'No description'}
132+
</span>
133+
),
132134
}
133-
return (
134-
<div className={classes.tagContainer}>
135-
{row.tags.map((tag, index) => (
136-
<Chip
137-
key={index}
138-
label={tag}
139-
size="small"
140-
variant="outlined"
141-
className={classes.tagChip}
142-
color="primary"
143-
/>
144-
))}
145-
</div>
146-
);
147-
},
148-
},
149-
];
135+
: undefined,
136+
showLabels
137+
? {
138+
title: 'Labels',
139+
field: 'tags',
140+
render: (row: MultiEnvironmentFlag) => {
141+
if (!row.tags || row.tags.length === 0) {
142+
return <span className={classes.noTags}>No labels</span>;
143+
}
144+
return (
145+
<div className={classes.tagContainer}>
146+
{row.tags.map((tag, index) => (
147+
<Chip
148+
key={index}
149+
label={tag}
150+
size="small"
151+
variant="outlined"
152+
className={classes.tagChip}
153+
color="primary"
154+
/>
155+
))}
156+
</div>
157+
);
158+
},
159+
}
160+
: undefined,
161+
].filter(Boolean) as TableColumn<MultiEnvironmentFlag>[];
150162

151163
const environmentColumns: TableColumn<MultiEnvironmentFlag>[] = [];
152164
envs.forEach(envKey => {
@@ -170,7 +182,7 @@ export const EntityLaunchdarklyCard = (props: EntityLaunchdarklyCardProps) => {
170182
});
171183

172184
return [...baseColumns, ...environmentColumns];
173-
}, [envs, classes]);
185+
}, [showDescription, showLabels, envs, classes]);
174186

175187
// Check if the required annotation is present
176188
if (!entity.metadata?.annotations?.[LAUNCHDARKLY_PROJECT_KEY_ANNOTATION]) {

plugins/frontend/backstage-plugin-launchdarkly/src/hooks/useLaunchdarklyMultiEnvironmentFlags.ts

Lines changed: 79 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export type MultiEnvironmentFlag = {
2828
>;
2929
};
3030

31+
// LaunchDarkly API allows maximum of 3 environments per request
32+
const MAX_ENVIRONMENTS_PER_REQUEST = 3;
33+
3134
export const useLaunchdarklyMultiEnvironmentFlags = (
3235
entity: Entity,
3336
envs?: string[],
@@ -44,22 +47,43 @@ export const useLaunchdarklyMultiEnvironmentFlags = (
4447

4548
const url = `${await discovery.getBaseUrl('proxy')}/launchdarkly/api`;
4649

47-
const envQueryParams = (envs ?? []).map(env => `env=${env}`).join('&');
48-
49-
const flagsResponse = await fetch(
50-
`${url}/v2/flags/${projectKey}?limit=100&offset=0&${envQueryParams}${
51-
query ? `&filter=${query}` : ''
52-
}`,
53-
);
54-
55-
if (!flagsResponse.ok) {
56-
throw new Error(
57-
`Failed to retrieve LaunchDarkly flags for project ${projectKey}: ${flagsResponse.statusText}`,
50+
const environments = envs ?? [];
51+
52+
// Split environments into batches of maximum 3
53+
const environmentBatches = [];
54+
for (
55+
let i = 0;
56+
i < environments.length;
57+
i += MAX_ENVIRONMENTS_PER_REQUEST
58+
) {
59+
environmentBatches.push(
60+
environments.slice(i, i + MAX_ENVIRONMENTS_PER_REQUEST),
5861
);
5962
}
6063

61-
const flags = (await flagsResponse.json()).items || [];
64+
// Fetch flags for each batch of environments
65+
const flagsBatches = await Promise.all(
66+
environmentBatches.map(async envBatch => {
67+
const envQueryParams = envBatch.map(env => `env=${env}`).join('&');
68+
69+
const flagsResponse = await fetch(
70+
`${url}/v2/flags/${projectKey}?limit=100&offset=0&${envQueryParams}${
71+
query ? `&filter=${query}` : ''
72+
}`,
73+
);
74+
75+
if (!flagsResponse.ok) {
76+
throw new Error(
77+
`Failed to retrieve LaunchDarkly flags for project ${projectKey}: ${flagsResponse.statusText}`,
78+
);
79+
}
80+
81+
const flags = (await flagsResponse.json()).items || [];
82+
return { flags, environments: envBatch };
83+
}),
84+
);
6285

86+
// Get environments data once
6387
const environmentsResponse = await fetch(
6488
`${url}/v2/projects/${projectKey}`,
6589
);
@@ -71,48 +95,54 @@ export const useLaunchdarklyMultiEnvironmentFlags = (
7195
}
7296

7397
const projectData = await environmentsResponse.json();
74-
const environments = projectData.environments || {};
98+
const environmentsData = projectData.environments || {};
7599

100+
// Merge flags from all batches
76101
const flagsMap = new Map();
77-
flags.forEach((flag: any) => {
78-
const environmentsData: Record<string, any> = {};
79-
80-
(envs ?? []).forEach(env => {
81-
const envData = environments[env];
82-
const envName = envData?.name || env;
83-
const link = `https://app.launchdarkly.com/projects/${projectKey}/flags/${flag.key}/targeting?env=${env}&selected-env=${env}`;
84-
85-
const envFlag = flag.environments?.[env];
86-
const isOn = envFlag?.on ?? false;
87-
88-
const flagStatus = envData?.[flag.key];
89102

90-
let status = isOn ? 'Enabled' : 'Disabled';
91-
if (flagStatus && flagStatus.status) {
92-
status = flagStatus.status;
93-
}
94-
95-
environmentsData[env] = {
96-
name: envName,
97-
status,
98-
link,
99-
on: isOn,
100-
...(flagStatus && {
101-
lastRequested: flagStatus.lastRequested,
102-
lastEvaluated: flagStatus.lastEvaluated,
103-
evaluationCount: flagStatus.evaluationCount,
104-
}),
103+
flagsBatches.forEach(({ flags, environments: batchEnvs }) => {
104+
flags.forEach((flag: any) => {
105+
// Get existing flag data or create new entry
106+
const existingFlag = flagsMap.get(flag.key) || {
107+
name: flag.name,
108+
key: flag.key,
109+
description: flag.description,
110+
tags: flag.tags,
111+
maintainer: flag._maintainer?.email,
112+
variations: flag.variations,
113+
environments: {},
105114
};
106-
});
107115

108-
flagsMap.set(flag.key, {
109-
name: flag.name,
110-
key: flag.key,
111-
description: flag.description,
112-
tags: flag.tags,
113-
maintainer: flag._maintainer?.email,
114-
variations: flag.variations,
115-
environments: environmentsData,
116+
// Add environment data for this batch
117+
batchEnvs.forEach(env => {
118+
const envData = environmentsData[env];
119+
const envName = envData?.name || env;
120+
const link = `https://app.launchdarkly.com/projects/${projectKey}/flags/${flag.key}/targeting?env=${env}&selected-env=${env}`;
121+
122+
const envFlag = flag.environments?.[env];
123+
const isOn = envFlag?.on ?? false;
124+
125+
const flagStatus = envData?.[flag.key];
126+
127+
let status = isOn ? 'Enabled' : 'Disabled';
128+
if (flagStatus && flagStatus.status) {
129+
status = flagStatus.status;
130+
}
131+
132+
existingFlag.environments[env] = {
133+
name: envName,
134+
status,
135+
link,
136+
on: isOn,
137+
...(flagStatus && {
138+
lastRequested: flagStatus.lastRequested,
139+
lastEvaluated: flagStatus.lastEvaluated,
140+
evaluationCount: flagStatus.evaluationCount,
141+
}),
142+
};
143+
});
144+
145+
flagsMap.set(flag.key, existingFlag);
116146
});
117147
});
118148

0 commit comments

Comments
 (0)