From 8d26fb5c5f1225c5d4908fca691a054a2916aa64 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Wed, 27 May 2020 16:34:11 +0200 Subject: [PATCH 1/9] feature: naive implementation of entities API usage --- packages/app/package.json | 7 ++++ plugins/catalog/src/data/component.ts | 1 + plugins/catalog/src/data/mock-factory.ts | 48 ++++++++++++------------ plugins/circleci/src/index.ts | 1 - plugins/circleci/src/proxy.ts | 25 ------------ 5 files changed, 31 insertions(+), 51 deletions(-) delete mode 100644 plugins/circleci/src/proxy.ts diff --git a/packages/app/package.json b/packages/app/package.json index 0a3d68603b..41a4dace53 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -72,6 +72,13 @@ "pathRewrite": { "^/circleci/api/": "/" } + }, + "/catalog/api": { + "target": "http://localhost:3003", + "changeOrigin": true, + "pathRewrite": { + "^/catalog/api/": "/" + } } } } diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts index ec6e2368b8..9a073f0a42 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -15,6 +15,7 @@ */ export type Component = { name: string; + status: string; }; export interface ComponentFactory { diff --git a/plugins/catalog/src/data/mock-factory.ts b/plugins/catalog/src/data/mock-factory.ts index bf39fa436c..15be051fb0 100644 --- a/plugins/catalog/src/data/mock-factory.ts +++ b/plugins/catalog/src/data/mock-factory.ts @@ -14,35 +14,33 @@ * limitations under the License. */ import { Component, ComponentFactory } from './component'; -import mock from './mock-factory-data.json'; +import { DescriptorEnvelope } from '../../../catalog-backend/src/ingestion/types'; + +function transformEnvelopeToComponent(data: DescriptorEnvelope): Component { + return { + name: data.metadata?.name ?? '', + status: data.metadata?.labels?.status ?? 'Up and running', + }; +} + +let inMemoryStore: Promise; -const ARTIFICIAL_TIMEOUT = 800; -let inMemoryStore = [...mock]; export const MockComponentFactory: ComponentFactory = { getAllComponents(): Promise { - return new Promise((resolve) => - setTimeout(() => resolve(inMemoryStore), ARTIFICIAL_TIMEOUT), - ); + inMemoryStore = + inMemoryStore ?? + fetch('//localhost:3000/catalog/api/entities') + .then(response => response.json()) + .then(data => data.map(transformEnvelopeToComponent)); + return inMemoryStore; }, - getComponentByName(name: string): Promise { - return new Promise((resolve, reject) => - setTimeout(() => { - const mockComponent = inMemoryStore.find( - (component) => component.name === name, - ); - if (mockComponent) return resolve(mockComponent); - return reject({ code: 'Component not found!' }); - }, ARTIFICIAL_TIMEOUT), - ); + async getComponentByName(name: string): Promise { + const components = await this.getAllComponents(); + const mockComponent = components.find(component => component.name === name); + if (mockComponent) return mockComponent; + throw new Error(`'Component not found: ${name}`); }, - removeComponentByName(name: string): Promise { - return new Promise((resolve) => - setTimeout(() => { - inMemoryStore = inMemoryStore.filter( - (component) => component.name !== name, - ); - resolve(true); - }, ARTIFICIAL_TIMEOUT), - ); + async removeComponentByName(_: string): Promise { + return true; }, }; diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts index 41cde5e83a..11f2c80b88 100644 --- a/plugins/circleci/src/index.ts +++ b/plugins/circleci/src/index.ts @@ -16,6 +16,5 @@ export { plugin } from './plugin'; export * from './api'; -export * from './proxy'; export * from './route-refs'; export { CircleCIWidget } from './components/App'; diff --git a/plugins/circleci/src/proxy.ts b/plugins/circleci/src/proxy.ts deleted file mode 100644 index 8a5ae460ab..0000000000 --- a/plugins/circleci/src/proxy.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 const proxySettings = { - '/circleci/api': { - target: 'https://circleci.com/api/v1.1', - changeOrigin: true, - logLevel: 'debug', - pathRewrite: { - '^/circleci/api/': '/', - }, - }, -}; From f4887f08b168f564a184aa21c4cab7c9a255968f Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 00:38:25 +0200 Subject: [PATCH 2/9] feature: implement useApi logic for fetching components --- packages/app/src/apis.ts | 9 + plugins/catalog/src/api/index.ts | 49 +++++ plugins/catalog/src/api/types.ts | 185 ++++++++++++++++++ .../CatalogPage/CatalogPage.test.tsx | 9 +- .../components/CatalogPage/CatalogPage.tsx | 16 +- .../components/CatalogTable/CatalogTable.tsx | 30 +-- .../ComponentPage/ComponentPage.test.tsx | 20 -- .../ComponentPage/ComponentPage.tsx | 23 ++- plugins/catalog/src/data/component.ts | 9 +- .../catalog/src/data/mock-factory-data.json | 35 ---- plugins/catalog/src/data/mock-factory.ts | 46 ----- .../data/{with-mock-store.tsx => utils.ts} | 19 +- plugins/catalog/src/index.ts | 1 + plugins/catalog/src/plugin.ts | 5 +- 14 files changed, 281 insertions(+), 175 deletions(-) create mode 100644 plugins/catalog/src/api/index.ts create mode 100644 plugins/catalog/src/api/types.ts delete mode 100644 plugins/catalog/src/data/mock-factory-data.json delete mode 100644 plugins/catalog/src/data/mock-factory.ts rename plugins/catalog/src/data/{with-mock-store.tsx => utils.ts} (61%) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index c11ea24aca..bf9980bf5f 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -37,6 +37,7 @@ import { import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar'; import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci'; +import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog'; const builder = ApiRegistry.builder(); @@ -72,4 +73,12 @@ builder.add( }), ); +builder.add( + catalogApiRef, + new CatalogApi({ + apiOrigin: 'http://localhost:3000', + basePath: '/catalog/api', + }), +); + export default builder.build() as ApiHolder; diff --git a/plugins/catalog/src/api/index.ts b/plugins/catalog/src/api/index.ts new file mode 100644 index 0000000000..1bc739b56f --- /dev/null +++ b/plugins/catalog/src/api/index.ts @@ -0,0 +1,49 @@ +/* + * 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 { createApiRef } from '@backstage/core'; +import { DescriptorEnvelope } from './types'; + +export const catalogApiRef = createApiRef({ + id: 'plugin.catalog.service', + description: + 'Used by the Catalog plugin to make requests to accompanying backend', +}); + +export class CatalogApi { + private apiOrigin: string; + private basePath: string; + constructor({ + apiOrigin, + basePath, + }: { + apiOrigin: string; + basePath: string; + }) { + this.apiOrigin = apiOrigin; + this.basePath = basePath; + } + async getEntities(): Promise { + const response = await fetch(`${this.apiOrigin}${this.basePath}/entities`); + return await response.json(); + } + async getEntityByName(name: string): Promise { + const entities = await this.getEntities(); + const entity = entities.find(e => e.metadata.name === name); + if (entity) return entity; + throw new Error(`'Entity not found: ${name}`); + } +} diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts new file mode 100644 index 0000000000..9f1cfb8529 --- /dev/null +++ b/plugins/catalog/src/api/types.ts @@ -0,0 +1,185 @@ +/* + * 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 { ComponentDescriptorV1beta1 } from './descriptors/ComponentDescriptorV1beta1Parser'; + +export type ComponentDescriptor = ComponentDescriptorV1beta1; + +/** + * Metadata fields common to all versions/kinds of entity. + * + * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta + */ +export type EntityMeta = { + /** + * A globally unique ID for the entity. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. The field can (optionally) be specified when performing + * update or delete operations, but the server is free to reject requests + * that do so in such a way that it breaks semantics. + */ + uid?: string; + + /** + * An opaque string that changes for each update operation to any part of + * the entity, including metadata. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. The field can (optionally) be specified when performing + * update or delete operations, and the server will then reject the + * operation if it does not match the current stored value. + */ + etag?: string; + + /** + * A positive nonzero number that indicates the current generation of data + * for this entity; the value is incremented each time the spec changes. + * + * This field can not be set by the user at creation time, and the server + * will reject an attempt to do so. The field will be populated in read + * operations. + */ + generation?: number; + + /** + * The name of the entity. + * + * Must be uniqe within the catalog at any given point in time, for any + * given namespace, for any given kind. + */ + name: string; + + /** + * The namespace that the entity belongs to. + */ + namespace?: string; + + /** + * Key/value pairs of identifying information attached to the entity. + */ + labels?: Record; + + /** + * Key/value pairs of non-identifying auxiliary information attached to the + * entity. + */ + annotations?: Record; +}; + +/** + * The format envelope that's common to all versions/kinds. + * + * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ + */ +export type DescriptorEnvelope = { + /** + * The version of specification format for this particular entity that + * this is written against. + */ + apiVersion: string; + + /** + * The high level entity type being described. + */ + kind: string; + + /** + * Optional metadata related to the entity. + */ + metadata: EntityMeta; + + /** + * The specification data describing the entity itself. + */ + spec?: object; +}; + +/** + * Parses and validates descriptors. + * + * The output must be validated and well formed. + */ +export type DescriptorParser = { + /** + * Parses and validates a single raw descriptor. + * + * @param descriptor A raw descriptor object + * @returns A structure describing the parsed and validated descriptor + * @throws An Error if the descriptor was malformed + */ + parse(descriptor: object): Promise; +}; + +/** + * Parses and validates a single envelope into its materialized kind. + * + * These parsers may assume that the envelope is already validated and well + * formed. + */ +export type KindParser = { + /** + * Try to parse an envelope into a materialized kind. + * + * @param envelope A valid descriptor envelope + * @returns A materialized type, or undefined if the given version/kind is + * not meant to be handled by this parser + * @throws An Error if the type was handled and found to not be properly + * formatted + */ + tryParse( + envelope: DescriptorEnvelope, + ): Promise; +}; + +export class ParserError extends Error { + constructor(message?: string, private _entityName?: string | undefined) { + super(message); + } + get entityName() { + return this._entityName; + } +} + +export type ReaderOutput = + | { type: 'error'; error: Error } + | { type: 'data'; data: object }; + +export type LocationReader = { + /** + * Reads the contents of a single location. + * + * @param type The type of location to read + * @param target The location target (type-specific) + * @returns The parsed contents, as an array of unverified descriptors or + * errors where the individual documents could not be parsed. + * @throws An error if the location as a whole could not be read + */ + read(type: string, target: string): Promise; +}; + +export type LocationSource = { + /** + * Reads the contents of a single location. + * + * @param target The location target to read + * @returns The parsed contents, as an array of unverified descriptors + * @throws An error if the location target could not be read + */ + read(target: string): Promise; +}; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 60af5f7d24..df7fcc1cd2 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -19,19 +19,12 @@ import { render } from '@testing-library/react'; import CatalogPage from './CatalogPage'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { ComponentFactory } from '../../data/component'; - -const testComponentFactory: ComponentFactory = { - getAllComponents: jest.fn(() => Promise.resolve([{ name: 'test' }])), - getComponentByName: jest.fn(() => Promise.resolve({ name: 'test' })), - removeComponentByName: jest.fn(() => Promise.resolve(true)), -}; describe('CatalogPage', () => { it('should render', async () => { const rendered = render( - + , ); expect( diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index ec6c1cf09f..63bb4e0771 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -23,17 +23,19 @@ import { SupportButton, Page, pageTheme, + useApi, } from '@backstage/core'; import { useAsync } from 'react-use'; -import { ComponentFactory } from '../../data/component'; import CatalogTable from '../CatalogTable/CatalogTable'; import { Button } from '@material-ui/core'; -type CatalogPageProps = { - componentFactory: ComponentFactory; -}; -const CatalogPage: FC = ({ componentFactory }) => { - const { value, error, loading } = useAsync(componentFactory.getAllComponents); +import { catalogApiRef } from '../..'; +import { envelopeToComponent } from '../../data/utils'; + +const CatalogPage: FC<{}> = () => { + const catalogApi = useApi(catalogApiRef); + const { value, error, loading } = useAsync(() => catalogApi.getEntities()); + return (
@@ -47,7 +49,7 @@ const CatalogPage: FC = ({ componentFactory }) => { All your components diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 57f1dfe7f5..27e59973b5 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -15,13 +15,7 @@ */ import React, { FC } from 'react'; import { Component } from '../../data/component'; -import { - InfoCard, - Progress, - Table, - TableColumn, - StatusOK, -} from '@backstage/core'; +import { InfoCard, Progress, Table, TableColumn } from '@backstage/core'; import { Typography, Link } from '@material-ui/core'; const columns: TableColumn[] = [ @@ -34,26 +28,8 @@ const columns: TableColumn[] = [ ), }, { - title: 'System', - field: 'system', - }, - { - title: 'Owner', - field: 'owner', - }, - { - title: 'Lifecycle', - field: 'lifecycle', - }, - { - title: 'Status', - field: 'status', - render: (componentData: any) => ( - <> - - {componentData.status || 'Up and running'} - - ), + title: 'Kind', + field: 'kind', }, { title: 'Description', diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index 369b80de80..d0f67d9cb3 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -30,11 +30,6 @@ const getTestProps = (componentName: string) => { history: { push: jest.fn(), }, - componentFactory: { - getAllComponents: jest.fn(() => Promise.resolve([{ name: 'test' }])), - getComponentByName: jest.fn(() => Promise.resolve({ name: 'test' })), - removeComponentByName: jest.fn(() => Promise.resolve(true)), - }, }; }; @@ -52,19 +47,4 @@ describe('ComponentPage', () => { ); expect(props.history.push).toHaveBeenCalledWith('/catalog'); }); - it('should use factory to fetch component by name and display it', async () => { - await act(async () => { - const props = getTestProps('test'); - await render( - wrapInTheme( - - - , - ), - ); - expect(props.componentFactory.getComponentByName).toHaveBeenCalledWith( - 'test', - ); - }); - }); }); diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx index 0cf7017173..0ee0e1d09a 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -15,7 +15,6 @@ */ import React, { FC, useEffect, useState } from 'react'; import { useAsync } from 'react-use'; -import { ComponentFactory } from '../../data/component'; import ComponentMetadataCard from '../ComponentMetadataCard/ComponentMetadataCard'; import { Content, @@ -30,11 +29,12 @@ import ComponentContextMenu from '../ComponentContextMenu/ComponentContextMenu'; import ComponentRemovalDialog from '../ComponentRemovalDialog/ComponentRemovalDialog'; import { SentryIssuesWidget } from '@backstage/plugin-sentry'; import { Grid } from '@material-ui/core'; +import { catalogApiRef } from '../..'; +import { envelopeToComponent } from '../../data/utils'; const REDIRECT_DELAY = 1000; type ComponentPageProps = { - componentFactory: ComponentFactory; match: { params: { name: string; @@ -45,11 +45,7 @@ type ComponentPageProps = { }; }; -const ComponentPage: FC = ({ - match, - history, - componentFactory, -}) => { +const ComponentPage: FC = ({ match, history }) => { const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const [removingPending, setRemovingPending] = useState(false); const showRemovalDialog = () => setConfirmationDialogOpen(true); @@ -62,8 +58,9 @@ const ComponentPage: FC = ({ return null; } + const catalogApi = useApi(catalogApiRef); const catalogRequest = useAsync(() => - componentFactory.getComponentByName(match.params.name), + catalogApi.getEntityByName(match.params.name), ); useEffect(() => { @@ -78,18 +75,20 @@ const ComponentPage: FC = ({ const removeComponent = async () => { setConfirmationDialogOpen(false); setRemovingPending(true); - await componentFactory.removeComponentByName(componentName); + // await componentFactory.removeComponentByName(componentName); history.push('/catalog'); }; + const component = envelopeToComponent(catalogRequest.value! ?? {}); + return ( -
+
{confirmationDialogOpen && catalogRequest.value && ( = ({ diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts index 9a073f0a42..57cba03a9a 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -15,11 +15,6 @@ */ export type Component = { name: string; - status: string; + kind: string; + description: string; }; - -export interface ComponentFactory { - getAllComponents(): Promise; - getComponentByName(name: string): Promise; - removeComponentByName(name: string): Promise; -} diff --git a/plugins/catalog/src/data/mock-factory-data.json b/plugins/catalog/src/data/mock-factory-data.json deleted file mode 100644 index 8fc61a87a6..0000000000 --- a/plugins/catalog/src/data/mock-factory-data.json +++ /dev/null @@ -1,35 +0,0 @@ -[ - { - "name": "example.com" - }, - { - "name": "subdomain.example.com" - }, - { - "name": "subdomain2.example.com" - }, - { - "name": "User data pipeline 1" - }, - { - "name": "User data pipeline 2" - }, - { - "name": "User data pipeline 3" - }, - { - "name": "Aggregation CRON job" - }, - { - "name": "Authentication service" - }, - { - "name": "Payments service" - }, - { - "name": "Backstage supervisor" - }, - { - "name": "Identity service" - } -] diff --git a/plugins/catalog/src/data/mock-factory.ts b/plugins/catalog/src/data/mock-factory.ts deleted file mode 100644 index 15be051fb0..0000000000 --- a/plugins/catalog/src/data/mock-factory.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 { Component, ComponentFactory } from './component'; -import { DescriptorEnvelope } from '../../../catalog-backend/src/ingestion/types'; - -function transformEnvelopeToComponent(data: DescriptorEnvelope): Component { - return { - name: data.metadata?.name ?? '', - status: data.metadata?.labels?.status ?? 'Up and running', - }; -} - -let inMemoryStore: Promise; - -export const MockComponentFactory: ComponentFactory = { - getAllComponents(): Promise { - inMemoryStore = - inMemoryStore ?? - fetch('//localhost:3000/catalog/api/entities') - .then(response => response.json()) - .then(data => data.map(transformEnvelopeToComponent)); - return inMemoryStore; - }, - async getComponentByName(name: string): Promise { - const components = await this.getAllComponents(); - const mockComponent = components.find(component => component.name === name); - if (mockComponent) return mockComponent; - throw new Error(`'Component not found: ${name}`); - }, - async removeComponentByName(_: string): Promise { - return true; - }, -}; diff --git a/plugins/catalog/src/data/with-mock-store.tsx b/plugins/catalog/src/data/utils.ts similarity index 61% rename from plugins/catalog/src/data/with-mock-store.tsx rename to plugins/catalog/src/data/utils.ts index 2e5425d03e..b30ac61d9e 100644 --- a/plugins/catalog/src/data/with-mock-store.tsx +++ b/plugins/catalog/src/data/utils.ts @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as React from 'react'; -import { ComponentFactory } from './component'; -import { MockComponentFactory } from './mock-factory'; +import { DescriptorEnvelope } from '../api/types'; +import { Component } from './component'; -const componentFactory: ComponentFactory = MockComponentFactory; - -export const withMockStore = (Component: React.ElementType) => { - return (props: any) => ( - - ); -}; +export function envelopeToComponent(envelope: DescriptorEnvelope): Component { + return { + name: envelope.metadata?.name ?? '', + kind: envelope.kind ?? 'unknown', + description: 'placeholder', + }; +} diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 3a0a0fe2d3..d67bc6a864 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -15,3 +15,4 @@ */ export { plugin } from './plugin'; +export * from './api'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 456a6d2446..cf9edee4da 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -17,12 +17,11 @@ import { createPlugin } from '@backstage/core'; import CatalogPage from './components/CatalogPage'; import ComponentPage from './components/ComponentPage/ComponentPage'; -import { withMockStore } from './data/with-mock-store'; export const plugin = createPlugin({ id: 'catalog', register({ router }) { - router.registerRoute('/catalog', withMockStore(CatalogPage)); - router.registerRoute('/catalog/:name/', withMockStore(ComponentPage)); + router.registerRoute('/catalog', CatalogPage); + router.registerRoute('/catalog/:name/', ComponentPage); }, }); From 02531bb872feda427dcf3a3a23d98decf5658aa1 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 13:26:24 +0200 Subject: [PATCH 3/9] fix: tests --- .../CatalogPage/CatalogPage.test.tsx | 21 ++++++++++++++----- .../ComponentPage/ComponentPage.test.tsx | 1 - 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index df7fcc1cd2..d4cd6e7606 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -17,15 +17,26 @@ import React from 'react'; import { render } from '@testing-library/react'; import CatalogPage from './CatalogPage'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; +import { ApiRegistry, ApiProvider, errorApiRef } from '@backstage/core'; +import { wrapInTheme } from '@backstage/test-utils'; +import { catalogApiRef } from '../..'; + +const errorApi = { post: () => {} }; +const catalogApi = { getEntities: () => Promise.resolve([{ kind: '' }]) }; describe('CatalogPage', () => { it('should render', async () => { const rendered = render( - - - , + wrapInTheme( + + + , + ), ); expect( await rendered.findByText('Keep track of your software'), diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index d0f67d9cb3..0430fb7d3c 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -17,7 +17,6 @@ import ComponentPage from './ComponentPage'; import { render } from '@testing-library/react'; import * as React from 'react'; import { wrapInTheme } from '@backstage/test-utils'; -import { act } from 'react-dom/test-utils'; import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; const getTestProps = (componentName: string) => { From 13dae319352c6c43a6fcbf4746f4f5cdb2a0bc38 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 13:29:25 +0200 Subject: [PATCH 4/9] fix: add missing type --- plugins/catalog/src/api/types.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 9f1cfb8529..8ed5748e7c 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { ComponentDescriptorV1beta1 } from './descriptors/ComponentDescriptorV1beta1Parser'; +export interface ComponentDescriptorV1beta1 extends DescriptorEnvelope { + spec: { + type: string; + }; +} export type ComponentDescriptor = ComponentDescriptorV1beta1; From 3fa037ad18b439cff1c66b790ed4e4b545a47b28 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 13:42:49 +0200 Subject: [PATCH 5/9] feature: add description field to Component --- plugins/catalog/src/api/types.ts | 7 +++++++ plugins/catalog/src/data/utils.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 8ed5748e7c..4097b76098 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -69,6 +69,13 @@ export type EntityMeta = { */ name: string; + /** + * The short description of the entity. + * + * A a human readable string. + */ + description: string; + /** * The namespace that the entity belongs to. */ diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts index b30ac61d9e..39a84cfbca 100644 --- a/plugins/catalog/src/data/utils.ts +++ b/plugins/catalog/src/data/utils.ts @@ -20,6 +20,6 @@ export function envelopeToComponent(envelope: DescriptorEnvelope): Component { return { name: envelope.metadata?.name ?? '', kind: envelope.kind ?? 'unknown', - description: 'placeholder', + description: envelope.metadata?.description ?? 'placeholder', }; } From 81229772aa1072ac51b2d707a466f2b3e6da0c8b Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 14:36:01 +0200 Subject: [PATCH 6/9] fix:cleanup --- .../ComponentMetadataCard.test.tsx | 2 + plugins/catalog/src/data/mock-factory.ts | 48 ------------------- 2 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 plugins/catalog/src/data/mock-factory.ts diff --git a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx index f4b07c020a..62e3d52c29 100644 --- a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx +++ b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx @@ -22,6 +22,8 @@ describe('ComponentMetadataCard component', () => { it('should display component name if provided', async () => { const testComponent: Component = { name: 'test', + kind: 'Component', + description: 'Placeholder', }; const rendered = await render( , diff --git a/plugins/catalog/src/data/mock-factory.ts b/plugins/catalog/src/data/mock-factory.ts deleted file mode 100644 index 19f04195f9..0000000000 --- a/plugins/catalog/src/data/mock-factory.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { Component, ComponentFactory } from './component'; -import mock from './mock-factory-data.json'; - -const ARTIFICIAL_TIMEOUT = 800; -let inMemoryStore = [...mock]; -export const MockComponentFactory: ComponentFactory = { - getAllComponents(): Promise { - return new Promise(resolve => - setTimeout(() => resolve(inMemoryStore), ARTIFICIAL_TIMEOUT), - ); - }, - getComponentByName(name: string): Promise { - return new Promise((resolve, reject) => - setTimeout(() => { - const mockComponent = inMemoryStore.find( - component => component.name === name, - ); - if (mockComponent) return resolve(mockComponent); - return reject({ code: 'Component not found!' }); - }, ARTIFICIAL_TIMEOUT), - ); - }, - removeComponentByName(name: string): Promise { - return new Promise(resolve => - setTimeout(() => { - inMemoryStore = inMemoryStore.filter( - component => component.name !== name, - ); - resolve(true); - }, ARTIFICIAL_TIMEOUT), - ); - }, -}; From 4e9d5a10c44c560b0dc4dc67ae8601b5ccba23c3 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Thu, 28 May 2020 16:04:52 +0200 Subject: [PATCH 7/9] feature: add real get entity by name --- plugins/catalog/src/api/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/catalog/src/api/index.ts b/plugins/catalog/src/api/index.ts index 1bc739b56f..83c0eb1cfd 100644 --- a/plugins/catalog/src/api/index.ts +++ b/plugins/catalog/src/api/index.ts @@ -41,8 +41,10 @@ export class CatalogApi { return await response.json(); } async getEntityByName(name: string): Promise { - const entities = await this.getEntities(); - const entity = entities.find(e => e.metadata.name === name); + const response = await fetch( + `${this.apiOrigin}${this.basePath}/entities/by-name/Component/default/${name}`, + ); + const entity = await response.json(); if (entity) return entity; throw new Error(`'Entity not found: ${name}`); } From d90913b028bc35ac11921689bc9094cf8600794e Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Fri, 29 May 2020 10:08:55 +0200 Subject: [PATCH 8/9] fix: split catalog api into types and implementation --- packages/app/src/apis.ts | 4 +- plugins/catalog/src/api/index.ts | 51 ----------- plugins/catalog/src/api/types.ts | 153 +++---------------------------- plugins/catalog/src/index.ts | 4 +- 4 files changed, 16 insertions(+), 196 deletions(-) delete mode 100644 plugins/catalog/src/api/index.ts diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 8efc3757eb..8f5198bd5a 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -38,7 +38,7 @@ import { import { techRadarApiRef, TechRadar } from '@backstage/plugin-tech-radar'; import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci'; -import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog'; +import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; const builder = ApiRegistry.builder(); @@ -74,7 +74,7 @@ builder.add( builder.add( catalogApiRef, - new CatalogApi({ + new CatalogClient({ apiOrigin: 'http://localhost:3000', basePath: '/catalog/api', }), diff --git a/plugins/catalog/src/api/index.ts b/plugins/catalog/src/api/index.ts deleted file mode 100644 index 83c0eb1cfd..0000000000 --- a/plugins/catalog/src/api/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { createApiRef } from '@backstage/core'; -import { DescriptorEnvelope } from './types'; - -export const catalogApiRef = createApiRef({ - id: 'plugin.catalog.service', - description: - 'Used by the Catalog plugin to make requests to accompanying backend', -}); - -export class CatalogApi { - private apiOrigin: string; - private basePath: string; - constructor({ - apiOrigin, - basePath, - }: { - apiOrigin: string; - basePath: string; - }) { - this.apiOrigin = apiOrigin; - this.basePath = basePath; - } - async getEntities(): Promise { - const response = await fetch(`${this.apiOrigin}${this.basePath}/entities`); - return await response.json(); - } - async getEntityByName(name: string): Promise { - const response = await fetch( - `${this.apiOrigin}${this.basePath}/entities/by-name/Component/default/${name}`, - ); - const entity = await response.json(); - if (entity) return entity; - throw new Error(`'Entity not found: ${name}`); - } -} diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 0f91e1ed8e..6cc1dfd6cc 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -13,147 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { createApiRef } from '@backstage/core'; +import { DescriptorEnvelope } from '../types'; -export interface ComponentDescriptorV1beta1 extends DescriptorEnvelope { - spec: { - type: string; - }; +export const catalogApiRef = createApiRef({ + id: 'plugin.catalog.service', + description: + 'Used by the Catalog plugin to make requests to accompanying backend', +}); + +export interface CatalogApi { + getEntities(): Promise; + getEntityByName(name: string): Promise; } - -export type ComponentDescriptor = ComponentDescriptorV1beta1; - -/** - * Metadata fields common to all versions/kinds of entity. - * - * @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta - */ -export type EntityMeta = { - /** - * A globally unique ID for the entity. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. The field can (optionally) be specified when performing - * update or delete operations, but the server is free to reject requests - * that do so in such a way that it breaks semantics. - */ - uid?: string; - - /** - * An opaque string that changes for each update operation to any part of - * the entity, including metadata. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. The field can (optionally) be specified when performing - * update or delete operations, and the server will then reject the - * operation if it does not match the current stored value. - */ - etag?: string; - - /** - * A positive nonzero number that indicates the current generation of data - * for this entity; the value is incremented each time the spec changes. - * - * This field can not be set by the user at creation time, and the server - * will reject an attempt to do so. The field will be populated in read - * operations. - */ - generation?: number; - - /** - * The name of the entity. - * - * Must be uniqe within the catalog at any given point in time, for any - * given namespace, for any given kind. - */ - name: string; - - /** - * The short description of the entity. - * - * A a human readable string. - */ - description: string; - - /** - * The namespace that the entity belongs to. - */ - namespace?: string; - - /** - * Key/value pairs of identifying information attached to the entity. - */ - labels?: Record; - - /** - * Key/value pairs of non-identifying auxiliary information attached to the - * entity. - */ - annotations?: Record; -}; - -/** - * The format envelope that's common to all versions/kinds. - * - * @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/ - */ -export type DescriptorEnvelope = { - /** - * The version of specification format for this particular entity that - * this is written against. - */ - apiVersion: string; - - /** - * The high level entity type being described. - */ - kind: string; - - /** - * Optional metadata related to the entity. - */ - metadata: EntityMeta; - - /** - * The specification data describing the entity itself. - */ - spec?: object; -}; - -/** - * Parses and validates descriptors. - * - * The output must be validated and well formed. - */ -export type DescriptorParser = { - /** - * Parses and validates a single raw descriptor. - * - * @param descriptor A raw descriptor object - * @returns A structure describing the parsed and validated descriptor - * @throws An Error if the descriptor was malformed - */ - parse(descriptor: object): Promise; -}; - -/** - * Parses and validates a single envelope into its materialized kind. - * - * These parsers may assume that the envelope is already validated and well - * formed. - */ -export type KindParser = { - /** - * Try to parse an envelope into a materialized kind. - * - * @param envelope A valid descriptor envelope - * @returns A materialized type, or undefined if the given version/kind is - * not meant to be handled by this parser - * @throws An Error if the type was handled and found to not be properly - * formatted - */ - tryParse( - envelope: DescriptorEnvelope, - ): Promise; -}; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index d67bc6a864..f495bd3146 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -15,4 +15,6 @@ */ export { plugin } from './plugin'; -export * from './api'; +export * from './api/CatalogClient'; +export * from './api/types'; +export * from './types'; From df4dcd8640f1d7ab0af5b4628ae29c85e71cbff8 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Fri, 29 May 2020 10:37:23 +0200 Subject: [PATCH 9/9] fix: merge errors --- .../src/database/Database.test.ts | 8 ++++---- .../src/database/DatabaseManager.test.ts | 4 ++-- plugins/catalog/src/api/types.ts | 6 +++--- plugins/catalog/src/data/utils.ts | 6 +++--- yarn.lock | 17 ++--------------- 5 files changed, 14 insertions(+), 27 deletions(-) diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index bb1a66ac7c..a808e7e567 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -19,16 +19,16 @@ import { getVoidLogger, NotFoundError, } from '@backstage/backend-common'; -import { Entity } from '@backstage/catalog-model'; import Knex from 'knex'; import path from 'path'; -import { Database } from './Database'; import { - AddDatabaseLocation, DbEntityRequest, DbEntityResponse, + Database, + AddDatabaseLocation, DbLocationsRow, -} from './types'; +} from '.'; +import { Entity } from '@backstage/catalog-model'; describe('Database', () => { let database: Knex; diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts index 9a4297904a..a65bbb7811 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.test.ts @@ -15,12 +15,12 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { Entity, EntityPolicy } from '@backstage/catalog-model'; import Knex from 'knex'; -import { IngestionModel } from '../ingestion/types'; import { Database } from './Database'; import { DatabaseManager } from './DatabaseManager'; import { DatabaseLocationUpdateLogStatus, DbLocationsRow } from './types'; +import { EntityPolicy, Entity } from '@backstage/catalog-model'; +import { IngestionModel } from '..'; describe('DatabaseManager', () => { describe('refreshLocations', () => { diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 6cc1dfd6cc..eb2cdca562 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createApiRef } from '@backstage/core'; -import { DescriptorEnvelope } from '../types'; +import { Entity } from '@backstage/catalog-model'; export const catalogApiRef = createApiRef({ id: 'plugin.catalog.service', @@ -23,6 +23,6 @@ export const catalogApiRef = createApiRef({ }); export interface CatalogApi { - getEntities(): Promise; - getEntityByName(name: string): Promise; + getEntities(): Promise; + getEntityByName(name: string): Promise; } diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts index 39a84cfbca..05fb2fee46 100644 --- a/plugins/catalog/src/data/utils.ts +++ b/plugins/catalog/src/data/utils.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DescriptorEnvelope } from '../api/types'; import { Component } from './component'; +import { Entity } from '@backstage/catalog-model'; -export function envelopeToComponent(envelope: DescriptorEnvelope): Component { +export function envelopeToComponent(envelope: Entity): Component { return { name: envelope.metadata?.name ?? '', kind: envelope.kind ?? 'unknown', - description: envelope.metadata?.description ?? 'placeholder', + description: envelope.metadata?.annotations?.description ?? 'placeholder', }; } diff --git a/yarn.lock b/yarn.lock index 8439f5b0b9..b34d5505d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4290,15 +4290,7 @@ "@types/passport" "*" "@types/passport-oauth2" "*" -"@types/passport-oauth2-refresh@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@types/passport-oauth2-refresh/-/passport-oauth2-refresh-1.1.1.tgz#cbe466d4fcac36182fd75bf55279c0b1e953c382" - integrity sha512-Tw0JvfDPv9asgFPACd9oOGCaD/0/Uyi+QF7fmrJC74cJKC6I8N8wwhJJHyfd1N2E/qaLgTh431lhOa9jicpNdg== - dependencies: - "@types/oauth" "*" - "@types/passport-oauth2" "*" - -"@types/passport-oauth2@*", "@types/passport-oauth2@^1.4.9": +"@types/passport-oauth2@*": version "1.4.9" resolved "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.9.tgz#134007c4b505a82548c9cb19094c5baeb2205c92" integrity sha512-QP0q+NVQOaIu2r0e10QWkiUA0Ya5mOBHRJN0UrI+LolMLOP1/VN4EVIpJ3xVwFo+xqNFRoFvFwJhBvKnk7kpUA== @@ -16110,12 +16102,7 @@ passport-google-oauth20@^2.0.0: dependencies: passport-oauth2 "1.x.x" -passport-oauth2-refresh@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/passport-oauth2-refresh/-/passport-oauth2-refresh-2.0.0.tgz#7b19c77ff3cc000819c69f6ad9e318450f57b85e" - integrity sha512-yXvCB6nem/O+WThhiyI3TlPXpzSGY+9+hy9OTx9QF8e9GInplyRHxHaaOhFylKvnof9UmWHAufQFZk8cO1Fb2g== - -passport-oauth2@1.x.x, passport-oauth2@^1.5.0: +passport-oauth2@1.x.x: version "1.5.0" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==