diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/PageHeader.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/PageHeader.tsx index b32443c4c3a1f..3de87bda4b45d 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/components/PageHeader.tsx +++ b/js_modules/dagster-ui/packages/ui-components/src/components/PageHeader.tsx @@ -25,9 +25,8 @@ export const PageHeader = (props: Props) => { > {title && ( {title} 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 5df0bc40ec4be..9015c9f7e195c 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 @@ -317,17 +317,19 @@ export const AssetNodeOverview = ({ - + - - {location && ( - - Loaded {dayjs.unix(location.updatedTimestamp).fromNow()} - - )} + + + {location && ( + + Loaded {dayjs.unix(location.updatedTimestamp).fromNow()} + + )} + diff --git a/js_modules/dagster-ui/packages/ui-core/src/instance/backfill/ExecutionTimeline.tsx b/js_modules/dagster-ui/packages/ui-core/src/instance/backfill/ExecutionTimeline.tsx index e72c3221b1d22..3e8cdbef5f4da 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/instance/backfill/ExecutionTimeline.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/instance/backfill/ExecutionTimeline.tsx @@ -1,4 +1,4 @@ -import {Box, Colors, Spinner, useViewport} from '@dagster-io/ui-components'; +import {Box, Colors, Mono, Spinner, useViewport} from '@dagster-io/ui-components'; import {useVirtualizer} from '@tanstack/react-virtual'; import React from 'react'; import {Link} from 'react-router-dom'; @@ -12,6 +12,7 @@ import { TimelineRowContainer, } from '../../runs/RunTimeline'; import {TimelineRun} from '../../runs/RunTimelineTypes'; +import {titleForRun} from '../../runs/RunUtils'; import {TimeElapsed} from '../../runs/TimeElapsed'; import {RunBatch, batchRunsForTimeline} from '../../runs/batchRunsForTimeline'; import {mergeStatusToBackground} from '../../runs/mergeStatusToBackground'; @@ -165,7 +166,9 @@ export const ExecutionTimelineRow = ({ > - {run.id.slice(0, 8)} + + {titleForRun(run)} + diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/AssetTagCollections.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/AssetTagCollections.tsx index e0e6debf7737f..394ca93f0db74 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/AssetTagCollections.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/AssetTagCollections.tsx @@ -88,6 +88,7 @@ export const AssetKeyTagCollection = React.memo((props: AssetKeyTagCollectionPro const assetKey = assetKeys[0]!; return ( {useTags ? ( - {displayNameForAssetKey(assetKey)} + ) : ( - {displayNameForAssetKey(assetKey)} + )} @@ -176,16 +177,17 @@ export const AssetCheckTagCollection = React.memo((props: AssetCheckTagCollectio {useTags ? ( - {labelForAssetCheck(check)} + ) : ( - {labelForAssetCheck(check)} + )} diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/CreatedByTag.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/CreatedByTag.tsx index d3295d3ef242a..ca55de80285bb 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/CreatedByTag.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/CreatedByTag.tsx @@ -122,6 +122,7 @@ export const CreatedByTag = ({repoAddress, tags, onAddTag}: Props) => { return ( { return isHiddenAssetGroupJob(run.pipelineName) ? ( - + diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedRow.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedRow.tsx index 22237ecfd97ef..6064977629578 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedRow.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedRow.tsx @@ -14,7 +14,7 @@ import styled from 'styled-components'; import {CreatedByTagCell, CreatedByTagCellWrapper} from './CreatedByTag'; import {QueuedRunCriteriaDialog} from './QueuedRunCriteriaDialog'; -import {RUN_ACTIONS_MENU_RUN_FRAGMENT, RunActionsMenu} from './RunActionsMenu'; +import {RunActionsMenu} from './RunActionsMenu'; import {RunRowTags} from './RunRowTags'; import {RunStatusTag, RunStatusTagWithStats} from './RunStatusTag'; import {DagsterTag} from './RunTag'; @@ -22,14 +22,11 @@ import {RunTargetLink} from './RunTargetLink'; import {RunStateSummary, RunTime, titleForRun} from './RunUtils'; import {getBackfillPath} from './RunsFeedUtils'; import {RunFilterToken} from './RunsFilterInput'; -import {gql} from '../apollo-client'; import {RunTimeFragment} from './types/RunUtils.types'; -import {RunsFeedTableEntryFragment} from './types/RunsFeedRow.types'; +import {RunsFeedTableEntryFragment} from './types/RunsFeedTableEntryFragment.types'; import {RunStatus} from '../graphql/types'; import {BackfillActionsMenu, backfillCanCancelRuns} from '../instance/backfill/BackfillActionsMenu'; -import {BACKFILL_STEP_STATUS_DIALOG_BACKFILL_FRAGMENT} from '../instance/backfill/BackfillFragments'; import {BackfillTarget} from '../instance/backfill/BackfillRow'; -import {PARTITION_SET_FOR_BACKFILL_TABLE_FRAGMENT} from '../instance/backfill/BackfillTable'; import {HeaderCell, HeaderRow, RowCell} from '../ui/VirtualizedTable'; import {appendCurrentQueryParams} from '../util/appendCurrentQueryParams'; @@ -104,6 +101,10 @@ export const RunsFeedRow = ({ flex={{direction: 'row', alignItems: 'center', wrap: 'wrap'}} style={{gap: '4px 8px', lineHeight: 0}} > + {entry.__typename === 'PartitionBackfill' ? ( + Backfill + ) : undefined} + { return ( @@ -217,59 +218,3 @@ const RowGrid = styled(Box)` display: block; } `; - -export const RUNS_FEED_TABLE_ENTRY_FRAGMENT = gql` - fragment RunsFeedTableEntryFragment on RunsFeedEntry { - __typename - id - runStatus - creationTime - startTime - endTime - tags { - key - value - } - jobName - assetSelection { - ... on AssetKey { - path - } - } - assetCheckSelection { - name - assetKey { - path - } - } - ... on Run { - repositoryOrigin { - id - repositoryLocationName - repositoryName - } - ...RunActionsMenuRunFragment - } - ... on PartitionBackfill { - backfillStatus: status - partitionSetName - partitionSet { - id - ...PartitionSetForBackfillTableFragment - } - assetSelection { - path - } - - hasCancelPermission - hasResumePermission - isAssetBackfill - numCancelable - ...BackfillStepStatusDialogBackfillFragment - } - } - - ${RUN_ACTIONS_MENU_RUN_FRAGMENT} - ${PARTITION_SET_FOR_BACKFILL_TABLE_FRAGMENT} - ${BACKFILL_STEP_STATUS_DIALOG_BACKFILL_FRAGMENT} -`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedTable.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedTable.tsx index ade8e687ad5fd..b87b241e71f97 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedTable.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedTable.tsx @@ -8,7 +8,7 @@ import { ifPlural, } from '@dagster-io/ui-components'; import {useVirtualizer} from '@tanstack/react-virtual'; -import React, {useMemo, useRef} from 'react'; +import React, {useEffect, useMemo, useRef} from 'react'; import {RunBulkActionsMenu} from './RunActionsMenu'; import {RunTableEmptyState} from './RunTableEmptyState'; @@ -19,7 +19,7 @@ import {RunFilterToken} from './RunsFilterInput'; import { RunsFeedTableEntryFragment, RunsFeedTableEntryFragment_Run, -} from './types/RunsFeedRow.types'; +} from './types/RunsFeedTableEntryFragment.types'; import {useRunsFeedEntries} from './useRunsFeedEntries'; import {FIFTEEN_SECONDS, useQueryRefreshAtInterval} from '../app/QueryRefresh'; import {RunsFilter} from '../graphql/types'; @@ -81,6 +81,20 @@ export const RunsFeedTable = ({ ); const backfillsExcluded = selectedEntries.length - selectedRuns.length; + const resetScrollOnLoad = useRef(false); + useEffect(() => { + // When you click "Next page" from the bottom of page 1, we show the indeterminate + // loading state and want to scroll to the top when the new results arrive. It looks + // bad to do it immediately, and the `entries` can also change on their own (and + // sometimes with new rows), so we do this explicitly for pagination cases using a ref. + if (!loading && resetScrollOnLoad.current) { + resetScrollOnLoad.current = false; + if (parentRef.current) { + parentRef.current.scrollTop = 0; + } + } + }, [loading]); + const actionBar = ( {actionBarComponents ?? } - + { + resetScrollOnLoad.current = true; + paginationProps.popCursor(); + }} + advanceCursor={() => { + resetScrollOnLoad.current = true; + paginationProps.advanceCursor(); + }} + reset={() => { + resetScrollOnLoad.current = true; + paginationProps.reset(); + }} + /> onToggleAll(false)} selected={selectedRuns} @@ -122,27 +152,37 @@ export const RunsFeedTable = ({ ); function content() { + const header = ( + + } + /> + ); + if (entries.length === 0 && !loading) { const anyFilter = !!Object.keys(filter || {}).length; if (emptyState) { return <>{emptyState()}; } - return ; + return ( +
+ {header} + +
+ ); } + return (
- - } - /> + {header} {entries.length === 0 && loading && ( diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedTableEntryFragment.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedTableEntryFragment.tsx new file mode 100644 index 0000000000000..f3574f6727b6c --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedTableEntryFragment.tsx @@ -0,0 +1,60 @@ +import {RUN_ACTIONS_MENU_RUN_FRAGMENT} from './RunActionsMenu'; +import {gql} from '../apollo-client'; +import {BACKFILL_STEP_STATUS_DIALOG_BACKFILL_FRAGMENT} from '../instance/backfill/BackfillFragments'; +import {PARTITION_SET_FOR_BACKFILL_TABLE_FRAGMENT} from '../instance/backfill/BackfillTable'; + +export const RUNS_FEED_TABLE_ENTRY_FRAGMENT = gql` + fragment RunsFeedTableEntryFragment on RunsFeedEntry { + __typename + id + runStatus + creationTime + startTime + endTime + tags { + key + value + } + jobName + assetSelection { + ... on AssetKey { + path + } + } + assetCheckSelection { + name + assetKey { + path + } + } + ... on Run { + repositoryOrigin { + id + repositoryLocationName + repositoryName + } + ...RunActionsMenuRunFragment + } + ... on PartitionBackfill { + backfillStatus: status + partitionSetName + partitionSet { + id + ...PartitionSetForBackfillTableFragment + } + assetSelection { + path + } + + hasCancelPermission + hasResumePermission + isAssetBackfill + numCancelable + ...BackfillStepStatusDialogBackfillFragment + } + } + + ${RUN_ACTIONS_MENU_RUN_FRAGMENT} + ${PARTITION_SET_FOR_BACKFILL_TABLE_FRAGMENT} + ${BACKFILL_STEP_STATUS_DIALOG_BACKFILL_FRAGMENT} +`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedUtils.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedUtils.tsx index cd304560c649c..e55fa318e1fcf 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedUtils.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFeedUtils.tsx @@ -3,6 +3,8 @@ import {FeatureFlag} from 'shared/app/FeatureFlags.oss'; import {runsPathWithFilters} from './RunsFilterInput'; import {featureEnabled} from '../app/Flags'; +export const RUNS_FEED_CURSOR_KEY = `runs_before`; + export function getBackfillPath(id: string, isAssetBackfill: boolean) { if (featureEnabled(FeatureFlag.flagLegacyRunsPage)) { if (isAssetBackfill) { diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFilterInput.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFilterInput.tsx index 729fa14616682..3983c096aa0e7 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFilterInput.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/RunsFilterInput.tsx @@ -12,6 +12,7 @@ import {UserDisplay} from 'shared/runs/UserDisplay.oss'; import {DagsterTag} from './RunTag'; import {gql, useApolloClient, useLazyQuery} from '../apollo-client'; +import {RUNS_FEED_CURSOR_KEY} from './RunsFeedUtils'; import { RunTagKeysQuery, RunTagKeysQueryVariables, @@ -104,7 +105,11 @@ export function useQueryPersistedRunFilters(enabledFilters?: RunFilterTokenType[ return useQueryPersistedState( useMemo( () => ({ - encode: (tokens) => ({q: tokensAsStringArray(tokens), cursor: undefined}), + encode: (tokens) => ({ + q: tokensAsStringArray(tokens), + cursor: undefined, + [RUNS_FEED_CURSOR_KEY]: undefined, + }), decode: ({q = []}) => tokenizedValuesFromStringArray(q, RUN_PROVIDERS_EMPTY).filter( (t) => diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/UserDisplay.oss.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/UserDisplay.oss.tsx index f7a82bba8f4ef..bcea77acac6a9 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/UserDisplay.oss.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/UserDisplay.oss.tsx @@ -1,4 +1,4 @@ -import {BaseTag, Box, SubwayDot} from '@dagster-io/ui-components'; +import {BaseTag, Box, MiddleTruncate, SubwayDot} from '@dagster-io/ui-components'; type Props = { email: string; @@ -16,6 +16,12 @@ export function UserDisplay({email, isFilter}: Props) { {email} ) : ( - {icon}
} label={email} /> + + {icon}} + label={} + /> + ); } diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/types/RunsFeedRow.types.ts b/js_modules/dagster-ui/packages/ui-core/src/runs/types/RunsFeedTableEntryFragment.types.ts similarity index 100% rename from js_modules/dagster-ui/packages/ui-core/src/runs/types/RunsFeedRow.types.ts rename to js_modules/dagster-ui/packages/ui-core/src/runs/types/RunsFeedTableEntryFragment.types.ts diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/useCursorPaginatedQuery.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/useCursorPaginatedQuery.tsx index 3300226ce5966..736b147175069 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/useCursorPaginatedQuery.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/useCursorPaginatedQuery.tsx @@ -24,23 +24,28 @@ interface CursorPaginationQueryVariables { */ export function useCursorPaginatedQuery(options: { query: DocumentNode; - nextCursorForResult: (result: T) => string | undefined; skip?: boolean; variables: Omit; pageSize: number; - getResultArray: (result: T | undefined) => any[]; queryKey?: string; + getResultArray: (result: T | undefined) => any[]; + nextCursorForResult: (result: T) => string | undefined; + hasMoreForResult?: (result: T) => boolean; }) { const [cursorStack, setCursorStack] = useState(() => []); const [cursor, setCursor] = useQueryPersistedState({ queryKey: options.queryKey || 'cursor', }); - const queryVars: any = { - ...options.variables, - cursor, - limit: options.pageSize + 1, - }; + // If you don't provide a hasMoreForResult function for extracting hasMore from + // the response, we fall back to an old approach that fetched one extra item + // and used it's presence to determine if more items were available. If you use + // the old approach, your `nextCursorForResult` method needs to use + // `items[pageSize - 1]` NOT `items[items.length - 1]` to get the next cursor, + // or an item will be skipped when you advance. + // + const limit = options.hasMoreForResult ? options.pageSize : options.pageSize + 1; + const queryVars: any = {...options.variables, cursor, limit}; const queryResult = useQuery(options.query, { skip: options.skip, @@ -49,9 +54,17 @@ export function useCursorPaginatedQuery { const nextStack = [...cursorStack]; setCursor(nextStack.pop()); diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/useRunsFeedEntries.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/useRunsFeedEntries.tsx index c152b6bfded4f..bbda29a08c3a3 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/useRunsFeedEntries.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/useRunsFeedEntries.tsx @@ -1,5 +1,8 @@ -import {RUNS_FEED_TABLE_ENTRY_FRAGMENT} from './RunsFeedRow'; +import {useMemo} from 'react'; + +import {RUNS_FEED_TABLE_ENTRY_FRAGMENT} from './RunsFeedTableEntryFragment'; import {useSelectedRunsFeedTab} from './RunsFeedTabs'; +import {RUNS_FEED_CURSOR_KEY} from './RunsFeedUtils'; import {SCHEDULED_RUNS_LIST_QUERY} from './ScheduledRunListRoot'; import { ScheduledRunsListQuery, @@ -11,9 +14,7 @@ import {gql, useQuery} from '../apollo-client'; import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorFragment'; import {RunsFilter} from '../graphql/types'; -const PAGE_SIZE = 25; - -const RUNS_FEED_CURSOR_KEY = `runs_before`; +const PAGE_SIZE = 30; export function useRunsFeedEntries( filter: RunsFilter, @@ -30,11 +31,17 @@ export function useRunsFeedEntries( pageSize: PAGE_SIZE, variables: {filter, includeRunsFromBackfills}, skip: isScheduled, - nextCursorForResult: (runs) => { - if (runs.runsFeedOrError.__typename !== 'RunsFeedConnection') { + nextCursorForResult: (data) => { + if (data.runsFeedOrError.__typename !== 'RunsFeedConnection') { return undefined; } - return runs.runsFeedOrError.hasMore ? runs.runsFeedOrError.cursor : undefined; + return data.runsFeedOrError.hasMore ? data.runsFeedOrError.cursor : undefined; + }, + hasMoreForResult: (data) => { + if (data.runsFeedOrError.__typename !== 'RunsFeedConnection') { + return false; + } + return data.runsFeedOrError.hasMore; }, getResultArray: (data) => { if (!data || data.runsFeedOrError.__typename !== 'RunsFeedConnection') { @@ -46,8 +53,11 @@ export function useRunsFeedEntries( const data = queryResult.data || queryResult.previousData; - const entries = - data?.runsFeedOrError.__typename === 'RunsFeedConnection' ? data?.runsFeedOrError.results : []; + const entries = useMemo(() => { + return data?.runsFeedOrError.__typename === 'RunsFeedConnection' + ? data?.runsFeedOrError.results + : []; + }, [data]); const scheduledQueryResult = useQuery( SCHEDULED_RUNS_LIST_QUERY,