feat(catalog-client): create a basic catalog client (#3166)

This commit is contained in:
Fredrik Adelöw
2020-11-04 13:18:03 +01:00
committed by GitHub
parent bb547d5bf6
commit 42b0dbddcb
22 changed files with 155 additions and 61 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ backend:
credentials: true
csp:
connect-src: ["'self'", 'http:', 'https:']
# workingDirectory: /tmp # Use this to configure a working direcotry for the scaffolder, defaults to the OS temp-dir
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
# See README.md in the proxy-backend plugin for information on the configuration format
proxy:
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+17
View File
@@ -0,0 +1,17 @@
# Catalog Client
Contains a frontend and backend compatible client for communicating with the
Backstage Catalog.
Backend code may import and use this package directly.
However, frontend code will not want to import this package directly - use the
`@backstage/plugin-catalog` package instead, which re-exports all of the types
and classes from this package. Thereby, you will also gain access to its
`catalogApiRef`.
## Links
- [Default frontend part of the catalog](https://github.com/spotify/backstage/tree/master/plugins/catalog)
- [Default backend part of the catalog](https://github.com/spotify/backstage/tree/master/plugins/catalog-backend)
- [The Backstage homepage](https://backstage.io)
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@backstage/catalog-client",
"version": "0.2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli build",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-model": "^0.2.0",
"@backstage/config": "^0.1.1",
"cross-fetch": "^3.0.6"
},
"devDependencies": {
"@backstage/cli": "^0.2.0",
"@types/jest": "^26.0.7",
"msw": "^0.21.2"
},
"files": [
"dist"
]
}
@@ -18,17 +18,22 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { CatalogClient } from './CatalogClient';
import { Entity } from '@backstage/catalog-model';
import { UrlPatternDiscovery } from '@backstage/core';
import { msw } from '@backstage/test-utils';
import { DiscoveryApi } from './types';
const server = setupServer();
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
const discoveryApi: DiscoveryApi = {
async getBaseUrl(_pluginId) {
return mockBaseUrl;
},
};
describe('CatalogClient', () => {
let client = new CatalogClient({ discoveryApi });
let client: CatalogClient;
msw.setupDefaultHandlers(server);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
beforeEach(() => {
client = new CatalogClient({ discoveryApi });
@@ -16,16 +16,17 @@
import {
Entity,
EntityName,
Location,
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import fetch from 'cross-fetch';
import {
AddLocationRequest,
AddLocationResponse,
CatalogApi,
EntityCompoundName,
DiscoveryApi,
} from './types';
import { DiscoveryApi } from '@backstage/core';
export class CatalogClient implements CatalogApi {
private readonly discoveryApi: DiscoveryApi;
@@ -85,9 +86,7 @@ export class CatalogClient implements CatalogApi {
return await this.getRequired(path);
}
async getEntityByName(
compoundName: EntityCompoundName,
): Promise<Entity | undefined> {
async getEntityByName(compoundName: EntityName): Promise<Entity | undefined> {
const { kind, namespace = 'default', name } = compoundName;
return this.getOptional(`/entities/by-name/${kind}/${namespace}/${name}`);
}
+18
View File
@@ -0,0 +1,18 @@
/*
* 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 { CatalogClient } from './CatalogClient';
export type { CatalogApi } from './types';
+17
View File
@@ -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 {};
@@ -14,26 +14,11 @@
* limitations under the License.
*/
import { createApiRef } from '@backstage/core';
import { Entity, Location } from '@backstage/catalog-model';
export const catalogApiRef = createApiRef<CatalogApi>({
id: 'plugin.catalog.service',
description:
'Used by the Catalog plugin to make requests to accompanying backend',
});
export type EntityCompoundName = {
kind: string;
namespace?: string;
name: string;
};
import { Entity, EntityName, Location } from '@backstage/catalog-model';
export interface CatalogApi {
getLocationById(id: String): Promise<Location | undefined>;
getEntityByName(
compoundName: EntityCompoundName,
): Promise<Entity | undefined>;
getEntityByName(name: EntityName): Promise<Entity | undefined>;
getEntities(filter?: Record<string, string | string[]>): Promise<Entity[]>;
addLocation(location: AddLocationRequest): Promise<AddLocationResponse>;
getLocationByEntity(entity: Entity): Promise<Location | undefined>;
@@ -50,3 +35,10 @@ export type AddLocationResponse = {
location: Location;
entities: Entity[];
};
/**
* This is a copy of the core DiscoveryApi, to avoid importing core.
*/
export type DiscoveryApi = {
getBaseUrl(pluginId: string): Promise<string>;
};
+1
View File
@@ -21,6 +21,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/catalog-client": "^0.2.0",
"@backstage/catalog-model": "^0.2.0",
"@backstage/core": "^0.2.0",
"@backstage/plugin-scaffolder": "^0.2.0",
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import React, { FC } from 'react';
import { useApi } from '@backstage/core';
import { catalogApiRef } from '../../api/types';
import { useAsync } from 'react-use';
import { CircularProgress, useTheme } from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
import { catalogApiRef } from '../../plugin';
export const AllServicesCount: FC<{}> = () => {
export const AllServicesCount = () => {
const theme = useTheme();
const catalogApi = useApi(catalogApiRef);
const { value, loading } = useAsync(() => catalogApi.getEntities());
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import {
ApiProvider,
@@ -25,8 +26,8 @@ import {
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { CatalogApi, catalogApiRef } from '../../api/types';
import { EntityFilterGroupsProvider } from '../../filter';
import { catalogApiRef } from '../../plugin';
import { ButtonGroup, CatalogFilter } from './CatalogFilter';
describe('Catalog Filter', () => {
@@ -14,21 +14,21 @@
* limitations under the License.
*/
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import {
ApiProvider,
ApiRegistry,
IdentityApi,
identityApiRef,
storageApiRef,
ProfileInfo,
storageApiRef,
} from '@backstage/core';
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { catalogApiRef } from '../..';
import { CatalogApi } from '../../api/types';
import { EntityFilterGroupsProvider } from '../../filter';
import { catalogApiRef } from '../../plugin';
import { CatalogPage } from './CatalogPage';
describe('CatalogPage', () => {
@@ -29,9 +29,9 @@ import SettingsIcon from '@material-ui/icons/Settings';
import StarIcon from '@material-ui/icons/Star';
import React, { useCallback, useMemo, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { catalogApiRef } from '../../api/types';
import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter';
import { useStarredEntities } from '../../hooks/useStarredEntities';
import { catalogApiRef } from '../../plugin';
import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter';
import { CatalogTable } from '../CatalogTable/CatalogTable';
import { ResultsFilter } from '../ResultsFilter/ResultsFilter';
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import {
ApiProvider,
@@ -25,8 +26,8 @@ import {
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import React from 'react';
import { CatalogApi, catalogApiRef } from '../../api/types';
import { EntityFilterGroupsProvider } from '../../filter';
import { catalogApiRef } from '../../plugin';
import { ResultsFilter } from './ResultsFilter';
describe('Results Filter', () => {
@@ -15,7 +15,7 @@
*/
import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model';
import { Progress, useApi, alertApiRef } from '@backstage/core';
import { alertApiRef, Progress, useApi } from '@backstage/core';
import {
Button,
Dialog,
@@ -31,7 +31,7 @@ import Alert from '@material-ui/lab/Alert';
import React, { FC } from 'react';
import { useAsync } from 'react-use';
import { AsyncState } from 'react-use/lib/useAsync';
import { catalogApiRef } from '../../api/types';
import { catalogApiRef } from '../../plugin';
type Props = {
open: boolean;
@@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model';
import { useApi } from '@backstage/core';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useAsyncFn } from 'react-use';
import { catalogApiRef } from '../api/types';
import { catalogApiRef } from '../plugin';
import { filterGroupsContext, FilterGroupsContext } from './context';
import {
EntityFilterFn,
@@ -15,13 +15,13 @@
*/
import { ApiProvider, ApiRegistry, storageApiRef } from '@backstage/core';
import { MockStorageApi } from '@backstage/test-utils';
import { act, renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { catalogApiRef } from '../api/types';
import { catalogApiRef } from '../plugin';
import { EntityFilterGroupsProvider } from './EntityFilterGroupsProvider';
import { FilterGroupStatesReady, FilterGroup } from './types';
import { FilterGroup, FilterGroupStatesReady } from './types';
import { useEntityFilterGroup } from './useEntityFilterGroup';
import { MockStorageApi } from '@backstage/test-utils';
describe('useEntityFilterGroup', () => {
let catalogApi: jest.Mocked<typeof catalogApiRef.T>;
+1 -1
View File
@@ -18,8 +18,8 @@ import { errorApiRef, useApi } from '@backstage/core';
import { createContext, useContext, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { useAsync } from 'react-use';
import { catalogApiRef } from '../api/types';
import { useEntityCompoundName } from '../components/useEntityCompoundName';
import { catalogApiRef } from '../plugin';
type EntityLoadingStatus = {
entity?: Entity;
+6 -7
View File
@@ -14,12 +14,11 @@
* limitations under the License.
*/
export { plugin } from './plugin';
export * from './api/CatalogClient';
export * from './api/types';
export * from './routes';
export { useEntityCompoundName } from './components/useEntityCompoundName';
export { Router } from './components/Router';
export { useEntity, EntityContext } from './hooks/useEntity';
export * from '@backstage/catalog-client';
export { AboutCard } from './components/AboutCard';
export { EntityPageLayout } from './components/EntityPageLayout';
export { Router } from './components/Router';
export { useEntityCompoundName } from './components/useEntityCompoundName';
export { EntityContext, useEntity } from './hooks/useEntity';
export { catalogApiRef, plugin } from './plugin';
export * from './routes';
+9 -3
View File
@@ -14,13 +14,19 @@
* limitations under the License.
*/
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import {
createPlugin,
createApiFactory,
createApiRef,
createPlugin,
discoveryApiRef,
} from '@backstage/core';
import { catalogApiRef } from './api/types';
import { CatalogClient } from './api/CatalogClient';
export const catalogApiRef = createApiRef<CatalogApi>({
id: 'plugin.catalog.service',
description:
'Used by the Catalog plugin to make requests to accompanying backend',
});
export const plugin = createPlugin({
id: 'catalog',
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TechDocsHome } from './TechDocsHome';
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import { ApiRegistry, ApiProvider } from '@backstage/core-api';
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog';
import { Entity } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core-api';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
import { wrapInTestApp } from '@backstage/test-utils';
import { render } from '@testing-library/react';
import React from 'react';
import { TechDocsHome } from './TechDocsHome';
describe('TechDocs Home', () => {
const catalogApi: Partial<CatalogApi> = {