introduce @backstage/integration-react and its scmIntegrationsApiRef

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2021-03-09 11:50:06 +01:00
parent b2b670ad66
commit 3385b374bd
36 changed files with 476 additions and 119 deletions
+30
View File
@@ -0,0 +1,30 @@
---
'@backstage/create-app': patch
---
Supply a `scmIntegrationsApiRef` from the new `@backstage/integration-react`.
This is a new facility that plugins will start to use. You will have to add it to your local `packages/app` as described below. If this is not done, runtime errors will be seen in the frontend, on the form `No API factory available for dependency apiRef{integration.scmintegrations}`.
In `packages/app/package.json`:
```diff
"dependencies": {
+ "@backstage/integration-react": "^0.1.1",
```
In `packages/app/src/apis.ts`:
```diff
+import {
+ scmIntegrationsApiRef,
+ ScmIntegrationsApi,
+} from '@backstage/integration-react';
export const apis: AnyApiFactory[] = [
+ createApiFactory({
+ api: scmIntegrationsApiRef,
+ deps: { configApi: configApiRef },
+ factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
+ }),
```
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-scaffolder': patch
---
Use `scmIntegrationsApiRef` from the new `@backstage/integration-react`.
+1 -1
View File
@@ -23,7 +23,7 @@ describe('Catalog', () => {
cy.visit('/catalog');
cy.contains('Owned (7)').should('be.visible');
cy.contains('Owned (8)').should('be.visible');
});
});
});
+2 -1
View File
@@ -7,6 +7,7 @@
"@backstage/catalog-model": "^0.7.4",
"@backstage/cli": "^0.6.4",
"@backstage/core": "^0.7.1",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-api-docs": "^0.4.8",
"@backstage/plugin-catalog": "^0.4.2",
"@backstage/plugin-catalog-import": "^0.4.3",
@@ -29,8 +30,8 @@
"@backstage/plugin-register-component": "^0.2.12",
"@backstage/plugin-rollbar": "^0.3.3",
"@backstage/plugin-scaffolder": "^0.7.1",
"@backstage/plugin-sentry": "^0.3.8",
"@backstage/plugin-search": "^0.3.3",
"@backstage/plugin-sentry": "^0.3.8",
"@backstage/plugin-tech-radar": "^0.3.7",
"@backstage/plugin-techdocs": "^0.6.1",
"@backstage/plugin-user-settings": "^0.2.7",
+17 -7
View File
@@ -15,22 +15,32 @@
*/
import {
AnyApiFactory,
configApiRef,
createApiFactory,
errorApiRef,
githubAuthApiRef,
createApiFactory,
} from '@backstage/core';
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import {
costInsightsApiRef,
ExampleCostInsightsClient,
} from '@backstage/plugin-cost-insights';
import {
graphQlBrowseApiRef,
GraphQLEndpoints,
} from '@backstage/plugin-graphiql';
import {
costInsightsApiRef,
ExampleCostInsightsClient,
} from '@backstage/plugin-cost-insights';
export const apis: AnyApiFactory[] = [
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
export const apis = [
createApiFactory({
api: graphQlBrowseApiRef,
deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef },
+1
View File
@@ -69,6 +69,7 @@
"@backstage/plugin-techdocs": "*",
"@backstage/plugin-techdocs-backend": "*",
"@backstage/plugin-user-settings": "*",
"@backstage/integration-react": "*",
"@backstage/test-utils": "*",
"@backstage/theme": "*"
},
+2
View File
@@ -37,6 +37,7 @@ import { version as cli } from '../../../cli/package.json';
import { version as config } from '../../../config/package.json';
import { version as core } from '../../../core/package.json';
import { version as errors } from '../../../errors/package.json';
import { version as integrationReact } from '../../../integration-react/package.json';
import { version as testUtils } from '../../../test-utils/package.json';
import { version as theme } from '../../../theme/package.json';
@@ -69,6 +70,7 @@ export const packageVersions = {
'@backstage/config': config,
'@backstage/core': core,
'@backstage/errors': errors,
'@backstage/integration-react': integrationReact,
'@backstage/plugin-api-docs': pluginApiDocs,
'@backstage/plugin-app-backend': pluginAppBackend,
'@backstage/plugin-auth-backend': pluginAuthBackend,
@@ -19,6 +19,7 @@
"@backstage/plugin-github-actions": "^{{version '@backstage/plugin-github-actions'}}",
"@backstage/plugin-user-settings": "^{{version '@backstage/plugin-user-settings'}}",
"@backstage/plugin-search": "^{{version '@backstage/plugin-search'}}",
"@backstage/integration-react": "^{{version '@backstage/integration-react'}}",
"@backstage/test-utils": "^{{version '@backstage/test-utils'}}",
"@backstage/theme": "^{{version '@backstage/theme'}}",
"history": "^5.0.0",
@@ -1 +1,14 @@
export const apis = [];
import {
AnyApiFactory, configApiRef, createApiFactory
} from '@backstage/core';
import {
ScmIntegrationsApi, scmIntegrationsApiRef
} from '@backstage/integration-react';
export const apis: AnyApiFactory[] = [
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
];
+1
View File
@@ -31,6 +31,7 @@
"dependencies": {
"@backstage/core": "^0.7.0",
"@backstage/catalog-model": "^0.7.3",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.1",
"@backstage/test-utils": "^0.1.8",
"@backstage/theme": "^0.2.3",
+17
View File
@@ -19,6 +19,8 @@ import {
AnyApiFactory,
ApiFactory,
attachComponentData,
configApiRef,
createApiFactory,
createApp,
createPlugin,
createRouteRef,
@@ -31,6 +33,10 @@ import {
SidebarPage,
SidebarSpacer,
} from '@backstage/core';
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import { Box } from '@material-ui/core';
import BookmarkIcon from '@material-ui/icons/Bookmark';
import SentimentDissatisfiedIcon from '@material-ui/icons/SentimentDissatisfied';
@@ -136,6 +142,17 @@ class DevAppBuilder {
const DummyPage = () => <Box p={3}>Page belonging to another plugin.</Box>;
attachComponentData(DummyPage, 'core.mountPoint', dummyRouteRef);
const apis = [...this.apis];
if (!apis.some(api => api.api.id === scmIntegrationsApiRef.id)) {
apis.push(
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
);
}
const app = createApp({
apis: this.apis,
plugins: this.plugins,
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+5
View File
@@ -0,0 +1,5 @@
# Integrations React-specific functionality
Exposes a frontend API for interacting with configured integrations (as added
under the `integrations` root config key). Most of the actual code is in the
`@backstage/integration` package.
@@ -0,0 +1,64 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { Content, useApi } from '@backstage/core';
import { ScmIntegration, ScmIntegrationsGroup } from '@backstage/integration';
import { Typography } from '@material-ui/core';
import React from 'react';
import { scmIntegrationsApiRef } from '../src/ScmIntegrationsApi';
const Integrations = (props: {
group: ScmIntegrationsGroup<ScmIntegration>;
}) => {
const integrations = props.group.list();
if (!integrations) {
return (
<Typography color="textSecondary">
No integrations of this type
</Typography>
);
}
return (
<Typography variant="caption">
<pre>{JSON.stringify(integrations, undefined, 2)}</pre>
</Typography>
);
};
export const DevPage = () => {
const integrations = useApi(scmIntegrationsApiRef);
return (
<Content>
<Typography paragraph variant="h2">
Azure
</Typography>
<Integrations group={integrations.azure} />
<Typography paragraph variant="h2">
Bitbucket
</Typography>
<Integrations group={integrations.bitbucket} />
<Typography paragraph variant="h2">
GitHub
</Typography>
<Integrations group={integrations.github} />
<Typography paragraph variant="h2">
GitLab
</Typography>
<Integrations group={integrations.gitlab} />
</Content>
);
};
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { configApiRef, createApiFactory } from '@backstage/core';
import { createDevApp } from '@backstage/dev-utils';
import { ScmIntegrations } from '@backstage/integration';
import React from 'react';
import { scmIntegrationsApiRef } from '../src/ScmIntegrationsApi';
import { DevPage } from './DevPage';
createDevApp()
.registerApi(
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrations.fromConfig(configApi),
}),
)
.addPage({
element: <DevPage />,
title: 'Root Page',
})
.render();
+49
View File
@@ -0,0 +1,49 @@
{
"name": "@backstage/integration-react",
"version": "0.1.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/config": "^0.1.2",
"@backstage/core": "^0.7.0",
"@backstage/integration": "^0.5.0",
"@backstage/theme": "^0.2.3",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.6.3",
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.8",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.0.7",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"msw": "^0.21.2",
"cross-fetch": "^3.0.6"
},
"files": [
"dist"
]
}
@@ -0,0 +1,31 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { ConfigReader } from '@backstage/config';
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from './ScmIntegrationsApi';
describe('scmIntegrationsApiRef', () => {
it('should export api', () => {
expect(scmIntegrationsApiRef).toBeDefined();
});
it('should be instantiated', () => {
const i = ScmIntegrationsApi.fromConfig(new ConfigReader({}));
expect(i.list().length).toBe(4); // The default ones
});
});
@@ -0,0 +1,30 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { Config } from '@backstage/config';
import { ApiRef, createApiRef } from '@backstage/core';
import { ScmIntegrations } from '@backstage/integration';
export class ScmIntegrationsApi extends ScmIntegrations {
static fromConfig(config: Config): ScmIntegrationsApi {
return ScmIntegrations.fromConfig(config);
}
}
export const scmIntegrationsApiRef: ApiRef<ScmIntegrationsApi> = createApiRef({
id: 'integration.scmintegrations',
description: 'All of the registered SCM integrations of your config',
});
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2021 Spotify AB
*
* 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 {
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from './ScmIntegrationsApi';
@@ -0,0 +1,18 @@
/*
* Copyright 2021 Spotify AB
*
* 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';
+5 -1
View File
@@ -20,4 +20,8 @@ export * from './github';
export * from './gitlab';
export { defaultScmResolveUrl } from './helpers';
export { ScmIntegrations } from './ScmIntegrations';
export type { ScmIntegration, ScmIntegrationRegistry } from './types';
export type {
ScmIntegration,
ScmIntegrationRegistry,
ScmIntegrationsGroup,
} from './types';
+1
View File
@@ -34,6 +34,7 @@
"@backstage/catalog-client": "^0.3.6",
"@backstage/core": "^0.7.1",
"@backstage/integration": "^0.5.0",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.1",
"@backstage/theme": "^0.2.4",
"@material-ui/core": "^4.11.0",
@@ -52,7 +52,10 @@ jest.mock('./GitHub', () => ({
}));
import { ConfigReader, OAuthApi, UrlPatternDiscovery } from '@backstage/core';
import { GitHubIntegrationConfig } from '@backstage/integration';
import {
GitHubIntegrationConfig,
ScmIntegrations,
} from '@backstage/integration';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { msw } from '@backstage/test-utils';
import { Octokit } from '@octokit/rest';
@@ -87,7 +90,7 @@ describe('CatalogImportClient', () => {
},
};
const configApi = new ConfigReader({});
const scmIntegrationsApi = ScmIntegrations.fromConfig(new ConfigReader({}));
const catalogApi: jest.Mocked<typeof catalogApiRef.T> = {
getEntities: jest.fn(),
@@ -105,7 +108,7 @@ describe('CatalogImportClient', () => {
catalogImportClient = new CatalogImportClient({
discoveryApi,
githubAuthApi,
configApi,
scmIntegrationsApi,
identityApi,
catalogApi,
});
@@ -16,13 +16,11 @@
import { CatalogApi } from '@backstage/catalog-client';
import { EntityName } from '@backstage/catalog-model';
import { DiscoveryApi, IdentityApi, OAuthApi } from '@backstage/core';
import {
ConfigApi,
DiscoveryApi,
IdentityApi,
OAuthApi,
} from '@backstage/core';
import { GitHubIntegrationConfig } from '@backstage/integration';
GitHubIntegrationConfig,
ScmIntegrationRegistry,
} from '@backstage/integration';
import { Octokit } from '@octokit/rest';
import { PartialEntity } from '../types';
import { AnalyzeResult, CatalogImportApi } from './CatalogImportApi';
@@ -32,20 +30,20 @@ export class CatalogImportClient implements CatalogImportApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly githubAuthApi: OAuthApi;
private readonly configApi: ConfigApi;
private readonly scmIntegrationsApi: ScmIntegrationRegistry;
private readonly catalogApi: CatalogApi;
constructor(options: {
discoveryApi: DiscoveryApi;
githubAuthApi: OAuthApi;
identityApi: IdentityApi;
configApi: ConfigApi;
scmIntegrationsApi: ScmIntegrationRegistry;
catalogApi: CatalogApi;
}) {
this.discoveryApi = options.discoveryApi;
this.githubAuthApi = options.githubAuthApi;
this.identityApi = options.identityApi;
this.configApi = options.configApi;
this.scmIntegrationsApi = options.scmIntegrationsApi;
this.catalogApi = options.catalogApi;
}
@@ -72,7 +70,7 @@ export class CatalogImportClient implements CatalogImportApi {
};
}
const ghConfig = getGithubIntegrationConfig(this.configApi, url);
const ghConfig = getGithubIntegrationConfig(this.scmIntegrationsApi, url);
if (!ghConfig) {
throw new Error(
'This URL was not recognized as a valid GitHub URL because there was no configured integration that matched the given host name. You could try to paste the full URL to a catalog-info.yaml file instead.',
@@ -113,7 +111,10 @@ export class CatalogImportClient implements CatalogImportApi {
title: string;
body: string;
}): Promise<{ link: string; location: string }> {
const ghConfig = getGithubIntegrationConfig(this.configApi, repositoryUrl);
const ghConfig = getGithubIntegrationConfig(
this.scmIntegrationsApi,
repositoryUrl,
);
if (ghConfig) {
return await this.submitGitHubPrToRepo({
+6 -10
View File
@@ -14,26 +14,22 @@
* limitations under the License.
*/
import { ConfigApi } from '@backstage/core';
import { ScmIntegrations } from '@backstage/integration';
import { ScmIntegrationRegistry } from '@backstage/integration';
import parseGitUrl from 'git-url-parse';
export const getGithubIntegrationConfig = (
config: ConfigApi,
scmIntegrationsApi: ScmIntegrationRegistry,
location: string,
) => {
const { name: repo, owner } = parseGitUrl(location);
const scmIntegrations = ScmIntegrations.fromConfig(config);
const githubIntegrationConfig = scmIntegrations.github.byUrl(location);
if (!githubIntegrationConfig) {
const integration = scmIntegrationsApi.github.byUrl(location);
if (!integration) {
return undefined;
}
const { name: repo, owner } = parseGitUrl(location);
return {
repo,
owner,
githubIntegrationConfig: githubIntegrationConfig.config,
githubIntegrationConfig: integration.config,
};
};
@@ -60,7 +60,7 @@ describe('<ImportComponentPage />', () => {
getAccessToken: async () => 'token',
},
identityApi,
configApi: {} as any,
scmIntegrationsApi: {} as any,
catalogApi: {} as any,
}),
);
+5 -5
View File
@@ -15,15 +15,15 @@
*/
import {
identityApiRef,
configApiRef,
createApiFactory,
createPlugin,
createRoutableExtension,
createRouteRef,
discoveryApiRef,
githubAuthApiRef,
identityApiRef,
} from '@backstage/core';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { catalogImportApiRef, CatalogImportClient } from './api';
@@ -41,20 +41,20 @@ export const catalogImportPlugin = createPlugin({
discoveryApi: discoveryApiRef,
githubAuthApi: githubAuthApiRef,
identityApi: identityApiRef,
configApi: configApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
catalogApi: catalogApiRef,
},
factory: ({
discoveryApi,
githubAuthApi,
identityApi,
configApi,
scmIntegrationsApi,
catalogApi,
}) =>
new CatalogImportClient({
discoveryApi,
githubAuthApi,
configApi,
scmIntegrationsApi,
identityApi,
catalogApi,
}),
+1
View File
@@ -34,6 +34,7 @@
"@backstage/catalog-model": "^0.7.4",
"@backstage/core": "^0.7.1",
"@backstage/integration": "^0.5.1",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.1",
"@backstage/theme": "^0.2.4",
"@material-ui/core": "^4.11.0",
@@ -15,12 +15,11 @@
*/
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry, ConfigReader } from '@backstage/core';
import {
ApiProvider,
ApiRegistry,
configApiRef,
ConfigReader,
} from '@backstage/core';
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { act, fireEvent } from '@testing-library/react';
@@ -34,7 +33,7 @@ describe('<AboutCard />', () => {
kind: 'Component',
metadata: {
name: 'software',
description: 'This is the decription',
description: 'This is the description',
},
spec: {
owner: 'guest',
@@ -53,10 +52,12 @@ describe('<AboutCard />', () => {
],
};
const apis = ApiRegistry.with(
configApiRef,
new ConfigReader({
integrations: {},
}),
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
),
);
const { getByText } = await renderInTestApp(
@@ -70,7 +71,7 @@ describe('<AboutCard />', () => {
expect(getByText('service')).toBeInTheDocument();
expect(getByText('user:guest')).toBeInTheDocument();
expect(getByText('production')).toBeInTheDocument();
expect(getByText('This is the decription')).toBeInTheDocument();
expect(getByText('This is the description')).toBeInTheDocument();
});
it('renders "view source" link', async () => {
@@ -91,17 +92,19 @@ describe('<AboutCard />', () => {
},
};
const apis = ApiRegistry.with(
configApiRef,
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
);
const { getByText } = await renderInTestApp(
@@ -135,17 +138,19 @@ describe('<AboutCard />', () => {
},
};
const apis = ApiRegistry.with(
configApiRef,
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
);
const { getByTitle } = await renderInTestApp(
@@ -180,7 +185,10 @@ describe('<AboutCard />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(configApiRef, new ConfigReader({}));
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
);
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
@@ -21,11 +21,11 @@ import {
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import {
configApiRef,
HeaderIconLinkRow,
IconLinkVerticalProps,
useApi,
} from '@backstage/core';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { getEntityRelations, useEntity } from '@backstage/plugin-catalog-react';
import {
Card,
@@ -64,8 +64,11 @@ type AboutCardProps = {
export function AboutCard({ variant }: AboutCardProps) {
const classes = useStyles();
const { entity } = useEntity();
const configApi = useApi(configApiRef);
const entitySourceLocation = getEntitySourceLocation(entity, configApi);
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
const entitySourceLocation = getEntitySourceLocation(
entity,
scmIntegrationsApi,
);
const entityMetadataEditUrl = getEntityMetadataEditUrl(entity);
const providesApiRelations = getEntityRelations(
entity,
@@ -19,8 +19,7 @@ import {
parseLocationReference,
SOURCE_LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import { ConfigApi } from '@backstage/core';
import { ScmIntegrations } from '@backstage/integration';
import { ScmIntegrationRegistry } from '@backstage/integration';
export type EntitySourceLocation = {
locationTargetUrl: string;
@@ -29,7 +28,7 @@ export type EntitySourceLocation = {
export function getEntitySourceLocation(
entity: Entity,
config: ConfigApi,
scmIntegrationsApi: ScmIntegrationRegistry,
): EntitySourceLocation | undefined {
const sourceLocation =
entity.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION];
@@ -40,9 +39,7 @@ export function getEntitySourceLocation(
try {
const sourceLocationRef = parseLocationReference(sourceLocation);
const scmIntegrations = ScmIntegrations.fromConfig(config);
const integration = scmIntegrations.byUrl(sourceLocationRef.target);
const integration = scmIntegrationsApi.byUrl(sourceLocationRef.target);
return {
locationTargetUrl: sourceLocationRef.target,
integrationType: integration?.type,
+8 -6
View File
@@ -14,13 +14,14 @@
* limitations under the License.
*/
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { configApiRef, discoveryApiRef, identityApiRef } from '@backstage/core';
import { CatalogClient } from '@backstage/catalog-client';
import { configApiRef, discoveryApiRef, identityApiRef } from '@backstage/core';
import { createDevApp } from '@backstage/dev-utils';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import React from 'react';
import { scaffolderApiRef, ScaffolderClient } from '../src';
import { ScaffolderPage } from '../src/plugin';
import { ScaffolderClient, scaffolderApiRef } from '../src';
createDevApp()
.registerApi({
@@ -34,9 +35,10 @@ createDevApp()
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
configApi: configApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
},
factory: ({ discoveryApi, identityApi, configApi }) =>
new ScaffolderClient({ discoveryApi, identityApi, configApi }),
factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) =>
new ScaffolderClient({ discoveryApi, identityApi, scmIntegrationsApi }),
})
.addPage({
path: '/create',
+1
View File
@@ -35,6 +35,7 @@
"@backstage/config": "^0.1.3",
"@backstage/core": "^0.7.1",
"@backstage/integration": "^0.5.1",
"@backstage/integration-react": "^0.1.1",
"@backstage/plugin-catalog-react": "^0.1.1",
"@backstage/theme": "^0.2.4",
"@material-ui/core": "^4.11.0",
+14 -10
View File
@@ -13,23 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/core';
import { ScmIntegrations } from '@backstage/integration';
import { ScaffolderClient } from './api';
describe('api', () => {
const discoveryApi = {} as any;
const identityApi = {} as any;
const configApi = new ConfigReader({
integrations: {
github: [
{
host: 'hello.com',
},
],
},
});
const scmIntegrationsApi = ScmIntegrations.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'hello.com',
},
],
},
}),
);
const apiClient = new ScaffolderClient({
configApi,
scmIntegrationsApi,
discoveryApi,
identityApi,
});
+10 -12
View File
@@ -19,11 +19,10 @@ import { JsonObject } from '@backstage/config';
import {
createApiRef,
DiscoveryApi,
Observable,
ConfigApi,
IdentityApi,
Observable,
} from '@backstage/core';
import { ScmIntegrations } from '@backstage/integration';
import { ScmIntegrationRegistry } from '@backstage/integration';
import ObservableImpl from 'zen-observable';
import { ListActionsResponse, ScaffolderTask, Status } from './types';
@@ -83,29 +82,28 @@ export interface ScaffolderApi {
after?: number;
}): Observable<LogEvent>;
}
export class ScaffolderClient implements ScaffolderApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
private readonly configApi: ConfigApi;
private readonly scmIntegrationsApi: ScmIntegrationRegistry;
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
configApi: ConfigApi;
scmIntegrationsApi: ScmIntegrationRegistry;
}) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
this.configApi = options.configApi;
this.scmIntegrationsApi = options.scmIntegrationsApi;
}
async getIntegrationsList(options: { allowedHosts: string[] }) {
const integrations = ScmIntegrations.fromConfig(this.configApi);
return [
...integrations.azure.list(),
...integrations.bitbucket.list(),
...integrations.github.list(),
...integrations.gitlab.list(),
...this.scmIntegrationsApi.azure.list(),
...this.scmIntegrationsApi.bitbucket.list(),
...this.scmIntegrationsApi.github.list(),
...this.scmIntegrationsApi.gitlab.list(),
]
.map(c => ({ type: c.type, title: c.title, host: c.config.host }))
.filter(c => options.allowedHosts.includes(c.host));
+7 -7
View File
@@ -15,15 +15,15 @@
*/
import {
createPlugin,
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
identityApiRef,
configApiRef,
createRoutableExtension,
} from '@backstage/core';
import { rootRouteRef } from './routes';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { scaffolderApiRef, ScaffolderClient } from './api';
import { rootRouteRef } from './routes';
export const scaffolderPlugin = createPlugin({
id: 'scaffolder',
@@ -33,10 +33,10 @@ export const scaffolderPlugin = createPlugin({
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
configApi: configApiRef,
scmIntegrationsApi: scmIntegrationsApiRef,
},
factory: ({ discoveryApi, identityApi, configApi }) =>
new ScaffolderClient({ discoveryApi, identityApi, configApi }),
factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) =>
new ScaffolderClient({ discoveryApi, identityApi, scmIntegrationsApi }),
}),
],
routes: {