frontend/core: add some kinda app api
This commit is contained in:
@@ -1,19 +1,28 @@
|
||||
import React, { FC } from 'react';
|
||||
import {
|
||||
BackstageTheme,
|
||||
createApp,
|
||||
EntityLink,
|
||||
Header,
|
||||
InfoCard,
|
||||
Page,
|
||||
theme,
|
||||
withGlobalStyles,
|
||||
} from '@backstage/core';
|
||||
import helloWorld, { MyComponent } from '@backstage/plugin-hello-world';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import SideBar from './components/SideBar';
|
||||
//import PageHeader from './components/PageHeader';
|
||||
import { Header, Page, InfoCard } from '@backstage/core';
|
||||
import { LoginComponent } from '@backstage/plugin-login';
|
||||
import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import React, { FC } from 'react';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
Link as RouterLink,
|
||||
Route,
|
||||
Switch,
|
||||
} from 'react-router-dom';
|
||||
import { BackstageTheme, withGlobalStyles, theme } from '@backstage/core';
|
||||
import { CssBaseline, ThemeProvider, makeStyles } from '@material-ui/core';
|
||||
import HomePageTimer from './components/HomepageTimer';
|
||||
import SideBar from './components/SideBar';
|
||||
import entities from './entities';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
@@ -36,27 +45,6 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const App: FC<{}> = () => {
|
||||
return (
|
||||
<CssBaseline>
|
||||
<ThemeProvider theme={BackstageTheme}>
|
||||
<AppContent>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
<Route path="/login">
|
||||
<Login />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</AppContent>
|
||||
</ThemeProvider>
|
||||
</CssBaseline>
|
||||
);
|
||||
};
|
||||
|
||||
const Home: FC<{}> = () => {
|
||||
return (
|
||||
<InfoCard title="Home Page">
|
||||
@@ -67,6 +55,12 @@ const Home: FC<{}> = () => {
|
||||
<MyComponent />
|
||||
<div>
|
||||
<RouterLink to="/login">Go to Login</RouterLink>
|
||||
<EntityLink kind="service" id="backstage-backend">
|
||||
Backstage Backend
|
||||
</EntityLink>
|
||||
<EntityLink uri="entity:service:backstage-lb" subPath="ci-cd">
|
||||
Backstage LB CI/CD
|
||||
</EntityLink>
|
||||
</div>
|
||||
</InfoCard>
|
||||
);
|
||||
@@ -103,4 +97,31 @@ const AppShell: FC<{}> = ({ children }) => {
|
||||
|
||||
const AppContent = withGlobalStyles(AppShell);
|
||||
|
||||
const app = createApp();
|
||||
|
||||
app.registerEntityKind(...entities);
|
||||
app.setHomePage(Home);
|
||||
const AppComponent = app.build();
|
||||
|
||||
const App: FC<{}> = () => {
|
||||
return (
|
||||
<CssBaseline>
|
||||
<ThemeProvider theme={BackstageTheme}>
|
||||
<AppContent>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/login">
|
||||
<Login />
|
||||
</Route>
|
||||
<Route>
|
||||
<AppComponent />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</AppContent>
|
||||
</ThemeProvider>
|
||||
</CssBaseline>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import React, { FC } from 'react';
|
||||
import { useEntityUri } from '@backstage/core';
|
||||
|
||||
const MockEntityPage: FC<{}> = () => {
|
||||
const uri = useEntityUri();
|
||||
return <span>Mock card for {uri}, replace with some userful plugin</span>;
|
||||
};
|
||||
|
||||
export default MockEntityPage;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React, { FC } from 'react';
|
||||
import { useEntityUri } from '@backstage/core';
|
||||
|
||||
const MockEntityPage: FC<{}> = () => {
|
||||
const uri = useEntityUri();
|
||||
return <span>Mock page for {uri}, replace with some userful plugin</span>;
|
||||
};
|
||||
|
||||
export default MockEntityPage;
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
createEntityKind,
|
||||
createOverviewPage,
|
||||
createEntityView,
|
||||
} from '@backstage/core';
|
||||
import ComputerIcon from '@material-ui/icons/Computer';
|
||||
import MockEntityPage from './MockEntityPage';
|
||||
import MockEntityCard from './MockEntityCard';
|
||||
|
||||
/* SERVICE */
|
||||
const serviceOverviewPage = createOverviewPage()
|
||||
.addComponent(MockEntityCard)
|
||||
.addComponent(MockEntityCard);
|
||||
|
||||
const serviceView = createEntityView()
|
||||
.addPage('Overview', 'overview', serviceOverviewPage)
|
||||
.addComponent('CI/CD', 'ci-cd', MockEntityPage);
|
||||
|
||||
const serviceEntity = createEntityKind({
|
||||
kind: 'service',
|
||||
title: 'Service',
|
||||
color: {
|
||||
primary: '#f00',
|
||||
secondary: '#ba5',
|
||||
},
|
||||
icon: ComputerIcon,
|
||||
pages: {
|
||||
view: serviceView,
|
||||
},
|
||||
});
|
||||
|
||||
export default [serviceEntity];
|
||||
@@ -12,10 +12,12 @@
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9.0",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-helmet":"5.2.1",
|
||||
"react-addons-text-content": "0.0.4",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"recompose": "0.30.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import { AppContextProvider } from './AppContext';
|
||||
import { App, EntityConfig, AppComponentBuilder } from './types';
|
||||
import { Route, Switch, useParams } from 'react-router-dom';
|
||||
import EntityKind from './EntityKind';
|
||||
import { EntityContextProvider } from './EntityContext';
|
||||
|
||||
const DefaultHomePage: FC<{}> = () => {
|
||||
return <span>Hello! I am default home page</span>;
|
||||
};
|
||||
|
||||
class AppImpl implements App {
|
||||
constructor(private readonly entities: Map<string, EntityKind>) {}
|
||||
|
||||
getEntityConfig(kind: string): EntityConfig {
|
||||
const entity = this.entities.get(kind);
|
||||
if (!entity) {
|
||||
throw new Error('EntityKind not found');
|
||||
}
|
||||
return entity.config;
|
||||
}
|
||||
}
|
||||
|
||||
function builtComponent(
|
||||
app: App,
|
||||
component: ComponentType<any> | AppComponentBuilder,
|
||||
) {
|
||||
if (component instanceof AppComponentBuilder) {
|
||||
return component.build(app);
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
export default class AppBuilder {
|
||||
private readonly entities = new Map<string, EntityKind>();
|
||||
private homePage: ComponentType = DefaultHomePage;
|
||||
|
||||
registerEntityKind(...entity: EntityKind[]) {
|
||||
for (const e of entity) {
|
||||
const { kind } = e.config;
|
||||
if (this.entities.has(e.config.kind)) {
|
||||
throw new Error(`EntityKind '${kind}' is already registered`);
|
||||
}
|
||||
this.entities.set(e.config.kind, e);
|
||||
}
|
||||
}
|
||||
|
||||
setHomePage(page: ComponentType<{}>) {
|
||||
this.homePage = page;
|
||||
}
|
||||
|
||||
build(): ComponentType<{}> {
|
||||
const app = new AppImpl(this.entities);
|
||||
|
||||
const entityRoutes = [];
|
||||
|
||||
for (const { config } of this.entities.values()) {
|
||||
const { kind, pages } = config;
|
||||
const basePath = `/entity/${kind}`;
|
||||
|
||||
if (pages.list) {
|
||||
const ListComponent = builtComponent(app, pages.list);
|
||||
|
||||
const Component: FC<{}> = () => (
|
||||
<EntityContextProvider config={config}>
|
||||
<ListComponent />
|
||||
</EntityContextProvider>
|
||||
);
|
||||
|
||||
const path = basePath;
|
||||
entityRoutes.push(
|
||||
<Route key={path} path={path} component={Component} />,
|
||||
);
|
||||
}
|
||||
|
||||
if (pages.view) {
|
||||
const ViewComponent = builtComponent(app, pages.view);
|
||||
|
||||
const Component: FC<{}> = () => {
|
||||
const { entityId } = useParams<{ entityId: string }>();
|
||||
return (
|
||||
<EntityContextProvider config={config} id={entityId}>
|
||||
<ViewComponent />
|
||||
</EntityContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const path = `${basePath}/:entityId`;
|
||||
entityRoutes.push(
|
||||
<Route key={path} path={path} component={Component} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const routes = [...entityRoutes];
|
||||
|
||||
return () => (
|
||||
<AppContextProvider app={app}>
|
||||
<Switch>
|
||||
{routes}
|
||||
<Route exact path="/" component={this.homePage} />
|
||||
<Route component={() => <span>404 Not Found</span>} />
|
||||
</Switch>
|
||||
</AppContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React, { createContext, useContext, FC } from 'react';
|
||||
import { App } from './types';
|
||||
|
||||
const Context = createContext<App | undefined>(undefined);
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
};
|
||||
|
||||
export const AppContextProvider: FC<Props> = ({ app, children }) => (
|
||||
<Context.Provider value={app} children={children} />
|
||||
);
|
||||
|
||||
export const useApp = (): App => {
|
||||
const app = useContext(Context);
|
||||
if (!app) {
|
||||
throw new Error('No app context available');
|
||||
}
|
||||
return app;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { createContext, useContext, FC } from 'react';
|
||||
import { EntityConfig } from './types';
|
||||
|
||||
type Value = {
|
||||
config: EntityConfig;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const Context = createContext<Value | undefined>(undefined);
|
||||
|
||||
type Props = {
|
||||
config: EntityConfig;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export const EntityContextProvider: FC<Props> = ({ config, id, children }) => (
|
||||
<Context.Provider value={{ config, id }} children={children} />
|
||||
);
|
||||
|
||||
export const useEntity = (): { kind: string; id: string } => {
|
||||
const value = useContext(Context);
|
||||
if (!value) {
|
||||
throw new Error('No entity context available');
|
||||
}
|
||||
if (!value.id) {
|
||||
throw new Error('Entity context does not contain entity id');
|
||||
}
|
||||
return { kind: value.config.kind, id: value.id };
|
||||
};
|
||||
|
||||
export const useEntityConfig = (): EntityConfig => {
|
||||
const value = useContext(Context);
|
||||
if (!value) {
|
||||
throw new Error('No entity context available');
|
||||
}
|
||||
return value.config;
|
||||
};
|
||||
|
||||
export const useEntityUri = (): string => {
|
||||
const { kind, id } = useEntity();
|
||||
return `entity:${kind}:${id}`;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityConfig } from './types';
|
||||
|
||||
export default class EntityKind {
|
||||
constructor(readonly config: EntityConfig) {}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
subPath?: string;
|
||||
} & (
|
||||
| {
|
||||
kind: string;
|
||||
id?: string;
|
||||
}
|
||||
| {
|
||||
uri: string;
|
||||
}
|
||||
);
|
||||
|
||||
function buildPath(kind: string, id?: string, subPath?: string) {
|
||||
if (id) {
|
||||
if (subPath) {
|
||||
return `/entity/${kind}/${id}/${subPath}`;
|
||||
}
|
||||
return `/entity/${kind}/${id}`;
|
||||
}
|
||||
return `/entity/${kind}`;
|
||||
}
|
||||
|
||||
const EntityLink: FC<Props> = ({ subPath, children, ...props }) => {
|
||||
if ('kind' in props) {
|
||||
const { kind, id } = props;
|
||||
return <Link to={buildPath(kind, id, subPath)}>{children}</Link>;
|
||||
} else {
|
||||
const match = props.uri.match(/entity:([^:]+)(:[^:]+)?/);
|
||||
if (!match) {
|
||||
throw new TypeError(`Invalid entity uri: '${props.uri}'`);
|
||||
}
|
||||
|
||||
const [, kind, maybeId] = match;
|
||||
const id = maybeId ? maybeId.slice(1) : undefined;
|
||||
|
||||
return <Link to={buildPath(kind, id, subPath)}>{children}</Link>;
|
||||
}
|
||||
};
|
||||
|
||||
export default EntityLink;
|
||||
@@ -0,0 +1,124 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import { AppComponentBuilder, App } from './types';
|
||||
import { useEntity, useEntityUri, useEntityConfig } from './EntityContext';
|
||||
import { Route, Redirect, Switch } from 'react-router-dom';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import EntityLink from './EntityLink';
|
||||
|
||||
const EntityLayout: FC<{}> = ({ children }) => {
|
||||
const config = useEntityConfig();
|
||||
return (
|
||||
<div style={{ backgroundColor: config.color.primary }}>{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EntitySidebar: FC<{}> = ({ children }) => {
|
||||
return <List>{children}</List>;
|
||||
};
|
||||
|
||||
const EntitySidebarItem: FC<{ title: string; path: string }> = ({
|
||||
title,
|
||||
path,
|
||||
}) => {
|
||||
const entityUri = useEntityUri();
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<EntityLink uri={entityUri} subPath={path}>
|
||||
{title}
|
||||
</EntityLink>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
type EntityViewPage = {
|
||||
title: string;
|
||||
path: string;
|
||||
component: ComponentType<any>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
pages: EntityViewPage[];
|
||||
};
|
||||
|
||||
const EntityViewComponent: FC<Props> = ({ pages }) => {
|
||||
const { kind, id } = useEntity();
|
||||
const basePath = `/entity/${kind}/${id}`;
|
||||
|
||||
return (
|
||||
<EntityLayout>
|
||||
<EntitySidebar>
|
||||
{pages.map(({ title, path }) => (
|
||||
<EntitySidebarItem key={path} title={title} path={path} />
|
||||
))}
|
||||
</EntitySidebar>
|
||||
<Switch>
|
||||
{pages.map(({ path, component }) => (
|
||||
<Route
|
||||
key={path}
|
||||
exact
|
||||
path={`${basePath}/${path}`}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
<Redirect from={basePath} to={`${basePath}/${pages[0].path}`} />
|
||||
</Switch>
|
||||
</EntityLayout>
|
||||
);
|
||||
};
|
||||
|
||||
type EntityViewRegistration =
|
||||
| {
|
||||
type: 'page';
|
||||
title: string;
|
||||
path: string;
|
||||
page: AppComponentBuilder;
|
||||
}
|
||||
| {
|
||||
type: 'component';
|
||||
title: string;
|
||||
path: string;
|
||||
component: ComponentType<any>;
|
||||
};
|
||||
|
||||
export default class EntityViewBuilder extends AppComponentBuilder {
|
||||
private readonly registrations = new Array<EntityViewRegistration>();
|
||||
|
||||
addPage(
|
||||
title: string,
|
||||
path: string,
|
||||
page: AppComponentBuilder,
|
||||
): EntityViewBuilder {
|
||||
this.registrations.push({ type: 'page', title, path, page });
|
||||
return this;
|
||||
}
|
||||
|
||||
addComponent(
|
||||
title: string,
|
||||
path: string,
|
||||
component: ComponentType<any>,
|
||||
): EntityViewBuilder {
|
||||
this.registrations.push({ type: 'component', title, path, component });
|
||||
return this;
|
||||
}
|
||||
|
||||
build(app: App): ComponentType<any> {
|
||||
const pages = this.registrations.map(registration => {
|
||||
switch (registration.type) {
|
||||
case 'page': {
|
||||
const { title, path, page } = registration;
|
||||
return { title, path, component: page.build(app) };
|
||||
}
|
||||
case 'component': {
|
||||
const { title, path, component } = registration;
|
||||
return { title, path, component };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown EntityViewBuilder registration`);
|
||||
}
|
||||
});
|
||||
|
||||
return () => <EntityViewComponent pages={pages} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import { App, AppComponentBuilder } from './types';
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
cards: ComponentType<any>[];
|
||||
};
|
||||
|
||||
const OverviewPageComponent: FC<Props> = ({ cards }) => {
|
||||
return (
|
||||
<div>
|
||||
{cards.map(CardComponent => (
|
||||
<CardComponent />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type OverviewPageRegistration = {
|
||||
type: 'component';
|
||||
component: ComponentType<any>;
|
||||
};
|
||||
|
||||
export default class OverviewPageBuilder extends AppComponentBuilder {
|
||||
private readonly registrations = new Array<OverviewPageRegistration>();
|
||||
private output?: ComponentType<any>;
|
||||
|
||||
addComponent(component: ComponentType<any>): OverviewPageBuilder {
|
||||
this.registrations.push({ type: 'component', component });
|
||||
return this;
|
||||
}
|
||||
|
||||
build(app: App): ComponentType<any> {
|
||||
if (this.output) {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
const cards = this.registrations.map(reg => {
|
||||
switch (reg.type) {
|
||||
case 'component':
|
||||
return reg.component;
|
||||
default:
|
||||
throw new Error(`Unknown OverviewPageBuilder registration`);
|
||||
}
|
||||
});
|
||||
|
||||
this.output = () => <OverviewPageComponent app={app} cards={cards} />;
|
||||
return this.output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import AppBuilder from './AppBuilder';
|
||||
import { EntityConfig } from './types';
|
||||
import EntityKind from './EntityKind';
|
||||
import OverviewPageBuilder from './OverviewPageBuilder';
|
||||
import EntityViewBuilder from './EntityViewPageBuilder';
|
||||
|
||||
export function createApp() {
|
||||
return new AppBuilder();
|
||||
}
|
||||
|
||||
export function createEntityKind(config: EntityConfig) {
|
||||
return new EntityKind(config);
|
||||
}
|
||||
|
||||
export function createOverviewPage() {
|
||||
return new OverviewPageBuilder();
|
||||
}
|
||||
|
||||
export function createEntityView() {
|
||||
return new EntityViewBuilder();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './types';
|
||||
export * from './api';
|
||||
export { useApp } from './AppContext';
|
||||
export { useEntity, useEntityConfig, useEntityUri } from './EntityContext';
|
||||
export { default as EntityLink } from './EntityLink';
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
export type EntityConfig = {
|
||||
kind: string;
|
||||
title: string;
|
||||
icon: React.ComponentType<{ fontSize: number }>;
|
||||
color: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
};
|
||||
pages: {
|
||||
list?: ComponentType<{}> | AppComponentBuilder;
|
||||
view?: ComponentType<{}> | AppComponentBuilder;
|
||||
};
|
||||
};
|
||||
|
||||
export type App = {
|
||||
getEntityConfig(kind: string): EntityConfig;
|
||||
};
|
||||
|
||||
export class AppComponentBuilder<T = any> {
|
||||
build(_app: App): ComponentType<T> {
|
||||
throw new Error('Must override build() in AppComponentBuilder');
|
||||
}
|
||||
}
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type UserApi = {
|
||||
isLoggedIn(): Promise<boolean>;
|
||||
|
||||
getUser(): Promise<User>;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './appApi';
|
||||
export * from './types';
|
||||
export { default as createPlugin } from './createPlugin';
|
||||
export { default as Page } from '../src/layout/Page';
|
||||
|
||||
Reference in New Issue
Block a user