diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/CollapsibleSection.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/CollapsibleSection.tsx index 322a9c36d22f5..449db327a3989 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/components/CollapsibleSection.tsx +++ b/js_modules/dagster-ui/packages/ui-components/src/components/CollapsibleSection.tsx @@ -39,11 +39,11 @@ export const CollapsibleSection = ({ name="arrow_drop_down" style={{transform: isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)'}} /> -
{header}
+
{header}
) : ( - -
{header}
+ +
{header}
void; selectNode?: (e: React.MouseEvent | React.KeyboardEvent, nodeId: string) => void; @@ -30,8 +41,8 @@ export const useAssetNodeMenu = ({ explorerPath, onChangeExplorerPath, }: AssetNodeMenuProps) => { - const upstream = Object.keys(graphData.upstream[node.id] ?? {}); - const downstream = Object.keys(graphData.downstream[node.id] ?? {}); + const upstream = graphData ? Object.keys(graphData.upstream[node.id] ?? {}) : []; + const downstream = graphData ? Object.keys(graphData.downstream[node.id] ?? {}) : []; const {executeItem, launchpadElement} = useExecuteAssetMenuItem( node.assetKey.path, @@ -80,7 +91,7 @@ export const useAssetNodeMenu = ({ {executeItem} {executeItem && (upstream.length || downstream.length) ? : null} - {upstream.length ? ( + {upstream.length && graphData ? ( ) : null} - {upstream.length ? ( + {upstream.length || !graphData ? ( showGraph(`*\"${tokenForAssetKey(node.assetKey)}\"`)} /> ) : null} - {downstream.length ? ( + {downstream.length || !graphData ? ( - + {graphData && ( + + )} {launchpadElement} ), diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNodeStatusContent.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNodeStatusContent.tsx index 5542fbd7bb415..0802fca5d9e10 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNodeStatusContent.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetNodeStatusContent.tsx @@ -49,7 +49,7 @@ const LOADING_STATUS_CONTENT = { ), }; -type StatusContentArgs = { +export type StatusContentArgs = { assetKey: AssetKeyInput; definition: {opNames: string[]; isSource: boolean; isObservable: boolean}; liveData: LiveDataForNode | null | undefined; diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/ContextMenuWrapper.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/ContextMenuWrapper.tsx index 3bad4ecebeb6f..94c4300f1808b 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/ContextMenuWrapper.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/ContextMenuWrapper.tsx @@ -18,14 +18,24 @@ export const ContextMenuWrapper = ({ wrapperInnerStyles?: React.CSSProperties; }) => { const [menuVisible, setMenuVisible] = React.useState(false); - const [menuPosition, setMenuPosition] = React.useState<{top: number; left: number}>({ - top: 0, - left: 0, + const [menuPosition, setMenuPosition] = React.useState<{ + x: number; + y: number; + anchor: 'left' | 'right'; + }>({ + anchor: 'left', + x: 0, + y: 0, }); const showMenu = (e: React.MouseEvent) => { + const anchor = window.innerWidth - e.pageX < 240 ? 'right' : 'left'; e.preventDefault(); - setMenuPosition({top: e.pageY, left: e.pageX}); + setMenuPosition({ + x: anchor === 'left' ? e.pageX : window.innerWidth - e.pageX, + y: e.pageY, + anchor, + }); if (!menuVisible) { setMenuVisible(true); @@ -78,8 +88,9 @@ export const ContextMenuWrapper = ({
{ const evt = new MouseEvent('contextmenu', e.nativeEvent); e.target.dispatchEvent(evt); e.stopPropagation(); + e.preventDefault(); }; diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/AssetSidebarNode.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/AssetSidebarNode.tsx index ff91f04ec502f..c4aaa94ea73e6 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/AssetSidebarNode.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/AssetSidebarNode.tsx @@ -2,7 +2,7 @@ import {Box, Colors, Icon, MiddleTruncate, UnstyledButton} from '@dagster-io/ui- import * as React from 'react'; import styled from 'styled-components'; -import {StatusDot} from './StatusDot'; +import {StatusDot, StatusDotNode} from './StatusDot'; import { FolderNodeCodeLocationType, FolderNodeGroupType, @@ -11,13 +11,13 @@ import { } from './util'; import {ExplorerPath} from '../../pipelines/PipelinePathUtils'; import {AssetGroup} from '../AssetGraphExplorer'; -import {useAssetNodeMenu} from '../AssetNodeMenu'; +import {AssetNodeMenuProps, useAssetNodeMenu} from '../AssetNodeMenu'; import {useGroupNodeContextMenu} from '../CollapsedGroupNode'; import {ContextMenuWrapper, triggerContextMenu} from '../ContextMenuWrapper'; import {GraphData, GraphNode} from '../Utils'; type AssetSidebarNodeProps = { - fullAssetGraphData: GraphData; + fullAssetGraphData?: GraphData; node: GraphNode | FolderNodeNonAssetType; level: number; toggleOpen: () => void; @@ -42,10 +42,9 @@ export const AssetSidebarNode = (props: AssetSidebarNodeProps) => { const showArrow = !isAssetNode; return ( - + !e.metaKey && toggleOpen()} @@ -93,6 +92,16 @@ export const AssetSidebarNode = (props: AssetSidebarNodeProps) => { ); }; +type AssetSidebarAssetLabelProps = { + fullAssetGraphData?: GraphData; + node: AssetNodeMenuProps['node'] & StatusDotNode; + selectNode: (e: React.MouseEvent | React.KeyboardEvent, nodeId: string) => void; + isLastSelected: boolean; + isSelected: boolean; + explorerPath: ExplorerPath; + onChangeExplorerPath: (path: ExplorerPath, mode: 'replace' | 'push') => void; +}; + const AssetSidebarAssetLabel = ({ node, isSelected, @@ -101,7 +110,7 @@ const AssetSidebarAssetLabel = ({ selectNode, explorerPath, onChangeExplorerPath, -}: Omit & {node: GraphNode}) => { +}: AssetSidebarAssetLabelProps) => { const {menu, dialog} = useAssetNodeMenu({ graphData: fullAssetGraphData, node, @@ -196,6 +205,7 @@ const FocusableLabelContainer = ({ @@ -231,7 +241,7 @@ const BoxWrapper = ({level, children}: {level: number; children: React.ReactNode const ExpandMore = styled(UnstyledButton)` position: absolute; top: 8px; - right: 20px; + right: 8px; visibility: hidden; `; @@ -240,8 +250,8 @@ const GrayOnHoverBox = styled(UnstyledButton)` user-select: none; width: 100%; display: grid; - grid-template-columns: auto minmax(0, 1fr); flex-direction: row; + height: 32px; align-items: center; padding: 5px 8px; justify-content: space-between; @@ -251,7 +261,7 @@ const GrayOnHoverBox = styled(UnstyledButton)` transition: background 100ms linear; `; -const ItemContainer = styled(Box)` +export const ItemContainer = styled(Box)` height: 32px; position: relative; cursor: pointer; diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/StatusDot.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/StatusDot.tsx index 35ad508fb9c2d..d47873d4dd147 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/StatusDot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/StatusDot.tsx @@ -1,9 +1,15 @@ import {StatusCaseDot} from './util'; import {useAssetBaseData} from '../../asset-data/AssetBaseDataProvider'; -import {StatusCase, buildAssetNodeStatusContent} from '../AssetNodeStatusContent'; -import {GraphNode} from '../Utils'; +import {AssetKeyInput} from '../../graphql/types'; +import { + StatusCase, + StatusContentArgs, + buildAssetNodeStatusContent, +} from '../AssetNodeStatusContent'; -export function StatusDot({node}: {node: Pick}) { +export type StatusDotNode = {assetKey: AssetKeyInput; definition: StatusContentArgs['definition']}; + +export function StatusDot({node}: {node: StatusDotNode}) { const {liveData} = useAssetBaseData(node.assetKey); if (!liveData) { diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/util.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/util.tsx index 923fd4770a58d..df00f6a67d7cd 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/util.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/sidebar/util.tsx @@ -2,6 +2,7 @@ import {Colors, Spinner, Tooltip} from '@dagster-io/ui-components'; import {useMemo} from 'react'; import styled, {keyframes} from 'styled-components'; +import {AssetKeyInput} from '../../graphql/types'; import {StatusCase} from '../AssetNodeStatusContent'; import {GraphNode} from '../Utils'; @@ -28,7 +29,7 @@ export function nodePathKey(node: {path: string; id: string} | {id: string}) { return 'path' in node ? node.path : node.id; } -export function getDisplayName(node: GraphNode) { +export function getDisplayName(node: {assetKey: AssetKeyInput}) { return node.assetKey.path[node.assetKey.path.length - 1]!; } diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetFeatureContext.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetFeatureContext.tsx index e6fa245cc4263..07e49f986b4ea 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetFeatureContext.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetFeatureContext.tsx @@ -27,6 +27,8 @@ type AssetFeatureContextType = { useTabBuilder: (input: AssetTabConfigInput) => AssetTabConfig[]; renderFeatureView: (input: AssetViewFeatureInput) => React.ReactNode; AssetColumnLinksCell: (input: {column: string | null}) => React.ReactNode; + + enableAssetHealthOverviewPreview: boolean; }; export const AssetFeatureContext = React.createContext({ @@ -35,6 +37,7 @@ export const AssetFeatureContext = React.createContext( AssetColumnLinksCell: () => undefined, LineageOptions: undefined, LineageGraph: undefined, + enableAssetHealthOverviewPreview: false, }); const renderFeatureView = () => ; @@ -47,6 +50,7 @@ export const AssetFeatureProvider = ({children}: {children: React.ReactNode}) => AssetColumnLinksCell: () => undefined, LineageOptions: undefined, LineageGraph: undefined, + enableAssetHealthOverviewPreview: false, }; }, []); diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeOverview.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeOverview.tsx index a806e6449f1e9..97fbb3004804a 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeOverview.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeOverview.tsx @@ -1,5 +1,4 @@ // eslint-disable-next-line no-restricted-imports -import {Collapse} from '@blueprintjs/core'; import { Body, Body2, @@ -10,14 +9,11 @@ import { Colors, ConfigTypeSchema, Icon, - IconName, MiddleTruncate, NonIdealState, Skeleton, - Subtitle1, Subtitle2, Tag, - UnstyledButton, } from '@dagster-io/ui-components'; import dayjs from 'dayjs'; import React, {useMemo, useState} from 'react'; @@ -30,6 +26,7 @@ import {metadataForAssetNode} from './AssetMetadata'; import {insitigatorsByType} from './AssetNodeInstigatorTag'; import {AutomaterializePolicyTag} from './AutomaterializePolicyTag'; import {DependsOnSelfBanner} from './DependsOnSelfBanner'; +import {LargeCollapsibleSection} from './LargeCollapsibleSection'; import {MaterializationTag} from './MaterializationTag'; import {OverdueTag, freshnessPolicyDescription} from './OverdueTag'; import {RecentUpdatesTimeline} from './RecentUpdatesTimeline'; @@ -56,7 +53,6 @@ import {StatusDot} from '../asset-graph/sidebar/StatusDot'; import {AssetNodeForGraphQueryFragment} from '../asset-graph/types/useAssetGraphData.types'; import {DagsterTypeSummary} from '../dagstertype/DagsterType'; import {AssetComputeKindTag} from '../graph/OpTags'; -import {useStateWithStorage} from '../hooks/useStateWithStorage'; import {useLaunchPadHooks} from '../launchpad/LaunchpadHooksContext'; import {TableSchema, TableSchemaAssetContext} from '../metadata/TableSchema'; import {RepositoryLink} from '../nav/RepositoryLink'; @@ -616,55 +612,6 @@ export const AssetNodeOverviewLoading = () => ( /> ); -// BG: This should probably be moved to ui-components, but waiting to see if we -// adopt it more broadly. - -const LargeCollapsibleSection = ({ - header, - icon, - children, - right, - collapsedByDefault = false, -}: { - header: string; - icon: IconName; - children: React.ReactNode; - right?: React.ReactNode; - collapsedByDefault?: boolean; -}) => { - const [isCollapsed, setIsCollapsed] = useStateWithStorage( - `collapsible-section-${header}`, - (storedValue) => - storedValue === true || storedValue === false ? storedValue : collapsedByDefault, - ); - - return ( - - setIsCollapsed(!isCollapsed)}> - - - - {header} - - {right} - - - - - {children} - - - ); -}; - const SectionEmptyState = ({ title, description, diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsCatalogTable.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsCatalogTable.tsx index 902e940c82866..d020664fe64ac 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsCatalogTable.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetsCatalogTable.tsx @@ -1,16 +1,8 @@ import {gql, useApolloClient} from '@apollo/client'; -import {Box, ButtonGroup, TextInput} from '@dagster-io/ui-components'; -import { - ChangeEvent, - useCallback, - useContext, - useEffect, - useLayoutEffect, - useMemo, - useState, -} from 'react'; - -import {useAssetGroupSelectorsForAssets} from './AssetGroupSuggest'; +import {Box, ButtonGroup} from '@dagster-io/ui-components'; +import * as React from 'react'; +import {useCallback, useEffect, useLayoutEffect, useMemo, useState} from 'react'; + import {AssetTable} from './AssetTable'; import {ASSET_TABLE_DEFINITION_FRAGMENT, ASSET_TABLE_FRAGMENT} from './AssetTableFragment'; import {AssetsEmptyState} from './AssetsEmptyState'; @@ -22,31 +14,16 @@ import { AssetCatalogTableQuery, AssetCatalogTableQueryVariables, } from './types/AssetsCatalogTable.types'; -import {useAssetDefinitionFilterState} from './useAssetDefinitionFilterState'; -import {useAssetSearch} from './useAssetSearch'; +import {useAssetCatalogFiltering} from './useAssetCatalogFiltering'; import {AssetViewType, useAssetView} from './useAssetView'; -import {CloudOSSContext} from '../app/CloudOSSContext'; import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorFragment'; import {PythonErrorInfo} from '../app/PythonErrorInfo'; import {FIFTEEN_SECONDS, useRefreshAtInterval} from '../app/QueryRefresh'; import {PythonErrorFragment} from '../app/types/PythonErrorFragment.types'; import {AssetGroupSelector} from '../graphql/types'; -import {useQueryPersistedState} from '../hooks/useQueryPersistedState'; import {PageLoadTrace} from '../performance'; import {useIndexedDBCachedQuery} from '../search/useIndexedDBCachedQuery'; -import {useFilters} from '../ui/Filters'; -import {useAssetGroupFilter} from '../ui/Filters/useAssetGroupFilter'; -import {useAssetOwnerFilter, useAssetOwnersForAssets} from '../ui/Filters/useAssetOwnerFilter'; -import {useAssetTagFilter, useAssetTagsForAssets} from '../ui/Filters/useAssetTagFilter'; -import {useChangedFilter} from '../ui/Filters/useChangedFilter'; -import {useCodeLocationFilter} from '../ui/Filters/useCodeLocationFilter'; -import { - useAssetKindTagsForAssets, - useComputeKindTagFilter, -} from '../ui/Filters/useComputeKindTagFilter'; -import {FilterObject} from '../ui/Filters/useFilter'; import {LoadingSpinner} from '../ui/Loading'; -import {WorkspaceContext} from '../workspace/WorkspaceContext'; type Asset = AssetTableFragment; const groupTableCache = new Map(); @@ -122,8 +99,6 @@ interface AssetCatalogTableProps { trace?: PageLoadTrace; } -const emptyArray: any[] = []; - export const AssetsCatalogTable = ({ prefixPath, setPrefixPath, @@ -131,34 +106,10 @@ export const AssetsCatalogTable = ({ trace, }: AssetCatalogTableProps) => { const [view, setView] = useAssetView(); - const [search, setSearch] = useQueryPersistedState({queryKey: 'q'}); - - const { - filters, - filterFn, - setAssetTags, - setChangedInBranch, - setComputeKindTags, - setGroups, - setOwners, - setRepos, - } = useAssetDefinitionFilterState(); - - const searchPath = (search || '') - .replace(/(( ?> ?)|\.|\/)/g, '/') - .toLowerCase() - .trim(); const {assets, query, error} = useAllAssets(groupSelector); - const pathMatches = useAssetSearch( - searchPath, - assets ?? (emptyArray as NonNullable), - ); - - const filtered = useMemo( - () => pathMatches.filter((a) => filterFn(a.definition ?? {})), - [filterFn, pathMatches], - ); + const {searchPath, filtered, isFiltered, filterButton, filterInput, activeFiltersJsx} = + useAssetCatalogFiltering(assets, prefixPath); const {displayPathForAsset, displayed} = view === 'flat' @@ -178,48 +129,7 @@ export const AssetsCatalogTable = ({ } }, [loaded, trace]); - const allAssetGroupOptions = useAssetGroupSelectorsForAssets(pathMatches); - const allComputeKindTags = useAssetKindTagsForAssets(pathMatches); - const allAssetOwners = useAssetOwnersForAssets(pathMatches); - - const groupsFilter = useAssetGroupFilter({ - allAssetGroups: allAssetGroupOptions, - assetGroups: filters.groups, - setGroups, - }); - const changedInBranchFilter = useChangedFilter({ - changedInBranch: filters.changedInBranch, - setChangedInBranch, - }); - const computeKindFilter = useComputeKindTagFilter({ - allComputeKindTags, - computeKindTags: filters.computeKindTags, - setComputeKindTags, - }); - const ownersFilter = useAssetOwnerFilter({ - allAssetOwners, - owners: filters.owners, - setOwners, - }); - const tagsFilter = useAssetTagFilter({ - allAssetTags: useAssetTagsForAssets(pathMatches), - tags: filters.tags, - setTags: setAssetTags, - }); - const uiFilters: FilterObject[] = [groupsFilter, computeKindFilter, ownersFilter, tagsFilter]; - const {isBranchDeployment} = useContext(CloudOSSContext); - if (isBranchDeployment) { - uiFilters.push(changedInBranchFilter); - } - const {allRepos} = useContext(WorkspaceContext); - - const reposFilter = useCodeLocationFilter({repos: filters.repos, setRepos}); - if (allRepos.length > 1) { - uiFilters.unshift(reposFilter); - } - const {button, activeFiltersJsx} = useFilters({filters: uiFilters}); - - useEffect(() => { + React.useEffect(() => { if (view !== 'directory' && prefixPath.length) { setView('directory'); } @@ -245,15 +155,7 @@ export const AssetsCatalogTable = ({ @@ -269,17 +171,8 @@ export const AssetsCatalogTable = ({ } }} /> - {button} - ) => setSearch(e.target.value)} - /> + {filterButton} + {filterInput} } belowActionBarComponents={ diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/LargeCollapsibleSection.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/LargeCollapsibleSection.tsx new file mode 100644 index 0000000000000..d9cb24ed34002 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/LargeCollapsibleSection.tsx @@ -0,0 +1,59 @@ +// eslint-disable-next-line no-restricted-imports +import {Collapse} from '@blueprintjs/core'; +import {Box, Icon, IconName, Subtitle1, UnstyledButton} from '@dagster-io/ui-components'; +import React from 'react'; + +import {useStateWithStorage} from '../hooks/useStateWithStorage'; + +export const LargeCollapsibleSection = ({ + header, + count, + icon, + children, + right, + collapsedByDefault = false, + padHeader = false, + padChildren = true, +}: { + header: string; + count?: number; + icon?: IconName; + children: React.ReactNode; + right?: React.ReactNode; + collapsedByDefault?: boolean; + padHeader?: boolean; + padChildren?: boolean; +}) => { + const [isCollapsed, setIsCollapsed] = useStateWithStorage( + `collapsible-section-${header}`, + (storedValue) => + storedValue === true || storedValue === false ? storedValue : collapsedByDefault, + ); + + return ( + + setIsCollapsed(!isCollapsed)}> + + {icon && } + + {header} + {count !== undefined ? ` (${count.toLocaleString()})` : ''} + + {right} + + + + + {children} + + + ); +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckStatusTag.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckStatusTag.tsx index 174d224ab9596..6da1890c8aa72 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckStatusTag.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckStatusTag.tsx @@ -20,7 +20,7 @@ import { } from '../../graphql/types'; import {linkToRunEvent} from '../../runs/RunUtils'; import {TimestampDisplay} from '../../schedules/TimestampDisplay'; -import {TagActionsPopover} from '../../ui/TagActions'; +import {TagAction, TagActionsPopover} from '../../ui/TagActions'; import {assetDetailsPathForAssetCheck} from '../assetDetailsPathForKey'; const CheckRow = ({ @@ -224,3 +224,33 @@ export const AssetCheckStatusTag = ({ ); }; + +export const AssetCheckErrorsTag = ({ + checks, + severity, +}: { + checks: AssetCheckLiveFragment[]; + severity: AssetCheckSeverity; +}) => { + const actions: TagAction[] = []; + const execution = checks[0]?.executionForLatestMaterialization; + if (execution) { + actions.push({ + label: 'View in run logs', + to: linkToRunEvent( + {id: execution.runId}, + {stepKey: execution.stepKey, timestamp: execution.timestamp}, + ), + }); + } + return ( + + + {checks.length === 1 ? checks[0]!.name : `${checks.length} failed`} + + + ); +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/useAssetCatalogFiltering.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/useAssetCatalogFiltering.tsx new file mode 100644 index 0000000000000..fd013e4f741c9 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/useAssetCatalogFiltering.tsx @@ -0,0 +1,126 @@ +import {TextInput} from '@dagster-io/ui-components'; +import * as React from 'react'; + +import {useAssetGroupSelectorsForAssets} from './AssetGroupSuggest'; +import {AssetTableFragment} from './types/AssetTableFragment.types'; +import {useAssetDefinitionFilterState} from './useAssetDefinitionFilterState'; +import {useAssetSearch} from './useAssetSearch'; +import {CloudOSSContext} from '../app/CloudOSSContext'; +import {useQueryPersistedState} from '../hooks/useQueryPersistedState'; +import {useFilters} from '../ui/Filters'; +import {useAssetGroupFilter} from '../ui/Filters/useAssetGroupFilter'; +import {useAssetOwnerFilter, useAssetOwnersForAssets} from '../ui/Filters/useAssetOwnerFilter'; +import {useAssetTagFilter, useAssetTagsForAssets} from '../ui/Filters/useAssetTagFilter'; +import {useChangedFilter} from '../ui/Filters/useChangedFilter'; +import {useCodeLocationFilter} from '../ui/Filters/useCodeLocationFilter'; +import { + useAssetKindTagsForAssets, + useComputeKindTagFilter, +} from '../ui/Filters/useComputeKindTagFilter'; +import {FilterObject} from '../ui/Filters/useFilter'; +import {WorkspaceContext} from '../workspace/WorkspaceContext'; + +const EMPTY_ARRAY: any[] = []; + +export function useAssetCatalogFiltering( + assets: AssetTableFragment[] | undefined, + prefixPath: string[], +) { + const [search, setSearch] = useQueryPersistedState({queryKey: 'q'}); + + const { + filters, + filterFn, + setAssetTags, + setChangedInBranch, + setComputeKindTags, + setGroups, + setOwners, + setRepos, + } = useAssetDefinitionFilterState(); + + const searchPath = (search || '') + .replace(/(( ?> ?)|\.|\/)/g, '/') + .toLowerCase() + .trim(); + + const pathMatches = useAssetSearch( + searchPath, + assets ?? (EMPTY_ARRAY as NonNullable), + ); + + const filtered = React.useMemo( + () => pathMatches.filter((a) => filterFn(a.definition ?? {})), + [filterFn, pathMatches], + ); + + const allAssetGroupOptions = useAssetGroupSelectorsForAssets(pathMatches); + const allComputeKindTags = useAssetKindTagsForAssets(pathMatches); + const allAssetOwners = useAssetOwnersForAssets(pathMatches); + + const groupsFilter = useAssetGroupFilter({ + allAssetGroups: allAssetGroupOptions, + assetGroups: filters.groups, + setGroups, + }); + const changedInBranchFilter = useChangedFilter({ + changedInBranch: filters.changedInBranch, + setChangedInBranch, + }); + const computeKindFilter = useComputeKindTagFilter({ + allComputeKindTags, + computeKindTags: filters.computeKindTags, + setComputeKindTags, + }); + const ownersFilter = useAssetOwnerFilter({ + allAssetOwners, + owners: filters.owners, + setOwners, + }); + const tagsFilter = useAssetTagFilter({ + allAssetTags: useAssetTagsForAssets(pathMatches), + tags: filters.tags, + setTags: setAssetTags, + }); + + const uiFilters: FilterObject[] = [groupsFilter, computeKindFilter, ownersFilter, tagsFilter]; + const {isBranchDeployment} = React.useContext(CloudOSSContext); + if (isBranchDeployment) { + uiFilters.push(changedInBranchFilter); + } + const {allRepos} = React.useContext(WorkspaceContext); + + const reposFilter = useCodeLocationFilter({repos: filters.repos, setRepos}); + if (allRepos.length > 1) { + uiFilters.unshift(reposFilter); + } + const components = useFilters({filters: uiFilters}); + + const filterInput = ( + ) => setSearch(e.target.value)} + /> + ); + + const isFiltered: boolean = !!( + filters.changedInBranch?.length || + filters.computeKindTags?.length || + filters.groups?.length || + filters.owners?.length || + filters.repos?.length + ); + + return { + searchPath, + activeFiltersJsx: components.activeFiltersJsx, + filterButton: components.button, + filterInput, + isFiltered, + filtered, + }; +} diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/useAssetSearch.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/useAssetSearch.tsx index d8b8f991afd9f..1b04e4aea0905 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/useAssetSearch.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/useAssetSearch.tsx @@ -1,6 +1,7 @@ import {useMemo} from 'react'; import {tokenForAssetKey} from '../asset-graph/Utils'; +import {AssetKeyInput} from '../graphql/types'; const useSanitizedAssetSearch = (searchValue: string) => { return useMemo(() => { @@ -11,33 +12,21 @@ const useSanitizedAssetSearch = (searchValue: string) => { }, [searchValue]); }; -export const useAssetSearch = ( +export const useAssetSearch = ( searchValue: string, assets: A[], ): A[] => { const sanitizedSearch = useSanitizedAssetSearch(searchValue); - return useMemo(() => { - // If there is no search value, match everything. - if (!sanitizedSearch) { - return assets; - } - return assets.filter((a) => tokenForAssetKey(a.key).toLowerCase().includes(sanitizedSearch)); - }, [assets, sanitizedSearch]); -}; - -export const useAssetNodeSearch = ( - searchValue: string, - assetNodes: A[], -): A[] => { - const sanitizedSearch = useSanitizedAssetSearch(searchValue); return useMemo(() => { // If there is no search value, match everything. if (!sanitizedSearch) { - return assetNodes; + return assets; } - return assetNodes.filter((a) => - tokenForAssetKey(a.assetKey).toLowerCase().includes(sanitizedSearch), + return assets.filter((a) => + tokenForAssetKey('assetKey' in a ? a.assetKey : a.key) + .toLowerCase() + .includes(sanitizedSearch), ); - }, [assetNodes, sanitizedSearch]); + }, [assets, sanitizedSearch]); }; diff --git a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewActivityRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewActivityRoot.tsx index 46a545032205c..6827ccd4649ae 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewActivityRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewActivityRoot.tsx @@ -7,6 +7,7 @@ import {OverviewPageHeader} from './OverviewPageHeader'; import {OverviewTabs} from './OverviewTabs'; import {OverviewTimelineRoot} from './OverviewTimelineRoot'; import {useTrackPageView} from '../app/analytics'; +import {AssetFeatureContext} from '../assets/AssetFeatureContext'; import {useDocumentTitle} from '../hooks/useDocumentTitle'; import {useStateWithStorage} from '../hooks/useStateWithStorage'; import {ActivatableButton} from '../runs/RunListTabs'; @@ -22,13 +23,19 @@ export const OverviewActivityRoot = () => { [], ); - const [defaultTab, setDefaultTab] = useStateWithStorage<'timeline' | 'assets'>( + const [_defaultTab, setDefaultTab] = useStateWithStorage<'timeline' | 'assets'>( 'overview-activity-tab', (json) => (['timeline', 'assets'].includes(json) ? json : 'timeline'), ); + const {enableAssetHealthOverviewPreview} = React.useContext(AssetFeatureContext); + const defaultTab = enableAssetHealthOverviewPreview ? 'timeline' : _defaultTab; + const tabButton = React.useCallback( ({selected}: {selected: 'timeline' | 'assets'}) => { + if (enableAssetHealthOverviewPreview) { + return null; + } if (defaultTab !== selected) { setDefaultTab(selected); } @@ -43,15 +50,17 @@ export const OverviewActivityRoot = () => { ); }, - [defaultTab, setDefaultTab], + [defaultTab, setDefaultTab, enableAssetHealthOverviewPreview], ); return ( - - - + {!enableAssetHealthOverviewPreview && ( + + + + )} diff --git a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewTabs.tsx b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewTabs.tsx index e7e17c8877c21..800c067349082 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewTabs.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewTabs.tsx @@ -1,7 +1,9 @@ import {QueryResult} from '@apollo/client'; import {Box, Colors, Spinner, Tabs} from '@dagster-io/ui-components'; +import {useContext} from 'react'; import {QueryRefreshCountdown, RefreshState} from '../app/QueryRefresh'; +import {AssetFeatureContext} from '../assets/AssetFeatureContext'; import {useAutoMaterializeSensorFlag} from '../assets/AutoMaterializeSensorFlag'; import {useAutomaterializeDaemonStatus} from '../assets/useAutomaterializeDaemonStatus'; import {TabLink} from '../ui/TabLink'; @@ -17,11 +19,15 @@ export const OverviewTabs = >(props: Props - + + {enableAssetHealthOverviewPreview && ( + + )} diff --git a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewTimelineRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewTimelineRoot.tsx index 906e097279c7e..f347d0672b965 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewTimelineRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewTimelineRoot.tsx @@ -113,7 +113,7 @@ export const OverviewTimelineRoot = ({Header, TabButton}: Props) => { flex={{alignItems: 'center', justifyContent: 'space-between'}} > - + {TabButton && } {allRepos.length > 1 && } void; +interface Props extends TableSectionHeaderProps { repoName: string; repoLocation: string; showLocation: boolean; - rightElement?: React.ReactNode; } export const RepoSectionHeader = (props: Props) => { - const {expanded, onClick, repoName, repoLocation, showLocation, rightElement} = props; + const {repoName, repoLocation, showLocation, ...rest} = props; const isDunderRepoName = repoName === DUNDER_REPO_NAME; return ( - - - - -
- {isDunderRepoName ? repoLocation : repoName} - {showLocation && !isDunderRepoName ? ( - {`@${repoLocation}`} - ) : null} -
-
- - {rightElement} - - - - + + + +
+ {isDunderRepoName ? repoLocation : repoName} + {showLocation && !isDunderRepoName ? ( + {`@${repoLocation}`} + ) : null} +
-
+ ); }; -const SectionHeaderButton = styled.button<{$open: boolean}>` - background-color: ${Colors.backgroundLight()}; - border: 0; - box-shadow: - inset 0px -1px 0 ${Colors.keylineDefault()}, - inset 0px 1px 0 ${Colors.keylineDefault()}; - color: ${Colors.textLight()}; - cursor: pointer; - display: block; - padding: 0; - width: 100%; - margin: 0; - height: ${SECTION_HEADER_HEIGHT}px; - text-align: left; - - :focus, - :active { - outline: none; - } - - :hover { - background-color: ${Colors.backgroundLightHover()}; - } - - ${IconWrapper}[aria-label="arrow_drop_down"] { - transition: transform 100ms linear; - ${({$open}) => ($open ? null : `transform: rotate(-90deg);`)} - } -`; - const RepoName = styled.span` font-weight: 600; `; diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/RunTimeline.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/RunTimeline.tsx index 4a4093f90f926..accacf7a05aaa 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/RunTimeline.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/RunTimeline.tsx @@ -16,7 +16,6 @@ import * as React from 'react'; import {Link} from 'react-router-dom'; import styled from 'styled-components'; -import {SECTION_HEADER_HEIGHT} from './RepoSectionHeader'; import {RunStatusDot} from './RunStatusDots'; import {failedStatuses, inProgressStatuses, successStatuses} from './RunStatuses'; import {TimeElapsed} from './TimeElapsed'; @@ -30,6 +29,7 @@ import {Container, Inner} from '../ui/VirtualizedTable'; import {findDuplicateRepoNames} from '../ui/findDuplicateRepoNames'; import {useFormatDateTime} from '../ui/useFormatDateTime'; import {useRepoExpansionState} from '../ui/useRepoExpansionState'; +import {SECTION_HEADER_HEIGHT} from '../workspace/TableSectionHeader'; import {RepoRow} from '../workspace/VirtualizedWorkspaceTable'; import {repoAddressAsURLString} from '../workspace/repoAddressAsString'; import {repoAddressFromPath} from '../workspace/repoAddressFromPath'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/ui/useRepoExpansionState.tsx b/js_modules/dagster-ui/packages/ui-core/src/ui/useRepoExpansionState.tsx index 97e8395b0fc65..f38d37ba34e10 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/ui/useRepoExpansionState.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/ui/useRepoExpansionState.tsx @@ -22,8 +22,8 @@ export const useRepoExpansionState = (collapsedKey: string, allKeys: string[]) = ); const onToggle = useCallback( - (repoAddress: RepoAddress) => { - const key = repoAddressAsHumanString(repoAddress); + (_key: string | RepoAddress) => { + const key = typeof _key === 'object' ? repoAddressAsHumanString(_key) : _key; setCollapsedKeys((current) => { const nextCollapsedKeys = new Set(current || []); if (nextCollapsedKeys.has(key)) { diff --git a/js_modules/dagster-ui/packages/ui-core/src/workspace/TableSectionHeader.tsx b/js_modules/dagster-ui/packages/ui-core/src/workspace/TableSectionHeader.tsx new file mode 100644 index 0000000000000..f6114589f58f5 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/workspace/TableSectionHeader.tsx @@ -0,0 +1,61 @@ +import {Box, Colors, Icon, IconWrapper} from '@dagster-io/ui-components'; +import styled from 'styled-components'; + +export const SECTION_HEADER_HEIGHT = 32; + +export interface TableSectionHeaderProps { + expanded: boolean; + onClick: (e: React.MouseEvent) => void; + children?: React.ReactNode; + rightElement?: React.ReactNode; +} + +export const TableSectionHeader = (props: TableSectionHeaderProps) => { + const {expanded, onClick, children, rightElement} = props; + return ( + + + {children} + + {rightElement} + + + + + + + ); +}; + +const SectionHeaderButton = styled.button<{$open: boolean}>` + background-color: ${Colors.backgroundLight()}; + border: 0; + box-shadow: + inset 0px -1px 0 ${Colors.keylineDefault()}, + inset 0px 1px 0 ${Colors.keylineDefault()}; + color: ${Colors.textLight()}; + cursor: pointer; + display: block; + padding: 0; + width: 100%; + margin: 0; + height: ${SECTION_HEADER_HEIGHT}px; + text-align: left; + + :focus, + :active { + outline: none; + } + + :hover { + background-color: ${Colors.backgroundLightHover()}; + } + + ${IconWrapper}[aria-label="arrow_drop_down"] { + transition: transform 100ms linear; + ${({$open}) => ($open ? null : `transform: rotate(-90deg);`)} + } +`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/workspace/WorkspaceAssetsRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/workspace/WorkspaceAssetsRoot.tsx index 7db2706915f34..1db9f1c34eb9a 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/workspace/WorkspaceAssetsRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/workspace/WorkspaceAssetsRoot.tsx @@ -14,7 +14,7 @@ import { import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorFragment'; import {FIFTEEN_SECONDS, useQueryRefreshAtInterval} from '../app/QueryRefresh'; import {useTrackPageView} from '../app/analytics'; -import {useAssetNodeSearch} from '../assets/useAssetSearch'; +import {useAssetSearch} from '../assets/useAssetSearch'; import {useDocumentTitle} from '../hooks/useDocumentTitle'; import {useQueryPersistedState} from '../hooks/useQueryPersistedState'; @@ -51,7 +51,7 @@ export const WorkspaceAssetsRoot = ({repoAddress}: {repoAddress: RepoAddress}) = return []; }, [data]); - const filteredBySearch = useAssetNodeSearch(searchValue, assetNodes); + const filteredBySearch = useAssetSearch(searchValue, assetNodes); const content = () => { if (loading && !data) {