Skip to content

Commit

Permalink
Merge pull request #1578 from RoadieHQ/launchdarkly
Browse files Browse the repository at this point in the history
Adds Launchdarkly plugin
  • Loading branch information
punkle authored Aug 27, 2024
2 parents e60b1ec + 92d52e3 commit 53e45bf
Show file tree
Hide file tree
Showing 14 changed files with 416 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/smooth-bulldogs-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@roadiehq/backstage-plugin-launchdarkly': patch
---

Initial version of LaunchDarkly plugin.
5 changes: 5 additions & 0 deletions plugins/frontend/backstage-plugin-launchdarkly/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
rules: {
'notice/notice': 'off',
},
});
47 changes: 47 additions & 0 deletions plugins/frontend/backstage-plugin-launchdarkly/README.md
Original file line number Diff line number Diff line change
@@ -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:

```
<EntitySwitch>
<EntitySwitch.Case if={isLaunchdarklyAvailable}>
<EntityLaunchdarklyOverviewCard />
</EntitySwitch.Case>
</EntitySwitch>
```

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'
```
54 changes: 54 additions & 0 deletions plugins/frontend/backstage-plugin-launchdarkly/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
Original file line number Diff line number Diff line change
@@ -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<TableColumn> = [
{
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 (
<MissingAnnotationEmptyState
annotation={unsetAnnotations[0]}
readMoreUrl="https://github.com/RoadieHQ/roadie-backstage-plugins/blob/main/plugins/frontend/backstage-plugin-launchdarkly/README.md"
/>
);
}

if (loading) {
return <Progress />;
}

if (error && error.message) {
return <ErrorPanel error={error} />;
}

return (
<Table
title="Feature flags from LaunchDarkly"
columns={columns}
data={value}
options={{
paging: true,
search: false,
draggable: false,
padding: 'dense',
}}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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';
21 changes: 21 additions & 0 deletions plugins/frontend/backstage-plugin-launchdarkly/src/constants.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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;
});
};
20 changes: 20 additions & 0 deletions plugins/frontend/backstage-plugin-launchdarkly/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { launchdarklyPlugin } from './plugin';

describe('launchdarkly', () => {
it('should export plugin', () => {
expect(launchdarklyPlugin).toBeDefined();
});
});
59 changes: 59 additions & 0 deletions plugins/frontend/backstage-plugin-launchdarkly/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Loading

0 comments on commit 53e45bf

Please sign in to comment.