Add "Organization" tab with a diagram

Signed-off-by: Kévin Gomez <kevin.gomez@voiapp.io>
This commit is contained in:
Kévin Gomez
2021-04-25 23:21:55 +02:00
parent 37ca09566a
commit defaa15e55
4 changed files with 257 additions and 0 deletions
@@ -16,6 +16,7 @@
import { TabbedLayout } from '@backstage/core';
import React from 'react';
import { DomainExplorerContent } from '../DomainExplorerContent';
import { OrganizationExplorerContent } from '../OrganizationExplorerContent';
import { ToolExplorerContent } from '../ToolExplorerContent';
export const ExploreTabs = () => (
@@ -23,6 +24,9 @@ export const ExploreTabs = () => (
<TabbedLayout.Route path="domains" title="Domains">
<DomainExplorerContent />
</TabbedLayout.Route>
<TabbedLayout.Route path="organization" title="Organization">
<OrganizationExplorerContent />
</TabbedLayout.Route>
<TabbedLayout.Route path="tools" title="Tools">
<ToolExplorerContent />
</TabbedLayout.Route>
@@ -0,0 +1,207 @@
/*
* Copyright 2021 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 {
Entity,
RELATION_CHILD_OF,
stringifyEntityRef,
ENTITY_DEFAULT_NAMESPACE,
parseEntityRef,
} from '@backstage/catalog-model';
import {
catalogApiRef,
entityRouteRef,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import {
DependencyGraph,
DependencyGraphTypes,
Progress,
useApi,
ResponseErrorPanel,
Link,
useRouteRef,
configApiRef,
} from '@backstage/core';
import { makeStyles, Typography } from '@material-ui/core';
import ZoomOutMap from '@material-ui/icons/ZoomOutMap';
import React from 'react';
import { useAsync } from 'react-use';
import { BackstageTheme } from '@backstage/theme';
const useStyles = makeStyles((theme: BackstageTheme) => ({
organizationNode: {
fill: 'coral',
stroke: theme.palette.border,
},
groupNode: {
fill: 'yellowgreen',
stroke: theme.palette.border,
},
}));
// Simplifies the diagram output by hiding the default namespace and kind
function readableEntityName(
ref:
| Entity
| {
kind: string;
namespace?: string;
name: string;
},
): string {
return stringifyEntityRef(ref)
.toLocaleLowerCase('en-US')
.replace(`:${ENTITY_DEFAULT_NAMESPACE}/`, ':')
.split(':')[1];
}
function RenderNode(props: DependencyGraphTypes.RenderNodeProps<any>) {
const classes = useStyles();
const catalogEntityRoute = useRouteRef(entityRouteRef);
if (props.node.id === 'root') {
return (
<g>
<rect
width={180}
height={80}
rx={20}
className={classes.organizationNode}
/>
<text
x={90}
y={45}
textAnchor="middle"
alignmentBaseline="baseline"
style={{ fontWeight: 'bold' }}
>
{props.node.name}
</text>
</g>
);
}
const ref = parseEntityRef(props.node.id);
return (
<g>
<rect width={180} height={80} rx={20} className={classes.groupNode} />
<Link
to={catalogEntityRoute({
kind: ref.kind,
namespace: ref.namespace,
name: ref.name,
})}
>
<text
x={90}
y={45}
textAnchor="middle"
alignmentBaseline="baseline"
style={{ fontWeight: 'bold' }}
>
{props.node.name}
</text>
</Link>
</g>
);
}
/**
* Dynamically generates a diagram of an organization.
*/
export function OrganizationDiagram() {
const nodes = new Array<{ id: string; kind: string; name: string }>();
const edges = new Array<{ from: string; to: string; label: string }>();
const configApi = useApi(configApiRef);
const catalogApi = useApi(catalogApiRef);
const organizationName =
configApi.getOptionalString('organization.name') ?? 'Backstage';
const { loading, error, value: catalogResponse } = useAsync(() => {
return catalogApi.getEntities({
filter: {
kind: ['Group'],
},
});
}, [catalogApi]);
if (loading) {
return <Progress />;
} else if (error) {
return <ResponseErrorPanel error={error} />;
}
// the root of this diagram is the organization
nodes.push({
id: 'root',
kind: 'Organization',
name: organizationName,
});
for (const catalogItem of catalogResponse?.items || []) {
const currentItemId = stringifyEntityRef(catalogItem);
nodes.push({
id: stringifyEntityRef(catalogItem),
kind: catalogItem.kind,
name: readableEntityName(catalogItem),
});
// Edge to parent
const catalogItemRelations_childOf = getEntityRelations(
catalogItem,
RELATION_CHILD_OF,
);
// if no parent is found, link the node to the root
if (catalogItemRelations_childOf.length === 0) {
edges.push({
from: currentItemId,
to: 'root',
label: '',
});
}
catalogItemRelations_childOf.forEach(relation => {
edges.push({
from: currentItemId,
to: stringifyEntityRef(relation),
label: '',
});
});
}
return (
<>
<DependencyGraph
nodes={nodes}
edges={edges}
nodeMargin={10}
direction={DependencyGraphTypes.Direction.RIGHT_LEFT}
renderNode={RenderNode}
/>
<Typography
variant="caption"
style={{ display: 'block', textAlign: 'right' }}
>
<ZoomOutMap style={{ verticalAlign: 'bottom' }} /> Use pinch &amp; zoom
to move around the diagram.
</Typography>
</>
);
}
@@ -0,0 +1,30 @@
/*
* Copyright 2021 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 { Content, ContentHeader, SupportButton } from '@backstage/core';
import React from 'react';
import { OrganizationDiagram } from './OrganizationDiagram';
export const OrganizationExplorerContent = () => {
return (
<Content noPadding>
<ContentHeader title="Organization">
<SupportButton>Explore your organization.</SupportButton>
</ContentHeader>
<OrganizationDiagram />
</Content>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2021 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 { OrganizationExplorerContent } from './OrganizationExplorerContent';