feat: IoC, DI, indirection, routes, tabs, catalog, app, github-actions
This commit is contained in:
@@ -26,6 +26,11 @@ import * as plugins from './plugins';
|
||||
import { apis } from './apis';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import { providers } from './identityProviders';
|
||||
import { CatalogPlugin } from '@backstage/plugin-catalog';
|
||||
// import { ExplorePlugin } from '@backstage/plugin-explore';
|
||||
import { Route, Routes } from 'react-router';
|
||||
|
||||
import { EntityPage } from './components/catalog';
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
@@ -46,8 +51,16 @@ const app = createApp({
|
||||
|
||||
const AppProvider = app.getProvider();
|
||||
const AppRouter = app.getRouter();
|
||||
const AppRoutes = app.getRoutes();
|
||||
|
||||
const AppRoutes = () => (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/catalog/*"
|
||||
element={<CatalogPlugin EntityPage={EntityPage} />}
|
||||
/>
|
||||
{/* <Route path="/explore" element={<ExplorePlugin />} /> */}
|
||||
</Routes>
|
||||
);
|
||||
const App: FC<{}> = () => (
|
||||
<AppProvider>
|
||||
<AlertDisplay />
|
||||
|
||||
@@ -89,7 +89,7 @@ const Root: FC<{}> = ({ children }) => (
|
||||
<SidebarSearchField onSearch={handleSearch} />
|
||||
<SidebarDivider />
|
||||
{/* Global nav, not org-specific */}
|
||||
<SidebarItem icon={HomeIcon} to="./" text="Home" />
|
||||
<SidebarItem icon={HomeIcon} to="/catalog" text="Home" />
|
||||
<SidebarItem icon={ExploreIcon} to="explore" text="Explore" />
|
||||
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
|
||||
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
|
||||
|
||||
@@ -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 {
|
||||
GitHubActionsPlugin,
|
||||
GITHUB_ACTIONS_ANNOTATION,
|
||||
} from '@backstage/plugin-github-actions';
|
||||
import {
|
||||
useEntity,
|
||||
EntityPageTabs as Tabs,
|
||||
EntityMetadataCard,
|
||||
} from '@backstage/plugin-catalog';
|
||||
import { Grid } from '@material-ui/core';
|
||||
|
||||
const OverviewPage = () => {
|
||||
const entity = useEntity();
|
||||
return (
|
||||
<Grid item sm={4}>
|
||||
<EntityMetadataCard entity={entity} />
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
// Just for illustration purposes, gonna live in plugin-circle-ci
|
||||
const CIRCLE_CI_ANNOTATION = 'circle-ci-slug-dummy';
|
||||
|
||||
const DefaultEntity = () => {
|
||||
const entity = useEntity();
|
||||
|
||||
const isCIAvailable = [
|
||||
entity!.metadata!.annotations?.[GITHUB_ACTIONS_ANNOTATION],
|
||||
entity!.metadata!.annotations?.[CIRCLE_CI_ANNOTATION],
|
||||
].some(Boolean);
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
<Tabs.Tab title="Overview" path="/" exact>
|
||||
<OverviewPage />
|
||||
</Tabs.Tab>
|
||||
{isCIAvailable && (
|
||||
<Tabs.Tab title="CI/CD" path="/ci-cd">
|
||||
{entity!.metadata!.annotations?.[GITHUB_ACTIONS_ANNOTATION] && (
|
||||
<GitHubActionsPlugin entity={entity} />
|
||||
)}
|
||||
</Tabs.Tab>
|
||||
)}
|
||||
<Tabs.Tab title="Docs" path="/docs">
|
||||
{/* eslint-disable-next-line no-console */}
|
||||
<div>{console.log('was here /docs')}Docs tab contents</div>
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const Service = () => <DefaultEntity />;
|
||||
const Website = () => <DefaultEntity />;
|
||||
const UnkownEntity = () => <div>Unknown entity!</div>;
|
||||
|
||||
export const ComponentEntity = () => {
|
||||
const entity = useEntity();
|
||||
switch (entity.spec!.type) {
|
||||
case 'service':
|
||||
return <Service />;
|
||||
case 'website':
|
||||
return <Website />;
|
||||
default:
|
||||
return <UnkownEntity />;
|
||||
}
|
||||
};
|
||||
+14
-1
@@ -13,5 +13,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useEntity } from '@backstage/plugin-catalog';
|
||||
import { ComponentEntity } from './Component';
|
||||
|
||||
export { WorkflowRunDetailsPage } from './WorkflowRunDetailsPage';
|
||||
const UnkownEntityKind = () => <div>Unknown entity kind!</div>;
|
||||
|
||||
export const EntityPage = () => {
|
||||
const entity = useEntity();
|
||||
switch (entity.kind) {
|
||||
case 'Component':
|
||||
return <ComponentEntity />;
|
||||
default:
|
||||
return <UnkownEntityKind />;
|
||||
}
|
||||
};
|
||||
@@ -36,6 +36,7 @@
|
||||
"moment": "^2.26.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^15.3.3",
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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, { FC, useState } from 'react';
|
||||
import { CatalogPage } from './components/CatalogPage/CatalogPage';
|
||||
// import { EntityPage } from './components/EntityPage/EntityPage';
|
||||
import { Route, Routes, useParams, useNavigate } from 'react-router';
|
||||
import { entityRoute, rootRoute, entityRouteDefault } from './routes';
|
||||
import { useEntityFromUrl, EntityContext, useEntity } from './hooks/useEntity';
|
||||
import {
|
||||
pageTheme,
|
||||
PageTheme,
|
||||
Page,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Content,
|
||||
Progress,
|
||||
} from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { FavouriteEntity } from './components/FavouriteEntity/FavouriteEntity';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { EntityContextMenu } from './components/EntityContextMenu/EntityContextMenu';
|
||||
import { UnregisterEntityDialog } from './components/UnregisterEntityDialog/UnregisterEntityDialog';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
// const EntityContext = React.createContext(null);
|
||||
export const getPageTheme = (entity?: Entity): PageTheme => {
|
||||
const themeKey = entity?.spec?.type?.toString() ?? 'home';
|
||||
return pageTheme[themeKey] ?? pageTheme.home;
|
||||
};
|
||||
|
||||
const EntityPageTitle = ({
|
||||
entity,
|
||||
title,
|
||||
}: {
|
||||
title: string;
|
||||
entity: Entity | undefined;
|
||||
}) => (
|
||||
<Box display="inline-flex" alignItems="center" height="1em">
|
||||
{title}
|
||||
{entity && <FavouriteEntity entity={entity} />}
|
||||
</Box>
|
||||
);
|
||||
|
||||
function headerProps(
|
||||
kind: string,
|
||||
namespace: string | undefined,
|
||||
name: string,
|
||||
entity: Entity | undefined,
|
||||
): { headerTitle: string; headerType: string } {
|
||||
return {
|
||||
headerTitle: `${name}${namespace ? ` in ${namespace}` : ''}`,
|
||||
headerType: (() => {
|
||||
let t = kind.toLowerCase();
|
||||
if (entity && entity.spec && 'type' in entity.spec) {
|
||||
t += ' — ';
|
||||
t += (entity.spec as { type: string }).type.toLowerCase();
|
||||
}
|
||||
return t;
|
||||
})(),
|
||||
};
|
||||
}
|
||||
const EntityPageLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
const { entity, loading, error } = useEntityFromUrl();
|
||||
const { optionalNamespaceAndName, kind } = useParams() as {
|
||||
optionalNamespaceAndName: string;
|
||||
kind: string;
|
||||
};
|
||||
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
|
||||
|
||||
const { headerTitle, headerType } = headerProps(
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
entity!,
|
||||
);
|
||||
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const cleanUpAfterRemoval = async () => {
|
||||
setConfirmationDialogOpen(false);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const showRemovalDialog = () => setConfirmationDialogOpen(true);
|
||||
|
||||
return (
|
||||
<Page theme={getPageTheme(entity!)}>
|
||||
<Header
|
||||
title={<EntityPageTitle title={headerTitle} entity={entity!} />}
|
||||
pageTitleOverride={headerTitle}
|
||||
type={headerType}
|
||||
>
|
||||
{entity && (
|
||||
<>
|
||||
<HeaderLabel
|
||||
label="Owner"
|
||||
value={entity.spec?.owner || 'unknown'}
|
||||
/>
|
||||
<HeaderLabel
|
||||
label="Lifecycle"
|
||||
value={entity.spec?.lifecycle || 'unknown'}
|
||||
/>
|
||||
<EntityContextMenu onUnregisterEntity={showRemovalDialog} />
|
||||
</>
|
||||
)}
|
||||
</Header>
|
||||
|
||||
{loading && <Progress />}
|
||||
|
||||
{entity && (
|
||||
<EntityContext.Provider value={entity}>
|
||||
{children}
|
||||
</EntityContext.Provider>
|
||||
)}
|
||||
{error && (
|
||||
<Content>
|
||||
<Alert severity="error">{error.toString()}</Alert>
|
||||
</Content>
|
||||
)}
|
||||
<UnregisterEntityDialog
|
||||
open={confirmationDialogOpen}
|
||||
entity={entity!}
|
||||
onConfirm={cleanUpAfterRemoval}
|
||||
onClose={() => setConfirmationDialogOpen(false)}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export const CatalogPlugin = ({ EntityPage }) => (
|
||||
<Routes>
|
||||
<Route path={`/${rootRoute.path}`} element={<CatalogPage />} />
|
||||
<Route
|
||||
path={`/${entityRoute.path}`}
|
||||
element={
|
||||
<EntityPageLayout>
|
||||
<EntityPage />
|
||||
</EntityPageLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`/${entityRouteDefault.path}`}
|
||||
element={
|
||||
<EntityPageLayout>
|
||||
<EntityPage />
|
||||
</EntityPageLayout>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export { EntityMetadataCard } from './components/EntityMetadataCard/EntityMetadataCard';
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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 {
|
||||
useParams,
|
||||
useNavigate,
|
||||
PartialRouteObject,
|
||||
matchRoutes,
|
||||
RouteObject,
|
||||
useRoutes,
|
||||
Navigate,
|
||||
RouteMatch,
|
||||
} from 'react-router';
|
||||
import { Tab, HeaderTabs, Content } from '@backstage/core';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const getSelectedIndex = (matchedRoute: RouteMatch, tabs: Tab[]) => {
|
||||
if (!matchedRoute) return 0;
|
||||
const tabIndex = tabs.findIndex(t => t.id === matchedRoute.route.path);
|
||||
return ~tabIndex ? tabIndex : 0;
|
||||
};
|
||||
export const EntityPageTabs = ({ children }: { children: React.ReactNode }) => {
|
||||
const routes: PartialRouteObject[] = [];
|
||||
const tabs: Tab[] = [];
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
React.Children.forEach(children, child => {
|
||||
if (!React.isValidElement(child)) {
|
||||
// Skip conditionals resolved to falses/nulls/undefineds etc
|
||||
return;
|
||||
}
|
||||
const pathAndId =
|
||||
(child as JSX.Element).props.path +
|
||||
((child as JSX.Element).props.exact ? '' : '/*');
|
||||
routes.push({
|
||||
path: pathAndId,
|
||||
element: (child as JSX.Element).props.children,
|
||||
});
|
||||
tabs.push({
|
||||
id: pathAndId,
|
||||
label: (child as JSX.Element).props.title,
|
||||
});
|
||||
routes.push({
|
||||
path: '/*',
|
||||
element: <Navigate to="." />,
|
||||
});
|
||||
});
|
||||
const [matchedRoute] =
|
||||
matchRoutes(routes as RouteObject[], `/${params['*']}`) ?? [];
|
||||
const selectedIndex = getSelectedIndex(matchedRoute, tabs);
|
||||
const currentTab = tabs[selectedIndex];
|
||||
const title = currentTab.label;
|
||||
|
||||
const onTabChange = (index: number) => navigate(tabs[index].id.slice(1, -2));
|
||||
|
||||
const currentRouteElement = useRoutes(routes);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderTabs
|
||||
tabs={tabs}
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={onTabChange}
|
||||
/>
|
||||
<Content>
|
||||
<Grid container spacing={3}>
|
||||
<Helmet title={title} />
|
||||
{currentRouteElement}
|
||||
</Grid>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
type TabProps = {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
path: string;
|
||||
exact?: boolean;
|
||||
};
|
||||
EntityPageTabs.Tab = (_props: TabProps) => null;
|
||||
+1
-2
@@ -13,5 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { WorkflowRunsPage } from './WorkflowRunsPage';
|
||||
export { EntityPageTabs } from './EntityPageTabs';
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { useEffect, createContext, useContext } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { useApi, errorApiRef } from '@backstage/core';
|
||||
import { catalogApiRef } from '../api/types';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
const REDIRECT_DELAY = 2000;
|
||||
|
||||
export const EntityContext = createContext<Entity>((null as any) as Entity);
|
||||
export const useEntityFromUrl = () => {
|
||||
const { optionalNamespaceAndName, kind } = useParams();
|
||||
const [name, namespace] = optionalNamespaceAndName.split(':').reverse();
|
||||
const navigate = useNavigate();
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const { value: entity, error, loading } = useAsync(
|
||||
() => catalogApi.getEntityByName({ kind, namespace, name }),
|
||||
[catalogApi, kind, namespace, name],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error && !loading && !entity) {
|
||||
errorApi.post(new Error('Entity not found!'));
|
||||
setTimeout(() => {
|
||||
navigate('/');
|
||||
}, REDIRECT_DELAY);
|
||||
}
|
||||
}, [errorApi, navigate, error, loading, entity]);
|
||||
|
||||
if (!name) {
|
||||
navigate('/catalog');
|
||||
return { entity: null, loading: null, error: new Error('No name in url') };
|
||||
}
|
||||
|
||||
return { entity, loading, error };
|
||||
};
|
||||
|
||||
export const useEntity = () => useContext(EntityContext);
|
||||
@@ -19,3 +19,6 @@ export * from './api/CatalogClient';
|
||||
export * from './api/types';
|
||||
export * from './routes';
|
||||
export { useEntityCompoundName } from './components/useEntityCompoundName';
|
||||
export * from './Router';
|
||||
export { useEntity } from './hooks/useEntity';
|
||||
export { EntityPageTabs } from './components/EntityPageTabs';
|
||||
|
||||
@@ -20,16 +20,16 @@ const NoIcon = () => null;
|
||||
|
||||
export const rootRoute = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: '/',
|
||||
path: '',
|
||||
title: 'Catalog',
|
||||
});
|
||||
export const entityRoute = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: '/catalog/:kind/:optionalNamespaceAndName/:selectedTabId/*',
|
||||
path: ':kind/:optionalNamespaceAndName/*',
|
||||
title: 'Entity',
|
||||
});
|
||||
export const entityRouteDefault = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: '/catalog/:kind/:optionalNamespaceAndName',
|
||||
path: ':kind/:optionalNamespaceAndName',
|
||||
title: 'Entity',
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"moment": "^2.27.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^15.3.3"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { Routes, Route } from 'react-router';
|
||||
import { rootRouteRef, buildRouteRef } from './plugin';
|
||||
import { WorkflowRunDetails } from './components/WorkflowRunDetails';
|
||||
import { WorkflowRunsTable } from './components/WorkflowRunsTable';
|
||||
|
||||
export const GitHubActionsPlugin = ({ entity }: { entity: Entity }) => (
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/${rootRouteRef.path}`}
|
||||
element={<WorkflowRunsTable entity={entity} />}
|
||||
/>
|
||||
<Route
|
||||
path={`/${buildRouteRef.path}`}
|
||||
element={<WorkflowRunDetails entity={entity} />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useEntityCompoundName } from '@backstage/plugin-catalog';
|
||||
import { useWorkflowRunsDetails } from './useWorkflowRunsDetails';
|
||||
import { useWorkflowRunJobs } from './useWorkflowRunJobs';
|
||||
import { useProjectName } from '../useProjectName';
|
||||
@@ -35,13 +34,16 @@ import {
|
||||
LinearProgress,
|
||||
CircularProgress,
|
||||
Theme,
|
||||
Link,
|
||||
Breadcrumbs,
|
||||
Link as MaterialLink,
|
||||
} from '@material-ui/core';
|
||||
import { Jobs, Job, Step } from '../../api';
|
||||
import moment from 'moment';
|
||||
import { WorkflowRunStatus } from '../WorkflowRunStatus';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import ExternalLinkIcon from '@material-ui/icons/Launch';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Link } from '@backstage/core';
|
||||
|
||||
const useStyles = makeStyles<Theme>(theme => ({
|
||||
root: {
|
||||
@@ -140,18 +142,8 @@ const JobListItem = ({ job, className }: { job: Job; className: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkflowRunDetails = () => {
|
||||
let entityCompoundName = useEntityCompoundName();
|
||||
if (!entityCompoundName.name) {
|
||||
// TODO(shmidt-i): remove when is fully integrated
|
||||
// into the entity view
|
||||
entityCompoundName = {
|
||||
kind: 'Component',
|
||||
name: 'backstage',
|
||||
namespace: 'default',
|
||||
};
|
||||
}
|
||||
const projectName = useProjectName(entityCompoundName);
|
||||
export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
|
||||
const projectName = useProjectName(entity);
|
||||
|
||||
const [owner, repo] = projectName.value ? projectName.value.split('/') : [];
|
||||
const details = useWorkflowRunsDetails(repo, owner);
|
||||
@@ -170,6 +162,10 @@ export const WorkflowRunDetails = () => {
|
||||
}
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Breadcrumbs aria-label="breadcrumb">
|
||||
<Link to="..">Workflow runs</Link>
|
||||
<Typography>Workflow run details</Typography>
|
||||
</Breadcrumbs>
|
||||
<TableContainer component={Paper} className={classes.table}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
@@ -211,10 +207,10 @@ export const WorkflowRunDetails = () => {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{details.value?.html_url && (
|
||||
<Link target="_blank" href={details.value.html_url}>
|
||||
<MaterialLink target="_blank" href={details.value.html_url}>
|
||||
Workflow runs on GitHub{' '}
|
||||
<ExternalLinkIcon className={classes.externalLinkIcon} />
|
||||
</Link>
|
||||
</MaterialLink>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
-65
@@ -1,65 +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 { Typography, Grid, Breadcrumbs } from '@material-ui/core';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Link,
|
||||
Page,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
pageTheme,
|
||||
} from '@backstage/core';
|
||||
|
||||
import { WorkflowRunDetails } from '../WorkflowRunDetails';
|
||||
|
||||
/**
|
||||
* A component for Jobs visualization. Jobs are a property of a Workflow Run.
|
||||
*/
|
||||
export const WorkflowRunDetailsPage = () => {
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="GitHub Actions"
|
||||
subtitle="See recent workflow runs and their status"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Spotify" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Workflow run details">
|
||||
<SupportButton>
|
||||
This plugin allows you to view and interact with your builds within
|
||||
the GitHub Actions environment.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<Breadcrumbs aria-label="breadcrumb">
|
||||
<Link to="/github-actions">Workflow runs</Link>
|
||||
<Typography>Workflow run details</Typography>
|
||||
</Breadcrumbs>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<WorkflowRunDetails />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -1,56 +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 {
|
||||
Header,
|
||||
HeaderLabel,
|
||||
pageTheme,
|
||||
Page,
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
} from '@backstage/core';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
||||
import { WorkflowRunsTable } from '../WorkflowRunsTable';
|
||||
|
||||
export const WorkflowRunsPage = () => {
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="GitHub Actions"
|
||||
subtitle="See recent workflow runs and their status"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Spotify" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Workflow runs">
|
||||
<SupportButton>
|
||||
This plugin allows you to view and interact with your builds within
|
||||
the GitHub Actions environment.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<WorkflowRunsTable />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import SyncIcon from '@material-ui/icons/Sync';
|
||||
import { buildRouteRef } from '../../plugin';
|
||||
import { useEntityCompoundName } from '@backstage/plugin-catalog';
|
||||
import { useProjectName } from '../useProjectName';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export type WorkflowRun = {
|
||||
id: string;
|
||||
@@ -134,6 +135,7 @@ export const WorkflowRunsTableView: FC<Props> = ({
|
||||
data={runs ?? []}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangePageSize}
|
||||
style={{ width: '100%' }}
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<GitHubIcon />
|
||||
@@ -146,25 +148,14 @@ export const WorkflowRunsTableView: FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkflowRunsTable = () => {
|
||||
let entityCompoundName = useEntityCompoundName();
|
||||
|
||||
if (!entityCompoundName.name) {
|
||||
// TODO(shmidt-i): remove when is fully integrated
|
||||
// into the entity view
|
||||
entityCompoundName = {
|
||||
kind: 'Component',
|
||||
name: 'backstage',
|
||||
namespace: 'default',
|
||||
};
|
||||
}
|
||||
|
||||
const { value: projectName, loading } = useProjectName(entityCompoundName);
|
||||
export const WorkflowRunsTable = ({ entity }: { entity: Entity }) => {
|
||||
const { value: projectName, loading } = useProjectName(entity);
|
||||
const [owner, repo] = (projectName ?? '/').split('/');
|
||||
const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
return (
|
||||
<WorkflowRunsTableView
|
||||
{...tableProps}
|
||||
|
||||
@@ -15,15 +15,13 @@
|
||||
*/
|
||||
|
||||
import { useAsync } from 'react-use';
|
||||
import { catalogApiRef, EntityCompoundName } from '@backstage/plugin-catalog';
|
||||
import { useApi } from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
|
||||
export const useProjectName = (name: EntityCompoundName) => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
export const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug';
|
||||
|
||||
export const useProjectName = (entity: Entity) => {
|
||||
const { value, loading, error } = useAsync(async () => {
|
||||
const entity = await catalogApi.getEntityByName(name);
|
||||
return entity?.metadata.annotations?.['github.com/project-slug'] ?? '';
|
||||
return entity?.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION] ?? '';
|
||||
});
|
||||
return { value, loading, error };
|
||||
};
|
||||
|
||||
@@ -17,3 +17,5 @@
|
||||
export { plugin } from './plugin';
|
||||
export * from './api';
|
||||
export { Widget } from './components/Widget';
|
||||
export { GitHubActionsPlugin } from './Router';
|
||||
export { GITHUB_ACTIONS_ANNOTATION } from './components/useProjectName';
|
||||
|
||||
@@ -15,28 +15,19 @@
|
||||
*/
|
||||
|
||||
import { createPlugin, createRouteRef } from '@backstage/core';
|
||||
import { WorkflowRunDetailsPage } from './components/WorkflowRunDetailsPage';
|
||||
import { WorkflowRunsPage } from './components/WorkflowRunsPage';
|
||||
|
||||
// TODO(freben): This is just a demo route for now
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '/github-actions',
|
||||
path: '',
|
||||
title: 'GitHub Actions',
|
||||
});
|
||||
export const projectRouteRef = createRouteRef({
|
||||
path: '/github-actions/:kind/:optionalNamespaceAndName/',
|
||||
title: 'GitHub Actions for project',
|
||||
});
|
||||
|
||||
export const buildRouteRef = createRouteRef({
|
||||
path: '/github-actions/workflow-run/:id',
|
||||
path: ':id',
|
||||
title: 'GitHub Actions Workflow Run',
|
||||
});
|
||||
|
||||
export const plugin = createPlugin({
|
||||
id: 'github-actions',
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, WorkflowRunsPage);
|
||||
router.addRoute(projectRouteRef, WorkflowRunsPage);
|
||||
router.addRoute(buildRouteRef, WorkflowRunDetailsPage);
|
||||
},
|
||||
register() {},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user