Merge pull request #4341 from backstage/rugvip/toptools

plugins: port user-settings, api-docs, and gcp-projects to new composability API
This commit is contained in:
Patrik Oldsberg
2021-02-02 13:53:40 +01:00
committed by GitHub
31 changed files with 467 additions and 156 deletions
+124
View File
@@ -0,0 +1,124 @@
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: petstore
description: The petstore API
tags:
- store
- rest
spec:
type: openapi
lifecycle: experimental
owner: team-c
definition: |
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
summary: Create a pet
operationId: createPets
tags:
- pets
responses:
'201':
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/pets/{petId}:
get:
summary: Info for a specific pet
operationId: showPetById
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
items:
$ref: "#/components/schemas/Pet"
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
+18 -2
View File
@@ -14,7 +14,23 @@
* limitations under the License.
*/
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
import { ApiExplorerPage, apiDocsPlugin } from '../src/plugin';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import petstoreApiEntity from './example-api.yaml';
createDevApp().registerPlugin(plugin).render();
createDevApp()
.registerApi({
api: catalogApiRef,
deps: {},
factory: () =>
(({
async getEntities() {
return { items: [petstoreApiEntity] };
},
} as unknown) as typeof catalogApiRef.T),
})
.registerPlugin(apiDocsPlugin)
.addPage({ element: <ApiExplorerPage /> })
.render();
@@ -16,6 +16,7 @@
import { ApiEntity } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
@@ -54,7 +55,7 @@ paths:
get:
summary: List all artists
responses:
"200":
"200":
description: Success
`;
const apiEntity: ApiEntity = {
@@ -81,7 +82,9 @@ paths:
const { getByText } = await renderInTestApp(
<Wrapper>
<ApiDefinitionCard apiEntity={apiEntity} />
<EntityProvider entity={apiEntity}>
<ApiDefinitionCard />
</EntityProvider>
</Wrapper>,
);
@@ -110,7 +113,9 @@ paths:
const { getByText } = await renderInTestApp(
<Wrapper>
<ApiDefinitionCard apiEntity={apiEntity} />
<EntityProvider entity={apiEntity}>
<ApiDefinitionCard />
</EntityProvider>
</Wrapper>,
);
@@ -15,6 +15,7 @@
*/
import { ApiEntity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { CardTab, TabbedCard, useApi } from '@backstage/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
@@ -22,29 +23,31 @@ import { apiDocsConfigRef } from '../../config';
import { PlainApiDefinitionWidget } from '../PlainApiDefinitionWidget';
type Props = {
/** @deprecated The entity is now grabbed from context instead */
apiEntity?: ApiEntity;
};
export const ApiDefinitionCard = ({ apiEntity }: Props) => {
export const ApiDefinitionCard = (_: Props) => {
const entity = useEntity().entity as ApiEntity;
const config = useApi(apiDocsConfigRef);
const { getApiDefinitionWidget } = config;
if (!apiEntity) {
if (!entity) {
return <Alert severity="error">Could not fetch the API</Alert>;
}
const definitionWidget = getApiDefinitionWidget(apiEntity);
const definitionWidget = getApiDefinitionWidget(entity);
if (definitionWidget) {
return (
<TabbedCard title={apiEntity.metadata.name}>
<TabbedCard title={entity.metadata.name}>
<CardTab label={definitionWidget.title} key="widget">
{definitionWidget.component(apiEntity.spec.definition)}
{definitionWidget.component(entity.spec.definition)}
</CardTab>
<CardTab label="Raw" key="raw">
<PlainApiDefinitionWidget
definition={apiEntity.spec.definition}
language={definitionWidget.rawLanguage || apiEntity.spec.type}
definition={entity.spec.definition}
language={definitionWidget.rawLanguage || entity.spec.type}
/>
</CardTab>
</TabbedCard>
@@ -53,13 +56,13 @@ export const ApiDefinitionCard = ({ apiEntity }: Props) => {
return (
<TabbedCard
title={apiEntity.metadata.name}
title={entity.metadata.name}
children={[
// Has to be an array, otherwise typescript doesn't like that this has only a single child
<CardTab label={apiEntity.spec.type} key="raw">
<CardTab label={entity.spec.type} key="raw">
<PlainApiDefinitionWidget
definition={apiEntity.spec.definition}
language={apiEntity.spec.type}
definition={entity.spec.definition}
language={entity.spec.type}
/>
</CardTab>,
]}
@@ -21,7 +21,11 @@ import {
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import {
CatalogApi,
catalogApiRef,
EntityProvider,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
@@ -68,7 +72,9 @@ describe('<ConsumedApisCard />', () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ConsumedApisCard entity={entity} />
<EntityProvider entity={entity}>
<ConsumedApisCard />
</EntityProvider>
</Wrapper>,
);
@@ -134,7 +140,9 @@ describe('<ConsumedApisCard />', () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ConsumedApisCard entity={entity} />
<EntityProvider entity={entity}>
<ConsumedApisCard />
</EntityProvider>
</Wrapper>,
);
@@ -19,6 +19,7 @@ import {
Entity,
RELATION_CONSUMES_API,
} from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { EmptyState, InfoCard, Progress } from '@backstage/core';
import React, { PropsWithChildren } from 'react';
import { ApisTable } from './ApisTable';
@@ -37,11 +38,13 @@ const ApisCard = ({
};
type Props = {
entity: Entity;
/** @deprecated The entity is now grabbed from context instead */
entity?: Entity;
variant?: string;
};
export const ConsumedApisCard = ({ entity, variant = 'gridItem' }: Props) => {
export const ConsumedApisCard = ({ variant = 'gridItem' }: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(
entity,
RELATION_CONSUMES_API,
@@ -21,7 +21,11 @@ import {
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import {
CatalogApi,
catalogApiRef,
EntityProvider,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
@@ -68,7 +72,9 @@ describe('<ProvidedApisCard />', () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ProvidedApisCard entity={entity} />
<EntityProvider entity={entity}>
<ProvidedApisCard />
</EntityProvider>
</Wrapper>,
);
@@ -134,7 +140,9 @@ describe('<ProvidedApisCard />', () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ProvidedApisCard entity={entity} />
<EntityProvider entity={entity}>
<ProvidedApisCard />
</EntityProvider>
</Wrapper>,
);
@@ -19,6 +19,7 @@ import {
Entity,
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { EmptyState, InfoCard, Progress } from '@backstage/core';
import React, { PropsWithChildren } from 'react';
import { ApisTable } from './ApisTable';
@@ -37,11 +38,13 @@ const ApisCard = ({
};
type Props = {
entity: Entity;
/** @deprecated The entity is now grabbed from context instead */
entity?: Entity;
variant?: string;
};
export const ProvidedApisCard = ({ entity, variant = 'gridItem' }: Props) => {
export const ProvidedApisCard = ({ variant = 'gridItem' }: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(
entity,
RELATION_PROVIDES_API,
@@ -21,7 +21,11 @@ import {
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import {
CatalogApi,
catalogApiRef,
EntityProvider,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
@@ -67,7 +71,9 @@ describe('<ConsumingComponentsCard />', () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ConsumingComponentsCard entity={entity} />
<EntityProvider entity={entity}>
<ConsumingComponentsCard />
</EntityProvider>
</Wrapper>,
);
@@ -133,7 +139,9 @@ describe('<ConsumingComponentsCard />', () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ConsumingComponentsCard entity={entity} />
<EntityProvider entity={entity}>
<ConsumingComponentsCard />
</EntityProvider>
</Wrapper>,
);
@@ -19,6 +19,7 @@ import {
Entity,
RELATION_API_CONSUMED_BY,
} from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { EmptyState, InfoCard, Progress } from '@backstage/core';
import React, { PropsWithChildren } from 'react';
import { MissingConsumesApisEmptyState } from '../EmptyState';
@@ -37,14 +38,13 @@ const ComponentsCard = ({
};
type Props = {
entity: Entity;
/** @deprecated The entity is now grabbed from context instead */
entity?: Entity;
variant?: string;
};
export const ConsumingComponentsCard = ({
entity,
variant = 'gridItem',
}: Props) => {
export const ConsumingComponentsCard = ({ variant = 'gridItem' }: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(
entity,
RELATION_API_CONSUMED_BY,
@@ -21,7 +21,11 @@ import {
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import {
CatalogApi,
catalogApiRef,
EntityProvider,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
@@ -67,7 +71,9 @@ describe('<ProvidingComponentsCard />', () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ProvidingComponentsCard entity={entity} />
<EntityProvider entity={entity}>
<ProvidingComponentsCard />
</EntityProvider>
</Wrapper>,
);
@@ -133,7 +139,9 @@ describe('<ProvidingComponentsCard />', () => {
const { getByText } = await renderInTestApp(
<Wrapper>
<ProvidingComponentsCard entity={entity} />
<EntityProvider entity={entity}>
<ProvidingComponentsCard />
</EntityProvider>
</Wrapper>,
);
@@ -19,6 +19,7 @@ import {
Entity,
RELATION_API_PROVIDED_BY,
} from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { EmptyState, InfoCard, Progress } from '@backstage/core';
import React, { PropsWithChildren } from 'react';
import { MissingProvidesApisEmptyState } from '../EmptyState';
@@ -37,14 +38,13 @@ const ComponentsCard = ({
};
type Props = {
entity: Entity;
/** @deprecated The entity is now grabbed from context instead */
entity?: Entity;
variant?: string;
};
export const ProvidingComponentsCard = ({
entity,
variant = 'gridItem',
}: Props) => {
export const ProvidingComponentsCard = ({ variant = 'gridItem' }: Props) => {
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(
entity,
RELATION_API_PROVIDED_BY,
+10 -1
View File
@@ -15,4 +15,13 @@
*/
export * from './components';
export { plugin } from './plugin';
export {
apiDocsPlugin,
apiDocsPlugin as plugin,
ApiExplorerPage,
EntityApiDefinitionCard,
EntityConsumedApisCard,
EntityConsumingComponentsCard,
EntityProvidedApisCard,
EntityProvidingComponentsCard,
} from './plugin';
+2 -2
View File
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { plugin } from './plugin';
import { apiDocsPlugin } from './plugin';
describe('api-docs', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
expect(apiDocsPlugin).toBeDefined();
});
});
+69 -4
View File
@@ -15,14 +15,22 @@
*/
import { ApiEntity } from '@backstage/catalog-model';
import { createApiFactory, createPlugin } from '@backstage/core';
import { ApiExplorerPage } from './components/ApiExplorerPage/ApiExplorerPage';
import {
createApiFactory,
createPlugin,
createRoutableExtension,
createComponentExtension,
} from '@backstage/core';
import { ApiExplorerPage as Page } from './components/ApiExplorerPage/ApiExplorerPage';
import { defaultDefinitionWidgets } from './components/ApiDefinitionCard';
import { rootRoute } from './routes';
import { apiDocsConfigRef } from './config';
export const plugin = createPlugin({
export const apiDocsPlugin = createPlugin({
id: 'api-docs',
routes: {
root: rootRoute,
},
apis: [
createApiFactory({
api: apiDocsConfigRef,
@@ -38,6 +46,63 @@ export const plugin = createPlugin({
}),
],
register({ router }) {
router.addRoute(rootRoute, ApiExplorerPage);
router.addRoute(rootRoute, Page);
},
});
export const ApiExplorerPage = apiDocsPlugin.provide(
createRoutableExtension({
component: () =>
import('./components/ApiExplorerPage').then(m => m.ApiExplorerPage),
mountPoint: rootRoute,
}),
);
export const EntityApiDefinitionCard = apiDocsPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/ApiDefinitionCard').then(m => m.ApiDefinitionCard),
},
}),
);
export const EntityConsumedApisCard = apiDocsPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/ApisCards').then(m => m.ConsumedApisCard),
},
}),
);
export const EntityConsumingComponentsCard = apiDocsPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/ComponentsCards').then(
m => m.ConsumingComponentsCard,
),
},
}),
);
export const EntityProvidedApisCard = apiDocsPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/ApisCards').then(m => m.ProvidedApisCard),
},
}),
);
export const EntityProvidingComponentsCard = apiDocsPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/ComponentsCards').then(
m => m.ProvidingComponentsCard,
),
},
}),
);
+2 -2
View File
@@ -15,6 +15,6 @@
*/
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
import { gcpProjectsPlugin } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
createDevApp().registerPlugin(gcpProjectsPlugin).render();
+1 -1
View File
@@ -37,7 +37,7 @@
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-router-dom": "^6.0.0-beta.0",
"react-use": "^15.3.3"
},
"devDependencies": {
@@ -0,0 +1,29 @@
/*
* Copyright 2020 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 React from 'react';
import { Route, Routes } from 'react-router-dom';
import { NewProjectPage } from '../NewProjectPage';
import { ProjectDetailsPage } from '../ProjectDetailsPage';
import { ProjectListPage } from '../ProjectListPage';
export const GcpProjectsPage = () => (
<Routes>
<Route path="/" element={<ProjectListPage />} />
<Route path="/new" element={<NewProjectPage />} />
<Route path="/project" element={<ProjectDetailsPage />} />
</Routes>
);
@@ -0,0 +1,17 @@
/*
* Copyright 2020 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 { GcpProjectsPage } from './GcpProjectsPage';
+5 -1
View File
@@ -14,5 +14,9 @@
* limitations under the License.
*/
export { plugin } from './plugin';
export {
gcpProjectsPlugin,
gcpProjectsPlugin as plugin,
GcpProjectsPage,
} from './plugin';
export * from './api';
+2 -2
View File
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { plugin } from './plugin';
import { gcpProjectsPlugin } from './plugin';
describe('gcp-projects', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
expect(gcpProjectsPlugin).toBeDefined();
});
});
+14 -15
View File
@@ -17,29 +17,20 @@
import {
createApiFactory,
createPlugin,
createRouteRef,
createRoutableExtension,
googleAuthApiRef,
} from '@backstage/core';
import { gcpApiRef, GcpClient } from './api';
import { NewProjectPage } from './components/NewProjectPage';
import { ProjectDetailsPage } from './components/ProjectDetailsPage';
import { ProjectListPage } from './components/ProjectListPage';
import { rootRouteRef, projectRouteRef, newProjectRouteRef } from './routes';
export const rootRouteRef = createRouteRef({
path: '/gcp-projects',
title: 'GCP Projects',
});
export const projectRouteRef = createRouteRef({
path: '/gcp-projects/project',
title: 'GCP Project Page',
});
export const newProjectRouteRef = createRouteRef({
path: '/gcp-projects/new',
title: 'GCP Project Page',
});
export const plugin = createPlugin({
export const gcpProjectsPlugin = createPlugin({
id: 'gcp-projects',
routes: {
root: rootRouteRef,
},
apis: [
createApiFactory({
api: gcpApiRef,
@@ -55,3 +46,11 @@ export const plugin = createPlugin({
router.addRoute(newProjectRouteRef, NewProjectPage);
},
});
export const GcpProjectsPage = gcpProjectsPlugin.provide(
createRoutableExtension({
component: () =>
import('./components/GcpProjectsPage').then(m => m.GcpProjectsPage),
mountPoint: rootRouteRef,
}),
);
+30
View File
@@ -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 { createRouteRef } from '@backstage/core';
export const rootRouteRef = createRouteRef({
path: '/gcp-projects',
title: 'GCP Projects',
});
export const projectRouteRef = createRouteRef({
path: '/gcp-projects/project',
title: 'GCP Project Page',
});
export const newProjectRouteRef = createRouteRef({
path: '/gcp-projects/new',
title: 'GCP Project Page',
});
+11 -3
View File
@@ -13,7 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { userSettingsPlugin, UserSettingsPage } from '../src/plugin';
createDevApp()
.registerPlugin(userSettingsPlugin)
.addPage({
title: 'Settings',
element: <UserSettingsPage />,
})
.render();
+6 -1
View File
@@ -13,5 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { plugin } from './plugin';
export {
userSettingsPlugin,
userSettingsPlugin as plugin,
UserSettingsPage,
} from './plugin';
export * from './components/';
+2 -2
View File
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { plugin } from './plugin';
import { userSettingsPlugin } from './plugin';
describe('user-settings', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
expect(userSettingsPlugin).toBeDefined();
});
});
+18 -3
View File
@@ -13,13 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createPlugin, createRouteRef } from '@backstage/core';
import {
createPlugin,
createRoutableExtension,
createRouteRef,
} from '@backstage/core';
export const settingsRouteRef = createRouteRef({
path: '/settings',
title: 'Settings',
});
export const plugin = createPlugin({
export const userSettingsPlugin = createPlugin({
id: 'user-settings',
routes: {
settingsPage: settingsRouteRef,
},
});
export const UserSettingsPage = userSettingsPlugin.provide(
createRoutableExtension({
component: () =>
import('./components/SettingsPage').then(m => m.SettingsPage),
mountPoint: settingsRouteRef,
}),
);