Skip to content

Commit

Permalink
[dagit] Add "shared key path" outlines, experimental asset graph flag (
Browse files Browse the repository at this point in the history
…#7608)

* WIP

* [dagit] Add experimental flag for asset graph, outlines for folders

* Better support for multi layer folders

* Edge highlighting on hover and folder colorization

* Don’t allow asset node titles to get so long

* rm log

* Better color
  • Loading branch information
bengotow committed Apr 28, 2022
1 parent 4cb262c commit d71cb6f
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 49 deletions.
1 change: 1 addition & 0 deletions js_modules/dagit/packages/core/src/app/Flags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {getJSONForKey} from '../hooks/useStateWithStorage';
const DAGIT_FLAGS_KEY = 'DAGIT_FLAGS';

export enum FeatureFlag {
flagExperimentalAssetDAG = 'flagExperimentalAssetDAG',
flagDebugConsoleLogging = 'flagDebugConsoleLogging',
flagAlwaysCollapseNavigation = 'flagAlwaysCollapseNavigation',
flagDisableWebsockets = 'flagDisableWebsockets',
Expand Down
10 changes: 10 additions & 0 deletions js_modules/dagit/packages/core/src/app/SettingsRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ const SettingsRoot = () => {
/>
),
},
{
key: 'Experimental asset graph display',
value: (
<Checkbox
format="switch"
checked={flags.includes(FeatureFlag.flagExperimentalAssetDAG)}
onChange={() => toggleFlag(FeatureFlag.flagExperimentalAssetDAG)}
/>
),
},
]}
/>
</Box>
Expand Down
57 changes: 28 additions & 29 deletions js_modules/dagit/packages/core/src/asset-graph/AssetEdges.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
import {Colors} from '@dagster-io/ui';
import React from 'react';
import styled from 'styled-components/macro';

import {buildSVGPath} from './Utils';
import {AssetLayoutEdge} from './layout';

export const AssetEdges: React.FC<{edges: AssetLayoutEdge[]}> = React.memo(({edges}) => (
<>
<defs>
<marker
id="arrow"
viewBox="0 0 8 10"
refX="1"
refY="5"
markerUnits="strokeWidth"
markerWidth="4"
orient="auto"
>
<path d="M 0 0 L 8 5 L 0 10 z" fill={Colors.KeylineGray} />
</marker>
</defs>
{edges.map((edge, idx) => (
<StyledPath
key={idx}
d={buildSVGPath({source: edge.from, target: edge.to})}
dashed={edge.dashed}
markerEnd="url(#arrow)"
/>
))}
</>
));
export const AssetEdges: React.FC<{edges: AssetLayoutEdge[]; color: string}> = React.memo(
({edges, color}) => (
<>
<defs>
<marker
id={`arrow${btoa(color)}`}
viewBox="0 0 8 10"
refX="1"
refY="5"
markerUnits="strokeWidth"
markerWidth="4"
orient="auto"
>
<path d="M 0 0 L 8 5 L 0 10 z" fill={color} />
</marker>
</defs>
{edges.map((edge, idx) => (
<StyledPath
key={idx}
d={buildSVGPath({source: edge.from, target: edge.to})}
stroke={color}
markerEnd={`url(#arrow${btoa(color)})`}
/>
))}
</>
),
);

