diff --git a/applications/client/src/views/Campaign/Explore/Panels/Meta/components/NodePreview.tsx b/applications/client/src/views/Campaign/Explore/Panels/Meta/components/NodePreview.tsx index 43e1e117..0d71e940 100644 --- a/applications/client/src/views/Campaign/Explore/Panels/Meta/components/NodePreview.tsx +++ b/applications/client/src/views/Campaign/Explore/Panels/Meta/components/NodePreview.tsx @@ -11,7 +11,7 @@ type NodePreviewProps = FlexProps & }; export const NodePreview = observer(({ type, shape, text, color = 'default', size: _, ...props }) => ( - + {text && – {text === 'color' ? color : shape}} diff --git a/applications/client/src/views/Campaign/Graph/Graph.tsx b/applications/client/src/views/Campaign/Graph/Graph.tsx index ad956839..6ee49a21 100644 --- a/applications/client/src/views/Campaign/Graph/Graph.tsx +++ b/applications/client/src/views/Campaign/Graph/Graph.tsx @@ -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'; @@ -40,14 +41,22 @@ export const Graph = observer((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); @@ -70,11 +79,16 @@ export const Graph = observer((props) => { ) : null} - + ); diff --git a/applications/client/src/views/Campaign/Graph/GraphControls.tsx b/applications/client/src/views/Campaign/Graph/GraphControls.tsx index 8363a70a..a7126507 100644 --- a/applications/client/src/views/Campaign/Graph/GraphControls.tsx +++ b/applications/client/src/views/Campaign/Graph/GraphControls.tsx @@ -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'; @@ -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(({ zoomIn, zoomOut, zoomToFit, exportSVG, ...props }) => { - const [isOpen, setIsOpen] = useState(false); +export const GraphControls = observer>( + ({ + zoomIn, + zoomOut, + zoomToFit, + exportSVG, + isSimpleForces = false, + toggleSimpleForces, + showMoreLabels = false, + setShowMoreLabels, + ...props + }) => { + const [isOpen, setIsOpen] = useState(false); - return ( -
- {isOpen ? ( -
+ return ( +
+ {isOpen ? ( +
+
+ ) : ( +
- ) : ( -
- ); -}); +
+ ); + } +); const rootStyle = css` display: flex; @@ -188,6 +219,7 @@ const svgStyle = { height: 24, width: 56, center: 12, + radius: 6, }; const legendTitle = css` margin-bottom: 0.5rem; diff --git a/applications/client/src/views/Campaign/Graph/graph-styles.tsx b/applications/client/src/views/Campaign/Graph/graph-styles.tsx index b62e60d5..bdd7d892 100644 --- a/applications/client/src/views/Campaign/Graph/graph-styles.tsx +++ b/applications/client/src/views/Campaign/Graph/graph-styles.tsx @@ -19,15 +19,22 @@ export const graphStyles = css` cursor: pointer; /* &:active { cursor: grabbing; } */ } - .${GCN.groupNode} { - /* pointer-events: none; */ // set in code - } + /* .${GCN.groupNode} { pointer-events: none; } // set in GroupGraphRenderer */ &:not(.${GCN.isZooming}) .${GCN.superNode} { transition: r 0.2s cubic-bezier(0, 1, 0, 1); } - .${GCN.occludedLabel}, .${GCN.subNodeNameLabel}:not(.${GCN.selectedFocus}):not(.${GCN.previewedFocus}) { + + /* HIDE LABELS */ + .${GCN.occludedLabel}, .${GCN.hiddenLabel} { display: none; } + .${GCN.subNodeNameLabel} { + // &:not(...) is to avoid incorrectly applied syntax error + &:not(.${GCN.selectedFocus}):not(.${GCN.previewedFocus}) { + display: none; + } + } + .${GCN.parentLinkNode} { display: none; } @@ -81,7 +88,7 @@ export const graphStyles = css` } } circle { - stroke-width: 1px; + /* stroke-width: 1px; */ } .${GCN.groupNode} { @@ -139,7 +146,6 @@ export const graphStyles = css` .${GCN.softwareNode}, .${GCN.serverNode} { fill: ${GraphTokens.PresentFgColor}; stroke: ${GraphTokens.PresentBgColor}; - r: 4px; &.${GCN.past} { fill: ${GraphTokens.PastFgColor}; @@ -150,7 +156,6 @@ export const graphStyles = css` &.${GCN.selected} { fill: ${GraphTokens.PreviewFgColor}; // not GraphTokens.SelectedFgColor - r: 5px; } &.${GCN.previewed} { fill: ${GraphTokens.PreviewFgColor}; @@ -158,9 +163,24 @@ export const graphStyles = css` &.${GCN.selectedFocus} { fill: ${GraphTokens.SelectedFocusFgColor}; stroke: ${GraphTokens.PreviewFgColor}; - stroke-width: ${GraphTokens.SelectedThickness}; - r: 6px; + stroke-width: ${GraphTokens.PreviewThickness}; filter: drop-shadow(0 0 1px ${CoreTokens.Colors.Black}); } } `; + +export const showMoreLabelsGraphStyles = css` + .${GCN.hiddenLabel} { + display: initial; // show more labels + } + .${GCN.superNodeCountLabel}.${GCN.hiddenLabel} { + display: none; // but still hide the counts + } + .${GCN.superGraph} { + &.${GCN.selectedParent}, &.${GCN.previewedParent} { + .${GCN.hiddenLabel} { + fill: ${CoreTokens.TextMuted}; + } + } + } +`; diff --git a/applications/client/src/views/Campaign/Graph/node-colors.tsx b/applications/client/src/views/Campaign/Graph/node-colors.tsx index 0191b44b..324fc436 100644 --- a/applications/client/src/views/Campaign/Graph/node-colors.tsx +++ b/applications/client/src/views/Campaign/Graph/node-colors.tsx @@ -1,22 +1,88 @@ import { css } from '@emotion/react'; import { CoreTokens } from '@redeye/ui-styles'; import { RedEyeGraphClassNames as GCN } from '@redeye/graph'; +import { Classes } from '@blueprintjs/core'; export const nodeColor = { - default: { className: 'defaultNode', token: CoreTokens.TextBody }, - vermilion: { className: 'vermilionNode', token: CoreTokens.Colors.Vermilion3 }, - red: { className: 'redNode', token: CoreTokens.Colors.Red3 }, - rose: { className: 'roseNode', token: CoreTokens.Colors.Rose3 }, - violet: { className: 'violetNode', token: CoreTokens.Colors.Violet3 }, - indigo: { className: 'indigoNode', token: CoreTokens.Colors.Indigo3 }, - // blue: { className: 'blueNode', token: CoreTokens.Colors.Blue3 }, // excluded because its the selection color - // cerulean: { className: 'ceruleanNode', token: CoreTokens.Colors.Cerulean3 }, // excluded because its almost blue - turquoise: { className: 'turquoiseNode', token: CoreTokens.Colors.Turquoise4 }, - green: { className: 'greenNode', token: CoreTokens.Colors.Green4 }, - forest: { className: 'forestNode', token: CoreTokens.Colors.Forest4 }, - lime: { className: 'limeNode', token: CoreTokens.Colors.Lime4 }, - gold: { className: 'goldNode', token: CoreTokens.Colors.Gold4 }, - orange: { className: 'orangeNode', token: CoreTokens.Colors.Orange3 }, + default: { className: 'defaultNode', token: CoreTokens.TextBody, fgToken: 'none', bgToken: 'none' }, + vermilion: { + className: 'vermilionNode', + token: CoreTokens.Colors.Vermilion3, + fgToken: CoreTokens.Colors.Vermilion5, + bgToken: CoreTokens.Colors.Vermilion1, + }, + red: { + className: 'redNode', + token: CoreTokens.Colors.Red3, + fgToken: CoreTokens.Colors.Red5, + bgToken: CoreTokens.Colors.Red1, + }, + rose: { + className: 'roseNode', + token: CoreTokens.Colors.Rose3, + fgToken: CoreTokens.Colors.Rose5, + bgToken: CoreTokens.Colors.Rose1, + }, + violet: { + className: 'violetNode', + token: CoreTokens.Colors.Violet3, + fgToken: CoreTokens.Colors.Violet5, + bgToken: CoreTokens.Colors.Violet1, + }, + indigo: { + className: 'indigoNode', + token: CoreTokens.Colors.Indigo3, + fgToken: CoreTokens.Colors.Indigo5, + bgToken: CoreTokens.Colors.Indigo1, + }, + /* blue: { // excluded because its the selection color + className: 'blueNode', + token: CoreTokens.Colors.Blue3, + fgToken: CoreTokens.Colors.Blue5, + bgToken: CoreTokens.Colors.Blue1, + }, + cerulean: { // excluded because its almost blue + className: 'ceruleanNode', + token: CoreTokens.Colors.Cerulean3, + fgToken: CoreTokens.Colors.Cerulean5, + bgToken: CoreTokens.Colors.Cerulean1, + }, */ + turquoise: { + className: 'turquoiseNode', + token: CoreTokens.Colors.Turquoise4, + fgToken: CoreTokens.Colors.Turquoise5, + bgToken: CoreTokens.Colors.Turquoise1, + }, + green: { + className: 'greenNode', + token: CoreTokens.Colors.Green4, + fgToken: CoreTokens.Colors.Green5, + bgToken: CoreTokens.Colors.Green1, + }, + forest: { + className: 'forestNode', + token: CoreTokens.Colors.Forest4, + fgToken: CoreTokens.Colors.Forest5, + bgToken: CoreTokens.Colors.Forest1, + }, + lime: { + className: 'limeNode', + token: CoreTokens.Colors.Lime4, + fgToken: CoreTokens.Colors.Lime5, + bgToken: CoreTokens.Colors.Lime1, + }, + gold: { + className: 'goldNode', + token: CoreTokens.Colors.Gold4, + fgToken: CoreTokens.Colors.Gold5, + bgToken: CoreTokens.Colors.Gold1, + }, + orange: { + className: 'orangeNode', + token: CoreTokens.Colors.Orange3, + fgToken: CoreTokens.Colors.Orange5, + bgToken: CoreTokens.Colors.Orange1, + }, }; export type NodeColor = keyof typeof nodeColor; @@ -27,13 +93,28 @@ const iNodeColorStyles = nodeColors .filter((colorKey) => colorKey !== 'default') .map( (colorKey) => css` + text.${nodeColor[colorKey].className} { + &:not(.${GCN.superNodeCountLabel}) { + fill: ${nodeColor[colorKey].fgToken}; + } + } + &.${nodeColor[colorKey].className}, .${nodeColor[colorKey].className} { - color: ${nodeColor[colorKey].token}; + color: ${nodeColor[colorKey].fgToken}; + &.${Classes.ICON} { + color: ${nodeColor[colorKey].token}; + } + &.${GCN.softwareNode}, &.${GCN.serverNode} { fill: ${nodeColor[colorKey].token}; } + &.${GCN.computerNode} { stroke: ${nodeColor[colorKey].token}; + &.${GCN.selectedFocus} { + stroke: ${nodeColor[colorKey].fgToken}; + fill: ${nodeColor[colorKey].bgToken}; + } } } ` diff --git a/applications/redeye-e2e/src/integration/e2e/redteam/timeline.cy.js b/applications/redeye-e2e/src/integration/e2e/redteam/timeline.cy.js index 123de907..59ca5cd5 100644 --- a/applications/redeye-e2e/src/integration/e2e/redteam/timeline.cy.js +++ b/applications/redeye-e2e/src/integration/e2e/redteam/timeline.cy.js @@ -162,6 +162,7 @@ describe('Timeline tests', () => { const day = timelineDay; const timelineDate = month.concat('/').concat(day); + cy.wait(1000); cy.get('[cy-test=command-date-time]').each(($date) => { expect($date.text()).to.contain(timelineDate); }); diff --git a/packages/graph/src/GraphHandler.ts b/packages/graph/src/GraphHandler.ts index da1605e2..bd748ec6 100644 --- a/packages/graph/src/GraphHandler.ts +++ b/packages/graph/src/GraphHandler.ts @@ -21,7 +21,7 @@ import { SuperGraphRenderer } from './GraphRenderers/SuperGraphRenderer'; import type { HierarchicalGraphRenderer } from './GraphRenderers/HierarchicalGraphRenderer'; import { textOcclusion, textOcclusionSort } from './GraphRenderers/textOcclusion'; import { initializeTesting, noOp } from './utils'; -import { NodeShape } from './GraphRenderers/polygon-utils'; +import type { NodeShape } from './GraphRenderers/polygon-utils'; /** The root graph handler for all subgraphs and interactions */ export class GraphHandler { @@ -65,8 +65,7 @@ export class GraphHandler { previouslyParsedGraphData, }); - this.svg = d3Select(element) - // + this.svg = d3Select(element) // .html('') .classed([classNames.graphRoot, 'dotGrid'].join(' '), true); this.resize(); @@ -354,12 +353,19 @@ export class GraphHandler { */ } - useGraphForces() { - this.graphRoot.callChildrenRecursively('useGraphForces'); - } - useSimpleForces() { - this.graphRoot.callChildrenRecursively('useSimpleForces'); + forceMode: ForceMode = 'graph'; + useForceMode(mode: ForceMode) { + if (mode === 'simple') { + this.graphRoot.callChildrenRecursively('useSimpleForces'); + this.forceMode = 'simple'; + } else { + // if (mode === 'graph') { + this.graphRoot.callChildrenRecursively('useGraphForces'); + this.forceMode = 'graph'; + } } static scaleRadius = (zk: number) => Math.min(zk, (zk - 1) * 0.3 + 1); } + +type ForceMode = 'graph' | 'simple'; diff --git a/packages/graph/src/GraphRenderers/SubGraphRenderer.ts b/packages/graph/src/GraphRenderers/SubGraphRenderer.ts index 4635636b..b632801f 100644 --- a/packages/graph/src/GraphRenderers/SubGraphRenderer.ts +++ b/packages/graph/src/GraphRenderers/SubGraphRenderer.ts @@ -99,6 +99,7 @@ export class SubGraphRenderer extends HierarchicalGraphRenderer { } drawUpdateNodeVisual() { + this.labelSelection?.each(updateClassName); this.nodeSelection.each(updateClassName); this.nodeSelection.selectChildren().remove(); this.nodeSelection @@ -127,7 +128,7 @@ export class SubGraphRenderer extends HierarchicalGraphRenderer { } drawInteraction() { - this.labelSelection?.style('display', (d) => (isInteractionRelated(d) ? '' : 'none')); + this.labelSelection?.classed(classNames.hiddenLabel, (d) => !isInteractionRelated(d)); if (isInteractionFocus(this.parentNode!)) { this.showLayout(); diff --git a/packages/graph/src/GraphRenderers/SuperGraphRenderer.ts b/packages/graph/src/GraphRenderers/SuperGraphRenderer.ts index 9ac9bb73..ff039c2e 100644 --- a/packages/graph/src/GraphRenderers/SuperGraphRenderer.ts +++ b/packages/graph/src/GraphRenderers/SuperGraphRenderer.ts @@ -126,7 +126,7 @@ export class SuperGraphRenderer extends HierarchicalGraphRenderer { .attr('id', assignIdLabel) .each(updateClassName) .classed(classNames.superNodeNameLabel, true) - .style('display', 'none') // start hidden + .classed(classNames.hiddenLabel, true) // start hidden .text(createLabel); this.countLabelSelection = this.rootGroupSelection @@ -144,6 +144,8 @@ export class SuperGraphRenderer extends HierarchicalGraphRenderer { drawUpdateNodeVisual() { this.nodeSelection.each(updateClassName); + this.labelSelection?.each(updateClassName); + this.countLabelSelection.each(updateClassName); } drawTime() { @@ -177,8 +179,8 @@ export class SuperGraphRenderer extends HierarchicalGraphRenderer { drawInteraction() { this.drawDynamicLayout(); - this.countLabelSelection?.style('display', (d) => (isInteractionFocus(d) ? 'none' : '')); - this.labelSelection?.style('display', (d) => (isInteractionRelated(d) ? '' : 'none')); + this.countLabelSelection?.classed(classNames.hiddenLabel, (d) => isInteractionFocus(d)); + this.labelSelection?.classed(classNames.hiddenLabel, (d) => !isInteractionRelated(d)); super.drawInteraction(); this.graphSelection.selectChildren().sort(interactionSort); } diff --git a/packages/graph/src/GraphRenderers/layout-utils.ts b/packages/graph/src/GraphRenderers/layout-utils.ts index 9060b1f3..48a94577 100644 --- a/packages/graph/src/GraphRenderers/layout-utils.ts +++ b/packages/graph/src/GraphRenderers/layout-utils.ts @@ -237,6 +237,7 @@ export const classNames = { superNodeNameLabel: 'superNodeNameLabel', subNodeNameLabel: 'subNodeNameLabel', occludedLabel: 'occludedLabel', + hiddenLabel: 'hiddenLabel', // Simulation Nodes & Links // keyNode: 'keyNode', diff --git a/packages/graph/src/style.css b/packages/graph/src/style.css index 0d0a4455..f130ffb7 100644 --- a/packages/graph/src/style.css +++ b/packages/graph/src/style.css @@ -56,6 +56,9 @@ text { .subNodeNameLabel:not(.selectedFocus):not(.previewedFocus) { opacity: 0.1; } +.hiddenLabel { + display: none; +} .graphRoot { cursor: grab; diff --git a/packages/ui-styles/src/styles/tokens.ts b/packages/ui-styles/src/styles/tokens.ts index c9ab607b..dd7921c4 100644 --- a/packages/ui-styles/src/styles/tokens.ts +++ b/packages/ui-styles/src/styles/tokens.ts @@ -162,6 +162,7 @@ export const GraphTokens = { PresentBgColor: `var(--PresentBgColor)`, // PresentDasharray: // none, + // OVERRIDDEN BELOW // FutureFgColor: `var(--FutureFgColor)`, FutureBgColor: `var(--FutureBgColor)`, FutureDasharray: '0px 6px', // Dotted @@ -179,6 +180,11 @@ export const GraphTokens = { TextOutlineColor: `var(--TextOutlineColor)`, }; +// Simplify Graph Colors by using the same for Past & Future // +GraphTokens.FutureFgColor = GraphTokens.PastFgColor; +GraphTokens.FutureBgColor = GraphTokens.PastBgColor; +GraphTokens.FutureDasharray = GraphTokens.PastDasharray; + const lightThemeGraphAndTimelineCssVars = declareCssVars([ [TimelineTokens.PresentBgTimeline, CoreTokens.Colors.Gray2], [TimelineTokens.PastBgTimeline, CoreTokens.Colors.LightGray2], @@ -190,8 +196,8 @@ const lightThemeGraphAndTimelineCssVars = declareCssVars([ [GraphTokens.PresentBgColor, CoreTokens.Colors.White], [GraphTokens.PastFgColor, CoreTokens.Colors.LightGray2], [GraphTokens.PastBgColor, CoreTokens.Colors.LightGray4], - [GraphTokens.FutureFgColor, CoreTokens.Colors.White], - [GraphTokens.FutureBgColor, CoreTokens.Colors.LightGray5], + // [GraphTokens.FutureFgColor, CoreTokens.Colors.White], + // [GraphTokens.FutureBgColor, CoreTokens.Colors.LightGray5], [GraphTokens.SelectedFgColor, CoreTokens.Colors.Gray2], [GraphTokens.PreviewFgColor, CoreTokens.Colors.DarkGray5], [GraphTokens.SelectedFocusFgColor, CoreTokens.Intent.Primary4], @@ -211,14 +217,14 @@ const darkThemeGraphAndTimelineCssVars = declareCssVars([ [GraphTokens.PresentBgColor, CoreTokens.Colors.DarkGray3], [GraphTokens.PastFgColor, CoreTokens.Colors.DarkGray5], [GraphTokens.PastBgColor, CoreTokens.Colors.DarkGray2], - [GraphTokens.FutureFgColor, CoreTokens.Colors.Black], // Black? - [GraphTokens.FutureBgColor, CoreTokens.Colors.DarkGray2], // DarkGray2? + // [GraphTokens.FutureFgColor, CoreTokens.Colors.Black], // Black? + // [GraphTokens.FutureBgColor, CoreTokens.Colors.DarkGray2], // DarkGray2? [GraphTokens.SelectedFgColor, CoreTokens.Colors.LightGray1], [GraphTokens.PreviewFgColor, CoreTokens.Colors.White], [GraphTokens.SelectedFocusFgColor, CoreTokens.Intent.Primary3], [GraphTokens.SelectedFocusBgColor, CoreTokens.Intent.Primary1], [GraphTokens.GroupNodeStrokeColor, CoreTokens.transparentWhite(0.03)], - [GraphTokens.TextOutlineColor, CoreTokens.transparentBlack(0.6)], + [GraphTokens.TextOutlineColor, CoreTokens.transparentBlack(0.8)], ]); export const customCssVars = css`