diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/TextInput.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/TextInput.tsx index c50ce17e8a121..7b8e09869668a 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/components/TextInput.tsx +++ b/js_modules/dagster-ui/packages/ui-components/src/components/TextInput.tsx @@ -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 { diff --git a/js_modules/dagster-ui/packages/ui-components/src/index.ts b/js_modules/dagster-ui/packages/ui-components/src/index.ts index cca86333fa3a5..0930eda54d044 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/index.ts +++ b/js_modules/dagster-ui/packages/ui-components/src/index.ts @@ -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'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/Flags.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/Flags.tsx index 7b2d81f1a598c..f41bc56ac09d7 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/app/Flags.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/app/Flags.tsx @@ -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; diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx index edc4803aebdbb..ceb9f51b36f9c 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx @@ -20,7 +20,7 @@ import {TimezoneSelect} from './time/TimezoneSelect'; import {automaticLabel} from './time/browserTimezone'; type OnCloseFn = (event: React.SyntheticEvent) => void; -type VisibleFlag = {key: string; flagType: FeatureFlagType}; +type VisibleFlag = {key: string; label?: React.ReactNode; flagType: FeatureFlagType}; interface DialogProps { isOpen: boolean; @@ -43,7 +43,7 @@ export const UserSettingsDialog: React.FC = ({isOpen, onClose, visi interface DialogContentProps { onClose: OnCloseFn; - visibleFlags: {key: string; flagType: FeatureFlagType}[]; + visibleFlags: {key: string; label?: React.ReactNode; flagType: FeatureFlagType}[]; } /** @@ -138,8 +138,9 @@ const UserSettingsDialogContent: React.FC = ({onClose, visib Experimental features ({ + rows={visibleFlags.map(({key, label, flagType}) => ({ key, + label, value: ( [ key: 'Experimental horizontal asset DAGs', flagType: FeatureFlag.flagHorizontalDAGs, }, + { + key: 'New asset lineage sidebar', + label: ( + + New asset lineage sidebar, + + + ), + flagType: FeatureFlag.flagDAGSidebar, + }, ]; diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx index 495215bbecca4..9b4c7477a03d7 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx @@ -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'; @@ -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 { @@ -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'; @@ -136,11 +140,12 @@ const AssetGraphExplorerWithData: React.FC = ({ applyingEmptyDefault, fetchOptions, fetchOptionFilters, + allAssetKeys, }) => { const findAssetLocation = useFindAssetLocation(); const {layout, loading, async} = useAssetLayout(assetGraphData); const viewportEl = React.useRef(); - const {flagHorizontalDAGs} = useFeatureFlags(); + const {flagHorizontalDAGs, flagDAGSidebar} = useFeatureFlags(); const [highlighted, setHighlighted] = React.useState(null); @@ -240,7 +245,10 @@ const AssetGraphExplorerWithData: React.FC = ({ // 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); @@ -262,17 +270,32 @@ const AssetGraphExplorerWithData: React.FC = ({ 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 | React.KeyboardEvent, 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 = ( = ({ {graphQueryItems.length === 0 ? ( ) : applyingEmptyDefault ? ( - + ) : Object.keys(assetGraphData.nodes).length === 0 ? ( ) : undefined} @@ -301,7 +324,7 @@ const AssetGraphExplorerWithData: React.FC = ({ viewportEl.current?.autocenter(true); e.stopPropagation(); }} - maxZoom={1.2} + maxZoom={DEFAULT_MAX_ZOOM} maxAutocenterZoom={1.0} > {({scale}) => ( @@ -400,10 +423,7 @@ const AssetGraphExplorerWithData: React.FC = ({ )} - + = ({ + {showSidebar || !flagDAGSidebar ? null : ( + + + + + ); +}; + +const BoxWrapper = ({level, children}: {level: number; children: React.ReactNode}) => { + const wrapper = React.useMemo(() => { + let sofar = children; + for (let i = 0; i < level; i++) { + sofar = ( + + {sofar} + + ); + } + return sofar; + }, [level, children]); + + return <>{wrapper}; +}; + +function getDisplayName(node: GraphNode) { + return node.assetKey.path[node.assetKey.path.length - 1]!; +} + +const SearchFilter = ({ + values, + onSelectValue, +}: { + values: {label: string; value: T}[]; + onSelectValue: (e: React.MouseEvent, value: T) => void; +}) => { + const [searchValue, setSearchValue] = React.useState(''); + const filteredValues = React.useMemo(() => { + if (searchValue) { + return values.filter(({label}) => label.toLowerCase().includes(searchValue.toLowerCase())); + } + return values; + }, [searchValue, values]); + + const {viewport, containerProps} = useViewport(); + return ( + + + {filteredValues.length ? ( + filteredValues.map((value) => ( + { + onSelectValue(e, value.value); + setSearchValue(''); + }} + text={value.label} + /> + )) + ) : ( + No results + )} + + + ) : ( +
+ ) + } + > +
+ { + setSearchValue(e.target.value); + }} + placeholder="Search assets" + {...(containerProps as any)} + /> +
+ + ); +}; + +const ExpandMore = styled.div``; + +const GrayOnHoverBox = styled(Box)` + border-radius: 8px; + &:hover { + background: ${Colors.Gray100}; + transition: background 100ms linear; + ${ExpandMore} { + visibility: visible; + } + } + ${ExpandMore} { + visibility: hidden; + } +`; + +const ButtonGroupWrapper = styled.div` + > * { + display: grid; + grid-template-columns: 1fr 1fr; + > * { + place-content: center; + } + } +`; + +function nodeId(node: {path: string; id: string} | {id: string}) { + return 'path' in node ? node.path : node.id; +} diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNode.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNode.tsx index a105c3bb98a48..9f1561cef6632 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNode.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNode.tsx @@ -330,7 +330,7 @@ const NameTooltipCSS: CSSObject = { fontSize: 16.8, }; -const NameTooltipStyle = JSON.stringify({ +export const NameTooltipStyle = JSON.stringify({ ...NameTooltipCSS, background: Colors.Blue50, border: `1px solid ${Colors.Blue100}`, diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/Utils.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/Utils.tsx index ffdf4a5c948ce..149762b469203 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/Utils.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/Utils.tsx @@ -240,3 +240,21 @@ export const itemWithAssetKey = (key: {path: string[]}) => { const token = tokenForAssetKey(key); return (asset: {assetKey: {path: string[]}}) => tokenForAssetKey(asset.assetKey) === token; }; + +export function walkTreeUpwards( + nodeId: string, + graphData: GraphData, + callback: (nodeId: string) => void, +) { + // TODO + console.log({nodeId, graphData, callback}); +} + +export function walkTreeDownwards( + nodeId: string, + graphData: GraphData, + callback: (nodeId: string) => void, +) { + // TODO + console.log({nodeId, graphData, callback}); +} diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetGroupRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetGroupRoot.tsx index 5b744b9275156..ef3242eff5a9f 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetGroupRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetGroupRoot.tsx @@ -65,7 +65,7 @@ export const AssetGroupRoot: React.FC<{repoAddress: RepoAddress; tab: 'lineage' const onNavigateToSourceAssetNode = React.useCallback( (node: AssetLocation) => { if (node.groupName && node.repoAddress) { - history.replace( + history.push( workspacePathFromAddress( node.repoAddress, `/asset-groups/${node.groupName}/lineage/${node.assetKey.path diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeLineageGraph.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeLineageGraph.tsx index d729dd4656a50..3eab93656a98a 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeLineageGraph.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeLineageGraph.tsx @@ -9,7 +9,7 @@ import {AssetGroupNode} from '../asset-graph/AssetGroupNode'; import {AssetNodeMinimal, AssetNode} from '../asset-graph/AssetNode'; import {AssetNodeLink} from '../asset-graph/ForeignNode'; import {GraphData, LiveData, toGraphId} from '../asset-graph/Utils'; -import {SVGViewport} from '../graph/SVGViewport'; +import {DEFAULT_MAX_ZOOM, SVGViewport} from '../graph/SVGViewport'; import {useAssetLayout} from '../graph/asyncGraphLayout'; import {AssetKeyInput} from '../graphql/types'; import {getJSONForKey} from '../hooks/useStateWithStorage'; @@ -64,8 +64,8 @@ export const AssetNodeLineageGraph: React.FC<{ viewportEl.current?.autocenter(true); e.stopPropagation(); }} - maxZoom={1.2} - maxAutocenterZoom={1.2} + maxZoom={DEFAULT_MAX_ZOOM} + maxAutocenterZoom={DEFAULT_MAX_ZOOM} > {({scale}) => ( diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsGroupsGlobalGraphRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsGroupsGlobalGraphRoot.tsx index 0cda0623eb016..1f348adccfc18 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsGroupsGlobalGraphRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsGroupsGlobalGraphRoot.tsx @@ -4,17 +4,17 @@ import * as React from 'react'; import {useHistory, useParams} from 'react-router-dom'; import {AssetGraphExplorer} from '../asset-graph/AssetGraphExplorer'; +import {AssetGraphExplorerFilters} from '../asset-graph/AssetGraphExplorerFilters'; import {AssetGraphFetchScope} from '../asset-graph/useAssetGraphData'; import {AssetLocation} from '../asset-graph/useFindAssetLocation'; import {AssetGroupSelector} from '../graphql/types'; import {useDocumentTitle} from '../hooks/useDocumentTitle'; import {useQueryPersistedState} from '../hooks/useQueryPersistedState'; -import {RepoFilterButton} from '../instance/RepoFilterButton'; import {ExplorerPath} from '../pipelines/PipelinePathUtils'; import {ReloadAllButton} from '../workspace/ReloadAllButton'; import {WorkspaceContext} from '../workspace/WorkspaceContext'; -import {AssetGroupSuggest, buildAssetGroupSelector} from './AssetGroupSuggest'; +import {buildAssetGroupSelector} from './AssetGroupSuggest'; import {assetDetailsPathForKey} from './assetDetailsPathForKey'; import { globalAssetGraphPathFromString, @@ -27,7 +27,7 @@ interface AssetGroupRootParams { export const AssetsGroupsGlobalGraphRoot: React.FC = () => { const {0: path} = useParams(); - const {allRepos, visibleRepos} = React.useContext(WorkspaceContext); + const {visibleRepos} = React.useContext(WorkspaceContext); const history = useHistory(); const [filters, setFilters] = useQueryPersistedState<{groups: AssetGroupSelector[]}>({ @@ -103,14 +103,14 @@ export const AssetsGroupsGlobalGraphRoot: React.FC = () => { - {allRepos.length > 1 && } - setFilters({...filters, groups})} - /> - + filters.groups || [], [filters.groups])} + setGroupFilters={React.useCallback((groups) => setFilters({...filters, groups}), [ + filters, + setFilters, + ])} + /> } options={{preferAssetRendering: true, explodeComposites: true}} explorerPath={globalAssetGraphPathFromString(path)} diff --git a/js_modules/dagster-ui/packages/ui-core/src/graph/OpGraph.tsx b/js_modules/dagster-ui/packages/ui-core/src/graph/OpGraph.tsx index 72fcfb6d581f7..ebca923fcd8b8 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/graph/OpGraph.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/graph/OpGraph.tsx @@ -8,7 +8,7 @@ import {OpNameOrPath} from '../ops/OpNameOrPath'; import {OpEdges} from './OpEdges'; import {OpNode, OP_NODE_DEFINITION_FRAGMENT, OP_NODE_INVOCATION_FRAGMENT} from './OpNode'; import {ParentOpNode, SVGLabeledParentRect} from './ParentOpNode'; -import {DETAIL_ZOOM, SVGViewport, SVGViewportInteractor} from './SVGViewport'; +import {DEFAULT_MAX_ZOOM, DETAIL_ZOOM, SVGViewport, SVGViewportInteractor} from './SVGViewport'; import {OpGraphLayout} from './asyncGraphLayout'; import { Edge, @@ -199,7 +199,7 @@ export class OpGraph extends React.Component { ` white-space: nowrap; `; -const LabelTooltipStyles = JSON.stringify({ +export const LabelTooltipStyles = JSON.stringify({ background: Colors.Gray100, filter: `brightness(97%)`, color: Colors.Gray900, @@ -140,11 +140,18 @@ const TruncatingName = styled.div` export const TruncatedTextWithFullTextOnHover = React.forwardRef( ( - {text, tooltipStyle, ...rest}: {text: string; tooltipStyle?: string}, + { + text, + tooltipStyle, + tooltipText, + ...rest + }: + | {text: string; tooltipStyle?: string; tooltipText?: null} + | {text: React.ReactNode; tooltipStyle?: string; tooltipText: string}, ref: React.ForwardedRef, ) => (