const StyledPath = styled('path')<{dashed: boolean}>`
const StyledPath = styled('path')`
stroke-width: 4;
stroke: ${Colors.KeylineGray};
${({dashed}) => (dashed ? `stroke-dasharray: 8 2;` : '')}
fill: none;
`;
124 changes: 120 additions & 4 deletions js_modules/dagit/packages/core/src/asset-graph/AssetGraphExplorer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Box, Checkbox, NonIdealState, SplitPanelContainer} from '@dagster-io/ui';
import {Box, Checkbox, Colors, Mono, NonIdealState, SplitPanelContainer} from '@dagster-io/ui';
import flatMap from 'lodash/flatMap';
import isEqual from 'lodash/isEqual';
import pickBy from 'lodash/pickBy';
import uniq from 'lodash/uniq';
import uniqBy from 'lodash/uniqBy';
Expand All @@ -8,6 +9,7 @@ import React from 'react';
import {useHistory} from 'react-router-dom';
import styled from 'styled-components/macro';

import {useFeatureFlags} from '../app/Flags';
import {GraphQueryItem} from '../app/GraphQueryImpl';
import {
FIFTEEN_SECONDS,
Expand Down Expand Up @@ -42,7 +44,7 @@ import {GraphQueryInput} from '../ui/GraphQueryInput';
import {Loading} from '../ui/Loading';

import {AssetEdges} from './AssetEdges';
import {AssetNode} from './AssetNode';
import {AssetNode, AssetNodeMinimal} from './AssetNode';
import {ForeignNode} from './ForeignNode';
import {OmittedAssetsNotice} from './OmittedAssetsNotice';
import {SidebarAssetInfo} from './SidebarAssetInfo';
Expand All @@ -53,6 +55,7 @@ import {
GraphNode,
isSourceAsset,
tokenForAssetKey,
displayNameForAssetKey,
} from './Utils';
import {AssetGraphLayout} from './layout';
import {AssetGraphQuery_assetNodes} from './types/AssetGraphQuery';
Expand All @@ -73,6 +76,8 @@ interface Props {
onChangeExplorerPath: (path: ExplorerPath, mode: 'replace' | 'push') => void;
}

const EXPERIMENTAL_MINI_SCALE = 0.5;

export const AssetGraphExplorer: React.FC<Props> = (props) => {
const {
fetchResult,
Expand Down Expand Up @@ -152,8 +157,11 @@ const AssetGraphExplorerWithData: React.FC<
pipelineSelector,
} = props;

const findJobForAsset = useFindJobForAsset();
const history = useHistory();
const findJobForAsset = useFindJobForAsset();
const {flagExperimentalAssetDAG: experiments} = useFeatureFlags();

const [highlighted, setHighlighted] = React.useState<string | null>(null);

const selectedAssetValues = explorerPath.opNames[explorerPath.opNames.length - 1].split(',');
const selectedGraphNodes = Object.values(assetGraphData.nodes).filter((node) =>
Expand Down Expand Up @@ -335,7 +343,80 @@ const AssetGraphExplorerWithData: React.FC<
>
{({scale: _scale}, viewportRect) => (
<SVGContainer width={layout.width} height={layout.height}>
<AssetEdges edges={layout.edges} />
<AssetEdges
edges={layout.edges}
color={Colors.KeylineGray}
// extradark={experiments && _scale < EXPERIMENTAL_MINI_SCALE}
/>
<AssetEdges
color={Colors.Blue500}
edges={layout.edges.filter(
({fromId, toId}) => highlighted === fromId || highlighted === toId,
)}
/>

{Object.values(layout.bundles)
.sort((a, b) => a.id.length - b.id.length)
.map(({id, bounds}) => {
if (experiments && _scale < EXPERIMENTAL_MINI_SCALE) {
const path = JSON.parse(id);
return (
<foreignObject
x={bounds.x}
y={bounds.y}
width={bounds.width}
height={bounds.height + 10}
key={id}
onDoubleClick={(e) => {
viewportEl.current?.zoomToSVGBox(bounds, true, 1.2);
e.stopPropagation();
}}
>
<AssetNodeMinimal
color="rgba(248, 223, 196, 0.4)"
definition={{assetKey: {path}}}
fontSize={18 / _scale}
selected={selectedGraphNodes.some((g) =>
hasPathPrefix(g.assetKey.path, path),
)}
/>
</foreignObject>
);
}
return (
<foreignObject
x={bounds.x}
y={bounds.y}
width={bounds.width}
height={bounds.height + 10}
key={id}
>
<Mono
style={{
opacity:
_scale > EXPERIMENTAL_MINI_SCALE
? (_scale - EXPERIMENTAL_MINI_SCALE) / 0.2
: 0,
fontWeight: 600,
}}
>
{displayNameForAssetKey({path: JSON.parse(id)})}
</Mono>
<div
style={{
inset: 0,
top: 24,
position: 'absolute',
borderRadius: 10,
border: `${3 / _scale}px dashed rgba(0,0,0,0.4)`,
background: `rgba(248, 223, 196, ${
0.4 - Math.max(0, _scale - EXPERIMENTAL_MINI_SCALE) * 0.3
})`,
}}
/>
</foreignObject>
);
})}

{Object.values(layout.nodes).map(({id, bounds}, index) => {
const graphNode = assetGraphData.nodes[id];
Expand All @@ -351,10 +432,41 @@ const AssetGraphExplorerWithData: React.FC<
) : null;
}

if (experiments && _scale < EXPERIMENTAL_MINI_SCALE) {
const isWithinBundle = Object.keys(layout.bundles).some((bundleId) =>
hasPathPrefix(path, JSON.parse(bundleId)),
);
if (isWithinBundle) {
return null;
}

return (
<foreignObject
{...bounds}
key={id}
onMouseEnter={() => setHighlighted(id)}
onMouseLeave={() => setHighlighted(null)}
onClick={(e) => onSelectNode(e, {path}, graphNode)}
onDoubleClick={(e) => {
viewportEl.current?.zoomToSVGBox(bounds, true, 1.2);
e.stopPropagation();
}}
>
<AssetNodeMinimal
definition={graphNode.definition}
selected={selectedGraphNodes.includes(graphNode)}
fontSize={18 / _scale}
/>
</foreignObject>
);
}

return (
<foreignObject
{...bounds}
key={id}
onMouseEnter={() => setHighlighted(id)}
onMouseLeave={() => setHighlighted(null)}
onClick={(e) => onSelectNode(e, {path}, graphNode)}
onDoubleClick={(e) => {
viewportEl.current?.zoomToSVGBox(bounds, true, 1.2);
Expand Down Expand Up @@ -468,6 +580,10 @@ const SVGContainer = styled.svg`

