Merge pull request #16353 from rr-wfm/plugin-octopus-deploy

Plugin Octopus Deploy
This commit is contained in:
Fredrik Adelöw
2023-02-21 15:20:31 +01:00
committed by GitHub
20 changed files with 836 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-octopus-deploy': minor
---
Initial version
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+61
View File
@@ -0,0 +1,61 @@
# Octopus Deploy Plugin
Welcome to the octopus-deploy plugin!
## Features
- Display the deployment status of the most recent releases for a project in Octopus Deploy straight from the Backstage catalog
## Getting started
This plugin (currently) uses the Backstage proxy to securely communicate with the Octopus Deploy API.
To use it, you will need to generate an [API Key](https://octopus.com/docs/octopus-rest-api/how-to-create-an-api-key) within Octopus Deploy.
1. Add the following to your app-config.yaml to enable the proxy:
```
// app-config.yaml
proxy:
'/octopus-deploy':
target: '<your-octopus-server-url>'
headers:
X-Octopus-ApiKey: ${OCTOPUS_API_KEY}
```
2. Add the following to `EntityPage.tsx` to display Octopus Releases
```
// In packages/app/src/components/catalog/EntityPage.tsx
import {
isOctopusDeployAvailable
EntityOctopusDeployContent
} from '@backstage/plugin-octopus-deploy';
const cicdContent = (
<EntitySwitch>
{/* other components... */}
<EntitySwitch.Case if={isOctopusDeployAvailable}>
<EntityOctopusDeployContent defaultLimit={25} />
</EntitySwitch.Case>
</EntitySwitch>
)
```
3. Add `octopus.com/project-id` annotation in catalog descriptor file
To obtain a projects ID you will have to query the Octopus API. In the future we'll add support for using a projects slug as well.
```
// catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
# ...
annotations:
octopus.com/project-id: Projects-102
spec:
type: service
```
All set , you will be able to see the plugin in action!
+86
View File
@@ -0,0 +1,86 @@
## API Report File for "@backstage/plugin-octopus-deploy"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { FetchApi } from '@backstage/core-plugin-api';
// @public (undocumented)
export const EntityOctopusDeployContent: (props: {
defaultLimit?: number | undefined;
}) => JSX.Element;
// @public (undocumented)
export const isOctopusDeployAvailable: (entity: Entity) => boolean;
// @public (undocumented)
export const OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION = 'octopus.com/project-id';
// @public (undocumented)
export interface OctopusDeployApi {
// (undocumented)
getReleaseProgression(
projectId: string,
releaseHistoryCount: number,
): Promise<OctopusProgression>;
}
// @public (undocumented)
export const octopusDeployApiRef: ApiRef<OctopusDeployApi>;
// @public (undocumented)
export class OctopusDeployClient implements OctopusDeployApi {
constructor(options: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
proxyPathBase?: string;
});
// (undocumented)
getReleaseProgression(
projectId: string,
releaseHistoryCount: number,
): Promise<OctopusProgression>;
}
// @public (undocumented)
export type OctopusDeployment = {
State: string;
};
// @public (undocumented)
export const octopusDeployPlugin: BackstagePlugin<{}, {}, {}>;
// @public (undocumented)
export type OctopusEnvironment = {
Id: string;
Name: string;
};
// @public (undocumented)
export type OctopusProgression = {
Environments: OctopusEnvironment[];
Releases: OctopusReleaseProgression[];
};
// @public (undocumented)
export type OctopusRelease = {
Id: string;
Version: string;
};
// @public (undocumented)
export type OctopusReleaseProgression = {
Release: OctopusRelease;
Deployments: {
[key: string]: OctopusDeployment[];
};
};
// (No @packageDocumentation comment for this package)
```
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { createDevApp } from '@backstage/dev-utils';
import { octopusDeployPlugin } from '../src/plugin';
import { EntityPageOctopusDeploy } from '../src/components/EntityPageOctopusDeploy';
createDevApp()
.registerPlugin(octopusDeployPlugin)
.addPage({
element: <EntityPageOctopusDeploy />,
title: 'Root Page',
path: '/octopus-deploy',
})
.render();
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@backstage/plugin-octopus-deploy",
"version": "0.0.0",
"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"
},
"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": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^0.49.0"
},
"files": [
"dist"
]
}
+118
View File
@@ -0,0 +1,118 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 {
createApiRef,
DiscoveryApi,
FetchApi,
} from '@backstage/core-plugin-api';
/** @public */
export type OctopusProgression = {
Environments: OctopusEnvironment[];
Releases: OctopusReleaseProgression[];
};
/** @public */
export type OctopusEnvironment = {
Id: string;
Name: string;
};
/** @public */
export type OctopusReleaseProgression = {
Release: OctopusRelease;
Deployments: { [key: string]: OctopusDeployment[] };
};
/** @public */
export type OctopusRelease = {
Id: string;
Version: string;
};
/** @public */
export type OctopusDeployment = {
State: string;
};
/** @public */
export const octopusDeployApiRef = createApiRef<OctopusDeployApi>({
id: 'plugin.octopusdeploy.service',
});
const DEFAULT_PROXY_PATH_BASE = '/octopus-deploy';
/** @public */
export interface OctopusDeployApi {
getReleaseProgression(
projectId: string,
releaseHistoryCount: number,
): Promise<OctopusProgression>;
}
/** @public */
export class OctopusDeployClient implements OctopusDeployApi {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
private readonly proxyPathBase: string;
constructor(options: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
proxyPathBase?: string;
}) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi;
this.proxyPathBase = options.proxyPathBase ?? DEFAULT_PROXY_PATH_BASE;
}
async getReleaseProgression(
projectId: string,
releaseHistoryCount: number,
): Promise<OctopusProgression> {
const url = await this.getApiUrl(projectId, releaseHistoryCount);
const response = await this.fetchApi.fetch(url);
let responseJson;
try {
responseJson = await response.json();
} catch (e) {
responseJson = { releases: [] };
}
if (response.status !== 200) {
throw new Error(
`Error communicating with Octopus Deploy: ${
responseJson?.error?.title || response.statusText
}`,
);
}
return responseJson;
}
private async getApiUrl(projectId: string, releaseHistoryCount: number) {
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');
const queryParameters = new URLSearchParams({
releaseHistoryCount: releaseHistoryCount.toString(),
});
return `${proxyUrl}${this.proxyPathBase}/projects/${encodeURIComponent(
projectId,
)}/progression?${queryParameters}`;
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { useEntity } from '@backstage/plugin-catalog-react';
import { useReleases } from '../../hooks/useReleases';
import { getAnnotationFromEntity } from '../../utils/getAnnotationFromEntity';
import React from 'react';
import { ReleaseTable } from '../ReleaseTable';
export const EntityPageOctopusDeploy = (props: { defaultLimit?: number }) => {
const { entity } = useEntity();
const projectId = getAnnotationFromEntity(entity);
const { environments, releases, loading, error } = useReleases(
projectId,
props.defaultLimit ?? 3,
);
return (
<ReleaseTable
environments={environments}
releases={releases}
loading={loading}
error={error}
/>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { EntityPageOctopusDeploy } from './EntityPageOctopusDeploy';
@@ -0,0 +1,150 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { Box, Typography } from '@material-ui/core';
import { OctopusEnvironment, OctopusReleaseProgression } from '../../api';
import React from 'react';
import {
ResponseErrorPanel,
StatusAborted,
StatusError,
StatusOK,
StatusPending,
StatusRunning,
StatusWarning,
Table,
TableColumn,
} from '@backstage/core-components';
type ReleaseTableProps = {
environments?: OctopusEnvironment[];
releases?: OctopusReleaseProgression[];
loading: boolean;
error?: Error;
};
export const getDeploymentStatusComponent = (state: string | undefined) => {
switch (state) {
case 'Success':
return (
<Typography component="span">
<StatusOK /> Success
</Typography>
);
case 'Queued':
return (
<Typography component="span">
<StatusPending /> Queued
</Typography>
);
case 'Executing':
return (
<Typography component="span">
<StatusRunning /> Executing
</Typography>
);
case 'Failed':
return (
<Typography component="span">
<StatusError /> Failed
</Typography>
);
case 'Cancelling':
return (
<Typography component="span">
<StatusPending /> Cancelling
</Typography>
);
case 'Canceled':
return (
<Typography component="span">
<StatusAborted /> Canceled
</Typography>
);
case 'TimedOut':
return (
<Typography component="span">
<StatusWarning /> Timed Out
</Typography>
);
default:
return (
<Typography component="span">
<StatusWarning /> Unknown
</Typography>
);
}
};
export const ReleaseTable = ({
environments,
releases,
loading,
error,
}: ReleaseTableProps) => {
if (error) {
return <ResponseErrorPanel error={error} />;
}
const columns: TableColumn[] = [
{
title: 'Version',
field: 'Release.Version',
highlight: false,
width: 'auto',
},
...(environments?.map(env => ({
title: env.Name,
width: 'auto',
render: (row: Partial<OctopusReleaseProgression>) => {
const deploymentsForEnvironment = row.Deployments
? row.Deployments[env.Id]
: null;
if (deploymentsForEnvironment) {
return (
<Box display="flex" alignItems="center">
<Typography variant="button">
{getDeploymentStatusComponent(
deploymentsForEnvironment[0].State,
)}
</Typography>
</Box>
);
}
return <Box display="flex" alignItems="center" />;
},
})) ?? []),
];
return (
<Table
isLoading={loading}
columns={columns}
options={{
search: true,
paging: true,
pageSize: 5,
showEmptyDataSourceMessage: !loading,
}}
title={
<Box display="flex" alignItems="center">
Octopus Deploy - Releases ({releases ? releases.length : 0})
</Box>
}
data={releases ?? []}
/>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { ReleaseTable } from './ReleaseTable';
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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.
*/
/** @public */
export const OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION = 'octopus.com/project-id';
@@ -0,0 +1,45 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import {
octopusDeployApiRef,
OctopusEnvironment,
OctopusReleaseProgression,
} from '../api';
export function useReleases(
projectId: string,
releaseHistoryCount: number,
): {
environments?: OctopusEnvironment[];
releases?: OctopusReleaseProgression[];
loading: boolean;
error?: Error;
} {
const api = useApi(octopusDeployApiRef);
const { value, loading, error } = useAsync(() => {
return api.getReleaseProgression(projectId, releaseHistoryCount);
}, [api, projectId, releaseHistoryCount]);
return {
environments: value?.Environments,
releases: value?.Releases,
loading,
error,
};
}
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 {
octopusDeployPlugin,
EntityOctopusDeployContent,
isOctopusDeployAvailable,
} from './plugin';
export * from './api';
export { OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION } from './constants';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { octopusDeployPlugin } from './plugin';
describe('octopus-deploy', () => {
it('should export plugin', () => {
expect(octopusDeployPlugin).toBeDefined();
});
});
+58
View File
@@ -0,0 +1,58 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION } from './constants';
import { octopusDeployEntityContentRouteRef } from './routes';
import { OctopusDeployClient, octopusDeployApiRef } from './api';
import {
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
/** @public */
export const isOctopusDeployAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION]);
/** @public */
export const octopusDeployPlugin = createPlugin({
id: 'octopus-deploy',
apis: [
createApiFactory({
api: octopusDeployApiRef,
deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
factory: ({ discoveryApi, fetchApi }) =>
new OctopusDeployClient({ discoveryApi, fetchApi }),
}),
],
});
/** @public */
export const EntityOctopusDeployContent = octopusDeployPlugin.provide(
createRoutableExtension({
name: 'EntityOctopusDeployContent',
component: () =>
import('./components/EntityPageOctopusDeploy').then(
m => m.EntityPageOctopusDeploy,
),
mountPoint: octopusDeployEntityContentRouteRef,
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 octopusDeployEntityContentRouteRef = createRouteRef({
id: 'octopus-deploy-entity-content',
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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';
import 'cross-fetch/polyfill';
@@ -0,0 +1,30 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION } from '../constants';
import { Entity } from '@backstage/catalog-model';
export function getAnnotationFromEntity(entity: Entity): string {
const annotation =
entity.metadata.annotations?.[OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION];
if (!annotation) {
throw new Error(
`Value for annotation ${OCTOPUS_DEPLOY_PROJECT_ID_ANNOTATION} was not found`,
);
}
return annotation;
}
+28
View File
@@ -6953,6 +6953,34 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-octopus-deploy@workspace:plugins/octopus-deploy":
version: 0.0.0-use.local
resolution: "@backstage/plugin-octopus-deploy@workspace:plugins/octopus-deploy"
dependencies:
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/plugin-catalog-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.9.13
"@material-ui/icons": ^4.9.1
"@material-ui/lab": 4.0.0-alpha.57
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@testing-library/user-event": ^14.0.0
"@types/node": "*"
cross-fetch: ^3.1.5
msw: ^0.49.0
react-use: ^17.2.4
peerDependencies:
react: ^16.13.1 || ^17.0.0
languageName: unknown
linkType: soft
"@backstage/plugin-org-react@workspace:plugins/org-react":
version: 0.0.0-use.local
resolution: "@backstage/plugin-org-react@workspace:plugins/org-react"