feat: allow opening dependency graph in fullscreen
Signed-off-by: Hellgren Heikki <heikki.hellgren@op.fi>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Dependency graph can now be opened in full screen mode
|
||||
@@ -80,6 +80,7 @@
|
||||
"pluralize": "^8.0.0",
|
||||
"qs": "^6.9.4",
|
||||
"rc-progress": "3.5.1",
|
||||
"react-full-screen": "^1.1.1",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-hook-form": "^7.12.2",
|
||||
"react-idle-timer": "5.7.2",
|
||||
@@ -106,6 +107,7 @@
|
||||
"@types/d3-selection": "^3.0.1",
|
||||
"@types/d3-shape": "^3.0.1",
|
||||
"@types/d3-zoom": "^3.0.1",
|
||||
"@types/fscreen": "^1",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-helmet": "^6.1.0",
|
||||
|
||||
@@ -61,6 +61,7 @@ export const coreComponentsTranslationRef: TranslationRef<
|
||||
readonly 'alertDisplay.message_other': '({{ count }} newer messages)';
|
||||
readonly 'autoLogout.stillTherePrompt.title': 'Logging out due to inactivity';
|
||||
readonly 'autoLogout.stillTherePrompt.buttonText': "Yes! Don't log me out";
|
||||
readonly 'dependencyGraph.fullscreenTooltip': 'Toggle fullscreen';
|
||||
readonly 'proxiedSignInPage.title': 'You do not appear to be signed in. Please try reloading the browser page.';
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -257,6 +257,7 @@ export interface DependencyGraphProps<NodeData, EdgeData>
|
||||
extends SVGProps<SVGSVGElement> {
|
||||
acyclicer?: 'greedy';
|
||||
align?: DependencyGraphTypes.Alignment;
|
||||
allowFullscreen?: boolean;
|
||||
curve?: 'curveStepBefore' | 'curveMonotoneX';
|
||||
defs?: JSX.Element | JSX.Element[];
|
||||
direction?: DependencyGraphTypes.Direction;
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { DependencyGraph } from './DependencyGraph';
|
||||
import { DependencyGraphTypes as Types } from './types';
|
||||
import { EDGE_TEST_ID, LABEL_TEST_ID, NODE_TEST_ID } from './constants';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('<DependencyGraph />', () => {
|
||||
beforeAll(() => {
|
||||
@@ -36,9 +36,8 @@ describe('<DependencyGraph />', () => {
|
||||
const CUSTOM_TEST_ID = 'custom-test-id';
|
||||
|
||||
it('renders each node and edge supplied', async () => {
|
||||
const { getByText, queryAllByTestId, findAllByTestId } = render(
|
||||
<DependencyGraph nodes={nodes} edges={edges} />,
|
||||
);
|
||||
const { getByText, queryAllByTestId, findAllByTestId } =
|
||||
await renderInTestApp(<DependencyGraph nodes={nodes} edges={edges} />);
|
||||
const renderedNodes = await findAllByTestId(NODE_TEST_ID);
|
||||
expect(renderedNodes).toHaveLength(3);
|
||||
expect(getByText(nodes[0].id)).toBeInTheDocument();
|
||||
@@ -49,9 +48,10 @@ describe('<DependencyGraph />', () => {
|
||||
});
|
||||
|
||||
it('update render if already referenced nodes are added later', async () => {
|
||||
const { getByText, queryAllByTestId, findAllByTestId, rerender } = render(
|
||||
<DependencyGraph nodes={nodes.slice(0, 2)} edges={edges} />,
|
||||
);
|
||||
const { getByText, queryAllByTestId, findAllByTestId, rerender } =
|
||||
await renderInTestApp(
|
||||
<DependencyGraph nodes={nodes.slice(0, 2)} edges={edges} />,
|
||||
);
|
||||
|
||||
let renderedNodes = await findAllByTestId(NODE_TEST_ID);
|
||||
expect(renderedNodes).toHaveLength(2);
|
||||
@@ -75,9 +75,10 @@ describe('<DependencyGraph />', () => {
|
||||
{ ...edges[0], label: 'first' },
|
||||
{ ...edges[1], label: 'second' },
|
||||
];
|
||||
const { getByText, getAllByTestId, findAllByTestId } = render(
|
||||
<DependencyGraph nodes={nodes} edges={labeledEdges} />,
|
||||
);
|
||||
const { getByText, getAllByTestId, findAllByTestId } =
|
||||
await renderInTestApp(
|
||||
<DependencyGraph nodes={nodes} edges={labeledEdges} />,
|
||||
);
|
||||
const renderedEdges = await findAllByTestId(EDGE_TEST_ID);
|
||||
expect(renderedEdges).toHaveLength(2);
|
||||
expect(getAllByTestId(LABEL_TEST_ID)).toHaveLength(2);
|
||||
@@ -94,7 +95,7 @@ describe('<DependencyGraph />', () => {
|
||||
<circle data-testid={CUSTOM_TEST_ID} r={100} />
|
||||
</g>
|
||||
);
|
||||
const { getByText, findByTestId, container } = render(
|
||||
const { getByText, findByTestId, container } = await renderInTestApp(
|
||||
<DependencyGraph nodes={singleNode} edges={[]} renderNode={renderNode} />,
|
||||
);
|
||||
const node = await findByTestId(CUSTOM_TEST_ID);
|
||||
@@ -112,7 +113,7 @@ describe('<DependencyGraph />', () => {
|
||||
<circle data-testid={CUSTOM_TEST_ID} r={100} />
|
||||
</g>
|
||||
);
|
||||
const { getByText, findByTestId, container } = render(
|
||||
const { getByText, findByTestId, container } = await renderInTestApp(
|
||||
<DependencyGraph
|
||||
nodes={nodes}
|
||||
edges={labeledEdge}
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
import {
|
||||
SVGProps,
|
||||
useState,
|
||||
useRef,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import * as d3Zoom from 'd3-zoom';
|
||||
import * as d3Selection from 'd3-selection';
|
||||
@@ -31,6 +31,25 @@ import { DependencyGraphTypes as Types } from './types';
|
||||
import { Node } from './Node';
|
||||
import { Edge, GraphEdge } from './Edge';
|
||||
import { ARROW_MARKER_ID } from './constants';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import FullscreenIcon from '@material-ui/icons/Fullscreen';
|
||||
import FullscreenExitIcon from '@material-ui/icons/FullscreenExit';
|
||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { coreComponentsTranslationRef } from '../../translation';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
root: {
|
||||
overflow: 'hidden',
|
||||
minHeight: '100%',
|
||||
minWidth: '100%',
|
||||
},
|
||||
fullscreen: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* Properties of {@link DependencyGraph}
|
||||
@@ -181,9 +200,18 @@ export interface DependencyGraphProps<NodeData, EdgeData>
|
||||
* Default: 'grow'
|
||||
*/
|
||||
fit?: 'grow' | 'contain';
|
||||
/**
|
||||
* Controls if user can toggle fullscreen mode
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Default: true
|
||||
*/
|
||||
allowFullscreen?: boolean;
|
||||
}
|
||||
|
||||
const WORKSPACE_ID = 'workspace';
|
||||
const DEPENDENCY_GRAPH_SVG = 'dependency-graph';
|
||||
|
||||
/**
|
||||
* Graph component used to visualize relations between entities
|
||||
@@ -216,11 +244,15 @@ export function DependencyGraph<NodeData, EdgeData>(
|
||||
curve = 'curveMonotoneX',
|
||||
showArrowHeads = false,
|
||||
fit = 'grow',
|
||||
allowFullscreen = true,
|
||||
...svgProps
|
||||
} = props;
|
||||
const theme = useTheme();
|
||||
const [containerWidth, setContainerWidth] = useState<number>(100);
|
||||
const [containerHeight, setContainerHeight] = useState<number>(100);
|
||||
const fullScreenHandle = useFullScreenHandle();
|
||||
const styles = useStyles();
|
||||
const { t } = useTranslationRef(coreComponentsTranslationRef);
|
||||
|
||||
const graph = useRef<dagre.graphlib.Graph<Types.DependencyNode<NodeData>>>(
|
||||
new dagre.graphlib.Graph(),
|
||||
@@ -242,11 +274,17 @@ export function DependencyGraph<NodeData, EdgeData>(
|
||||
|
||||
const containerRef = useMemo(
|
||||
() =>
|
||||
debounce((node: SVGSVGElement) => {
|
||||
if (!node) {
|
||||
debounce((root: HTMLDivElement) => {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
// Set up zooming + panning
|
||||
const node: SVGSVGElement = root.querySelector(
|
||||
`svg#${DEPENDENCY_GRAPH_SVG}`,
|
||||
) as SVGSVGElement;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const container = d3Selection.select<SVGSVGElement, null>(node);
|
||||
const workspace = d3Selection.select(node.getElementById(WORKSPACE_ID));
|
||||
|
||||
@@ -282,7 +320,7 @@ export function DependencyGraph<NodeData, EdgeData>(
|
||||
}
|
||||
|
||||
const { width: newContainerWidth, height: newContainerHeight } =
|
||||
node.getBoundingClientRect();
|
||||
root.getBoundingClientRect();
|
||||
if (containerWidth !== newContainerWidth) {
|
||||
setContainerWidth(newContainerWidth);
|
||||
}
|
||||
@@ -406,68 +444,94 @@ export function DependencyGraph<NodeData, EdgeData>(
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={containerRef}
|
||||
{...svgProps}
|
||||
width="100%"
|
||||
height={scalableHeight}
|
||||
viewBox={`0 0 ${maxWidth} ${maxHeight}`}
|
||||
>
|
||||
<defs>
|
||||
<marker
|
||||
id={ARROW_MARKER_ID}
|
||||
viewBox="0 0 24 24"
|
||||
markerWidth="14"
|
||||
markerHeight="14"
|
||||
refX="16"
|
||||
refY="12"
|
||||
orient="auto"
|
||||
markerUnits="strokeWidth"
|
||||
>
|
||||
<path
|
||||
fill={theme.palette.textSubtle}
|
||||
d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"
|
||||
/>
|
||||
</marker>
|
||||
{defs}
|
||||
</defs>
|
||||
<g id={WORKSPACE_ID}>
|
||||
<div ref={containerRef} className={styles.root}>
|
||||
<FullScreen
|
||||
handle={fullScreenHandle}
|
||||
className={fullScreenHandle.active ? styles.fullscreen : styles.root}
|
||||
>
|
||||
{allowFullscreen && (
|
||||
<Tooltip title={t('dependencyGraph.fullscreenTooltip')}>
|
||||
<IconButton
|
||||
style={{ float: 'right' }}
|
||||
onClick={
|
||||
fullScreenHandle.active
|
||||
? fullScreenHandle.exit
|
||||
: fullScreenHandle.enter
|
||||
}
|
||||
>
|
||||
{fullScreenHandle.active ? (
|
||||
<FullscreenExitIcon />
|
||||
) : (
|
||||
<FullscreenIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<svg
|
||||
width={graphWidth}
|
||||
height={graphHeight}
|
||||
y={maxHeight / 2 - graphHeight / 2}
|
||||
x={maxWidth / 2 - graphWidth / 2}
|
||||
viewBox={`0 0 ${graphWidth} ${graphHeight}`}
|
||||
{...svgProps}
|
||||
width="100%"
|
||||
height={scalableHeight}
|
||||
viewBox={`0 0 ${maxWidth} ${maxHeight}`}
|
||||
id={DEPENDENCY_GRAPH_SVG}
|
||||
>
|
||||
{graphEdges.map(e => {
|
||||
const edge = graph.current.edge(e) as GraphEdge<EdgeData>;
|
||||
if (!edge) return null;
|
||||
return (
|
||||
<Edge
|
||||
key={`${e.v}-${e.w}`}
|
||||
id={e}
|
||||
setEdge={setEdge}
|
||||
render={renderLabel}
|
||||
edge={edge}
|
||||
curve={curve}
|
||||
showArrowHeads={showArrowHeads}
|
||||
<defs>
|
||||
<marker
|
||||
id={ARROW_MARKER_ID}
|
||||
viewBox="0 0 24 24"
|
||||
markerWidth="14"
|
||||
markerHeight="14"
|
||||
refX="16"
|
||||
refY="12"
|
||||
orient="auto"
|
||||
markerUnits="strokeWidth"
|
||||
>
|
||||
<path
|
||||
fill={theme.palette.textSubtle}
|
||||
d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{graphNodes.map((id: string) => {
|
||||
const node = graph.current.node(id);
|
||||
if (!node) return null;
|
||||
return (
|
||||
<Node
|
||||
key={id}
|
||||
setNode={setNode}
|
||||
render={renderNode}
|
||||
node={node}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</marker>
|
||||
{defs}
|
||||
</defs>
|
||||
<g id={WORKSPACE_ID}>
|
||||
<svg
|
||||
width={graphWidth}
|
||||
height={graphHeight}
|
||||
y={maxHeight / 2 - graphHeight / 2}
|
||||
x={maxWidth / 2 - graphWidth / 2}
|
||||
viewBox={`0 0 ${graphWidth} ${graphHeight}`}
|
||||
>
|
||||
{graphEdges.map(e => {
|
||||
const edge = graph.current.edge(e) as GraphEdge<EdgeData>;
|
||||
if (!edge) return null;
|
||||
return (
|
||||
<Edge
|
||||
key={`${e.v}-${e.w}`}
|
||||
id={e}
|
||||
setEdge={setEdge}
|
||||
render={renderLabel}
|
||||
edge={edge}
|
||||
curve={curve}
|
||||
showArrowHeads={showArrowHeads}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{graphNodes.map((id: string) => {
|
||||
const node = graph.current.node(id);
|
||||
if (!node) return null;
|
||||
return (
|
||||
<Node
|
||||
key={id}
|
||||
setNode={setNode}
|
||||
render={renderNode}
|
||||
node={node}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</FullScreen>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ export const coreComponentsTranslationRef = createTranslationRef({
|
||||
buttonText: "Yes! Don't log me out",
|
||||
},
|
||||
},
|
||||
dependencyGraph: {
|
||||
fullscreenTooltip: 'Toggle fullscreen',
|
||||
},
|
||||
proxiedSignInPage: {
|
||||
title:
|
||||
'You do not appear to be signed in. Please try reloading the browser page.',
|
||||
|
||||
@@ -3182,6 +3182,7 @@ __metadata:
|
||||
"@types/d3-selection": "npm:^3.0.1"
|
||||
"@types/d3-shape": "npm:^3.0.1"
|
||||
"@types/d3-zoom": "npm:^3.0.1"
|
||||
"@types/fscreen": "npm:^1"
|
||||
"@types/google-protobuf": "npm:^3.7.2"
|
||||
"@types/react": "npm:^18.0.0"
|
||||
"@types/react-helmet": "npm:^6.1.0"
|
||||
@@ -3207,6 +3208,7 @@ __metadata:
|
||||
rc-progress: "npm:3.5.1"
|
||||
react: "npm:^18.0.2"
|
||||
react-dom: "npm:^18.0.2"
|
||||
react-full-screen: "npm:^1.1.1"
|
||||
react-helmet: "npm:6.1.0"
|
||||
react-hook-form: "npm:^7.12.2"
|
||||
react-idle-timer: "npm:5.7.2"
|
||||
@@ -20266,6 +20268,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/fscreen@npm:^1":
|
||||
version: 1.0.4
|
||||
resolution: "@types/fscreen@npm:1.0.4"
|
||||
checksum: 10/78459a457ce7a6b7d72a5f17fdb54bbeb93c58ab77fd2858aac610fed2435bc4be9e5d2fb9883b6669b7f3a1204115cc2be59a027ab937ee8b5186225d2ea53d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/git-url-parse@npm:^9.0.0":
|
||||
version: 9.0.3
|
||||
resolution: "@types/git-url-parse@npm:9.0.3"
|
||||
@@ -31293,6 +31302,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fscreen@npm:^1.0.2":
|
||||
version: 1.2.0
|
||||
resolution: "fscreen@npm:1.2.0"
|
||||
checksum: 10/ac50f9ac52a157b8fe6aaecdf9efa7c1cfa90b42a76c3bc6b85372fab05c5a9cd72c1b7f4c2e273eba1a0e630e381fd72ae135fcc57acd05a0943d5d0c21b451
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@npm:2.3.2":
|
||||
version: 2.3.2
|
||||
resolution: "fsevents@npm:2.3.2"
|
||||
@@ -42823,6 +42839,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-full-screen@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "react-full-screen@npm:1.1.1"
|
||||
dependencies:
|
||||
fscreen: "npm:^1.0.2"
|
||||
peerDependencies:
|
||||
react: ">= 16.8.0"
|
||||
checksum: 10/70ad927b9d6c485ac46b5bb4b1639ef9a860da28290b3a1c419c42b9c427d78b80e8dba403eb6451458af56838012c81d5e12ef05097395f154defc32fe06c34
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-grid-layout@npm:1.3.4":
|
||||
version: 1.3.4
|
||||
resolution: "react-grid-layout@npm:1.3.4"
|
||||
|
||||
Reference in New Issue
Block a user