Skip to content

Commit

Permalink
[1/n] Asset graph sidebar (#16447)
Browse files Browse the repository at this point in the history
## Summary & Motivation

https://www.loom.com/share/647d76108782411f8ef1c9b06f55eae5

This adds a sidebar to the left of the asset graph that makes it easier
to navigate the asset graph without needing to scroll around. This is
useful for cases where the asset graph is very large and it's hard to
see the ancestors / descendants of a particular asset.

It would be nice if we could figure out how to combine this with the
existing asset graph sidebar that shows up on the right side somehow so
that there's only one sidebar.

I put minimal effort into the design for now since this is just a
prototype I built to demonstrate one solution to the problem of
navigating large asset graphs. Looking for input as to whether we should
pursue this direction or not.
  • Loading branch information
salazarm authored Sep 20, 2023
1 parent 8c7e644 commit 27e5b66
Show file tree
Hide file tree
Showing 17 changed files with 1,030 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const TextInputContainerStyles = css`
position: relative;
`;

export const TextInputContainer = styled.div<{$disabled: boolean}>`
export const TextInputContainer = styled.div<{$disabled?: boolean}>`
${TextInputContainerStyles}
> ${IconWrapper}:first-child {
Expand Down
1 change: 1 addition & 0 deletions js_modules/dagster-ui/packages/ui-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export * from './components/styles';
export * from './components/useSuggestionsForString';
export * from './components/ErrorBoundary';
export * from './components/useViewport';
export * from './components/UnstyledButton';
export * from './components/StyledRawCodeMirror';
export * from './components/useDelayedState';

Expand Down
1 change: 1 addition & 0 deletions js_modules/dagster-ui/packages/ui-core/src/app/Flags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const FeatureFlag = {
flagSidebarResources: 'flagSidebarResources' as const,
flagHorizontalDAGs: 'flagHorizontalDAGs' as const,
flagDisableAutoLoadDefaults: 'flagDisableAutoLoadDefaults' as const,
flagDAGSidebar: 'flagDAGSidebar' as const,
};
export type FeatureFlagType = keyof typeof FeatureFlag;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {TimezoneSelect} from './time/TimezoneSelect';
import {automaticLabel} from './time/browserTimezone';

type OnCloseFn = (event: React.SyntheticEvent<HTMLElement>) => void;
type VisibleFlag = {key: string; flagType: FeatureFlagType};
type VisibleFlag = {key: string; label?: React.ReactNode; flagType: FeatureFlagType};

interface DialogProps {
isOpen: boolean;
Expand All @@ -43,7 +43,7 @@ export const UserSettingsDialog: React.FC<DialogProps> = ({isOpen, onClose, visi

interface DialogContentProps {
onClose: OnCloseFn;
visibleFlags: {key: string; flagType: FeatureFlagType}[];
visibleFlags: {key: string; label?: React.ReactNode; flagType: FeatureFlagType}[];
}

/**
Expand Down Expand Up @@ -138,8 +138,9 @@ const UserSettingsDialogContent: React.FC<DialogContentProps> = ({onClose, visib
<Subheading>Experimental features</Subheading>
</Box>
<MetadataTable
rows={visibleFlags.map(({key, flagType}) => ({
rows={visibleFlags.map(({key, label, flagType}) => ({
key,
label,
value: (
<Checkbox
format="switch"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import {Box} from '@dagster-io/ui-components';
import React from 'react';

import {FeatureFlag} from './Flags';

/**
Expand Down Expand Up @@ -32,4 +35,22 @@ export const getVisibleFeatureFlagRows = () => [
key: 'Experimental horizontal asset DAGs',
flagType: FeatureFlag.flagHorizontalDAGs,
},
{
key: 'New asset lineage sidebar',
label: (
<Box flex={{direction: 'column'}}>
New asset lineage sidebar,
<div>
<a
href="https://github.com/dagster-io/dagster/discussions/16657"
target="_blank"
rel="noreferrer"
>
Learn more
</a>
</div>
</Box>
),
flagType: FeatureFlag.flagDAGSidebar,
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
NonIdealState,
SplitPanelContainer,
ErrorBoundary,
Button,
Icon,
Tooltip,
} from '@dagster-io/ui-components';
import pickBy from 'lodash/pickBy';
import uniq from 'lodash/uniq';
Expand All @@ -17,7 +20,7 @@ import {QueryRefreshCountdown, QueryRefreshState} from '../app/QueryRefresh';
import {LaunchAssetExecutionButton} from '../assets/LaunchAssetExecutionButton';
import {LaunchAssetObservationButton} from '../assets/LaunchAssetObservationButton';
import {AssetKey} from '../assets/types';
import {SVGViewport} from '../graph/SVGViewport';
import {DEFAULT_MAX_ZOOM, SVGViewport} from '../graph/SVGViewport';
import {useAssetLayout} from '../graph/asyncGraphLayout';
import {closestNodeInDirection} from '../graph/common';
import {
Expand All @@ -38,6 +41,7 @@ import {GraphQueryInput} from '../ui/GraphQueryInput';
import {Loading} from '../ui/Loading';

import {AssetEdges} from './AssetEdges';
import {AssetGraphExplorerSidebar} from './AssetGraphExplorerSidebar';
import {AssetGraphJobSidebar} from './AssetGraphJobSidebar';
import {AssetGroupNode} from './AssetGroupNode';
import {AssetNode, AssetNodeMinimal} from './AssetNode';
Expand Down Expand Up @@ -136,11 +140,12 @@ const AssetGraphExplorerWithData: React.FC<WithDataProps> = ({
applyingEmptyDefault,
fetchOptions,
fetchOptionFilters,
allAssetKeys,
}) => {
const findAssetLocation = useFindAssetLocation();
const {layout, loading, async} = useAssetLayout(assetGraphData);
const viewportEl = React.useRef<SVGViewport>();
const {flagHorizontalDAGs} = useFeatureFlags();
const {flagHorizontalDAGs, flagDAGSidebar} = useFeatureFlags();

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

Expand Down Expand Up @@ -240,7 +245,10 @@ const AssetGraphExplorerWithData: React.FC<WithDataProps> = ({
// focus on the selected node. (If selection was specified in the URL).
// Don't animate this change.
if (lastSelectedNode) {
// viewportEl.current.zoomToSVGBox(layout.nodes[lastSelectedNode.id].bounds, false);
const layoutNode = layout.nodes[lastSelectedNode.id];
if (layoutNode) {
viewportEl.current.zoomToSVGBox(layoutNode.bounds, false);
}
viewportEl.current.focus();
} else {
viewportEl.current.autocenter(false);
Expand All @@ -262,17 +270,32 @@ const AssetGraphExplorerWithData: React.FC<WithDataProps> = ({
const layoutWithoutExternalLinks = {...layout, nodes: pickBy(layout.nodes, hasDefinition)};

const nextId = closestNodeInDirection(layoutWithoutExternalLinks, lastSelectedNode.id, dir);
const node = nextId && assetGraphData.nodes[nextId];
if (node && viewportEl.current) {
onSelectNode(e, node.assetKey, node);
viewportEl.current.zoomToSVGBox(layout.nodes[nextId]!.bounds, true);
}
selectNodeById(e, nextId);
};

const selectNodeById = React.useCallback(
(e: React.MouseEvent<any> | React.KeyboardEvent<any>, nodeId?: string) => {
if (!nodeId) {
return;
}
const node = assetGraphData.nodes[nodeId];
if (node) {
onSelectNode(e, node.assetKey, node);
if (layout && viewportEl.current) {
viewportEl.current.zoomToSVGBox(layout.nodes[nodeId]!.bounds, true);
}
}
},
[assetGraphData.nodes, layout, onSelectNode],
);

const allowGroupsOnlyZoomLevel = !!(layout && Object.keys(layout.groups).length);

return (
const [showSidebar, setShowSidebar] = React.useState(true);

const explorer = (
<SplitPanelContainer
key="explorer"
identifier="explorer"
firstInitialPercent={70}
firstMinSize={400}
Expand All @@ -281,7 +304,7 @@ const AssetGraphExplorerWithData: React.FC<WithDataProps> = ({
{graphQueryItems.length === 0 ? (
<EmptyDAGNotice nodeType="asset" isGraph />
) : applyingEmptyDefault ? (
<LargeDAGNotice nodeType="asset" anchorLeft={fetchOptionFilters ? '300px' : '40px'} />
<LargeDAGNotice nodeType="asset" anchorLeft="40px" />
) : Object.keys(assetGraphData.nodes).length === 0 ? (
<EntirelyFilteredDAGNotice nodeType="asset" />
) : undefined}
Expand All @@ -301,7 +324,7 @@ const AssetGraphExplorerWithData: React.FC<WithDataProps> = ({
viewportEl.current?.autocenter(true);
e.stopPropagation();
}}
maxZoom={1.2}
maxZoom={DEFAULT_MAX_ZOOM}
maxAutocenterZoom={1.0}
>
{({scale}) => (
Expand Down Expand Up @@ -400,10 +423,7 @@ const AssetGraphExplorerWithData: React.FC<WithDataProps> = ({
</OptionsOverlay>
)}

<Box
flex={{direction: 'column', alignItems: 'flex-end', gap: 8}}
style={{position: 'absolute', right: 12, top: 8}}
>
<Box style={{position: 'absolute', right: 12, top: 8}}>
<Box flex={{alignItems: 'center', gap: 12}}>
<QueryRefreshCountdown
refreshState={liveDataRefreshState}
Expand All @@ -429,6 +449,16 @@ const AssetGraphExplorerWithData: React.FC<WithDataProps> = ({
</Box>
</Box>
<QueryOverlay>
{showSidebar || !flagDAGSidebar ? null : (
<Tooltip content="Show sidebar">
<Button
icon={<Icon name="panel_show_left" />}
onClick={() => {
setShowSidebar(true);
}}
/>
</Tooltip>
)}
{fetchOptionFilters}

<GraphQueryInput
Expand Down Expand Up @@ -466,6 +496,34 @@ const AssetGraphExplorerWithData: React.FC<WithDataProps> = ({
}
/>
);

if (showSidebar && flagDAGSidebar) {
return (
<SplitPanelContainer
key="explorer-wrapper"
identifier="explorer-wrapper"
firstMinSize={300}
firstInitialPercent={0}
first={
showSidebar ? (
<AssetGraphExplorerSidebar
allAssetKeys={allAssetKeys}
assetGraphData={assetGraphData}
lastSelectedNode={lastSelectedNode}
selectNode={selectNodeById}
explorerPath={explorerPath}
onChangeExplorerPath={onChangeExplorerPath}
hideSidebar={() => {
setShowSidebar(false);
}}
/>
) : null
}
second={explorer}
/>
);
}
return explorer;
};

const SVGContainer = styled.svg`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {Box, Icon} from '@dagster-io/ui-components';
import React from 'react';

import {AssetGroupSelector} from '../graphql/types';
import {TruncatedTextWithFullTextOnHover} from '../nav/getLeftNavItemsForOption';
import {useFilters} from '../ui/Filters';
import {FilterObject} from '../ui/Filters/useFilter';
import {useStaticSetFilter} from '../ui/Filters/useStaticSetFilter';
import {DagsterRepoOption, WorkspaceContext} from '../workspace/WorkspaceContext';
import {buildRepoAddress, buildRepoPathForHuman} from '../workspace/buildRepoAddress';

export const AssetGraphExplorerFilters = React.memo(
({
assetGroups,
visibleAssetGroups,
setGroupFilters,
}:
| {
assetGroups: AssetGroupSelector[];
visibleAssetGroups: AssetGroupSelector[];
setGroupFilters: (groups: AssetGroupSelector[]) => void;
}
| {assetGroups?: null; setGroupFilters?: null; visibleAssetGroups?: null}) => {
const {allRepos, visibleRepos, toggleVisible} = React.useContext(WorkspaceContext);

const visibleReposSet = React.useMemo(() => new Set(visibleRepos), [visibleRepos]);

const reposFilter = useStaticSetFilter<DagsterRepoOption>({
name: 'Repository',
icon: 'repo',
allValues: allRepos.map((repo) => ({
key: repo.repository.id,
value: repo,
match: [buildRepoPathForHuman(repo.repository.name, repo.repositoryLocation.name)],
})),
menuWidth: '300px',
renderLabel: ({value}) => (
<Box flex={{direction: 'row', gap: 4, alignItems: 'center'}}>
<Icon name="repo" />
<TruncatedTextWithFullTextOnHover
text={buildRepoPathForHuman(value.repository.name, value.repositoryLocation.name)}
/>
</Box>
),
getStringValue: (value) =>
buildRepoPathForHuman(value.repository.name, value.repositoryLocation.name),
initialState: visibleReposSet,
onStateChanged: (values) => {
allRepos.forEach((repo) => {
if (visibleReposSet.has(repo) !== values.has(repo)) {
toggleVisible([buildRepoAddress(repo.repository.name, repo.repositoryLocation.name)]);
}
});
},
});

const groupsFilter = useStaticSetFilter<AssetGroupSelector>({
name: 'Asset Groups',
icon: 'asset_group',
allValues: (assetGroups || []).map((group) => ({
key: group.groupName,
value:
visibleAssetGroups?.find(
(visibleGroup) =>
visibleGroup.groupName === group.groupName &&
visibleGroup.repositoryName === group.repositoryName &&
visibleGroup.repositoryLocationName === group.repositoryLocationName,
) ?? group,
match: [group.groupName],
})),
menuWidth: '300px',
renderLabel: ({value}) => (
<Box flex={{direction: 'row', gap: 4, alignItems: 'center'}}>
<Icon name="repo" />
<TruncatedTextWithFullTextOnHover
tooltipText={
value.groupName +
' - ' +
buildRepoPathForHuman(value.repositoryName, value.repositoryLocationName)
}
text={
<>
{value.groupName}
<span style={{opacity: 0.5, paddingLeft: '4px'}}>
{buildRepoPathForHuman(value.repositoryName, value.repositoryLocationName)}
</span>
</>
}
/>
</Box>
),
getStringValue: (group) => group.groupName,
initialState: React.useMemo(() => new Set(visibleAssetGroups ?? []), [visibleAssetGroups]),
onStateChanged: (values) => {
if (setGroupFilters) {
setGroupFilters(Array.from(values));
}
},
});

const filters: FilterObject[] = [];
if (allRepos.length > 1) {
filters.push(reposFilter);
}
if (assetGroups) {
filters.push(groupsFilter);
}
const {button} = useFilters({filters});
if (allRepos.length <= 1 && !assetGroups) {
return null;
}
return button;
},
);
Loading

2 comments on commit 27e5b66

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for dagit-storybook ready!

✅ Preview
https://dagit-storybook-d9p84zpkd-elementl.vercel.app

Built with commit 27e5b66.
This pull request is being automatically deployed with vercel-action

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for dagit-core-storybook ready!

✅ Preview
https://dagit-core-storybook-fpf148naf-elementl.vercel.app

Built with commit 27e5b66.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.