diff --git a/app-config.yaml b/app-config.yaml index ff7b7760..7483b5e9 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -65,7 +65,7 @@ proxy: '/harnessfme': target: 'https://api.split.io/' headers: - 'Authorization': 'Bearer ${FME_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 e3fa7f49..a9271e2d 100644 --- a/examples/entities.yaml +++ b/examples/entities.yaml @@ -15,8 +15,10 @@ 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/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 72df15e5..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,23 +28,50 @@ 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 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 + +proxy: + # ... existing proxy settings + '/harness/prod': + target: 'https://app.harness.io/' + headers: + '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': + '/harnessfme/internal': target: 'https://api.split.io/' headers: - 'Authorization': 'Bearer ' + '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 - @@ -92,14 +125,18 @@ 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) +4. Add required annotations to your software component's respective `catalog-info.yaml` file. -You can get these from the URL when you are logged in to the FME console. +The plugin supports two configuration modes based on your environment: -https://app.split.io/org//ws/>/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 @@ -107,14 +144,39 @@ kind: Component metadata: # ... annotations: - # mandatory annotation - harnessfme/projectId: - harnessfme/accountId: + # 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 829b3080..725c50ce 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/FMEFeatureList.tsx @@ -49,9 +49,17 @@ 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 { workspaceId, orgId } = useProjectSlugFromEntity(); + 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 const refresh = useCallback(() => { @@ -114,7 +122,10 @@ 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 = + 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} @@ -122,8 +133,32 @@ 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 ?? ''; @@ -184,7 +219,9 @@ 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 || ''; }) @@ -215,8 +252,36 @@ 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(',') || 'None'}{' '} + + + ); + }, + }, + { + title: 'Rollout Status', + field: 'col5', type: 'string', customSort: (row1: Partial, row2: Partial) => { const status1 = @@ -241,7 +306,7 @@ function FMEFeatureList() { }, { title: 'Default Treatment', - field: 'col5', + field: 'col6', type: 'string', customSort: (row1: Partial, row2: Partial) => { const a = row1.defaultTreatment?.toLowerCase() ?? ''; @@ -259,7 +324,7 @@ function FMEFeatureList() { { title: 'Flag Sets', - field: 'col6', + field: 'col7', type: 'string', customSort: (row1: Partial, row2: Partial) => { const sets1 = @@ -295,7 +360,7 @@ function FMEFeatureList() { }, { title: 'Created at', - field: 'col7', + field: 'col8', type: 'date', customSort: (row1: Partial, row2: Partial) => { const date1 = row1.creationTime @@ -315,7 +380,7 @@ function FMEFeatureList() { }, { title: 'Modified At', - field: 'col8', + field: 'col9', type: 'date', customSort: (row1: Partial, row2: Partial) => { const date1 = row1.lastUpdateTime @@ -335,7 +400,7 @@ function FMEFeatureList() { }, { title: 'Last Traffic Received', - field: 'col9', + field: 'col10', type: 'date', customSort: (row1: Partial, row2: Partial) => { const date1 = row1.lastTrafficReceivedAt @@ -367,7 +432,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 58177606..2676a616 100644 --- a/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx +++ b/plugins/harness-fme-feature-flags/src/components/FMEFeatureList/useProjectSlugEntity.tsx @@ -3,12 +3,42 @@ import { useEntity } from '@backstage/plugin-catalog-react'; export const useProjectSlugFromEntity = () => { const { entity } = useEntity(); - const workspaceIdAnnotation = 'harnessfme/projectId'; - const orgIdAnnotation = 'harnessfme/accountId'; - const workspaceId = entity.metadata.annotations?.[ - workspaceIdAnnotation + + const isMigratedAnnotation = 'harnessfme/isMigrated'; + let isMigrated = entity.metadata.annotations?.[ + isMigratedAnnotation ] as string; - const orgId = entity.metadata.annotations?.[orgIdAnnotation] as string; + if (isMigrated === '') { + isMigrated = 'false'; + } + 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, orgId }; + return { + workspaceId, + orgId, + 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 a08b13a5..312a8884 100644 --- a/plugins/harness-fme-feature-flags/src/components/Router.tsx +++ b/plugins/harness-fme-feature-flags/src/components/Router.tsx @@ -25,16 +25,25 @@ 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']) && + 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 d5fe57b9..2b44abfb 100644 --- a/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts +++ b/plugins/harness-fme-feature-flags/src/hooks/useGetOwners.ts @@ -1,12 +1,19 @@ import { useEffect, useState } from 'react'; -import { Owner } from '../types'; +import { HarnessUser, HarnessGroup, Owner } from '../types'; import { fetchApiRef, useApi } from '@backstage/core-plugin-api'; +import { useProjectSlugFromEntity } from '../components/FMEFeatureList/useProjectSlugEntity'; interface UseGetOwners { resolvedBackendBaseUrl: string; refresh: number; } +interface GroupResponse { + data: { + content: HarnessGroup[]; + }; +} + interface UserResponse { data: Owner[]; nextMarker: string | null; @@ -16,6 +23,8 @@ 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; @@ -26,94 +35,167 @@ const useGetOwners = ({ resolvedBackendBaseUrl, refresh }: UseGetOwners) => { }); setLoading(true); - let offset = 0; - let hasMore = true; const ownerList: Record = {}; - const pause = (duration: number) => - new Promise(resolve => setTimeout(resolve, duration)); + 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' }, + ); - // 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.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; + } + } - 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 (data.objects.length < 50) { - hasMore = false; + 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 { - offset += 50; + hasMore = false; } - } 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 { + } 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 }, + ); - // 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; + if (resp.status === 200) { + const data = await resp.json(); + data.objects.forEach((d: Owner) => { + ownerList[d.id] = d; + }); + + 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 { - nextMarker = dataUsers.nextMarker; + hasMore = false; } - } 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, + } 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 }, ); - const resetSeconds = Math.max(orgResetSeconds, ipResetSeconds); - await pause(resetSeconds * 1000); - } else { + + 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 { + hasMore = false; + } + } catch (error) { hasMore = false; } - } catch (error) { - hasMore = false; } } @@ -122,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; diff --git a/plugins/harness-fme-feature-flags/src/types.ts b/plugins/harness-fme-feature-flags/src/types.ts index 71aa2e2d..ff9b0e54 100644 --- a/plugins/harness-fme-feature-flags/src/types.ts +++ b/plugins/harness-fme-feature-flags/src/types.ts @@ -42,11 +42,23 @@ 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; name: string; email?: string; + identifier?: string; } export interface FlagSet { 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"