// Helpers

const hasPathPrefix = (path: string[], prefix: string[]) => {
return isEqual(prefix, path.slice(0, prefix.length));
};

const graphDirectionOf = ({
graph,
from,
Expand Down
37 changes: 37 additions & 0 deletions js_modules/dagit/packages/core/src/asset-graph/AssetNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {Link} from 'react-router-dom';
import styled from 'styled-components/macro';

import {withMiddleTruncation} from '../app/Util';
import {AssetKey} from '../assets/types';
import {NodeHighlightColors} from '../graph/OpNode';
import {OpTags} from '../graph/OpTags';
import {linkToRunEvent, titleForRun} from '../runs/RunUtils';
Expand Down Expand Up @@ -157,6 +158,32 @@ export const AssetNode: React.FC<{
);
}, isEqual);

export const AssetNodeMinimal: React.FC<{
selected: boolean;
definition: {assetKey: AssetKey};
fontSize: number;
color?: string;
}> = ({selected, definition, fontSize, color}) => {
const displayName = withMiddleTruncation(displayNameForAssetKey(definition.assetKey), {
maxLength: 17,
});
return (
<AssetNodeContainer $selected={selected} style={{position: 'absolute', borderRadius: 12}}>
<AssetNodeBox
style={{
border: `4px solid ${Colors.Blue200}`,
borderRadius: 10,
position: 'absolute',
inset: 4,
background: color,
}}
>
<NameMinimal style={{fontSize}}>{displayName}</NameMinimal>
</AssetNodeBox>
</AssetNodeContainer>
);
};

export const AssetRunLink: React.FC<{
runId: string;
event?: Parameters<typeof linkToRunEvent>[1];
Expand Down Expand Up @@ -251,6 +278,16 @@ const Name = styled.div`
gap: 4px;
`;

const NameMinimal = styled(Name)`
font-weight: 600;
white-space: nowrap;
position: absolute;
background: none;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`;

const Description = styled.div`
background: ${BoxColors.Description};
padding: 4px 8px;
Expand Down

0 comments on commit d71cb6f

Please sign in to comment.