Merge branch 'master' of github.com:spotify/backstage into feat/star-components
* 'master' of github.com:spotify/backstage: chore(catalog): consistent use of named exports fix(core): Tabs useEffect dependency list Optional namespace and name as one part of URL Remove deleted UserBadge component from Sidebar story remove LoggedUserBadge make the sidebar pin button show up again feat(backend-common): add common code for service shell await promise.all when setting isSignedIn PinButton wip List auth providers in UserSettings Collapsible sidebar item for auth providers fix(core): lint error refactor(core): update tabs Fix tests /catalog/:namespace?/:kind/:name/ feat(core): add Tabs component
This commit is contained in:
@@ -18,13 +18,10 @@
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.7",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.7",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"esm": "^3.2.25",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.0",
|
||||
"helmet": "^3.22.0",
|
||||
"knex": "^0.21.1",
|
||||
"lodash": "^4.17.15",
|
||||
"morgan": "^1.10.0",
|
||||
|
||||
@@ -1,71 +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 {
|
||||
errorHandler,
|
||||
notFoundHandler,
|
||||
requestLoggingHandler,
|
||||
} from '@backstage/backend-common';
|
||||
import compression from 'compression';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { Logger } from 'winston';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
|
||||
import { HigherOrderOperation } from '../ingestion';
|
||||
import { createRouter } from './router';
|
||||
|
||||
export interface ApplicationOptions {
|
||||
enableCors: boolean;
|
||||
entitiesCatalog: EntitiesCatalog;
|
||||
locationsCatalog?: LocationsCatalog;
|
||||
higherOrderOperation?: HigherOrderOperation;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function createStandaloneApplication(
|
||||
options: ApplicationOptions,
|
||||
): Promise<express.Application> {
|
||||
const {
|
||||
enableCors,
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
higherOrderOperation,
|
||||
logger,
|
||||
} = options;
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
if (enableCors) {
|
||||
app.use(cors());
|
||||
}
|
||||
app.use(compression());
|
||||
app.use(express.json());
|
||||
app.use(requestLoggingHandler());
|
||||
app.use(
|
||||
'/catalog',
|
||||
await createRouter({
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
higherOrderOperation,
|
||||
logger,
|
||||
}),
|
||||
);
|
||||
app.use(notFoundHandler());
|
||||
app.use(errorHandler());
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -14,13 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createServiceBuilder } from '@backstage/backend-common';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
import { HigherOrderOperations } from '..';
|
||||
import { DatabaseEntitiesCatalog } from '../catalog/DatabaseEntitiesCatalog';
|
||||
import { DatabaseLocationsCatalog } from '../catalog/DatabaseLocationsCatalog';
|
||||
import { DatabaseManager } from '../database/DatabaseManager';
|
||||
import { HigherOrderOperations, LocationReaders } from '../ingestion';
|
||||
import { createStandaloneApplication } from './standaloneApplication';
|
||||
import { createRouter } from './router';
|
||||
import { LocationReaders } from '../ingestion';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
@@ -33,11 +35,11 @@ export async function startStandaloneServer(
|
||||
): Promise<Server> {
|
||||
const logger = options.logger.child({ service: 'catalog-backend' });
|
||||
|
||||
logger.debug('Creating application...');
|
||||
const db = await DatabaseManager.createInMemoryDatabase(logger);
|
||||
|
||||
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
|
||||
const locationsCatalog = new DatabaseLocationsCatalog(db);
|
||||
const locationReader = new LocationReaders(options.logger);
|
||||
const locationReader = new LocationReaders();
|
||||
const higherOrderOperation = new HigherOrderOperations(
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
@@ -45,25 +47,18 @@ export async function startStandaloneServer(
|
||||
logger,
|
||||
);
|
||||
|
||||
logger.debug('Creating application...');
|
||||
const app = await createStandaloneApplication({
|
||||
enableCors: options.enableCors,
|
||||
logger.debug('Starting application server...');
|
||||
const router = await createRouter({
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
higherOrderOperation,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.debug('Starting application server...');
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = app.listen(options.port, (err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Listening on port ${options.port}`);
|
||||
resolve(server);
|
||||
});
|
||||
const service = createServiceBuilder()
|
||||
.enableCors({ origin: 'http://localhost:3000' })
|
||||
.addRouter('/catalog', router);
|
||||
return await service.start().catch(err => {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,9 +77,20 @@ export class CatalogClient implements CatalogApi {
|
||||
this.cache.set(`get:${JSON.stringify(filter)}`, value);
|
||||
return value;
|
||||
}
|
||||
async getEntityByName(name: string): Promise<DescriptorEnvelope> {
|
||||
|
||||
async getEntity({
|
||||
name,
|
||||
namespace,
|
||||
kind,
|
||||
}: {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
kind: string;
|
||||
}): Promise<DescriptorEnvelope> {
|
||||
const response = await fetch(
|
||||
`${this.apiOrigin}${this.basePath}/entities/by-name/Component/default/${name}`,
|
||||
`${this.apiOrigin}${this.basePath}/entities/by-name/${kind}/${
|
||||
namespace ?? 'default'
|
||||
}/${name}`,
|
||||
);
|
||||
const entity = await response.json();
|
||||
if (entity) return entity;
|
||||
|
||||
@@ -23,9 +23,13 @@ export const catalogApiRef = createApiRef<CatalogApi>({
|
||||
});
|
||||
|
||||
export interface CatalogApi {
|
||||
getEntity(params: {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
kind: string;
|
||||
}): Promise<Entity>;
|
||||
getLocationById(id: String): Promise<Location | undefined>;
|
||||
getEntities(filter?: Record<string, string>): Promise<Entity[]>;
|
||||
getEntityByName(name: string): Promise<Entity>;
|
||||
addLocation(type: string, target: string): Promise<AddLocationResponse>;
|
||||
getLocationByEntity(entity: Entity): Promise<Location | undefined>;
|
||||
}
|
||||
|
||||
@@ -14,20 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import CatalogPage from './CatalogPage';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
ApiRegistry,
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
errorApiRef,
|
||||
storageApiRef,
|
||||
WebStorage,
|
||||
} from '@backstage/core';
|
||||
import { wrapInTestApp, MockErrorApi } from '@backstage/test-utils';
|
||||
import { MockErrorApi, wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { catalogApiRef } from '../..';
|
||||
import { CatalogApi } from '../../api/types';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { CatalogPage } from './CatalogPage';
|
||||
|
||||
describe('CatalogPage', () => {
|
||||
const mockErrorApi = new MockErrorApi();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { LocationSpec, Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
@@ -26,27 +27,27 @@ import {
|
||||
SupportButton,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import { LocationSpec, Entity } from '@backstage/catalog-model';
|
||||
|
||||
import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder';
|
||||
import { Button, makeStyles, Typography, Link } from '@material-ui/core';
|
||||
import GitHub from '@material-ui/icons/GitHub';
|
||||
import StarOutline from '@material-ui/icons/StarBorder';
|
||||
import Star from '@material-ui/icons/Star';
|
||||
|
||||
import Edit from '@material-ui/icons/Edit';
|
||||
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '../..';
|
||||
import { defaultFilter, filterGroups, dataResolvers } from '../../data/filters';
|
||||
import { entityToComponent, findLocationForEntityMeta } from '../../data/utils';
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntites';
|
||||
import {
|
||||
CatalogFilter,
|
||||
CatalogFilterItem,
|
||||
} from '../CatalogFilter/CatalogFilter';
|
||||
|
||||
import { useStarredEntities } from '../../hooks/useStarredEntites';
|
||||
|
||||
import CatalogTable from '../CatalogTable/CatalogTable';
|
||||
import { CatalogTable } from '../CatalogTable/CatalogTable';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
contentWrapper: {
|
||||
@@ -61,7 +62,7 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const CatalogPage: FC<{}> = () => {
|
||||
export const CatalogPage: FC<{}> = () => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const {
|
||||
starredEntities,
|
||||
@@ -215,5 +216,3 @@ const CatalogPage: FC<{}> = () => {
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default CatalogPage;
|
||||
|
||||
@@ -1,17 +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 { default } from './CatalogPage';
|
||||
@@ -16,7 +16,7 @@
|
||||
import * as React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import CatalogTable from './CatalogTable';
|
||||
import { CatalogTable } from './CatalogTable';
|
||||
import { Component } from '../../data/component';
|
||||
|
||||
const components: Component[] = [
|
||||
|
||||
@@ -17,9 +17,8 @@ import { Table, TableColumn } from '@backstage/core';
|
||||
import { Link } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { FC } from 'react';
|
||||
import { Link as RouterLink, generatePath } from 'react-router-dom';
|
||||
import { generatePath, Link as RouterLink } from 'react-router-dom';
|
||||
import { Component } from '../../data/component';
|
||||
|
||||
import { entityRoute } from '../../routes';
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
@@ -30,7 +29,15 @@ const columns: TableColumn[] = [
|
||||
render: (componentData: any) => (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={generatePath(entityRoute.path, { name: componentData.name })}
|
||||
to={generatePath(entityRoute.path, {
|
||||
optionalNamespaceAndName: [
|
||||
componentData.namespace,
|
||||
componentData.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':'),
|
||||
kind: componentData.kind,
|
||||
})}
|
||||
>
|
||||
{componentData.name}
|
||||
</Link>
|
||||
@@ -54,7 +61,7 @@ type CatalogTableProps = {
|
||||
actions?: any;
|
||||
};
|
||||
|
||||
const CatalogTable: FC<CatalogTableProps> = ({
|
||||
export const CatalogTable: FC<CatalogTableProps> = ({
|
||||
components,
|
||||
loading,
|
||||
error,
|
||||
@@ -87,5 +94,3 @@ const CatalogTable: FC<CatalogTableProps> = ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CatalogTable;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import ComponentContextMenu from './ComponentContextMenu';
|
||||
import { ComponentContextMenu } from './ComponentContextMenu';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
Popover,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Cancel from '@material-ui/icons/Cancel';
|
||||
import MoreVert from '@material-ui/icons/MoreVert';
|
||||
import SwapHoriz from '@material-ui/icons/SwapHoriz';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
// TODO(freben): It should probably instead be the case that Header sets the theme text color to white inside itself unconditionally instead
|
||||
const useStyles = makeStyles({
|
||||
@@ -38,7 +38,7 @@ type ComponentContextMenuProps = {
|
||||
onUnregisterComponent: () => void;
|
||||
};
|
||||
|
||||
const ComponentContextMenu: FC<ComponentContextMenuProps> = ({
|
||||
export const ComponentContextMenu: FC<ComponentContextMenuProps> = ({
|
||||
onUnregisterComponent,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>();
|
||||
@@ -94,5 +94,3 @@ const ComponentContextMenu: FC<ComponentContextMenuProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentContextMenu;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import ComponentMetadataCard from './ComponentMetadataCard';
|
||||
import { ComponentMetadataCard } from './ComponentMetadataCard';
|
||||
import { Component } from '../../data/component';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
|
||||
@@ -13,18 +13,16 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { InfoCard, Progress, StructuredMetadataTable } from '@backstage/core';
|
||||
import React, { FC } from 'react';
|
||||
import { Component } from '../../data/component';
|
||||
import { Progress, InfoCard, StructuredMetadataTable } from '@backstage/core';
|
||||
|
||||
type ComponentMetadataCardProps = {
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
component: Component | undefined;
|
||||
};
|
||||
const ComponentMetadataCard: FC<ComponentMetadataCardProps> = ({
|
||||
loading,
|
||||
component,
|
||||
}) => {
|
||||
|
||||
export const ComponentMetadataCard: FC<Props> = ({ loading, component }) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<InfoCard title="Metadata">
|
||||
@@ -41,4 +39,3 @@ const ComponentMetadataCard: FC<ComponentMetadataCardProps> = ({
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
export default ComponentMetadataCard;
|
||||
|
||||
@@ -13,18 +13,19 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import ComponentPage from './ComponentPage';
|
||||
import { ComponentPage } from './ComponentPage';
|
||||
import { render, wait } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
|
||||
import { catalogApiRef, CatalogApi } from '../../api/types';
|
||||
|
||||
const getTestProps = (componentName: string) => {
|
||||
const getTestProps = (name: string) => {
|
||||
return {
|
||||
match: {
|
||||
params: {
|
||||
name: componentName,
|
||||
optionalNamespaceAndName: name,
|
||||
kind: 'Component',
|
||||
},
|
||||
},
|
||||
history: {
|
||||
@@ -46,7 +47,7 @@ describe('ComponentPage', () => {
|
||||
[
|
||||
catalogApiRef,
|
||||
({
|
||||
async getEntityByName() {},
|
||||
async getEntity() {},
|
||||
} as unknown) as CatalogApi,
|
||||
],
|
||||
])}
|
||||
|
||||
@@ -13,34 +13,34 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import ComponentMetadataCard from '../ComponentMetadataCard/ComponentMetadataCard';
|
||||
import {
|
||||
Content,
|
||||
Header,
|
||||
pageTheme,
|
||||
Page,
|
||||
useApi,
|
||||
ErrorApi,
|
||||
errorApiRef,
|
||||
Header,
|
||||
HeaderTabs,
|
||||
Page,
|
||||
pageTheme,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
import ComponentContextMenu from '../ComponentContextMenu/ComponentContextMenu';
|
||||
import ComponentRemovalDialog from '../ComponentRemovalDialog/ComponentRemovalDialog';
|
||||
|
||||
import { SentryIssuesWidget } from '@backstage/plugin-sentry';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef } from '../..';
|
||||
import { entityToComponent } from '../../data/utils';
|
||||
import { Component } from '../../data/component';
|
||||
import { entityToComponent } from '../../data/utils';
|
||||
import { ComponentContextMenu } from '../ComponentContextMenu/ComponentContextMenu';
|
||||
import { ComponentMetadataCard } from '../ComponentMetadataCard/ComponentMetadataCard';
|
||||
import { ComponentRemovalDialog } from '../ComponentRemovalDialog/ComponentRemovalDialog';
|
||||
|
||||
const REDIRECT_DELAY = 1000;
|
||||
|
||||
type ComponentPageProps = {
|
||||
match: {
|
||||
params: {
|
||||
name: string;
|
||||
optionalNamespaceAndName: string;
|
||||
kind: string;
|
||||
};
|
||||
};
|
||||
history: {
|
||||
@@ -48,17 +48,18 @@ type ComponentPageProps = {
|
||||
};
|
||||
};
|
||||
|
||||
const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
|
||||
export const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const [removingPending, setRemovingPending] = useState(false);
|
||||
const showRemovalDialog = () => setConfirmationDialogOpen(true);
|
||||
const hideRemovalDialog = () => setConfirmationDialogOpen(false);
|
||||
const componentName = match.params.name;
|
||||
const { optionalNamespaceAndName, kind } = match.params;
|
||||
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
|
||||
const errorApi = useApi<ErrorApi>(errorApiRef);
|
||||
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { value: component, error, loading } = useAsync<Component>(async () => {
|
||||
const entity = await catalogApi.getEntityByName(match.params.name);
|
||||
const entity = await catalogApi.getEntity({ name, namespace, kind });
|
||||
const location = await catalogApi.getLocationByEntity(entity);
|
||||
return { ...entityToComponent(entity), location };
|
||||
});
|
||||
@@ -72,7 +73,7 @@ const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
|
||||
}
|
||||
}, [error, errorApi, history]);
|
||||
|
||||
if (componentName === '') {
|
||||
if (name === '') {
|
||||
history.push('/catalog');
|
||||
return null;
|
||||
}
|
||||
@@ -149,4 +150,3 @@ const ComponentPage: FC<ComponentPageProps> = ({ match, history }) => {
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
export default ComponentPage;
|
||||
|
||||
@@ -51,7 +51,7 @@ function useColocatedEntities(component: Component): AsyncState<Entity[]> {
|
||||
}, [catalogApi, component]);
|
||||
}
|
||||
|
||||
const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
|
||||
export const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onClose,
|
||||
@@ -114,5 +114,3 @@ const ComponentRemovalDialog: FC<ComponentRemovalDialogProps> = ({
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentRemovalDialog;
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ReactNode } from 'react';
|
||||
|
||||
export type Component = {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
kind: string;
|
||||
metadata: EntityMeta;
|
||||
description: ReactNode;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Component } from './component';
|
||||
export function entityToComponent(envelope: Entity): Component {
|
||||
return {
|
||||
name: envelope.metadata.name,
|
||||
namespace: envelope.metadata.namespace,
|
||||
kind: envelope.kind,
|
||||
metadata: envelope.metadata,
|
||||
description: envelope.metadata.annotations?.description ?? 'placeholder',
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import CatalogPage from './components/CatalogPage';
|
||||
import ComponentPage from './components/ComponentPage/ComponentPage';
|
||||
import { rootRoute, entityRoute } from './routes';
|
||||
import { CatalogPage } from './components/CatalogPage/CatalogPage';
|
||||
import { ComponentPage } from './components/ComponentPage/ComponentPage';
|
||||
import { entityRoute, rootRoute } from './routes';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'catalog',
|
||||
|
||||
@@ -25,6 +25,6 @@ export const rootRoute = createRouteRef({
|
||||
});
|
||||
export const entityRoute = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: '/catalog/:name/',
|
||||
path: '/catalog/:kind/:optionalNamespaceAndName/',
|
||||
title: 'Entity',
|
||||
});
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ const catalogApi: jest.Mocked<typeof catalogApiRef.T> = {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
addLocation: jest.fn((_a, _b) => new Promise(() => {})),
|
||||
getEntities: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
getEntity: jest.fn(),
|
||||
getLocationByEntity: jest.fn(),
|
||||
getLocationById: jest.fn(),
|
||||
};
|
||||
|
||||
+33
-28
@@ -54,34 +54,39 @@ export const RegisterComponentResultDialog: FC<Props> = ({
|
||||
The following components have been succefully created:
|
||||
</DialogContentText>
|
||||
<List>
|
||||
{entities.map((entity: any, index: number) => (
|
||||
<React.Fragment
|
||||
key={`${entity.metadata.namespace}-${entity.metadata.name}`}
|
||||
>
|
||||
<ListItem>
|
||||
<StructuredMetadataTable
|
||||
dense
|
||||
metadata={{
|
||||
name: entity.metadata.name,
|
||||
type: entity.spec.type,
|
||||
link: (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={generatePath(entityRoute.path, {
|
||||
name: entity.metadata.name,
|
||||
})}
|
||||
>
|
||||
{generatePath(entityRoute.path, {
|
||||
name: entity.metadata.name,
|
||||
})}
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{index < entities.length - 1 && <Divider component="li" />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{entities.map((entity: any, index: number) => {
|
||||
const entityPath = generatePath(entityRoute.path, {
|
||||
optionalNamespaceAndName: [
|
||||
entity.metadata.namespace,
|
||||
entity.metadata.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':'),
|
||||
kind: entity.kind,
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment
|
||||
key={`${entity.metadata.namespace}-${entity.metadata.name}`}
|
||||
>
|
||||
<ListItem>
|
||||
<StructuredMetadataTable
|
||||
dense
|
||||
metadata={{
|
||||
name: entity.metadata.name,
|
||||
type: entity.spec.type,
|
||||
link: (
|
||||
<Link component={RouterLink} to={entityPath}>
|
||||
{entityPath}
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{index < entities.length - 1 && <Divider component="li" />}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
Reference in New Issue
Block a user