app-next: initial entity page implementation with cards and content
Co-authored-by: Camila Belo <camilaibs@gmail.com> Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
committed by
Camila Belo
parent
39cd2d7a1f
commit
b34e8bf3a3
@@ -17,6 +17,7 @@
|
||||
import React from 'react';
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { pagesPlugin } from './examples/pagesPlugin';
|
||||
import { entityPagePlugins } from './examples/entityPages';
|
||||
import graphiqlPlugin from '@backstage/plugin-graphiql/alpha';
|
||||
import techRadarPlugin from '@backstage/plugin-tech-radar/alpha';
|
||||
import userSettingsPlugin from '@backstage/plugin-user-settings/alpha';
|
||||
@@ -120,6 +121,7 @@ const app = createApp({
|
||||
techdocsPlugin,
|
||||
userSettingsPlugin,
|
||||
homePlugin,
|
||||
...entityPagePlugins,
|
||||
...collectedLegacyPlugins,
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* 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, { useEffect } from 'react';
|
||||
import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
|
||||
import {
|
||||
AnyExtensionInputMap,
|
||||
Extension,
|
||||
ExtensionBoundary,
|
||||
ExtensionInputValues,
|
||||
PortableSchema,
|
||||
RouteRef,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionDataRef,
|
||||
createExtensionInput,
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
createSchemaFromZod,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
AsyncEntityProvider,
|
||||
EntityLoadingStatus,
|
||||
catalogApiRef,
|
||||
entityRouteRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { Expand } from '../../../frontend-plugin-api/src/types';
|
||||
import { EntityAboutCard, EntityLayout } from '@backstage/plugin-catalog';
|
||||
import {
|
||||
useApi,
|
||||
errorApiRef,
|
||||
useRouteRefParams,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { useNavigate } from 'react-router';
|
||||
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
|
||||
export const useEntityFromUrl = (): EntityLoadingStatus => {
|
||||
const { kind, namespace, name } = useRouteRefParams(entityRouteRef);
|
||||
const navigate = useNavigate();
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const {
|
||||
value: entity,
|
||||
error,
|
||||
loading,
|
||||
retry: refresh,
|
||||
} = useAsyncRetry(
|
||||
() => catalogApi.getEntityByRef({ kind, namespace, name }),
|
||||
[catalogApi, kind, namespace, name],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!name) {
|
||||
errorApi.post(new Error('No name provided!'));
|
||||
navigate('/');
|
||||
}
|
||||
}, [errorApi, navigate, error, loading, entity, name]);
|
||||
|
||||
return { entity, loading, error, refresh };
|
||||
};
|
||||
|
||||
export const titleExtensionDataRef = createExtensionDataRef<string>(
|
||||
'plugin.catalog.entity.content.title',
|
||||
);
|
||||
|
||||
const CatalogEntityPage = createPageExtension({
|
||||
id: 'plugin.catalog.page.entity',
|
||||
defaultPath: '/catalog/:namespace/:kind/:name',
|
||||
routeRef: convertLegacyRouteRef(entityRouteRef),
|
||||
inputs: {
|
||||
contents: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
path: coreExtensionData.routePath,
|
||||
routeRef: coreExtensionData.routeRef.optional(),
|
||||
title: titleExtensionDataRef,
|
||||
}),
|
||||
},
|
||||
loader: async ({ inputs }) => {
|
||||
const Component = () => {
|
||||
return (
|
||||
<AsyncEntityProvider {...useEntityFromUrl()}>
|
||||
<EntityLayout>
|
||||
{inputs.contents.map(content => (
|
||||
<EntityLayout.Route
|
||||
key={content.path}
|
||||
path={content.path}
|
||||
title={content.title}
|
||||
>
|
||||
{content.element}
|
||||
</EntityLayout.Route>
|
||||
))}
|
||||
</EntityLayout>
|
||||
</AsyncEntityProvider>
|
||||
);
|
||||
};
|
||||
return <Component />;
|
||||
},
|
||||
});
|
||||
|
||||
export function createEntityCardExtension<
|
||||
TConfig,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
id: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
loader: (options: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
}): Extension<TConfig> {
|
||||
return createExtension({
|
||||
id: `entity.content.${options.id}`,
|
||||
attachTo: options.attachTo ?? {
|
||||
id: 'entity.content.overview',
|
||||
input: 'cards',
|
||||
},
|
||||
disabled: options.disabled ?? true,
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
inputs: options.inputs,
|
||||
configSchema: options.configSchema,
|
||||
factory({ bind, config, inputs, source }) {
|
||||
const LazyComponent = React.lazy(() =>
|
||||
options
|
||||
.loader({ config, inputs })
|
||||
.then(element => ({ default: () => element })),
|
||||
);
|
||||
|
||||
bind({
|
||||
element: (
|
||||
<ExtensionBoundary source={source}>
|
||||
<React.Suspense fallback="...">
|
||||
<LazyComponent />
|
||||
</React.Suspense>
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createEntityContentExtension<
|
||||
TConfig extends { path: string; title: string },
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(
|
||||
options: (
|
||||
| {
|
||||
defaultPath: string;
|
||||
defaultTitle: string;
|
||||
}
|
||||
| {
|
||||
configSchema: PortableSchema<TConfig>;
|
||||
}
|
||||
) & {
|
||||
id: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
routeRef?: RouteRef;
|
||||
loader: (options: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
},
|
||||
): Extension<TConfig> {
|
||||
const configSchema =
|
||||
'configSchema' in options
|
||||
? options.configSchema
|
||||
: (createSchemaFromZod(z =>
|
||||
z.object({
|
||||
path: z.string().default(options.defaultPath),
|
||||
title: z.string().default(options.defaultTitle),
|
||||
}),
|
||||
) as PortableSchema<TConfig>);
|
||||
|
||||
return createExtension({
|
||||
id: `entity.content.${options.id}`,
|
||||
attachTo: options.attachTo ?? {
|
||||
id: 'plugin.catalog.page.entity',
|
||||
input: 'contents',
|
||||
},
|
||||
disabled: options.disabled ?? true,
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
path: coreExtensionData.routePath,
|
||||
routeRef: coreExtensionData.routeRef.optional(),
|
||||
title: titleExtensionDataRef,
|
||||
},
|
||||
inputs: options.inputs,
|
||||
configSchema,
|
||||
factory({ bind, config, inputs, source }) {
|
||||
const LazyComponent = React.lazy(() =>
|
||||
options
|
||||
.loader({ config, inputs })
|
||||
.then(element => ({ default: () => element })),
|
||||
);
|
||||
|
||||
bind({
|
||||
path: config.path,
|
||||
element: (
|
||||
<ExtensionBoundary source={source}>
|
||||
<React.Suspense fallback="...">
|
||||
<LazyComponent />
|
||||
</React.Suspense>
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
routeRef: options.routeRef,
|
||||
title: config.title,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const entityAboutCardExtension = createEntityCardExtension({
|
||||
id: 'about',
|
||||
disabled: false,
|
||||
loader: async () => <EntityAboutCard variant="gridItem" />,
|
||||
// entityFilter: isDerp,
|
||||
});
|
||||
|
||||
const overviewContentExtension = createEntityContentExtension({
|
||||
id: 'overview',
|
||||
defaultPath: '/',
|
||||
defaultTitle: 'Overview',
|
||||
disabled: false,
|
||||
inputs: {
|
||||
cards: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
loader: async ({ inputs }) => (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{inputs.cards.map(card => (
|
||||
<Grid item md={6} xs={12}>
|
||||
{card.element}
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
),
|
||||
});
|
||||
|
||||
const bonusTechdocsPlugin = createPlugin({
|
||||
id: 'techdocs-entity',
|
||||
extensions: [
|
||||
createEntityContentExtension({
|
||||
id: 'techdocs',
|
||||
defaultPath: 'docs',
|
||||
defaultTitle: 'TechDocs',
|
||||
disabled: false,
|
||||
loader: () =>
|
||||
import('@backstage/plugin-techdocs').then(m => (
|
||||
<m.EmbeddedDocsRouter />
|
||||
)),
|
||||
// entityFilter: isPullRequestsAvailable,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const entityPagePlugins = [
|
||||
createPlugin({
|
||||
id: 'entity-pages',
|
||||
extensions: [
|
||||
CatalogEntityPage,
|
||||
overviewContentExtension,
|
||||
entityAboutCardExtension,
|
||||
],
|
||||
}),
|
||||
bonusTechdocsPlugin,
|
||||
// deploymentsPlugin,
|
||||
];
|
||||
@@ -231,22 +231,6 @@ const CatalogIndexPage = createPageExtension({
|
||||
},
|
||||
});
|
||||
|
||||
const CatalogEntityPage = createPageExtension({
|
||||
id: 'plugin.catalog.page.entity',
|
||||
defaultPath: '/catalog/:namespace/:kind/:name',
|
||||
routeRef: convertLegacyRouteRef(entityRouteRef),
|
||||
loader: async () => {
|
||||
const Component = () => {
|
||||
return (
|
||||
<AsyncEntityProvider {...useEntityFromUrl()}>
|
||||
<div>🚧 Work In Progress</div>
|
||||
</AsyncEntityProvider>
|
||||
);
|
||||
};
|
||||
return <Component />;
|
||||
},
|
||||
});
|
||||
|
||||
const CatalogNavItem = createNavItemExtension({
|
||||
id: 'catalog.nav.index',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
@@ -279,55 +263,7 @@ export default createPlugin({
|
||||
CatalogEntityProcessingStatusFilter,
|
||||
CatalogEntityNamespaceFilter,
|
||||
CatalogIndexPage,
|
||||
CatalogEntityPage,
|
||||
// CatalogEntityPage,
|
||||
CatalogNavItem,
|
||||
],
|
||||
});
|
||||
|
||||
/// IMAGINE THIS IS IN A DIFFERENT PLUGIN
|
||||
|
||||
// inside the @backstage/plugin-github-pull-requests plugin
|
||||
import {
|
||||
createEntityCardExtension,
|
||||
createEntityContentExtension,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
|
||||
const githubPullRequestsPlugin = createPlugin({
|
||||
id: 'github-pull-requests',
|
||||
extensions: [
|
||||
createEntityCardExtension({
|
||||
id: 'github-pull-requests',
|
||||
loader: () => import('./PullRequestsCard').then(m => m.PullRequestsCard),
|
||||
entityFilter: isPullRequestsAvailable,
|
||||
}),
|
||||
createEntityContentExtension({
|
||||
id: 'github-pull-requests',
|
||||
defaultPath: 'github-pull-requests',
|
||||
defaultTitle: 'GitHub Pull Requests',
|
||||
loader: () =>
|
||||
import('./PullRequestsContent').then(m => m.PullRequestsContent),
|
||||
entityFilter: isPullRequestsAvailable,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// /deployments
|
||||
const deploymentsPlugin = createPlugin({
|
||||
id: 'github-pull-requests',
|
||||
extensions: [
|
||||
createEntityCardExtension({
|
||||
id: 'github-pull-requests',
|
||||
attachTo: { id: 'plugin.deployments.content', input: 'cards' },
|
||||
loader: () => import('./PullRequestsCard').then(m => m.PullRequestsCard),
|
||||
entityFilter: isPullRequestsAvailable,
|
||||
}),
|
||||
createEntityContentExtension({
|
||||
id: 'github-pull-requests',
|
||||
defaultPath: 'github-pull-requests',
|
||||
defaultTitle: 'GitHub Pull Requests',
|
||||
loader: () =>
|
||||
import('./PullRequestsContent').then(m => m.PullRequestsContent),
|
||||
entityFilter: isPullRequestsAvailable,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user