diff --git a/.changeset/smooth-bulldogs-shave.md b/.changeset/smooth-bulldogs-shave.md new file mode 100644 index 000000000..d7d65821c --- /dev/null +++ b/.changeset/smooth-bulldogs-shave.md @@ -0,0 +1,5 @@ +--- +'@roadiehq/backstage-plugin-launchdarkly': patch +--- + +Initial version of LaunchDarkly plugin. diff --git a/plugins/frontend/backstage-plugin-launchdarkly/.eslintrc.js b/plugins/frontend/backstage-plugin-launchdarkly/.eslintrc.js new file mode 100644 index 000000000..831909090 --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'notice/notice': 'off', + }, +}); diff --git a/plugins/frontend/backstage-plugin-launchdarkly/README.md b/plugins/frontend/backstage-plugin-launchdarkly/README.md new file mode 100644 index 000000000..2c9555d30 --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/README.md @@ -0,0 +1,47 @@ +# launchdarkly + +Welcome to the launchdarkly plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Add a proxy configuration for LaunchDarkly in the `app-config.yaml` file + +``` +proxy: + '/launchdarkly/api': + target: https://app.launchdarkly.com/api + headers: + Authorization: ${LAUNCHDARKLY_API_KEY} +``` + +In the `packages/app/src/components/catalog/EntityPage.tsx` under `overviewContent` add the following: + +``` + + + + + +``` + +Set the `LAUNCHDARKLY_API_KEY` environment variable and run the backstage backend. + +Create an entity with the following annotations and import it: + +``` +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: launchdarklytest + annotations: + launchdarkly.com/project-key: default + launchdarkly.com/environment-key: test + launchdarkly.com/context: "{ \"kind\": \"tenant\", \"key\": \"blah\", \"name\": \"blah\" }" +spec: + type: service + lifecycle: unknown + owner: 'group:engineering' +``` diff --git a/plugins/frontend/backstage-plugin-launchdarkly/package.json b/plugins/frontend/backstage-plugin-launchdarkly/package.json new file mode 100644 index 000000000..579d2849d --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/package.json @@ -0,0 +1,54 @@ +{ + "name": "@roadiehq/backstage-plugin-launchdarkly", + "version": "0.0.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-model": "^1.6.0", + "@backstage/core-components": "^0.14.4", + "@backstage/core-plugin-api": "^1.9.3", + "@backstage/plugin-catalog-react": "^1.12.3", + "@backstage/theme": "^0.5.6", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.60", + "lodash": "^4.17.21", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.27.0", + "@backstage/core-app-api": "^1.14.2", + "@backstage/dev-utils": "^1.0.37", + "@backstage/test-utils": "^1.5.10", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.0.0", + "msw": "^1.0.0", + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/frontend/backstage-plugin-launchdarkly/src/components/EntityLaunchdarklyOverviewCard/EntityLaunchdarklyOverviewCard.tsx b/plugins/frontend/backstage-plugin-launchdarkly/src/components/EntityLaunchdarklyOverviewCard/EntityLaunchdarklyOverviewCard.tsx new file mode 100644 index 000000000..2d7338a63 --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/src/components/EntityLaunchdarklyOverviewCard/EntityLaunchdarklyOverviewCard.tsx @@ -0,0 +1,93 @@ +/* + * Copyright 2024 Larder Software Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + useEntity, + MissingAnnotationEmptyState, +} from '@backstage/plugin-catalog-react'; +import { + LAUNCHDARKLY_PROJECT_KEY_ANNOTATION, + LAUNCHDARKLY_CONTEXT_PROPERTIES_ANNOTATION, + LAUNCHDARKLY_ENVIRONMENT_KEY_ANNOTATION, +} from '../../constants'; +import difference from 'lodash/difference'; +import { + ErrorPanel, + Progress, + Table, + TableColumn, +} from '@backstage/core-components'; +import { useLaunchdarklyFlags } from '../../hooks/useLaunchdarklyFlags'; + +type EntityLaunchdarklyOverviewCardProps = {}; + +const columns: Array = [ + { + title: 'Name', + field: 'name', + }, + { + title: 'Value', + field: '_value', + }, +]; + +export const EntityLaunchdarklyOverviewCard = ( + _: EntityLaunchdarklyOverviewCardProps, +) => { + const { entity } = useEntity(); + const unsetAnnotations = difference( + [ + LAUNCHDARKLY_PROJECT_KEY_ANNOTATION, + LAUNCHDARKLY_CONTEXT_PROPERTIES_ANNOTATION, + LAUNCHDARKLY_ENVIRONMENT_KEY_ANNOTATION, + ], + Object.keys(entity.metadata?.annotations || {}), + ); + + const { value, error, loading } = useLaunchdarklyFlags(entity); + + if (unsetAnnotations.length > 0) { + return ( + + ); + } + + if (loading) { + return ; + } + + if (error && error.message) { + return ; + } + + return ( + + ); +}; diff --git a/plugins/frontend/backstage-plugin-launchdarkly/src/components/EntityLaunchdarklyOverviewCard/index.ts b/plugins/frontend/backstage-plugin-launchdarkly/src/components/EntityLaunchdarklyOverviewCard/index.ts new file mode 100644 index 000000000..6764a4d8d --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/src/components/EntityLaunchdarklyOverviewCard/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 Larder Software Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityLaunchdarklyOverviewCard } from './EntityLaunchdarklyOverviewCard'; diff --git a/plugins/frontend/backstage-plugin-launchdarkly/src/constants.ts b/plugins/frontend/backstage-plugin-launchdarkly/src/constants.ts new file mode 100644 index 000000000..50ca9449c --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/src/constants.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2024 Larder Software Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export const LAUNCHDARKLY_PROJECT_KEY_ANNOTATION = + 'launchdarkly.com/project-key'; +export const LAUNCHDARKLY_ENVIRONMENT_KEY_ANNOTATION = + 'launchdarkly.com/environment-key'; +export const LAUNCHDARKLY_CONTEXT_PROPERTIES_ANNOTATION = + 'launchdarkly.com/context'; diff --git a/plugins/frontend/backstage-plugin-launchdarkly/src/hooks/useLaunchdarklyFlags.ts b/plugins/frontend/backstage-plugin-launchdarkly/src/hooks/useLaunchdarklyFlags.ts new file mode 100644 index 000000000..114ad27df --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/src/hooks/useLaunchdarklyFlags.ts @@ -0,0 +1,46 @@ +import { Entity } from '@backstage/catalog-model'; +import { useAsync } from 'react-use'; +import { + LAUNCHDARKLY_CONTEXT_PROPERTIES_ANNOTATION, + LAUNCHDARKLY_ENVIRONMENT_KEY_ANNOTATION, + LAUNCHDARKLY_PROJECT_KEY_ANNOTATION, +} from '../constants'; +import { discoveryApiRef, useApi } from '@backstage/core-plugin-api'; + +export const useLaunchdarklyFlags = (entity: Entity) => { + const discovery = useApi(discoveryApiRef); + + return useAsync(async () => { + const projectKey = + entity.metadata.annotations?.[LAUNCHDARKLY_PROJECT_KEY_ANNOTATION] || + 'default'; + const environmentKey = + entity.metadata.annotations?.[LAUNCHDARKLY_ENVIRONMENT_KEY_ANNOTATION] || + 'production'; + const cntxt = + entity.metadata.annotations?.[LAUNCHDARKLY_CONTEXT_PROPERTIES_ANNOTATION]; + + if (projectKey && environmentKey && cntxt) { + const url = `${await discovery.getBaseUrl('proxy')}/launchdarkly/api`; + const response = await fetch( + `${url}/v2/projects/${projectKey}/environments/${environmentKey}/flags/evaluate`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: cntxt, + }, + ); + + if (!response.ok) { + throw new Error( + `Failed to retrieve launchdarkly environment ${environmentKey}: ${response.statusText}`, + ); + } + + return (await response.json()).items; + } + return undefined; + }); +}; diff --git a/plugins/frontend/backstage-plugin-launchdarkly/src/index.ts b/plugins/frontend/backstage-plugin-launchdarkly/src/index.ts new file mode 100644 index 000000000..872ff7bcb --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/src/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 Larder Software Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + launchdarklyPlugin, + EntityLaunchdarklyOverviewCard, + isLaunchdarklyAvailable, +} from './plugin'; diff --git a/plugins/frontend/backstage-plugin-launchdarkly/src/plugin.test.ts b/plugins/frontend/backstage-plugin-launchdarkly/src/plugin.test.ts new file mode 100644 index 000000000..9b1f8ba2a --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/src/plugin.test.ts @@ -0,0 +1,7 @@ +import { launchdarklyPlugin } from './plugin'; + +describe('launchdarkly', () => { + it('should export plugin', () => { + expect(launchdarklyPlugin).toBeDefined(); + }); +}); diff --git a/plugins/frontend/backstage-plugin-launchdarkly/src/plugin.ts b/plugins/frontend/backstage-plugin-launchdarkly/src/plugin.ts new file mode 100644 index 000000000..1aab0acaf --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/src/plugin.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2024 Larder Software Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createPlugin, + createComponentExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; +import { Entity } from '@backstage/catalog-model'; +import difference from 'lodash/difference'; +import { + LAUNCHDARKLY_CONTEXT_PROPERTIES_ANNOTATION, + LAUNCHDARKLY_ENVIRONMENT_KEY_ANNOTATION, + LAUNCHDARKLY_PROJECT_KEY_ANNOTATION, +} from './constants'; + +export const launchdarklyPlugin = createPlugin({ + id: 'launchdarkly', + routes: { + root: rootRouteRef, + }, +}); + +export const EntityLaunchdarklyOverviewCard = launchdarklyPlugin.provide( + createComponentExtension({ + name: 'EntityLaunchdarklyOverviewCard', + component: { + lazy: () => + import('./components/EntityLaunchdarklyOverviewCard').then( + m => m.EntityLaunchdarklyOverviewCard, + ), + }, + }), +); + +export const isLaunchdarklyAvailable = (entity: Entity) => { + const diff = difference( + [ + LAUNCHDARKLY_PROJECT_KEY_ANNOTATION, + LAUNCHDARKLY_CONTEXT_PROPERTIES_ANNOTATION, + LAUNCHDARKLY_ENVIRONMENT_KEY_ANNOTATION, + ], + Object.keys(entity.metadata?.annotations || {}), + ); + return diff.length === 0; +}; diff --git a/plugins/frontend/backstage-plugin-launchdarkly/src/routes.ts b/plugins/frontend/backstage-plugin-launchdarkly/src/routes.ts new file mode 100644 index 000000000..41eb58cb8 --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 Larder Software Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'launchdarkly', +}); diff --git a/plugins/frontend/backstage-plugin-launchdarkly/src/setupTests.ts b/plugins/frontend/backstage-plugin-launchdarkly/src/setupTests.ts new file mode 100644 index 000000000..608886c15 --- /dev/null +++ b/plugins/frontend/backstage-plugin-launchdarkly/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 Larder Software Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; diff --git a/yarn.lock b/yarn.lock index e648b846d..687997bc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28760,6 +28760,13 @@ react-window@^1.8.10, react-window@^1.8.6: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" +"react@^16.13.1 || ^17.0.0 || ^18.0.0": + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + react@^18.0.2: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"