Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ui] Dagre changes to support more flexible asset graph rendering #20525

Merged
merged 15 commits into from
Mar 19, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import chevron_right from '../icon-svgs/chevron_right.svg';
import close from '../icon-svgs/close.svg';
import code_location from '../icon-svgs/code_location.svg';
import collapse_arrows from '../icon-svgs/collapse_arrows.svg';
import column_lineage from '../icon-svgs/column_lineage.svg';
import concept_book from '../icon-svgs/concept-book.svg';
import console_icon from '../icon-svgs/console.svg';
import content_copy from '../icon-svgs/content_copy.svg';
Expand Down Expand Up @@ -284,6 +285,7 @@ export const Icons = {
console: console_icon,
content_copy,
collapse_arrows,
column_lineage,
corporate_fare,
delete: deleteSVG,
done,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {Box, Caption, Colors, Icon, StyledTag, Tooltip} from '@dagster-io/ui-components';
import {Link} from 'react-router-dom';
import styled from 'styled-components';

import {
AssetInsetForHoverEffect,
AssetNameRow,
AssetNodeBox,
AssetNodeContainer,
} from './AssetNode';
import {AssetNodeFragment} from './types/AssetNode.types';
import {Timestamp} from '../app/time/Timestamp';
import {assetDetailsPathForKey} from '../assets/assetDetailsPathForKey';
import {AssetColumnLineageLocalColumn} from '../assets/lineage/useColumnLineageDataForAssets';
import {AssetComputeKindTag} from '../graph/OpTags';
import {AssetKeyInput} from '../graphql/types';
import {iconForColumnType} from '../metadata/TableSchema';

export const AssetColumnsGroupNode = ({
selected,
definition,
height,
asOf,
}: {
selected: boolean;
definition: AssetNodeFragment;
asOf: string | undefined;
height: number;
}) => {
return (
<AssetInsetForHoverEffect style={{marginLeft: 12, marginRight: 12}}>
<AssetNodeContainer $selected={selected}>
<div style={{minHeight: 24}} />
<AssetNodeBox $selected={selected} $isSource={definition.isSource} $noScale>
<AssetNameRow definition={definition} />
<Box style={{height: height - 60}} flex={{direction: 'column'}}></Box>
<Box border="top" padding={{horizontal: 8, vertical: 2}} style={{minHeight: 22}}>
{asOf ? (
<Caption color={Colors.textLighter()}>
<Timestamp timestamp={{ms: Number(asOf)}} />
</Caption>
) : undefined}
</Box>
</AssetNodeBox>
<AssetComputeKindTag definition={definition} style={{right: 10, paddingTop: 7}} />
</AssetNodeContainer>
</AssetInsetForHoverEffect>
);
};

export const AssetColumnNode = ({
assetKey,
column,
selected,
}: {
assetKey: AssetKeyInput;
column: AssetColumnLineageLocalColumn;
selected: boolean;
}) => {
const icon = iconForColumnType(column.type ?? '');

return (
<Box margin={{horizontal: 12}} style={{height: 32}} flex={{direction: 'column'}}>
<Tooltip key={column.name} content={column.description || 'No description provided'}>
<ColumnLink
to={assetDetailsPathForKey(assetKey, {view: 'lineage', column: column.name})}
$selected={selected}
>
{icon ? <Icon name={icon} /> : <span style={{width: 16}} />}
<Caption style={{whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'}}>
{column.name}
</Caption>
</ColumnLink>
</Tooltip>
</Box>
);
};

const ColumnLink = styled(Link)<{$selected: boolean}>`
height: 28px;
margin: 2px 0;
padding-left: 2px;
padding-right: 4px;
display: flex;
gap: 4px;
align-items: center;
transition: background 100ms linear;
border-radius: 8px;

${StyledTag} {
background: none;
color: ${Colors.textLight()};
}
${(p) =>
p.$selected
? `
background: ${Colors.backgroundBlue()}`
: `
&:hover {
text-decoration: none;
background: ${Colors.backgroundLightHover()};
}`}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import pickBy from 'lodash/pickBy';
import uniq from 'lodash/uniq';
import without from 'lodash/without';
import * as React from 'react';
import {useMemo} from 'react';
import styled from 'styled-components';

import {AssetEdges} from './AssetEdges';
Expand Down Expand Up @@ -269,7 +270,11 @@ const AssetGraphExplorerWithData = ({
});
const focusGroupIdAfterLayoutRef = React.useRef('');

const {layout, loading, async} = useAssetLayout(assetGraphData, expandedGroups, direction);
const {layout, loading, async} = useAssetLayout(
assetGraphData,
expandedGroups,
useMemo(() => ({direction}), [direction]),
);

React.useEffect(() => {
if (!loading) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ interface Props {
}

export const AssetNode = React.memo(({definition, selected}: Props) => {
const displayName = definition.assetKey.path[definition.assetKey.path.length - 1]!;
const isSource = definition.isSource;

const {liveData} = useAssetLiveData(definition.assetKey);

return (
<AssetInsetForHoverEffect>
<Box flex={{direction: 'row', justifyContent: 'space-between', alignItems: 'center'}}>
<Box
flex={{direction: 'row', justifyContent: 'space-between', alignItems: 'center'}}
style={{minHeight: 24}}
bengotow marked this conversation as resolved.
Show resolved Hide resolved
>
<StaleReasonsTag liveData={liveData} assetKey={definition.assetKey} include="upstream" />
<ChangedReasonsTag
changedReasons={definition.changedReasons}
Expand All @@ -44,21 +46,7 @@ export const AssetNode = React.memo(({definition, selected}: Props) => {
</Box>
<AssetNodeContainer $selected={selected}>
<AssetNodeBox $selected={selected} $isSource={isSource}>
<AssetName $isSource={isSource}>
<span style={{marginTop: 1}}>
<Icon name={isSource ? 'source_asset' : 'asset'} />
</span>
<div
data-tooltip={displayName}
data-tooltip-style={isSource ? NameTooltipStyleSource : NameTooltipStyle}
style={{overflow: 'hidden', textOverflow: 'ellipsis'}}
>
{withMiddleTruncation(displayName, {
maxLength: ASSET_NODE_NAME_MAX_LENGTH,
})}
</div>
<div style={{flex: 1}} />
</AssetName>
<AssetNameRow definition={definition} />
<Box style={{padding: '6px 8px'}} flex={{direction: 'column', gap: 4}} border="top">
{definition.description ? (
<AssetDescription $color={Colors.textDefault()}>
Expand All @@ -83,6 +71,28 @@ export const AssetNode = React.memo(({definition, selected}: Props) => {
);
}, isEqual);

export const AssetNameRow = ({definition}: {definition: AssetNodeFragment}) => {
const displayName = definition.assetKey.path[definition.assetKey.path.length - 1]!;

return (
<AssetName $isSource={definition.isSource}>
<span style={{marginTop: 1}}>
<Icon name={definition.isSource ? 'source_asset' : 'asset'} />
</span>
<div
data-tooltip={displayName}
data-tooltip-style={definition.isSource ? NameTooltipStyleSource : NameTooltipStyle}
style={{overflow: 'hidden', textOverflow: 'ellipsis'}}
>
{withMiddleTruncation(displayName, {
maxLength: ASSET_NODE_NAME_MAX_LENGTH,
})}
</div>
<div style={{flex: 1}} />
</AssetName>
);
};

const AssetNodeRowBox = styled(Box)`
white-space: nowrap;
line-height: 12px;
Expand Down Expand Up @@ -244,7 +254,7 @@ export const ASSET_NODE_FRAGMENT = gql`
}
`;

const AssetInsetForHoverEffect = styled.div`
export const AssetInsetForHoverEffect = styled.div`
padding: 10px 4px 2px 4px;
height: 100%;

Expand All @@ -253,7 +263,7 @@ const AssetInsetForHoverEffect = styled.div`
}
`;

const AssetNodeContainer = styled.div<{$selected: boolean}>`
export const AssetNodeContainer = styled.div<{$selected: boolean}>`
user-select: none;
cursor: pointer;
padding: 6px;
Expand All @@ -264,7 +274,11 @@ const AssetNodeShowOnHover = styled.span`
display: none;
`;

const AssetNodeBox = styled.div<{$isSource: boolean; $selected: boolean}>`
export const AssetNodeBox = styled.div<{
$isSource: boolean;
$selected: boolean;
$noScale?: boolean;
}>`
${(p) =>
p.$isSource
? `border: 2px dashed ${p.$selected ? Colors.accentGrayHover() : Colors.accentGray()}`
Expand All @@ -280,7 +294,7 @@ const AssetNodeBox = styled.div<{$isSource: boolean; $selected: boolean}>`
&:hover {
${(p) => !p.$selected && `border: 2px solid ${Colors.lineageNodeBorderHover()};`};
box-shadow: ${Colors.shadowDefault()} 0px 1px 4px 0px;
scale: 1.03;
scale: ${(p) => (p.$noScale ? '1' : '1.03')};
${AssetNodeShowOnHover} {
display: initial;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import {DagsterTypeSummary} from '../dagstertype/DagsterType';
import {DagsterTypeFragment} from '../dagstertype/types/DagsterType.types';
import {METADATA_ENTRY_FRAGMENT} from '../metadata/MetadataEntry';
import {TableSchemaLineageContext} from '../metadata/TableSchema';
import {Description} from '../pipelines/Description';
import {SidebarSection, SidebarTitle} from '../pipelines/SidebarComponents';
import {ResourceContainer, ResourceHeader} from '../pipelines/SidebarOpHelpers';
Expand Down Expand Up @@ -159,7 +160,12 @@ export const SidebarAssetInfo = ({graphNode}: {graphNode: GraphNode}) => {

{assetMetadata.length > 0 && (
<SidebarSection title="Metadata">
<AssetMetadataTable assetMetadata={assetMetadata} repoLocation={repoAddress?.location} />
<TableSchemaLineageContext.Provider value={{assetKey: asset.assetKey}}>
<AssetMetadataTable
assetMetadata={assetMetadata}
repoLocation={repoAddress?.location}
/>
</TableSchemaLineageContext.Provider>
</SidebarSection>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function isHiddenAssetGroupJob(jobName: string) {
//
export type GraphId = string;
export const toGraphId = (key: {path: string[]}): GraphId => JSON.stringify(key.path);
export const fromGraphID = (graphId: GraphId): AssetNodeKeyFragment => ({
export const fromGraphId = (graphId: GraphId): AssetNodeKeyFragment => ({
path: JSON.parse(graphId),
__typename: 'AssetKey',
});
Expand Down Expand Up @@ -266,7 +266,7 @@ export const itemWithAssetKey = (key: {path: string[]}) => {
return (asset: {assetKey: {path: string[]}}) => tokenForAssetKey(asset.assetKey) === token;
};

export const isGroupId = (str: string) => /^[^@:]+@[^@:]+:[^@:]+$/.test(str);
export const isGroupId = (str: string) => /^[^@:]+@[^@:]+:.+$/.test(str);
bengotow marked this conversation as resolved.
Show resolved Hide resolved

export const groupIdForNode = (node: GraphNode) =>
[
Expand All @@ -281,7 +281,7 @@ export const groupIdForNode = (node: GraphNode) =>
export const getUpstreamNodes = memoize(
(assetKey: AssetNodeKeyFragment, graphData: GraphData): AssetNodeKeyFragment[] => {
const upstream = Object.keys(graphData.upstream[toGraphId(assetKey)] || {});
const currentUpstream = upstream.map((graphId) => fromGraphID(graphId));
const currentUpstream = upstream.map((graphId) => fromGraphId(graphId));
return [
assetKey,
...currentUpstream,
Expand Down
Loading
Loading