Skip to content
This repository has been archived by the owner on Oct 20, 2023. It is now read-only.

Graph Updates: Labels, Anchor Mode, Colors, Styles #182

Merged
merged 12 commits into from
Sep 20, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type NodePreviewProps = FlexProps &
};

export const NodePreview = observer<NodePreviewProps>(({ type, shape, text, color = 'default', size: _, ...props }) => (
<Flex align="center" gap="0.5ch" css={{ color: nodeColor[color].token }} {...props}>
<Flex align="center" gap="0.5ch" css={{ color: nodeColor[color].fgToken }} {...props}>
<NodeIcon {...{ type, shape, color }} />
{text && <Txt>– {text === 'color' ? color : shape}</Txt>}
</Flex>
Expand Down
26 changes: 20 additions & 6 deletions applications/client/src/views/Campaign/Graph/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { CampaignLoadingMessage, useStore } from '@redeye/client/store';
import { CoreTokens, Header, Spacer, ThemeClasses, Txt } from '@redeye/ui-styles';
import { observer } from 'mobx-react-lite';
import type { ComponentProps } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useResizeDetector } from 'react-resize-detector';
import { nodeColorStyles } from './node-colors';
import { graphStyles } from './graph-styles';
import { graphStyles, showMoreLabelsGraphStyles } from './graph-styles';
import type { GraphControlFunctions } from './GraphControls';
import { GraphControls } from './GraphControls';
import { LoadingOverlay } from './LoadingOverlay';

Expand Down Expand Up @@ -40,14 +41,22 @@ export const Graph = observer<GraphProps>((props) => {
}
}, [graphRef, store.campaign.isLoading]);

const zoomControls = useMemo(
const [showMoreLabels, setShowMoreLabels] = useState(false);
const [isSimpleForces, setIsSimpleForces] = useState(false);

const zoomControls: GraphControlFunctions = useMemo(
() => ({
zoomIn: () => store.campaign.graph?.zoomIn(),
zoomOut: () => store.campaign.graph?.zoomOut(),
zoomToFit: () => store.campaign.graph?.zoomToFit(),
exportSVG: () => store.campaign.graph?.exportSVG(CoreTokens.Background3),
toggleSimpleForces: (on) => {
store.campaign.graph?.useForceMode(on ? 'simple' : 'graph');
setIsSimpleForces(on);
},
setShowMoreLabels,
}),
[]
[setIsSimpleForces, setShowMoreLabels]
);

const currentMoment = store.settings.momentTz(store.campaign.timeline?.scrubberTime as Date);
Expand All @@ -70,11 +79,16 @@ export const Graph = observer<GraphProps>((props) => {
<LoadingOverlay />
) : null}
<svg
css={[graphLayoutStyles, graphStyles, nodeColorStyles]}
css={[graphLayoutStyles, graphStyles, nodeColorStyles, showMoreLabels && showMoreLabelsGraphStyles]}
ref={graphRef}
className={store.settings.theme === 'dark' ? ThemeClasses.DARK : ThemeClasses.LIGHT}
/>
<GraphControls {...zoomControls} css={controlsStyles} />
<GraphControls
{...zoomControls}
isSimpleForces={isSimpleForces}
showMoreLabels={showMoreLabels}
css={controlsStyles}
/>
</ErrorBoundary>
</div>
);
Expand Down
282 changes: 157 additions & 125 deletions applications/client/src/views/Campaign/Graph/GraphControls.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { Button, ButtonGroup, Classes, Divider } from '@blueprintjs/core';
import { Add16, CenterSquare16, Close16, Export16, Help16, Subtract16 } from '@carbon/icons-react';
import {
Add16,
CenterSquare16,
Close16,
Export16,
Harbor16,
Help16,
StringText16,
Subtract16,
} from '@carbon/icons-react';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { CarbonIcon } from '@redeye/client/components';
Expand All @@ -10,144 +19,166 @@ import type { ComponentProps } from 'react';
import { useState } from 'react';
import { graphStyles } from './graph-styles';

