From 5d0eaa7d1f6f83147d0542ccf9d9553fbd3ef1f8 Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Mon, 25 Aug 2025 21:40:50 -0400 Subject: [PATCH 01/12] feat: now updated for migrated FME environments --- app-config.yaml | 7 +- examples/entities.yaml | 7 +- plugins/harness-fme-feature-flags/README.md | 15 ++-- .../FMEFeatureList/FMEFeatureList.tsx | 36 ++++++--- .../FMEFeatureList/useProjectSlugEntity.tsx | 16 +++- .../src/hooks/useGetOwners.ts | 74 +++++++------------ .../harness-fme-feature-flags/src/types.ts | 12 +++ 7 files changed, 95 insertions(+), 72 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index ff7b7760..6dcb73a1 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -49,7 +49,7 @@ proxy: '/harness/prod': target: 'https://app.harness.io/' headers: - 'x-api-key': ${HARNESS_PROD_API_KEY} + 'x-api-key': sat.E0oDbbZUQn6gsBj-M7jUBA.68acec5c1d6fca7467b3a9d2.JcXmFTEluxwj3amU4cDD '/harness/qa': target: 'https://qa.harness.io/' headers: @@ -62,10 +62,7 @@ proxy: target: 'https://stress.harness.io/' headers: 'x-api-key': ${HARNESS_STRESS_API_KEY} - '/harnessfme': - target: 'https://api.split.io/' - headers: - 'Authorization': 'Bearer ${FME_API_KEY}' + # Reference documentation http://backstage.io/docs/features/techdocs/configuration # Note: After experimenting with basic setup, use CI/CD to generate docs diff --git a/examples/entities.yaml b/examples/entities.yaml index e3fa7f49..503b9dc9 100644 --- a/examples/entities.yaml +++ b/examples/entities.yaml @@ -15,8 +15,11 @@ metadata: annotations: # mandatory annotation # harness.io/project-url: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_NAME/feature-flags - # harnessfme/projectId: FME_PROJECT_ID - # harnessfme/accountId: FME_ACCOUNT_ID + harnessfme/projectId: 4baf0690-6d25-11e9-8497-12a5cc2af8fe # GUID + harnessfme/accountId: 4ba7daa0-6d25-11e9-8497-12a5cc2af8fe # GUID + harnessfme/projectIdentifier: Hotels + harnessfme/accountIdentifier: E0oDbbZUQn6gsBj-M7jUBA + harnessfme/orgIdentifier: SplitTrainingFME spec: type: service diff --git a/plugins/harness-fme-feature-flags/README.md b/plugins/harness-fme-feature-flags/README.md index 72df15e5..5e3da962 100644 --- a/plugins/harness-fme-feature-flags/README.md +++ b/plugins/harness-fme-feature-flags/README.md @@ -22,17 +22,17 @@ If you are looking to get started with Backstage, check out [backstage.io/docs]( For testing purposes, you can also clone this repository to try out the plugin. It contains an example Backstage app setup which is pre-installed with Harness plugins. However, you must create a new Backstage app if you are looking to get started with Backstage. -2. Configure proxy for harness in your `app-config.yaml` under the `proxy` config. Add your Harness FME Admin API Key for `Authorization: Bearer`. See the [Harness FME docs](https://help.split.io/hc/en-us/articles/360019916211-API-keys) for generating an API Key. +2. Configure proxy for harness in your `app-config.yaml` under the `proxy` config. Ensure you have a service account for `x-api-key`. See the [Harness FME docs](https://help.split.io/hc/en-us/articles/360019916211-API-keys) for generating an API Key. ```yaml # In app-config.yaml proxy: # ... existing proxy settings - '/harnessfme': - target: 'https://api.split.io/' + '/harness/prod': + target: 'https://app.harness.io/' headers: - 'Authorization': 'Bearer ' + 'x-api-key': '' # ... ``` @@ -108,8 +108,11 @@ metadata: # ... annotations: # mandatory annotation - harnessfme/projectId: - harnessfme/accountId: + harnessfme/projectId: # FME project identifier (UUID) + harnessfme/accountId: # FME org identifier (UUID) + harnessfme/harnessAccountId: # Harness org identifier (String) + harnessfme/harnessOrgId: # Harness org identifier (String) + harnessfme/harnessProjectId: # Harness project identifier (String) type: service # ... diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx index 829b3080..4db6ee86 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx @@ -51,7 +51,7 @@ function FMEFeatureList() { const config = useApi(configApiRef); const baseUrl = config.getOptionalString('harnessfme.baseUrl') ?? 'https://app.split.io/'; - const { workspaceId, orgId } = useProjectSlugFromEntity(); + const { workspaceId, orgId, harnessAccountId, harnessOrgId, harnessProjectId } = useProjectSlugFromEntity(); // Memoize the refresh callback const refresh = useCallback(() => { @@ -114,7 +114,7 @@ function FMEFeatureList() { const featureStatus = featureStatusMap[row.name as string] || { id: '', }; - const link = `${baseUrl}org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition`; + const link = `${baseUrl}ng/account/${harnessAccountId}A/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition` return ( {row.name} @@ -184,7 +184,7 @@ function FMEFeatureList() { if (owner?.type === 'user') { return `${owner.name}`; } else if (owner?.type === 'group') { - return ` ${owner.name} (Group) `; + return ` ${owner.name} (Group) `; } return owner?.name || ''; }) @@ -215,8 +215,26 @@ function FMEFeatureList() { }, }, { - title: 'Rollout Status', + title: 'Tags', field: 'col4', + customSort: (row1: Partial, row2: Partial) => { + const a = featureStatusMap[row1.name as string]?.tags?.map((tag: { name: String; }) => tag.name).join(','); + const b = featureStatusMap[row2.name as string]?.tags?.map((tag: { name: String; }) => tag.name).join(','); + return a.localeCompare(b); + }, + type: 'string', + render: (row: Partial) => { + const featureStatus = featureStatusMap[row.name as string]; + return ( + + {featureStatus.tags.map((tag: { name: String; }) => tag.name).join(',')} + + ); + }, + }, + { + title: 'Rollout Status', + field: 'col5', type: 'string', customSort: (row1: Partial, row2: Partial) => { const status1 = @@ -241,7 +259,7 @@ function FMEFeatureList() { }, { title: 'Default Treatment', - field: 'col5', + field: 'col6', type: 'string', customSort: (row1: Partial, row2: Partial) => { const a = row1.defaultTreatment?.toLowerCase() ?? ''; @@ -259,7 +277,7 @@ function FMEFeatureList() { { title: 'Flag Sets', - field: 'col6', + field: 'col7', type: 'string', customSort: (row1: Partial, row2: Partial) => { const sets1 = @@ -295,7 +313,7 @@ function FMEFeatureList() { }, { title: 'Created at', - field: 'col7', + field: 'col8', type: 'date', customSort: (row1: Partial, row2: Partial) => { const date1 = row1.creationTime @@ -315,7 +333,7 @@ function FMEFeatureList() { }, { title: 'Modified At', - field: 'col8', + field: 'col9', type: 'date', customSort: (row1: Partial, row2: Partial) => { const date1 = row1.lastUpdateTime @@ -335,7 +353,7 @@ function FMEFeatureList() { }, { title: 'Last Traffic Received', - field: 'col9', + field: 'col10', type: 'date', customSort: (row1: Partial, row2: Partial) => { const date1 = row1.lastTrafficReceivedAt diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx index 58177606..06ec8065 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx @@ -4,11 +4,19 @@ import { useEntity } from '@backstage/plugin-catalog-react'; export const useProjectSlugFromEntity = () => { const { entity } = useEntity(); const workspaceIdAnnotation = 'harnessfme/projectId'; + const workspaceId = entity.metadata.annotations?.[ workspaceIdAnnotation] as string; + const orgIdAnnotation = 'harnessfme/accountId'; - const workspaceId = entity.metadata.annotations?.[ - workspaceIdAnnotation - ] as string; const orgId = entity.metadata.annotations?.[orgIdAnnotation] as string; - return { workspaceId, orgId }; + const harnessAccountAnnotation = 'harnessfme/harnessAccountIdentifier' + const harnessAccountId = entity.metadata.annotations?.[harnessAccountAnnotation] as string; + + const harnessOrgIdentifier = 'harnessfme/harnessOrgIdentifier' + const harnessOrgId = entity.metadata.annotations?.[harnessOrgIdentifier] as string; + + const harnessProjectIdentifier = 'harnessfme/harnessProjectIdentifier' + const harnessProjectId = entity.metadata.annotations?.[harnessProjectIdentifier] as string; + + return { workspaceId, orgId, harnessAccountId, harnessOrgId, harnessProjectId }; }; diff --git a/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts b/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts index d5fe57b9..fd0853e9 100644 --- a/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts +++ b/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Owner } from '../types'; +import { HarnessUser, HarnessGroup, Owner } from '../types'; import { fetchApiRef, useApi } from '@backstage/core-plugin-api'; interface UseGetOwners { @@ -7,9 +7,12 @@ interface UseGetOwners { refresh: number; } -interface UserResponse { - data: Owner[]; - nextMarker: string | null; + + +interface GroupResponse { + data: { + content: Owner[]; + }; } const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { @@ -26,7 +29,7 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { }); setLoading(true); - let offset = 0; + let pageIndex = 0; let hasMore = true; const ownerList: Record = {}; @@ -37,32 +40,25 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { while (hasMore) { try { const resp = await fetchApi.fetch( - `${baseUrl}/harnessfme/internal/api/v2/groups?limit=50&offset=${offset}`, + `${baseUrl}/harness/prod/ng/api/user/aggregate?pageIndex=${pageIndex}`, { headers }, + { + method: 'POST', + } ); if (resp.status === 200) { const data = await resp.json(); - data.objects.forEach((d: Owner) => { - ownerList[d.id] = d; + data.data.content.forEach((d: {user: HarnessUser}) => { + ownerList[d.user.uuid] = {id: d.user.uuid, name: d.user.name, email: d.user.email, type: 'user'}; }); - if (data.objects.length < 50) { + if (data.data.content.length < 50) { hasMore = false; } else { - offset += 50; + pageIndex += 1; } - } else if (resp.status === 429) { - const orgResetSeconds = parseInt( - resp.headers.get('x-ratelimit-reset-seconds-org') || '0', - 10, - ); - const ipResetSeconds = parseInt( - resp.headers.get('x-ratelimit-reset-seconds-ip') || '0', - 10, - ); - const resetSeconds = Math.max(orgResetSeconds, ipResetSeconds); - await pause(resetSeconds * 1000); + } else { hasMore = false; } @@ -71,44 +67,30 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { } } - // Fetch users + // Fetch groups hasMore = true; - let nextMarker = null; + pageIndex = 0; while (hasMore) { try { - const respUsers = await fetchApi.fetch( - `${baseUrl}/harnessfme/internal/api/v2/users?limit=200${ - nextMarker !== null ? `&nextMarker=${nextMarker}` : '' - }`, + const respGroups = await fetchApi.fetch( + `${baseUrl}/harness/prod/ng/api/user-groups?pageIndex=${pageIndex}`, { headers }, ); - if (respUsers.status === 200) { - const dataUsers: UserResponse = await respUsers.json(); - dataUsers.data.forEach((d: Owner) => { - ownerList[d.id] = d; + if (respGroups.status === 200) { + const dataGroups: GroupResponse = await respGroups.json(); + dataGroups.data.content.forEach((d: HarnessGroup) => { + ownerList[d.identifier] = {id: d.identifier, name: d.name, type: 'group'}; }); if ( - dataUsers.data.length < 200 || - !dataUsers.nextMarker || - nextMarker === dataUsers.nextMarker + dataGroups.data.content.length < 50 ) { hasMore = false; } else { - nextMarker = dataUsers.nextMarker; + pageIndex += 1; } - } else if (respUsers.status === 429) { - const orgResetSeconds = parseInt( - respUsers.headers.get('x-ratelimit-reset-seconds-org') || '2', - 10, - ); - const ipResetSeconds = parseInt( - respUsers.headers.get('x-ratelimit-reset-seconds-ip') || '2', - 10, - ); - const resetSeconds = Math.max(orgResetSeconds, ipResetSeconds); - await pause(resetSeconds * 1000); + } else { hasMore = false; } diff --git a/plugins/harness-fme-feature-flags/src/types.ts b/plugins/harness-fme-feature-flags/src/types.ts index 71aa2e2d..73e39a4b 100644 --- a/plugins/harness-fme-feature-flags/src/types.ts +++ b/plugins/harness-fme-feature-flags/src/types.ts @@ -42,6 +42,18 @@ export interface FeatureStatus { creationTime: string; } +export interface HarnessGroup { + identifier: string, + name: string +} + +export interface HarnessUser { + uuid: string; + name: string; + email: string; +} + + export interface Owner { id: string; type: string; From 8b1bc7c391eccf30e347973a194a6772dcd54674 Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Mon, 25 Aug 2025 22:21:45 -0400 Subject: [PATCH 02/12] fix inconsistencies --- app-config.yaml | 2 +- examples/entities.yaml | 10 +++++----- examples/entities.yaml.fixed | 13 +++++++++++++ plugins/harness-fme-feature-flags/README.md | 6 +++--- .../components/FMEFeatureList/FMEFeatureList.tsx | 2 +- .../FMEFeatureList/useProjectSlugEntity.tsx | 6 +++--- 6 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 examples/entities.yaml.fixed diff --git a/app-config.yaml b/app-config.yaml index 6dcb73a1..413a102d 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -49,7 +49,7 @@ proxy: '/harness/prod': target: 'https://app.harness.io/' headers: - 'x-api-key': sat.E0oDbbZUQn6gsBj-M7jUBA.68acec5c1d6fca7467b3a9d2.JcXmFTEluxwj3amU4cDD + 'x-api-key': ${HARNESS_PROD_API_KEY} '/harness/qa': target: 'https://qa.harness.io/' headers: diff --git a/examples/entities.yaml b/examples/entities.yaml index 503b9dc9..8c54e202 100644 --- a/examples/entities.yaml +++ b/examples/entities.yaml @@ -15,11 +15,11 @@ metadata: annotations: # mandatory annotation # harness.io/project-url: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_NAME/feature-flags - harnessfme/projectId: 4baf0690-6d25-11e9-8497-12a5cc2af8fe # GUID - harnessfme/accountId: 4ba7daa0-6d25-11e9-8497-12a5cc2af8fe # GUID - harnessfme/projectIdentifier: Hotels - harnessfme/accountIdentifier: E0oDbbZUQn6gsBj-M7jUBA - harnessfme/orgIdentifier: SplitTrainingFME + harnessfme/projectId: Harness FME Project # GUID + harnessfme/accountId: Harness FME Account # GUID + harnessfme/projectIdentifier: + harnessfme/accountIdentifier: Harness Account Identifier (string) + harnessfme/orgIdentifier: Harness Org Identifier (string) spec: type: service diff --git a/examples/entities.yaml.fixed b/examples/entities.yaml.fixed new file mode 100644 index 00000000..35d39749 --- /dev/null +++ b/examples/entities.yaml.fixed @@ -0,0 +1,13 @@ +# The first part remains the same +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example-service + annotations: + # mandatory annotation + # harness.io/project-url: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_NAME/feature-flags + harnessfme/projectId: ${HARNESS_FME_PROJECT_ID} # GUID from fme + harnessfme/accountId: ${HARNESS_FME_ACCOUNT_ID} # GUID from fme + harnessfme/harnessProjectId: ${HARNESS_PROJECT_IDENTIFIER} # project identifier + harnessfme/harnessAccountId: ${HARNESS_ACCOUNT_IDENTIFIER} # accountIdentifier + harnessfme/harnessOrgId: ${HARNESS_ORG_IDENTIFIER} #org string diff --git a/plugins/harness-fme-feature-flags/README.md b/plugins/harness-fme-feature-flags/README.md index 5e3da962..a30c9fb4 100644 --- a/plugins/harness-fme-feature-flags/README.md +++ b/plugins/harness-fme-feature-flags/README.md @@ -22,7 +22,7 @@ If you are looking to get started with Backstage, check out [backstage.io/docs]( For testing purposes, you can also clone this repository to try out the plugin. It contains an example Backstage app setup which is pre-installed with Harness plugins. However, you must create a new Backstage app if you are looking to get started with Backstage. -2. Configure proxy for harness in your `app-config.yaml` under the `proxy` config. Ensure you have a service account for `x-api-key`. See the [Harness FME docs](https://help.split.io/hc/en-us/articles/360019916211-API-keys) for generating an API Key. +2. Configure proxy for harness in your `app-config.yaml` under the `proxy` config. Ensure you have a service account for `x-api-key`. See the [Harness API Docs](https://developer.harness.io/docs/platform/automation/api/api-quickstart/) for generating an API Key. ```yaml # In app-config.yaml @@ -32,7 +32,7 @@ proxy: '/harness/prod': target: 'https://app.harness.io/' headers: - 'x-api-key': '' + 'x-api-key': '' # ... ``` @@ -97,7 +97,7 @@ You will need your accountId (formerly Org ID) and projectId (formerly Workspace You can get these from the URL when you are logged in to the FME console. -https://app.split.io/org//ws/>/mywork +https://app.harness.io/ng/account//module/fme/orgs//projects//org//ws//mywork diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx index 4db6ee86..2d460db1 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx @@ -114,7 +114,7 @@ function FMEFeatureList() { const featureStatus = featureStatusMap[row.name as string] || { id: '', }; - const link = `${baseUrl}ng/account/${harnessAccountId}A/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition` + const link = `${baseUrl}ng/account/${harnessAccountId}/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition` return ( {row.name} diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx index 06ec8065..9c038740 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx @@ -9,13 +9,13 @@ export const useProjectSlugFromEntity = () => { const orgIdAnnotation = 'harnessfme/accountId'; const orgId = entity.metadata.annotations?.[orgIdAnnotation] as string; - const harnessAccountAnnotation = 'harnessfme/harnessAccountIdentifier' + const harnessAccountAnnotation = 'harnessfme/harnessAccountId' const harnessAccountId = entity.metadata.annotations?.[harnessAccountAnnotation] as string; - const harnessOrgIdentifier = 'harnessfme/harnessOrgIdentifier' + const harnessOrgIdentifier = 'harnessfme/harnessOrgId' const harnessOrgId = entity.metadata.annotations?.[harnessOrgIdentifier] as string; - const harnessProjectIdentifier = 'harnessfme/harnessProjectIdentifier' + const harnessProjectIdentifier = 'harnessfme/harnessProjectId' const harnessProjectId = entity.metadata.annotations?.[harnessProjectIdentifier] as string; return { workspaceId, orgId, harnessAccountId, harnessOrgId, harnessProjectId }; From 0a633bae84e42459db4347946d12376d4405853b Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Mon, 25 Aug 2025 22:27:16 -0400 Subject: [PATCH 03/12] minor annotation update --- examples/entities.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/entities.yaml b/examples/entities.yaml index 8c54e202..603c0863 100644 --- a/examples/entities.yaml +++ b/examples/entities.yaml @@ -17,7 +17,7 @@ metadata: # harness.io/project-url: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_NAME/feature-flags harnessfme/projectId: Harness FME Project # GUID harnessfme/accountId: Harness FME Account # GUID - harnessfme/projectIdentifier: + harnessfme/projectIdentifier: Harness Project Identifier (string) harnessfme/accountIdentifier: Harness Account Identifier (string) harnessfme/orgIdentifier: Harness Org Identifier (string) From 066331628e4c3aae93b8c73de13060914bf1161e Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Mon, 25 Aug 2025 22:29:01 -0400 Subject: [PATCH 04/12] readme update --- plugins/harness-fme-feature-flags/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/harness-fme-feature-flags/README.md b/plugins/harness-fme-feature-flags/README.md index a30c9fb4..69542f0d 100644 --- a/plugins/harness-fme-feature-flags/README.md +++ b/plugins/harness-fme-feature-flags/README.md @@ -97,7 +97,7 @@ You will need your accountId (formerly Org ID) and projectId (formerly Workspace You can get these from the URL when you are logged in to the FME console. -https://app.harness.io/ng/account//module/fme/orgs//projects//org//ws//mywork +https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/projectId/ws/workspaceId/mywork From 8b9c8b396387756934d56d176a3d1df4647a275f Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Tue, 26 Aug 2025 15:49:02 -0300 Subject: [PATCH 05/12] some fixes after getting it running locally --- app-config.yaml | 4 ++++ examples/entities.yaml | 10 +++++----- examples/entities.yaml.fixed | 13 ------------- plugins/harness-fme-feature-flags/README.md | 15 +++++++++++---- .../components/FMEFeatureList/FMEFeatureList.tsx | 13 +++++++------ .../FMEFeatureList/useProjectSlugEntity.tsx | 6 +++--- .../src/hooks/useGetOwners.ts | 5 +---- yarn.lock | 6 +++--- 8 files changed, 34 insertions(+), 38 deletions(-) delete mode 100644 examples/entities.yaml.fixed diff --git a/app-config.yaml b/app-config.yaml index 413a102d..5202e764 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -62,6 +62,10 @@ proxy: target: 'https://stress.harness.io/' headers: 'x-api-key': ${HARNESS_STRESS_API_KEY} + '/harnessfme': + target: 'https://api.split.io/' + headers: + 'x-api-key': ${HARNESS_PROD_API_KEY} # Reference documentation http://backstage.io/docs/features/techdocs/configuration diff --git a/examples/entities.yaml b/examples/entities.yaml index 603c0863..26a6df63 100644 --- a/examples/entities.yaml +++ b/examples/entities.yaml @@ -15,11 +15,11 @@ metadata: annotations: # mandatory annotation # harness.io/project-url: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_NAME/feature-flags - harnessfme/projectId: Harness FME Project # GUID - harnessfme/accountId: Harness FME Account # GUID - harnessfme/projectIdentifier: Harness Project Identifier (string) - harnessfme/accountIdentifier: Harness Account Identifier (string) - harnessfme/orgIdentifier: Harness Org Identifier (string) + harnessfme/projectId: ${FME_PROJECT_ID} # GUID + harnessfme/accountId: ${FME_ACCOUNT_ID} # GUID + harness/projectIdentifier: ${HARNESS_PROJECT_IDENTIFIER} # string + harness/accountIdentifier: ${HARNESS_ACCOUNT_IDENTIFIER} # string + harness/orgIdentifier: ${HARNESS_ORG_IDENTIFIER} # string spec: type: service diff --git a/examples/entities.yaml.fixed b/examples/entities.yaml.fixed deleted file mode 100644 index 35d39749..00000000 --- a/examples/entities.yaml.fixed +++ /dev/null @@ -1,13 +0,0 @@ -# The first part remains the same -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: example-service - annotations: - # mandatory annotation - # harness.io/project-url: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_NAME/feature-flags - harnessfme/projectId: ${HARNESS_FME_PROJECT_ID} # GUID from fme - harnessfme/accountId: ${HARNESS_FME_ACCOUNT_ID} # GUID from fme - harnessfme/harnessProjectId: ${HARNESS_PROJECT_IDENTIFIER} # project identifier - harnessfme/harnessAccountId: ${HARNESS_ACCOUNT_IDENTIFIER} # accountIdentifier - harnessfme/harnessOrgId: ${HARNESS_ORG_IDENTIFIER} #org string diff --git a/plugins/harness-fme-feature-flags/README.md b/plugins/harness-fme-feature-flags/README.md index 69542f0d..d37812a9 100644 --- a/plugins/harness-fme-feature-flags/README.md +++ b/plugins/harness-fme-feature-flags/README.md @@ -34,6 +34,13 @@ proxy: headers: 'x-api-key': '' # ... + +# You can also configure the base URLs in app-config.yaml +harness: + baseUrl: 'https://app.harness.io/' + +harnessfme: + baseUrl: 'https://api.split.io/' ``` Notes: @@ -97,7 +104,7 @@ You will need your accountId (formerly Org ID) and projectId (formerly Workspace You can get these from the URL when you are logged in to the FME console. -https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/projectId/ws/workspaceId/mywork +https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/fmeAcountId/ws/fmeProjectId/mywork @@ -110,9 +117,9 @@ metadata: # mandatory annotation harnessfme/projectId: # FME project identifier (UUID) harnessfme/accountId: # FME org identifier (UUID) - harnessfme/harnessAccountId: # Harness org identifier (String) - harnessfme/harnessOrgId: # Harness org identifier (String) - harnessfme/harnessProjectId: # Harness project identifier (String) + harness/accountIdentifier: # Harness account identifier (String) + harness/orgIdentifier: # Harness org identifier (String) + harness/projectIdentifier: # Harness project identifier (String) type: service # ... diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx index 2d460db1..d85d1689 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx @@ -51,6 +51,7 @@ function FMEFeatureList() { const config = useApi(configApiRef); const baseUrl = config.getOptionalString('harnessfme.baseUrl') ?? 'https://app.split.io/'; + const harnessBaseUrl = config.getOptionalString('harness.baseUrl') ?? 'https://app.harness.io/'; const { workspaceId, orgId, harnessAccountId, harnessOrgId, harnessProjectId } = useProjectSlugFromEntity(); // Memoize the refresh callback @@ -114,7 +115,7 @@ function FMEFeatureList() { const featureStatus = featureStatusMap[row.name as string] || { id: '', }; - const link = `${baseUrl}ng/account/${harnessAccountId}/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition` + const link = `${harnessBaseUrl}ng/account/${harnessAccountId}/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition` return ( {row.name} @@ -184,7 +185,7 @@ function FMEFeatureList() { if (owner?.type === 'user') { return `${owner.name}`; } else if (owner?.type === 'group') { - return ` ${owner.name} (Group) `; + return ` ${owner.name} (Group) `; } return owner?.name || ''; }) @@ -218,16 +219,16 @@ function FMEFeatureList() { title: 'Tags', field: 'col4', customSort: (row1: Partial, row2: Partial) => { - const a = featureStatusMap[row1.name as string]?.tags?.map((tag: { name: String; }) => tag.name).join(','); - const b = featureStatusMap[row2.name as string]?.tags?.map((tag: { name: String; }) => tag.name).join(','); + const a = featureStatusMap[row1.name as string]?.tags?.map((tag: { name: String; }) => tag.name).join(',') || ''; + const b = featureStatusMap[row2.name as string]?.tags?.map((tag: { name: String; }) => tag.name).join(',') || ''; return a.localeCompare(b); }, type: 'string', render: (row: Partial) => { const featureStatus = featureStatusMap[row.name as string]; return ( - - {featureStatus.tags.map((tag: { name: String; }) => tag.name).join(',')} + + {featureStatus.tags?.map((tag: { name: String; }) => tag.name).join(',')||'None'} ); }, diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx index 9c038740..65feae70 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx @@ -9,13 +9,13 @@ export const useProjectSlugFromEntity = () => { const orgIdAnnotation = 'harnessfme/accountId'; const orgId = entity.metadata.annotations?.[orgIdAnnotation] as string; - const harnessAccountAnnotation = 'harnessfme/harnessAccountId' + const harnessAccountAnnotation = 'harness/accountIdentifier' const harnessAccountId = entity.metadata.annotations?.[harnessAccountAnnotation] as string; - const harnessOrgIdentifier = 'harnessfme/harnessOrgId' + const harnessOrgIdentifier = 'harness/orgIdentifier' const harnessOrgId = entity.metadata.annotations?.[harnessOrgIdentifier] as string; - const harnessProjectIdentifier = 'harnessfme/harnessProjectId' + const harnessProjectIdentifier = 'harness/projectIdentifier' const harnessProjectId = entity.metadata.annotations?.[harnessProjectIdentifier] as string; return { workspaceId, orgId, harnessAccountId, harnessOrgId, harnessProjectId }; diff --git a/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts b/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts index fd0853e9..a310fe94 100644 --- a/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts +++ b/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts @@ -41,10 +41,7 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { try { const resp = await fetchApi.fetch( `${baseUrl}/harness/prod/ng/api/user/aggregate?pageIndex=${pageIndex}`, - { headers }, - { - method: 'POST', - } + { headers, method: 'POST' }, ); if (resp.status === 200) { diff --git a/yarn.lock b/yarn.lock index 1cdb177c..55555514 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11230,9 +11230,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001629: - version "1.0.30001636" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78" - integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg== + version "1.0.30001737" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz" + integrity sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw== caseless@~0.12.0: version "0.12.0" From 374d1f29f72bfd4ac9ccaf8628cdd97626aaaa6c Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Tue, 26 Aug 2025 20:21:11 -0300 Subject: [PATCH 06/12] ran tsc and prettifier - also simplified loading env variables --- app-config.yaml | 1 - examples/entities.yaml | 6 +--- plugins/harness-fme-feature-flags/README.md | 12 +++---- .../FMEFeatureList/FMEFeatureList.tsx | 31 ++++++++++++++----- .../FMEFeatureList/useProjectSlugEntity.tsx | 29 ++++++++--------- .../src/components/Router.tsx | 5 +-- .../src/hooks/useGetOwners.ts | 28 ++++++++--------- .../harness-fme-feature-flags/src/types.ts | 6 ++-- 8 files changed, 62 insertions(+), 56 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 5202e764..81605ad7 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -67,7 +67,6 @@ proxy: headers: 'x-api-key': ${HARNESS_PROD_API_KEY} - # Reference documentation http://backstage.io/docs/features/techdocs/configuration # Note: After experimenting with basic setup, use CI/CD to generate docs # and an external cloud storage when deploying TechDocs for production use-case. diff --git a/examples/entities.yaml b/examples/entities.yaml index 26a6df63..1e4b5372 100644 --- a/examples/entities.yaml +++ b/examples/entities.yaml @@ -15,11 +15,7 @@ metadata: annotations: # mandatory annotation # harness.io/project-url: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_NAME/feature-flags - harnessfme/projectId: ${FME_PROJECT_ID} # GUID - harnessfme/accountId: ${FME_ACCOUNT_ID} # GUID - harness/projectIdentifier: ${HARNESS_PROJECT_IDENTIFIER} # string - harness/accountIdentifier: ${HARNESS_ACCOUNT_IDENTIFIER} # string - harness/orgIdentifier: ${HARNESS_ORG_IDENTIFIER} # string + # harnessfme/mywork: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/fmeAcountId/ws/fmeProjectId/mywork spec: type: service diff --git a/plugins/harness-fme-feature-flags/README.md b/plugins/harness-fme-feature-flags/README.md index d37812a9..435336a7 100644 --- a/plugins/harness-fme-feature-flags/README.md +++ b/plugins/harness-fme-feature-flags/README.md @@ -100,10 +100,11 @@ const serviceEntityPage = ( ``` 4. Add required Harness FME specific annotations to your software component's respective `catalog-info.yaml` file. -You will need your accountId (formerly Org ID) and projectId (formerly Workspace ID) +) -You can get these from the URL when you are logged in to the FME console. +You will need your `My Work` URL from the Harness FME console and add that as an annotation to your software component's respective `catalog-info.yaml` file. To get that - log into your Harness FME console and navigate to the `My Work` section. Copy the URL from the browser and add it as an annotation to your software component's respective `catalog-info.yaml` file. +Example: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/fmeAcountId/ws/fmeProjectId/mywork @@ -115,11 +116,8 @@ metadata: # ... annotations: # mandatory annotation - harnessfme/projectId: # FME project identifier (UUID) - harnessfme/accountId: # FME org identifier (UUID) - harness/accountIdentifier: # Harness account identifier (String) - harness/orgIdentifier: # Harness org identifier (String) - harness/projectIdentifier: # Harness project identifier (String) + harnessfme/mywork: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/fmeAcountId/ws/fmeProjectId/mywork + type: service # ... diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx index d85d1689..ded3320f 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx @@ -49,10 +49,15 @@ function FMEFeatureList() { const discoveryApi = useApi(discoveryApiRef); discoveryApi.getBaseUrl('proxy').then(url => setResolvedBackendBaseUrl(url)); const config = useApi(configApiRef); - const baseUrl = - config.getOptionalString('harnessfme.baseUrl') ?? 'https://app.split.io/'; - const harnessBaseUrl = config.getOptionalString('harness.baseUrl') ?? 'https://app.harness.io/'; - const { workspaceId, orgId, harnessAccountId, harnessOrgId, harnessProjectId } = useProjectSlugFromEntity(); + const harnessBaseUrl = + config.getOptionalString('harness.baseUrl') ?? 'https://app.harness.io/'; + const { + workspaceId, + orgId, + harnessAccountId, + harnessOrgId, + harnessProjectId, + } = useProjectSlugFromEntity(); // Memoize the refresh callback const refresh = useCallback(() => { @@ -115,7 +120,7 @@ function FMEFeatureList() { const featureStatus = featureStatusMap[row.name as string] || { id: '', }; - const link = `${harnessBaseUrl}ng/account/${harnessAccountId}/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition` + const link = `${harnessBaseUrl}ng/account/${harnessAccountId}/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition`; return ( {row.name} @@ -219,8 +224,14 @@ function FMEFeatureList() { title: 'Tags', field: 'col4', customSort: (row1: Partial, row2: Partial) => { - const a = featureStatusMap[row1.name as string]?.tags?.map((tag: { name: String; }) => tag.name).join(',') || ''; - const b = featureStatusMap[row2.name as string]?.tags?.map((tag: { name: String; }) => tag.name).join(',') || ''; + const a = + featureStatusMap[row1.name as string]?.tags + ?.map((tag: { name: String }) => tag.name) + .join(',') || ''; + const b = + featureStatusMap[row2.name as string]?.tags + ?.map((tag: { name: String }) => tag.name) + .join(',') || ''; return a.localeCompare(b); }, type: 'string', @@ -228,7 +239,11 @@ function FMEFeatureList() { const featureStatus = featureStatusMap[row.name as string]; return ( - {featureStatus.tags?.map((tag: { name: String; }) => tag.name).join(',')||'None'} + + {featureStatus.tags + ?.map((tag: { name: String }) => tag.name) + .join(',') || 'None'}{' '} + ); }, diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx index 65feae70..0f1d7c78 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx @@ -3,20 +3,21 @@ import { useEntity } from '@backstage/plugin-catalog-react'; export const useProjectSlugFromEntity = () => { const { entity } = useEntity(); - const workspaceIdAnnotation = 'harnessfme/projectId'; - const workspaceId = entity.metadata.annotations?.[ workspaceIdAnnotation] as string; - const orgIdAnnotation = 'harnessfme/accountId'; - const orgId = entity.metadata.annotations?.[orgIdAnnotation] as string; + const myWorkAnnotation = 'harnessfme/mywork'; + const myWorkUrl = entity.metadata.annotations?.[myWorkAnnotation] as string; + const urlAsArray = myWorkUrl.split('/'); + const workspaceId = urlAsArray[urlAsArray.length - 2]; + const orgId = urlAsArray[urlAsArray.length - 4]; + const harnessProjectId = urlAsArray[urlAsArray.length - 6]; + const harnessAccountId = urlAsArray[urlAsArray.length - 12]; + const harnessOrgId = urlAsArray[urlAsArray.length - 8]; - const harnessAccountAnnotation = 'harness/accountIdentifier' - const harnessAccountId = entity.metadata.annotations?.[harnessAccountAnnotation] as string; - - const harnessOrgIdentifier = 'harness/orgIdentifier' - const harnessOrgId = entity.metadata.annotations?.[harnessOrgIdentifier] as string; - - const harnessProjectIdentifier = 'harness/projectIdentifier' - const harnessProjectId = entity.metadata.annotations?.[harnessProjectIdentifier] as string; - - return { workspaceId, orgId, harnessAccountId, harnessOrgId, harnessProjectId }; + return { + workspaceId, + orgId, + harnessAccountId, + harnessOrgId, + harnessProjectId, + }; }; diff --git a/plugins/harness-fme-feature-flags/src/components/Router.tsx b/plugins/harness-fme-feature-flags/src/components/Router.tsx index a08b13a5..35dbe5a9 100644 --- a/plugins/harness-fme-feature-flags/src/components/Router.tsx +++ b/plugins/harness-fme-feature-flags/src/components/Router.tsx @@ -25,10 +25,7 @@ import { MissingAnnotationEmptyState } from '@backstage/plugin-catalog-react'; /** @public */ export const isHarnessFMEFeatureFlagAvailable = (entity: Entity) => - Boolean( - entity.metadata.annotations?.['harnessfme/accountId'] && - entity.metadata.annotations?.['harnessfme/projectId'], - ); + Boolean(entity.metadata.annotations?.['harnessfme/mywork']); /** @public */ diff --git a/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts b/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts index a310fe94..f7380304 100644 --- a/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts +++ b/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts @@ -7,11 +7,9 @@ interface UseGetOwners { refresh: number; } - - interface GroupResponse { data: { - content: Owner[]; + content: HarnessGroup[]; }; } @@ -33,9 +31,6 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { let hasMore = true; const ownerList: Record = {}; - const pause = (duration: number) => - new Promise(resolve => setTimeout(resolve, duration)); - // Fetch groups while (hasMore) { try { @@ -46,8 +41,13 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { if (resp.status === 200) { const data = await resp.json(); - data.data.content.forEach((d: {user: HarnessUser}) => { - ownerList[d.user.uuid] = {id: d.user.uuid, name: d.user.name, email: d.user.email, type: 'user'}; + data.data.content.forEach((d: { user: HarnessUser }) => { + ownerList[d.user.uuid] = { + id: d.user.uuid, + name: d.user.name, + email: d.user.email, + type: 'user', + }; }); if (data.data.content.length < 50) { @@ -55,7 +55,6 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { } else { pageIndex += 1; } - } else { hasMore = false; } @@ -77,17 +76,18 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { if (respGroups.status === 200) { const dataGroups: GroupResponse = await respGroups.json(); dataGroups.data.content.forEach((d: HarnessGroup) => { - ownerList[d.identifier] = {id: d.identifier, name: d.name, type: 'group'}; + ownerList[d.identifier] = { + id: d.identifier, + name: d.name, + type: 'group', + }; }); - if ( - dataGroups.data.content.length < 50 - ) { + if (dataGroups.data.content.length < 50) { hasMore = false; } else { pageIndex += 1; } - } else { hasMore = false; } diff --git a/plugins/harness-fme-feature-flags/src/types.ts b/plugins/harness-fme-feature-flags/src/types.ts index 73e39a4b..ff9b0e54 100644 --- a/plugins/harness-fme-feature-flags/src/types.ts +++ b/plugins/harness-fme-feature-flags/src/types.ts @@ -43,8 +43,8 @@ export interface FeatureStatus { } export interface HarnessGroup { - identifier: string, - name: string + identifier: string; + name: string; } export interface HarnessUser { @@ -53,12 +53,12 @@ export interface HarnessUser { email: string; } - export interface Owner { id: string; type: string; name: string; email?: string; + identifier?: string; } export interface FlagSet { From 5b37a24445aa31b713d0d2b242230755d87fb3ca Mon Sep 17 00:00:00 2001 From: Joshua Klein Date: Tue, 26 Aug 2025 20:25:42 -0300 Subject: [PATCH 07/12] Update README.md --- plugins/harness-fme-feature-flags/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/harness-fme-feature-flags/README.md b/plugins/harness-fme-feature-flags/README.md index 435336a7..d5ff9ccc 100644 --- a/plugins/harness-fme-feature-flags/README.md +++ b/plugins/harness-fme-feature-flags/README.md @@ -99,7 +99,7 @@ const serviceEntityPage = ( ``` -4. Add required Harness FME specific annotations to your software component's respective `catalog-info.yaml` file. +4. Add required Harness FME specific annotations to your software component's respective yaml file. ) You will need your `My Work` URL from the Harness FME console and add that as an annotation to your software component's respective `catalog-info.yaml` file. To get that - log into your Harness FME console and navigate to the `My Work` section. Copy the URL from the browser and add it as an annotation to your software component's respective `catalog-info.yaml` file. From f6de0940584d73eec4ae9e671365cd5cf0f4741e Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Wed, 27 Aug 2025 08:49:16 -0300 Subject: [PATCH 08/12] upodated readme --- plugins/harness-fme-feature-flags/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/harness-fme-feature-flags/README.md b/plugins/harness-fme-feature-flags/README.md index d5ff9ccc..a2d742c0 100644 --- a/plugins/harness-fme-feature-flags/README.md +++ b/plugins/harness-fme-feature-flags/README.md @@ -100,7 +100,6 @@ const serviceEntityPage = ( ``` 4. Add required Harness FME specific annotations to your software component's respective yaml file. -) You will need your `My Work` URL from the Harness FME console and add that as an annotation to your software component's respective `catalog-info.yaml` file. To get that - log into your Harness FME console and navigate to the `My Work` section. Copy the URL from the browser and add it as an annotation to your software component's respective `catalog-info.yaml` file. From 1bf3b62735bebf72c6bb899de4fec0c2ec117c35 Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Wed, 27 Aug 2025 08:49:52 -0300 Subject: [PATCH 09/12] updated readme --- plugins/harness-fme-feature-flags/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/harness-fme-feature-flags/README.md b/plugins/harness-fme-feature-flags/README.md index a2d742c0..8854187c 100644 --- a/plugins/harness-fme-feature-flags/README.md +++ b/plugins/harness-fme-feature-flags/README.md @@ -101,7 +101,8 @@ const serviceEntityPage = ( 4. Add required Harness FME specific annotations to your software component's respective yaml file. -You will need your `My Work` URL from the Harness FME console and add that as an annotation to your software component's respective `catalog-info.yaml` file. To get that - log into your Harness FME console and navigate to the `My Work` section. Copy the URL from the browser and add it as an annotation to your software component's respective `catalog-info.yaml` file. +You will need your `My Work` URL from the Harness FME console and add that as an annotation to your software component's respective `catalog-info.yaml` file. To get that - log into your Harness FME console and navigate to the `My Work` section. Copy the URL from the browser and add it as an annotation. + Example: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/fmeAcountId/ws/fmeProjectId/mywork From 5c03fd816cd462428ef53904c8305e2d34d74556 Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Fri, 29 Aug 2025 18:11:45 -0400 Subject: [PATCH 10/12] updated to support both types --- app-config.yaml | 2 +- examples/entities.yaml | 5 + plugins/harness-fme-feature-flags/README.md | 76 ++++++- .../FMEFeatureList/FMEFeatureList.tsx | 37 ++- .../FMEFeatureList/useProjectSlugEntity.tsx | 33 ++- .../src/components/Router.tsx | 8 +- .../src/hooks/useGetOwners.ts | 210 +++++++++++++----- 7 files changed, 289 insertions(+), 82 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 81605ad7..7483b5e9 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -65,7 +65,7 @@ proxy: '/harnessfme': target: 'https://api.split.io/' headers: - 'x-api-key': ${HARNESS_PROD_API_KEY} + 'x-api-key': ${HARNESS_PROD_API_KEY} #use x-api-key for migrated environments, use `Authorization: Bearer ` for non-migrated environments # Reference documentation http://backstage.io/docs/features/techdocs/configuration # Note: After experimenting with basic setup, use CI/CD to generate docs diff --git a/examples/entities.yaml b/examples/entities.yaml index 1e4b5372..cf1378e3 100644 --- a/examples/entities.yaml +++ b/examples/entities.yaml @@ -16,6 +16,11 @@ metadata: # mandatory annotation # harness.io/project-url: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_NAME/feature-flags # harnessfme/mywork: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/fmeAcountId/ws/fmeProjectId/mywork + harnessfme/isMigrated: "false" + harnessfme/accountId: "ACCOUNT_ID" + harnessfme/projectId: "PROJECT_ID" + + spec: type: service diff --git a/plugins/harness-fme-feature-flags/README.md b/plugins/harness-fme-feature-flags/README.md index 8854187c..4b2a0d39 100644 --- a/plugins/harness-fme-feature-flags/README.md +++ b/plugins/harness-fme-feature-flags/README.md @@ -4,6 +4,12 @@ Website: [https://harness.io/](https://harness.io/) Welcome to the Harness FME Feature Flags plugin for Backstage! +This plugin supports both **migrated** and **non-migrated** Split.io environments: +- **Migrated environments**: Feature flags that have been migrated to Harness FME (uses Harness APIs) +- **Non-migrated environments**: Feature flags still running on Split.io (uses Split.io APIs) + +The plugin automatically detects which environment type you're using based on your entity annotations and routes API calls accordingly. + ## Screenshots @@ -22,7 +28,10 @@ If you are looking to get started with Backstage, check out [backstage.io/docs]( For testing purposes, you can also clone this repository to try out the plugin. It contains an example Backstage app setup which is pre-installed with Harness plugins. However, you must create a new Backstage app if you are looking to get started with Backstage. -2. Configure proxy for harness in your `app-config.yaml` under the `proxy` config. Ensure you have a service account for `x-api-key`. See the [Harness API Docs](https://developer.harness.io/docs/platform/automation/api/api-quickstart/) for generating an API Key. +2. Configure proxy settings in your `app-config.yaml` under the `proxy` config. + +### For Migrated Environments (Harness FME) +Ensure you have a service account for `x-api-key`. See the [Harness API Docs](https://developer.harness.io/docs/platform/automation/api/api-quickstart/) for generating an API Key. ```yaml # In app-config.yaml @@ -32,20 +41,37 @@ proxy: '/harness/prod': target: 'https://app.harness.io/' headers: - 'x-api-key': '' + 'x-api-key': '' # ... # You can also configure the base URLs in app-config.yaml harness: baseUrl: 'https://app.harness.io/' +``` + +### For Non-Migrated Environments (Split.io) +Configure Split.io proxy with your Split.io API token: + +```yaml +# In app-config.yaml + +proxy: + # ... existing proxy settings + '/harnessfme/internal': + target: 'https://api.split.io/' + headers: + 'Authorization': 'Bearer ' +# ... harnessfme: baseUrl: 'https://api.split.io/' ``` -Notes: +**Notes:** -- Plugin uses token configured here to make Harness FME API calls. Make sure this token has the necessary permissions +- For **migrated environments**: Plugin uses the Harness API token to make Harness FME API calls +- For **non-migrated environments**: Plugin uses the Split.io API token to make Split.io API calls +- Make sure tokens have the necessary permissions for feature flag operations 3. Inside your Backstage's `EntityPage.tsx`, add the new `FMEfeatureFlagList` component to render `` whenever the service is using Harness Feature Flags. Something like this - @@ -99,15 +125,18 @@ const serviceEntityPage = ( ``` -4. Add required Harness FME specific annotations to your software component's respective yaml file. - -You will need your `My Work` URL from the Harness FME console and add that as an annotation to your software component's respective `catalog-info.yaml` file. To get that - log into your Harness FME console and navigate to the `My Work` section. Copy the URL from the browser and add it as an annotation. +4. Add required annotations to your software component's respective `catalog-info.yaml` file. +The plugin supports two configuration modes based on your environment: -Example: -https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/fmeAcountId/ws/fmeProjectId/mywork +### For Migrated Environments (Harness FME) +You will need your `My Work` URL from the Harness FME console. Log into your Harness FME console, navigate to the `My Work` section, and copy the URL from the browser. +Example URL: +``` +https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/fmeAcountId/ws/fmeProjectId/mywork +``` ```yaml apiVersion: backstage.io/v1alpha1 @@ -115,14 +144,39 @@ kind: Component metadata: # ... annotations: - # mandatory annotation + # Required for migrated environments harnessfme/mywork: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/fmeAcountId/ws/fmeProjectId/mywork - + harnessfme/isMigrated: "true" # This tells the plugin to use Harness APIs +spec: + type: service + # ... +``` + +### For Non-Migrated Environments (Split.io) + +For environments still running on Split.io, you need to provide the account and project IDs directly: + +You can get these from the URL when you are logged in to the Split.io console on the mywork page. +https://app.split.io/org/ACCOUNT_ID/ws/PROJECT_ID/mywork + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + # ... + annotations: + # Required for non-migrated environments + harnessfme/accountId: ACCOUNT_ID # Your Split.io account ID + harnessfme/projectId: PROJECT_ID # Your Split.io workspace ID + harnessfme/isMigrated: "false" # This tells the plugin to use Split.io APIs +spec: type: service # ... ``` +**Important:** The `harnessfme/isMigrated` annotation determines which API endpoints and authentication methods the plugin uses. Set it to `"true"` for migrated environments or `"false"` for Split.io environments. + ## Features diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx index ded3320f..43be8804 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx @@ -51,12 +51,14 @@ function FMEFeatureList() { const config = useApi(configApiRef); const harnessBaseUrl = config.getOptionalString('harness.baseUrl') ?? 'https://app.harness.io/'; + const fmeBaseUrl = 'https://app.split.io/'; const { workspaceId, orgId, harnessAccountId, harnessOrgId, harnessProjectId, + isMigrated, } = useProjectSlugFromEntity(); // Memoize the refresh callback @@ -120,7 +122,9 @@ function FMEFeatureList() { const featureStatus = featureStatusMap[row.name as string] || { id: '', }; - const link = `${harnessBaseUrl}ng/account/${harnessAccountId}/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition`; + const link = isMigrated == 'true' ? + `${harnessBaseUrl}ng/account/${harnessAccountId}/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition` : + `${fmeBaseUrl}org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition`; return ( {row.name} @@ -128,8 +132,28 @@ function FMEFeatureList() { ); }, customFilterAndSearch: (term, row: Partial) => { - const temp = row?.name ?? ''; - return temp.toLowerCase().indexOf(term.toLowerCase()) > -1; + const featureStatus = featureStatusMap[row.name as string] || {}; + + // Concatenate all searchable fields + const searchableText = [ + row.name || '', + row.killed ? 'killed' : 'live', + row.trafficType || '', + row.defaultTreatment || '', + featureStatus?.rolloutStatus?.name || '', + // Owners + featureStatus?.owners?.map((owner: { id: string }) => + ownersMap[owner.id]?.name || '' + ).join(' ') || '', + // Tags + featureStatus?.tags?.map((tag: { name: string }) => tag.name).join(' ') || '', + // Flag Sets + row.flagSets?.map((f: { id: string }) => + flagSetsMap[f.id]?.name || '' + ).join(' ') || '' + ].join(' ').toLowerCase(); + + return searchableText.indexOf(term.toLowerCase()) > -1; }, customSort: (row1: Partial, row2: Partial) => { const a = row1.name ?? ''; @@ -190,7 +214,8 @@ function FMEFeatureList() { if (owner?.type === 'user') { return `${owner.name}`; } else if (owner?.type === 'group') { - return ` ${owner.name} (Group) `; + return isMigrated === 'true' ? ` ${owner.name} (Group) ` : + ` ${owner.name} (Group) `; } return owner?.name || ''; }) @@ -240,7 +265,7 @@ function FMEFeatureList() { return ( - {featureStatus.tags + {featureStatus?.tags ?.map((tag: { name: String }) => tag.name) .join(',') || 'None'}{' '} @@ -401,7 +426,7 @@ function FMEFeatureList() { 'Could not find any Feature Flags, the bearer auth token is either missing or incorrect in app-config.yaml under proxy settings.'; } else if (!workspaceId && !orgId) { description = - 'Could not find any Feature Flags, please check your workspaceId and orgId configuration in catalog-info.yaml.'; + 'Could not find any Feature Flags, please check your annotation configuration in catalog-info.yaml.'; } else { description = 'Could not find any Feature Flags, please check your configurations in catalog-info.yaml or check your token permissions.'; diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx index 0f1d7c78..2b9d2286 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx @@ -4,14 +4,30 @@ import { useEntity } from '@backstage/plugin-catalog-react'; export const useProjectSlugFromEntity = () => { const { entity } = useEntity(); - const myWorkAnnotation = 'harnessfme/mywork'; - const myWorkUrl = entity.metadata.annotations?.[myWorkAnnotation] as string; - const urlAsArray = myWorkUrl.split('/'); - const workspaceId = urlAsArray[urlAsArray.length - 2]; - const orgId = urlAsArray[urlAsArray.length - 4]; - const harnessProjectId = urlAsArray[urlAsArray.length - 6]; - const harnessAccountId = urlAsArray[urlAsArray.length - 12]; - const harnessOrgId = urlAsArray[urlAsArray.length - 8]; + const isMigratedAnnotation = 'harnessfme/isMigrated'; + const isMigrated = entity.metadata.annotations?.[isMigratedAnnotation] as string; + + let workspaceId = ''; + let orgId = ''; + let harnessAccountId = ''; + let harnessOrgId = ''; + let harnessProjectId = ''; + + if (isMigrated === 'true') { + const myWorkAnnotation = 'harnessfme/mywork'; + const myWorkUrl = entity.metadata.annotations?.[myWorkAnnotation] as string; + const urlAsArray = myWorkUrl.split('/'); + workspaceId = urlAsArray[urlAsArray.length - 2]; + orgId = urlAsArray[urlAsArray.length - 4]; + harnessProjectId = urlAsArray[urlAsArray.length - 6]; + harnessAccountId = urlAsArray[urlAsArray.length - 12]; + harnessOrgId = urlAsArray[urlAsArray.length - 8]; + } else { + const accountIdAnnotation = 'harnessfme/accountId'; + orgId = entity.metadata.annotations?.[accountIdAnnotation] as string; + const projectIdAnnotation = 'harnessfme/projectId'; + workspaceId = entity.metadata.annotations?.[projectIdAnnotation] as string; + } return { workspaceId, @@ -19,5 +35,6 @@ export const useProjectSlugFromEntity = () => { harnessAccountId, harnessOrgId, harnessProjectId, + isMigrated, }; }; diff --git a/plugins/harness-fme-feature-flags/src/components/Router.tsx b/plugins/harness-fme-feature-flags/src/components/Router.tsx index 35dbe5a9..ed96c04a 100644 --- a/plugins/harness-fme-feature-flags/src/components/Router.tsx +++ b/plugins/harness-fme-feature-flags/src/components/Router.tsx @@ -25,13 +25,17 @@ import { MissingAnnotationEmptyState } from '@backstage/plugin-catalog-react'; /** @public */ export const isHarnessFMEFeatureFlagAvailable = (entity: Entity) => - Boolean(entity.metadata.annotations?.['harnessfme/mywork']); + (Boolean(entity.metadata.annotations?.['harnessfme/mywork']) && + Boolean(entity.metadata.annotations?.['harnessfme/isMigrated'] === 'true')) + || (Boolean(entity.metadata.annotations?.['harnessfme/isMigrated'] === 'false') && + Boolean(entity.metadata.annotations?.['harnessfme/accountId']) && + Boolean(entity.metadata.annotations?.['harnessfme/projectId'])); /** @public */ export const Router = () => { const { entity } = useEntity(); - const requiredAnnotations = ['harnessfme/accountId', 'harnessfme/projectId']; + const requiredAnnotations = ['harnessfme/accountId', 'harnessfme/projectId', 'harnessfme/isMigrated']; if (!isHarnessFMEFeatureFlagAvailable(entity)) { return ; diff --git a/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts b/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts index f7380304..2b44abfb 100644 --- a/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts +++ b/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { HarnessUser, HarnessGroup, Owner } from '../types'; import { fetchApiRef, useApi } from '@backstage/core-plugin-api'; +import { useProjectSlugFromEntity } from '../components/FMEFeatureList/useProjectSlugEntity'; interface UseGetOwners { resolvedBackendBaseUrl: string; @@ -13,10 +14,17 @@ interface GroupResponse { }; } +interface UserResponse { + data: Owner[]; + nextMarker: string | null; +} + const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { const [ownersMap, setOwnersMap] = useState>({}); const [loading, setLoading] = useState(false); const fetchApi = useApi(fetchApiRef); + const { isMigrated } = useProjectSlugFromEntity(); + useEffect(() => { const fetchOwners = async () => { if (!resolvedBackendBaseUrl) return; @@ -27,72 +35,167 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { }); setLoading(true); - let pageIndex = 0; - let hasMore = true; const ownerList: Record = {}; - // Fetch groups - while (hasMore) { - try { - const resp = await fetchApi.fetch( - `${baseUrl}/harness/prod/ng/api/user/aggregate?pageIndex=${pageIndex}`, - { headers, method: 'POST' }, - ); - - if (resp.status === 200) { - const data = await resp.json(); - data.data.content.forEach((d: { user: HarnessUser }) => { - ownerList[d.user.uuid] = { - id: d.user.uuid, - name: d.user.name, - email: d.user.email, - type: 'user', - }; - }); - - if (data.data.content.length < 50) { + if (isMigrated === 'true') { + // Migrated path - Harness API + let pageIndex = 0; + let hasMore = true; + + // Fetch users + while (hasMore) { + try { + const resp = await fetchApi.fetch( + `${baseUrl}/harness/prod/ng/api/user/aggregate?pageIndex=${pageIndex}`, + { headers, method: 'POST' }, + ); + + if (resp.status === 200) { + const data = await resp.json(); + data.data.content.forEach((d: { user: HarnessUser }) => { + ownerList[d.user.uuid] = { + id: d.user.uuid, + name: d.user.name, + email: d.user.email, + type: 'user', + }; + }); + + if (data.data.content.length < 50) { + hasMore = false; + } else { + pageIndex += 1; + } + } else { hasMore = false; + } + } catch (error) { + hasMore = false; + } + } + + // Fetch groups + hasMore = true; + pageIndex = 0; + while (hasMore) { + try { + const respGroups = await fetchApi.fetch( + `${baseUrl}/harness/prod/ng/api/user-groups?pageIndex=${pageIndex}`, + { headers }, + ); + + if (respGroups.status === 200) { + const dataGroups: GroupResponse = await respGroups.json(); + dataGroups.data.content.forEach((d: HarnessGroup) => { + ownerList[d.identifier] = { + id: d.identifier, + name: d.name, + type: 'group', + }; + }); + + if (dataGroups.data.content.length < 50) { + hasMore = false; + } else { + pageIndex += 1; + } } else { - pageIndex += 1; + hasMore = false; } - } else { + } catch (error) { hasMore = false; } - } catch (error) { - hasMore = false; } - } + } else { + // Non-migrated path - Split.io API + const pause = (duration: number) => + new Promise(resolve => setTimeout(resolve, duration)); + + let offset = 0; + let hasMore = true; + + // Fetch groups + while (hasMore) { + try { + const resp = await fetchApi.fetch( + `${baseUrl}/harnessfme/internal/api/v2/groups?limit=50&offset=${offset}`, + { headers }, + ); + + if (resp.status === 200) { + const data = await resp.json(); + data.objects.forEach((d: Owner) => { + ownerList[d.id] = d; + }); - // Fetch groups - hasMore = true; - pageIndex = 0; - while (hasMore) { - try { - const respGroups = await fetchApi.fetch( - `${baseUrl}/harness/prod/ng/api/user-groups?pageIndex=${pageIndex}`, - { headers }, - ); - - if (respGroups.status === 200) { - const dataGroups: GroupResponse = await respGroups.json(); - dataGroups.data.content.forEach((d: HarnessGroup) => { - ownerList[d.identifier] = { - id: d.identifier, - name: d.name, - type: 'group', - }; - }); - - if (dataGroups.data.content.length < 50) { + if (data.objects.length < 50) { + hasMore = false; + } else { + offset += 50; + } + } else if (resp.status === 429) { + const orgResetSeconds = parseInt( + resp.headers.get('x-ratelimit-reset-seconds-org') || '0', + 10, + ); + const ipResetSeconds = parseInt( + resp.headers.get('x-ratelimit-reset-seconds-ip') || '0', + 10, + ); + const resetSeconds = Math.max(orgResetSeconds, ipResetSeconds); + await pause(resetSeconds * 1000); + } else { hasMore = false; + } + } catch (error) { + hasMore = false; + } + } + + // Fetch users + hasMore = true; + let nextMarker = null; + while (hasMore) { + try { + const respUsers = await fetchApi.fetch( + `${baseUrl}/harnessfme/internal/api/v2/users?limit=200${ + nextMarker !== null ? `&nextMarker=${nextMarker}` : '' + }`, + { headers }, + ); + + if (respUsers.status === 200) { + const dataUsers: UserResponse = await respUsers.json(); + dataUsers.data.forEach((d: Owner) => { + ownerList[d.id] = d; + }); + + if ( + dataUsers.data.length < 200 || + !dataUsers.nextMarker || + nextMarker === dataUsers.nextMarker + ) { + hasMore = false; + } else { + nextMarker = dataUsers.nextMarker; + } + } else if (respUsers.status === 429) { + const orgResetSeconds = parseInt( + respUsers.headers.get('x-ratelimit-reset-seconds-org') || '2', + 10, + ); + const ipResetSeconds = parseInt( + respUsers.headers.get('x-ratelimit-reset-seconds-ip') || '2', + 10, + ); + const resetSeconds = Math.max(orgResetSeconds, ipResetSeconds); + await pause(resetSeconds * 1000); } else { - pageIndex += 1; + hasMore = false; } - } else { + } catch (error) { hasMore = false; } - } catch (error) { - hasMore = false; } } @@ -101,9 +204,8 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { }; fetchOwners(); - }, [resolvedBackendBaseUrl, refresh, fetchApi]); // Include dependencies in useEffect + }, [resolvedBackendBaseUrl, refresh, fetchApi, isMigrated]); return { ownersMap, loading }; }; - export default useGetOwners; From 7a7c2f008f700a437dbe495e00e4aa02849ba75d Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Fri, 29 Aug 2025 19:54:51 -0400 Subject: [PATCH 11/12] prettied up files --- examples/entities.yaml | 8 ++--- .../FMEFeatureList/FMEFeatureList.tsx | 36 +++++++++++-------- .../FMEFeatureList/useProjectSlugEntity.tsx | 4 ++- .../src/components/Router.tsx | 14 ++++++-- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/examples/entities.yaml b/examples/entities.yaml index cf1378e3..a9271e2d 100644 --- a/examples/entities.yaml +++ b/examples/entities.yaml @@ -16,11 +16,9 @@ metadata: # mandatory annotation # harness.io/project-url: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_NAME/feature-flags # harnessfme/mywork: https://app.harness.io/ng/account/HARNESS_ACCOUNT_ID/module/fme/orgs/HARNESS_ORG_ID/projects/HARNESS_PROJECT_ID/org/fmeAcountId/ws/fmeProjectId/mywork - harnessfme/isMigrated: "false" - harnessfme/accountId: "ACCOUNT_ID" - harnessfme/projectId: "PROJECT_ID" - - + harnessfme/isMigrated: 'false' + harnessfme/accountId: 'ACCOUNT_ID' + harnessfme/projectId: 'PROJECT_ID' spec: type: service diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx index 43be8804..725c50ce 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx @@ -122,9 +122,10 @@ function FMEFeatureList() { const featureStatus = featureStatusMap[row.name as string] || { id: '', }; - const link = isMigrated == 'true' ? - `${harnessBaseUrl}ng/account/${harnessAccountId}/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition` : - `${fmeBaseUrl}org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition`; + const link = + isMigrated === 'true' + ? `${harnessBaseUrl}ng/account/${harnessAccountId}/module/fme/orgs/${harnessOrgId}/projects/${harnessProjectId}/org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition` + : `${fmeBaseUrl}org/${orgId}/ws/${workspaceId}/splits/${featureStatus.id}/env/${envId.id}/definition`; return ( {row.name} @@ -133,7 +134,7 @@ function FMEFeatureList() { }, customFilterAndSearch: (term, row: Partial) => { const featureStatus = featureStatusMap[row.name as string] || {}; - + // Concatenate all searchable fields const searchableText = [ row.name || '', @@ -142,17 +143,21 @@ function FMEFeatureList() { row.defaultTreatment || '', featureStatus?.rolloutStatus?.name || '', // Owners - featureStatus?.owners?.map((owner: { id: string }) => - ownersMap[owner.id]?.name || '' - ).join(' ') || '', + featureStatus?.owners + ?.map((owner: { id: string }) => ownersMap[owner.id]?.name || '') + .join(' ') || '', // Tags - featureStatus?.tags?.map((tag: { name: string }) => tag.name).join(' ') || '', + featureStatus?.tags + ?.map((tag: { name: string }) => tag.name) + .join(' ') || '', // Flag Sets - row.flagSets?.map((f: { id: string }) => - flagSetsMap[f.id]?.name || '' - ).join(' ') || '' - ].join(' ').toLowerCase(); - + row.flagSets + ?.map((f: { id: string }) => flagSetsMap[f.id]?.name || '') + .join(' ') || '', + ] + .join(' ') + .toLowerCase(); + return searchableText.indexOf(term.toLowerCase()) > -1; }, customSort: (row1: Partial, row2: Partial) => { @@ -214,8 +219,9 @@ function FMEFeatureList() { if (owner?.type === 'user') { return `${owner.name}`; } else if (owner?.type === 'group') { - return isMigrated === 'true' ? ` ${owner.name} (Group) ` : - ` ${owner.name} (Group) `; + return isMigrated === 'true' + ? ` ${owner.name} (Group) ` + : ` ${owner.name} (Group) `; } return owner?.name || ''; }) diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx index 2b9d2286..d67729ae 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx @@ -5,7 +5,9 @@ export const useProjectSlugFromEntity = () => { const { entity } = useEntity(); const isMigratedAnnotation = 'harnessfme/isMigrated'; - const isMigrated = entity.metadata.annotations?.[isMigratedAnnotation] as string; + const isMigrated = entity.metadata.annotations?.[ + isMigratedAnnotation + ] as string; let workspaceId = ''; let orgId = ''; diff --git a/plugins/harness-fme-feature-flags/src/components/Router.tsx b/plugins/harness-fme-feature-flags/src/components/Router.tsx index ed96c04a..312a8884 100644 --- a/plugins/harness-fme-feature-flags/src/components/Router.tsx +++ b/plugins/harness-fme-feature-flags/src/components/Router.tsx @@ -26,8 +26,12 @@ import { MissingAnnotationEmptyState } from '@backstage/plugin-catalog-react'; /** @public */ export const isHarnessFMEFeatureFlagAvailable = (entity: Entity) => (Boolean(entity.metadata.annotations?.['harnessfme/mywork']) && - Boolean(entity.metadata.annotations?.['harnessfme/isMigrated'] === 'true')) - || (Boolean(entity.metadata.annotations?.['harnessfme/isMigrated'] === 'false') && + Boolean( + entity.metadata.annotations?.['harnessfme/isMigrated'] === 'true', + )) || + (Boolean( + entity.metadata.annotations?.['harnessfme/isMigrated'] === 'false', + ) && Boolean(entity.metadata.annotations?.['harnessfme/accountId']) && Boolean(entity.metadata.annotations?.['harnessfme/projectId'])); @@ -35,7 +39,11 @@ export const isHarnessFMEFeatureFlagAvailable = (entity: Entity) => export const Router = () => { const { entity } = useEntity(); - const requiredAnnotations = ['harnessfme/accountId', 'harnessfme/projectId', 'harnessfme/isMigrated']; + const requiredAnnotations = [ + 'harnessfme/accountId', + 'harnessfme/projectId', + 'harnessfme/isMigrated', + ]; if (!isHarnessFMEFeatureFlagAvailable(entity)) { return ; From 88d0d54655fb277881a058e8d19697e691551872 Mon Sep 17 00:00:00 2001 From: Josh Klein Date: Tue, 2 Sep 2025 09:26:12 -0400 Subject: [PATCH 12/12] added default when isMigrated annotation not present --- .../src/components/FMEFeatureList/useProjectSlugEntity.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx index d67729ae..2676a616 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx @@ -5,10 +5,12 @@ export const useProjectSlugFromEntity = () => { const { entity } = useEntity(); const isMigratedAnnotation = 'harnessfme/isMigrated'; - const isMigrated = entity.metadata.annotations?.[ + let isMigrated = entity.metadata.annotations?.[ isMigratedAnnotation ] as string; - + if (isMigrated === '') { + isMigrated = 'false'; + } let workspaceId = ''; let orgId = ''; let harnessAccountId = '';