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

[1/n] Asset graph sidebar #16447

Merged
merged 15 commits into from
Sep 20, 2023
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';

// Global font styles, exported as styled-component components to render in
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 @@ -21,7 +21,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 @@ -44,7 +44,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 @@ -139,8 +139,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