type GraphControlsProps = ComponentProps<'div'> & {
export type GraphControlFunctions = {
zoomIn: () => void;
zoomOut: () => void;
zoomToFit: () => void;
exportSVG: () => void;
isSimpleForces?: boolean;
toggleSimpleForces: (on: boolean) => void;
showMoreLabels?: boolean;
setShowMoreLabels: (on: boolean) => void;
};

export const GraphControls = observer<GraphControlsProps>(({ zoomIn, zoomOut, zoomToFit, exportSVG, ...props }) => {
const [isOpen, setIsOpen] = useState(false);
export const GraphControls = observer<GraphControlFunctions & ComponentProps<'div'>>(
({
zoomIn,
zoomOut,
zoomToFit,
exportSVG,
isSimpleForces = false,
toggleSimpleForces,
showMoreLabels = false,
setShowMoreLabels,
...props
}) => {
const [isOpen, setIsOpen] = useState(false);

return (
<div css={rootStyle} {...props}>
{isOpen ? (
<div cy-test="legend-box" css={[controlGroupStyle, settingsWrapperStyle]}>
return (
<div css={rootStyle} {...props}>
{isOpen ? (
<div cy-test="legend-box" css={[controlGroupStyle, settingsWrapperStyle]}>
<Button
icon={<CarbonIcon icon={Close16} />}
css={settingsCloseStyle}
onClick={() => {
setIsOpen(false);
}}
minimal
small
/>
<Header small css={legendTitle}>
Legend
</Header>
{(
[
['Selected', [GCN.selectedFocus], [GCN.selected], [GCN.selected]],
['Preview', [GCN.previewed], [GCN.previewed]],
['Active', [GCN.present], [GCN.present]],
['Exited', [GCN.past], [GCN.past]],
['Future', [GCN.future], [GCN.future]],
] as [string, string[], string[], string[]?][]
).map((legendItem) => (
<div css={legendItemStyle}>
<svg height={svgStyle.height} width={svgStyle.width} css={graphStyles}>
<line
x1={svgStyle.center}
y1={svgStyle.center}
x2={svgStyle.width - svgStyle.center}
y2={svgStyle.center}
css={[legendLineStyle]}
className={[GCN.siblingLink, ...legendItem[2]].join(' ')}
/>
<circle
r={svgStyle.radius}
cx={svgStyle.center}
cy={svgStyle.center}
css={[legendNodeStyle]}
className={[GCN.subNode, GCN.softwareNode, ...legendItem[1]].join(' ')}
/>
<circle
r={svgStyle.radius}
cx={svgStyle.width - svgStyle.center}
cy={svgStyle.center}
css={[legendNodeStyle]}
className={[GCN.subNode, GCN.softwareNode, ...(legendItem[3] ?? legendItem[1])].join(' ')}
/>
</svg>
<Txt css={[legendLabelStyle]}>{legendItem[0]}</Txt>
</div>
))}
</div>
) : (
<GraphControlButtonGroup vertical hidden={isOpen}>
<Button
cy-test="graph-legend"
icon={<CarbonIcon icon={Help16} />}
onClick={() => {
setIsOpen(true);
}}
title="Settings"
minimal
/>
</GraphControlButtonGroup>
)}
<GraphControlButtonGroup vertical>
<Button
icon={<CarbonIcon icon={Close16} />}
css={settingsCloseStyle}
onClick={() => {
setIsOpen(false);
}}
active={isSimpleForces}
intent={isSimpleForces ? 'primary' : 'none'}
rightIcon={<CarbonIcon icon={Harbor16} />}
onClick={() => toggleSimpleForces(!isSimpleForces)}
title="Anchor Nodes on Drag"
text={isOpen && 'Anchor Drag'}
minimal
small
alignText="left"
/>
<Header small css={legendTitle}>
Legend
</Header>
{(
[
['Selected', [GCN.selectedFocus], [GCN.selected], [GCN.selected]],
['Preview', [GCN.previewed], [GCN.previewed]],
['Active', [GCN.present], [GCN.present]],
['Exited', [GCN.past], [GCN.past]],
['Future', [GCN.future], [GCN.future]],
] as [string, string[], string[], string[]?][]
).map((legendItem) => (
<div css={legendItemStyle}>
<svg height={svgStyle.height} width={svgStyle.width} css={graphStyles}>
<line
x1={svgStyle.center}
y1={svgStyle.center}
x2={svgStyle.width - svgStyle.center}
y2={svgStyle.center}
css={[legendLineStyle]}
className={[GCN.siblingLink, ...legendItem[2]].join(' ')}
/>
<circle
r={svgStyle.center}
cx={svgStyle.center}
cy={svgStyle.center}
css={[legendNodeStyle]}
className={[GCN.subNode, GCN.softwareNode, ...legendItem[1]].join(' ')}
/>
<circle
r={svgStyle.center}
cx={svgStyle.width - svgStyle.center}
cy={svgStyle.center}
css={[legendNodeStyle]}
className={[GCN.subNode, GCN.softwareNode, ...(legendItem[3] ?? legendItem[1])].join(' ')}
/>
</svg>
<Txt css={[legendLabelStyle]}>{legendItem[0]}</Txt>
</div>
))}
</div>
) : (
<GraphControlButtonGroup vertical hidden={isOpen}>
<GraphControlDivider />
<Button
cy-test="graph-legend"
icon={<CarbonIcon icon={Help16} />}
onClick={() => {
setIsOpen(true);
}}
title="Settings"
active={showMoreLabels}
intent={showMoreLabels ? 'primary' : 'none'}
rightIcon={<CarbonIcon icon={StringText16} />}
onClick={() => setShowMoreLabels(!showMoreLabels)}
title="Show Labels"
alignText="left"
text={isOpen && 'Show Labels'}
minimal
/>
<GraphControlDivider />
<Button
cy-test="export-graph"
rightIcon={<CarbonIcon icon={Export16} />}
onClick={exportSVG}
title="Export Graph"
text={isOpen && 'Export Graph'}
alignText="left"
minimal
/>
</GraphControlButtonGroup>
<GraphControlButtonGroup vertical>
<Button
cy-test="zoom-in"
rightIcon={<CarbonIcon icon={Add16} />}
onClick={zoomIn}
title="Zoom In"
text={isOpen && 'Zoom In'}
alignText="left"
minimal
/>
<GraphControlDivider />
<Button
cy-test="zoom-out"
rightIcon={<CarbonIcon icon={Subtract16} />}
onClick={zoomOut}
title="Zoom Out"
text={isOpen && 'Zoom Out'}
alignText="left"
minimal
/>
<GraphControlDivider />
<Button
cy-test="center-graph"
rightIcon={<CarbonIcon icon={CenterSquare16} />}
onClick={zoomToFit}
title="Zoom To Fit"
text={isOpen && 'Zoom To Fit'}
alignText="left"
minimal
/>
{/* <GraphControlDivider />
<Button
active={store.campaign?.graph.dragMode}
icon={<CarbonIcon icon={DataRefineryReference16} />}
onClick={() => store.campaign?.graph.setDragMode()}
disabled // no-op
minimal
/> */}
</GraphControlButtonGroup>
)}
{/* <GraphControlButtonGroup vertical>
<Button
rightIcon={<CarbonIcon icon={Network_216} />}
onClick={async () => {
await store.campaign?.graph.buildGraphTree();
zoomToFit();
}}
title="Tree Layout"
text={state.isOpen ? 'Tree Layout' : undefined}
minimal
/>
<GraphControlDivider />
<Button
rightIcon={<CarbonIcon icon={DataVis_116} />}
onClick={async () => {
await store.campaign?.graph.layout.colaLayout();
zoomToFit();
}}
title="Web Layout"
text={state.isOpen ? 'Web Layout' : undefined}
minimal
/>
</GraphControlButtonGroup> */}
<GraphControlButtonGroup vertical>
<Button cy-test="zoom-in" rightIcon={<CarbonIcon icon={Add16} />} onClick={zoomIn} title="Zoom In" minimal />
<GraphControlDivider />
<Button
cy-test="zoom-out"
rightIcon={<CarbonIcon icon={Subtract16} />}
onClick={zoomOut}
title="Zoom Out"
minimal
/>
<GraphControlDivider />
<Button
cy-test="center-graph"
rightIcon={<CarbonIcon icon={CenterSquare16} />}
onClick={zoomToFit}
title="Zoom To Fit"
minimal
/>
<GraphControlDivider />
<Button
cy-test="export-graph"
rightIcon={<CarbonIcon icon={Export16} />}
onClick={exportSVG}
title="Export Graph"
minimal
/>
</GraphControlButtonGroup>
</div>
);
});
</div>
);
}
);

const rootStyle = css`
display: flex;
Expand Down Expand Up @@ -188,6 +219,7 @@ const svgStyle = {
height: 24,
width: 56,
center: 12,
radius: 6,
};
const legendTitle = css`
margin-bottom: 0.5rem;
Expand Down
Loading