Merge branch 'master' of github.com:spotify/backstage into migrate-to-msw
* 'master' of github.com:spotify/backstage: (139 commits) Cleanup Update PinButton.test.tsx feat: update github insights plugin version (#2973) Ignore IntelliJ *.iml files (#2971) chore(deps): bump rollup-plugin-dts from 1.4.11 to 1.4.13 fix the plugin card on plugins page align 'Add to Marketplace' button on plugins page fix the PluginGrid on mobiles sizes use getBy query instead of queryBy when asserting for elements present in document (#2951) Update PinButton.tsx Add test case for Progress component (#2953) fix the styling of footer copy on mobile add changeset handle the case where no entities are available to show core-api: work around issue with ApiRef export const declarations core-api: move utility api system implementation into apis/system Update docs regarding npm config ignore-scripts flag Another try Fix Core Features configuration id (#2948) Fix test? ...
This commit is contained in:
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 { Button, makeStyles, Typography } from '@material-ui/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { CodeSnippet, EmptyState } from '@backstage/core';
|
||||
|
||||
const COMPONENT_YAML = `# Example
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: example
|
||||
spec:
|
||||
type: service
|
||||
lifecycle: production
|
||||
owner: guest
|
||||
implementsApis:
|
||||
- example-api
|
||||
`;
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
code: {
|
||||
borderRadius: 6,
|
||||
margin: `${theme.spacing(2)}px 0px`,
|
||||
background: theme.palette.type === 'dark' ? '#444' : '#fff',
|
||||
},
|
||||
}));
|
||||
|
||||
export const MissingImplementsApisEmptyState = () => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<EmptyState
|
||||
missing="field"
|
||||
title="No APIs implemented by this entity"
|
||||
description={
|
||||
<Typography>
|
||||
Components can implement APIs that are displayed on this page. You
|
||||
need to fill the <code>implementsApis</code> field to enable this
|
||||
tool.
|
||||
</Typography>
|
||||
}
|
||||
action={
|
||||
<>
|
||||
<Typography variant="body1">
|
||||
Link an API to your component as shown in the highlighted example
|
||||
below:
|
||||
</Typography>
|
||||
<div className={classes.code}>
|
||||
<CodeSnippet
|
||||
text={COMPONENT_YAML}
|
||||
language="yaml"
|
||||
showLineNumbers
|
||||
highlightedNumbers={[10, 11]}
|
||||
customStyle={{ background: 'inherit', fontSize: '115%' }}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href="https://backstage.io/docs/features/software-catalog/descriptor-format#specimplementsapis-optional"
|
||||
>
|
||||
Read more
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 { MissingImplementsApisEmptyState } from './MissingImplementsApisEmptyState';
|
||||
@@ -17,20 +17,17 @@
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Route, Routes } from 'react-router';
|
||||
import { WarningPanel } from '@backstage/core';
|
||||
import { catalogRoute } from '../routes';
|
||||
import { EntityPageApi } from './EntityPageApi';
|
||||
import { MissingImplementsApisEmptyState } from './MissingImplementsApisEmptyState';
|
||||
|
||||
const isPluginApplicableToEntity = (entity: Entity) => {
|
||||
return ((entity.spec?.implementsApis as string[]) || []).length > 0;
|
||||
};
|
||||
|
||||
export const Router = ({ entity }: { entity: Entity }) =>
|
||||
// TODO(shmidt-i): move warning to a separate standardized component
|
||||
!isPluginApplicableToEntity(entity) ? (
|
||||
<WarningPanel title="API Docs plugin:">
|
||||
The entity doesn't implement any APIs.
|
||||
</WarningPanel>
|
||||
<MissingImplementsApisEmptyState />
|
||||
) : (
|
||||
<Routes>
|
||||
<Route
|
||||
|
||||
@@ -13,14 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { ApiEntityPage, getPageTheme } from './ApiEntityPage';
|
||||
import { ApiEntityPage } from './ApiEntityPage';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
@@ -71,32 +69,3 @@ describe('ApiEntityPage', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPageTheme', () => {
|
||||
const defaultPageTheme = getPageTheme();
|
||||
it.each(['service', 'app', 'library', 'tool', 'documentation', 'website'])(
|
||||
'should select right theme for predefined type: %p ̰ ',
|
||||
type => {
|
||||
const theme = getPageTheme(({
|
||||
spec: {
|
||||
type,
|
||||
},
|
||||
} as any) as Entity);
|
||||
expect(theme).toBeDefined();
|
||||
expect(theme).not.toBe(defaultPageTheme);
|
||||
},
|
||||
);
|
||||
|
||||
it('should select default theme for unknown/unspecified types', () => {
|
||||
const theme1 = getPageTheme(({
|
||||
spec: {
|
||||
type: 'unknown-type',
|
||||
},
|
||||
} as any) as Entity);
|
||||
const theme2 = getPageTheme(({
|
||||
spec: {},
|
||||
} as any) as Entity);
|
||||
expect(theme1).toBe(defaultPageTheme);
|
||||
expect(theme2).toBe(defaultPageTheme);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
errorApiRef,
|
||||
Header,
|
||||
Page,
|
||||
pageTheme,
|
||||
PageTheme,
|
||||
Progress,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
@@ -54,11 +52,6 @@ function headerProps(
|
||||
};
|
||||
}
|
||||
|
||||
export const getPageTheme = (entity?: Entity): PageTheme => {
|
||||
const themeKey = entity?.spec?.type?.toString() ?? 'home';
|
||||
return pageTheme[themeKey] ?? pageTheme.home;
|
||||
};
|
||||
|
||||
type EntityPageTitleProps = {
|
||||
title: string;
|
||||
entity: Entity | undefined;
|
||||
@@ -107,7 +100,7 @@ export const ApiEntityPage = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Page theme={getPageTheme(entity)}>
|
||||
<Page themeId={entity?.spec?.type?.toString() ?? 'home'}>
|
||||
<Header
|
||||
title={<EntityPageTitle title={headerTitle} entity={entity} />}
|
||||
pageTitleOverride={headerTitle}
|
||||
|
||||
@@ -14,22 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Header, Page, pageTheme } from '@backstage/core';
|
||||
import { Header, Page } from '@backstage/core';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ApiExplorerLayout = ({ children }: Props) => {
|
||||
return (
|
||||
<Page theme={pageTheme.home}>
|
||||
<Header
|
||||
title="APIs"
|
||||
subtitle="Backstage API Explorer"
|
||||
pageTitleOverride="APIs"
|
||||
/>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
export const ApiExplorerLayout = ({ children }: Props) => (
|
||||
<Page themeId="home">
|
||||
<Header
|
||||
title="APIs"
|
||||
subtitle="Backstage API Explorer"
|
||||
pageTitleOverride="APIs"
|
||||
/>
|
||||
{children}
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -72,6 +72,6 @@ describe('ApiCatalogPage', () => {
|
||||
// https://github.com/mbrn/material-table/issues/1293
|
||||
it('should render', async () => {
|
||||
const { findByText } = renderWrapped(<ApiExplorerPage />);
|
||||
expect(await findByText(/APIs \(2\)/)).toBeInTheDocument();
|
||||
expect(await findByText(/Backstage API Explorer/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,7 +44,6 @@ export const ApiExplorerPage = () => {
|
||||
<SupportButton>All your APIs</SupportButton>
|
||||
</ContentHeader>
|
||||
<ApiExplorerTable
|
||||
titlePreamble="APIs"
|
||||
entities={matchingEntities!}
|
||||
loading={loading}
|
||||
error={error}
|
||||
|
||||
@@ -53,7 +53,6 @@ describe('ApiCatalogTable component', () => {
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<ApiExplorerTable
|
||||
titlePreamble="APIs"
|
||||
entities={[]}
|
||||
loading={false}
|
||||
error={{ code: 'error' }}
|
||||
@@ -71,15 +70,10 @@ describe('ApiCatalogTable component', () => {
|
||||
const rendered = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<ApiExplorerTable
|
||||
titlePreamble="APIs"
|
||||
entities={entites}
|
||||
loading={false}
|
||||
/>
|
||||
<ApiExplorerTable entities={entites} loading={false} />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByText(/APIs \(3\)/)).toBeInTheDocument();
|
||||
expect(rendered.getByText(/api1/)).toBeInTheDocument();
|
||||
expect(rendered.getByText(/api2/)).toBeInTheDocument();
|
||||
expect(rendered.getByText(/api3/)).toBeInTheDocument();
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model';
|
||||
import { Table, TableColumn, useApi } from '@backstage/core';
|
||||
import { Table, TableFilter, TableColumn, useApi } from '@backstage/core';
|
||||
import { Chip, Link } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React from 'react';
|
||||
@@ -90,9 +90,27 @@ const columns: TableColumn<Entity>[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const filters: TableFilter[] = [
|
||||
{
|
||||
column: 'Owner',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
column: 'Type',
|
||||
type: 'multiple-select',
|
||||
},
|
||||
{
|
||||
column: 'Lifecycle',
|
||||
type: 'multiple-select',
|
||||
},
|
||||
{
|
||||
column: 'Tags',
|
||||
type: 'checkbox-tree',
|
||||
},
|
||||
];
|
||||
|
||||
type ExplorerTableProps = {
|
||||
entities: Entity[];
|
||||
titlePreamble: string;
|
||||
loading: boolean;
|
||||
error?: any;
|
||||
};
|
||||
@@ -101,7 +119,6 @@ export const ApiExplorerTable = ({
|
||||
entities,
|
||||
loading,
|
||||
error,
|
||||
titlePreamble,
|
||||
}: ExplorerTableProps) => {
|
||||
if (error) {
|
||||
return (
|
||||
@@ -114,7 +131,7 @@ export const ApiExplorerTable = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Table<Entity>
|
||||
<Table
|
||||
isLoading={loading}
|
||||
columns={columns}
|
||||
options={{
|
||||
@@ -123,8 +140,8 @@ export const ApiExplorerTable = ({
|
||||
loadingType: 'linear',
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
title={`${titlePreamble} (${(entities && entities.length) || 0})`}
|
||||
data={entities}
|
||||
filters={filters}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
|
||||
import { useAsyncRetry } from 'react-use';
|
||||
import { errorApiRef, useApi } from '@backstage/core';
|
||||
import { ApiEntity, ComponentEntity } from '@backstage/catalog-model';
|
||||
import {
|
||||
ApiEntity,
|
||||
ComponentEntity,
|
||||
parseEntityName,
|
||||
} from '@backstage/catalog-model';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog';
|
||||
import { useComponentApiNames } from './useComponentApiNames';
|
||||
|
||||
@@ -43,10 +47,20 @@ export function useComponentApiEntities({
|
||||
await Promise.all(
|
||||
apiNames.map(async name => {
|
||||
try {
|
||||
const api = (await catalogApi.getEntityByName({
|
||||
kind: 'API',
|
||||
name,
|
||||
})) as ApiEntity | undefined;
|
||||
const apiEntityName = parseEntityName(name, {
|
||||
defaultNamespace: entity.metadata.namespace,
|
||||
defaultKind: 'API',
|
||||
});
|
||||
|
||||
if (apiEntityName.kind !== 'API') {
|
||||
throw new Error(
|
||||
`Referenced entity of kind "${apiEntityName.kind}" as an API`,
|
||||
);
|
||||
}
|
||||
|
||||
const api = (await catalogApi.getEntityByName(apiEntityName)) as
|
||||
| ApiEntity
|
||||
| undefined;
|
||||
|
||||
if (api) {
|
||||
resultMap.set(api.metadata.name, api);
|
||||
|
||||
Reference in New Issue
Block a user