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"