Add "Organization" tab with a diagram
Signed-off-by: Kévin Gomez <kevin.gomez@voiapp.io>
This commit is contained in:
@@ -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 & zoom
|
||||
to move around the diagram.
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+30
@@ -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';
|
||||
Reference in New Issue
Block a user