Skip to content

Commit db52909

Browse files
bengotowclaude
authored andcommitted
[ui] Redesign RecentUpdatesTimeline (design updates, no health regions) (#25187)
## Summary & Motivation This PR pulls the new design in dagster-io/internal#24118 off the stack, because there are some perf concerns about the new resolver added in that stack and it still needs some tweaks. I also applied the PR feedback from that branch to this one, and made a few improvements so it refreshes every few minutes. <img width="1199" height="414" alt="image" src="https://github.com/user-attachments/assets/6761c7da-865c-4f15-9633-1b5b4aff623f" /> <img width="1031" height="314" alt="image" src="https://github.com/user-attachments/assets/2dbb4081-a1bb-492f-82a5-115e3f3f43a2" /> --------- Co-authored-by: Claude <noreply@anthropic.com> Internal-RevId: 7225a3c4ba40f281371e4b5120dcbacab587863b
1 parent 4a84ee6 commit db52909

9 files changed

Lines changed: 594 additions & 429 deletions

File tree

js_modules/ui-core/src/assets/RecentUpdatesTimeline.tsx

Lines changed: 413 additions & 220 deletions
Large diffs are not rendered by default.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {buildMaterializationEvent, buildObservationEvent} from '../../graphql/builders';
2+
import {AssetEventFragment} from '../useRecentAssetEvents';
3+
4+
export const ONE_HOUR_MS = 60 * 60 * 1000;
5+
6+
// Fixed reference time the fixture timestamps are anchored to, so stories render
7+
// deterministically regardless of when they're viewed: 2024-11-15T15:36:40Z.
8+
export const FIXTURE_BASE_TIME = new Date('2024-11-15T15:36:40Z').getTime();
9+
10+
export const MultipleMaterializationEvents: AssetEventFragment[] = [
11+
buildMaterializationEvent({timestamp: '1731685015904'}),
12+
buildMaterializationEvent({timestamp: '1731685020904'}),
13+
buildMaterializationEvent({timestamp: '1731685032904'}),
14+
buildMaterializationEvent({timestamp: '1731685043904'}),
15+
buildMaterializationEvent({timestamp: '1731685044904'}),
16+
buildMaterializationEvent({timestamp: '1731685045904'}),
17+
];
18+
19+
export const MixedMaterializationsAndObservationsEvents: AssetEventFragment[] = [
20+
buildMaterializationEvent({timestamp: `${FIXTURE_BASE_TIME}`}),
21+
buildObservationEvent({timestamp: `${FIXTURE_BASE_TIME + 1 * ONE_HOUR_MS}`}),
22+
buildMaterializationEvent({timestamp: `${FIXTURE_BASE_TIME + 2 * ONE_HOUR_MS}`}),
23+
buildObservationEvent({timestamp: `${FIXTURE_BASE_TIME + 3 * ONE_HOUR_MS}`}),
24+
buildObservationEvent({timestamp: `${FIXTURE_BASE_TIME + 4 * ONE_HOUR_MS}`}),
25+
buildMaterializationEvent({timestamp: `${FIXTURE_BASE_TIME + 5 * ONE_HOUR_MS}`}),
26+
buildObservationEvent({timestamp: `${FIXTURE_BASE_TIME + 6 * ONE_HOUR_MS}`}),
27+
buildMaterializationEvent({timestamp: `${FIXTURE_BASE_TIME + 7 * ONE_HOUR_MS}`}),
28+
buildMaterializationEvent({timestamp: `${FIXTURE_BASE_TIME + 8 * ONE_HOUR_MS}`}),
29+
buildObservationEvent({timestamp: `${FIXTURE_BASE_TIME + 9 * ONE_HOUR_MS}`}),
30+
];
Lines changed: 96 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,118 @@
1+
import {useMemo, useState} from 'react';
2+
13
import {
24
buildAssetKey,
5+
buildFailedToMaterializeEvent,
36
buildMaterializationEvent,
4-
buildObservationEvent,
57
} from '../../graphql/builders';
6-
import {RecentUpdatesTimeline} from '../RecentUpdatesTimeline';
8+
import {
9+
RecentUpdatesTimelineView,
10+
RecentUpdatesTimelineViewProps,
11+
TimeRangeFilter,
12+
} from '../RecentUpdatesTimeline';
13+
import {
14+
FIXTURE_BASE_TIME,
15+
MixedMaterializationsAndObservationsEvents,
16+
MultipleMaterializationEvents,
17+
ONE_HOUR_MS,
18+
} from '../__fixtures__/RecentUpdatesTimeline.fixtures';
719

820
// eslint-disable-next-line import/no-default-export
921
export default {
1022
title: 'Assets/RecentUpdatesTimeline',
11-
component: RecentUpdatesTimeline,
23+
component: RecentUpdatesTimelineView,
1224
};
1325

14-
export const Loading = () => (
15-
<RecentUpdatesTimeline assetKey={buildAssetKey()} loading={true} events={undefined} />
16-
);
26+
type Events = RecentUpdatesTimelineViewProps['events'];
1727

18-
export const Empty = () => (
19-
<RecentUpdatesTimeline assetKey={buildAssetKey()} loading={false} events={[]} />
20-
);
28+
const TimelineStoryWrapper = ({events, now}: {events: Events; now?: number}) => {
29+
const [timeRange, setTimeRange] = useState<TimeRangeFilter>('24h');
30+
const [eventFilter, setEventFilter] =
31+
useState<RecentUpdatesTimelineViewProps['eventFilter']>('all');
32+
return (
33+
<RecentUpdatesTimelineView
34+
assetKey={buildAssetKey()}
35+
events={events}
36+
timeRange={timeRange}
37+
setTimeRange={setTimeRange}
38+
eventFilter={eventFilter}
39+
setEventFilter={setEventFilter}
40+
now={now}
41+
/>
42+
);
43+
};
2144

22-
export const Single = () => (
23-
<RecentUpdatesTimeline
24-
assetKey={buildAssetKey()}
25-
loading={false}
26-
events={[buildMaterializationEvent({timestamp: '1731685045904'})]}
27-
/>
28-
);
45+
// Anchor the timeline's "now" so fixture timestamps spread across the bar.
46+
const STORY_NOW = FIXTURE_BASE_TIME + 10 * ONE_HOUR_MS;
47+
48+
export const Loading = () => <TimelineStoryWrapper events={null} now={STORY_NOW} />;
49+
50+
export const Empty = () => <TimelineStoryWrapper events={[]} now={STORY_NOW} />;
2951

3052
export const Multiple = () => (
31-
<RecentUpdatesTimeline
32-
assetKey={buildAssetKey()}
33-
loading={false}
53+
<TimelineStoryWrapper events={MultipleMaterializationEvents} now={STORY_NOW} />
54+
);
55+
56+
// Interactive story: drag the slider to spread three materializations apart in time.
57+
// At low spread values they group into a single "3" batch; as spread increases they
58+
// separate into individual ticks. Use this to explore the grouping threshold.
59+
export const GroupingExplorer = () => {
60+
const [spreadMinutes, setSpreadMinutes] = useState(0);
61+
const spreadMs = spreadMinutes * 60 * 1000;
62+
const center = FIXTURE_BASE_TIME + 5 * ONE_HOUR_MS;
63+
64+
const events = useMemo(
65+
() => [
66+
buildMaterializationEvent({timestamp: `${FIXTURE_BASE_TIME}`}),
67+
buildMaterializationEvent({timestamp: `${center - spreadMs}`}),
68+
buildMaterializationEvent({timestamp: `${center}`}),
69+
buildMaterializationEvent({timestamp: `${center + spreadMs}`}),
70+
],
71+
[center, spreadMs],
72+
);
73+
74+
return (
75+
<div>
76+
<div style={{marginBottom: 12}}>
77+
<label>
78+
Spread between events:{' '}
79+
<strong>
80+
{spreadMinutes < 1
81+
? `${Math.round(spreadMinutes * 60)}s`
82+
: `${spreadMinutes.toFixed(1)} min`}
83+
</strong>
84+
<br />
85+
<input
86+
type="range"
87+
min={0}
88+
max={60}
89+
step={0.1}
90+
value={spreadMinutes}
91+
onChange={(e) => setSpreadMinutes(Number(e.target.value))}
92+
style={{width: '100%', marginTop: 4}}
93+
/>
94+
</label>
95+
</div>
96+
<TimelineStoryWrapper events={events} now={STORY_NOW} />
97+
</div>
98+
);
99+
};
100+
101+
// 2 materializations + 1 failure within 500ms of each other — triggers grouping with mixed color.
102+
// One isolated event 1 hour earlier creates a wide time range so the cluster collapses.
103+
export const GroupedTwoSuccessesOneFailed = () => (
104+
<TimelineStoryWrapper
105+
now={STORY_NOW}
34106
events={[
35-
buildMaterializationEvent({
36-
timestamp: '1731685015904',
37-
}),
38-
buildMaterializationEvent({
39-
timestamp: '1731685020904',
40-
}),
41-
buildMaterializationEvent({
42-
timestamp: '1731685032904',
43-
}),
44-
buildMaterializationEvent({
45-
timestamp: '1731685043904',
46-
}),
47-
buildMaterializationEvent({
48-
timestamp: '1731685044904',
49-
}),
50-
buildMaterializationEvent({
51-
timestamp: '1731685045904',
52-
}),
107+
buildMaterializationEvent({timestamp: '1731685000000'}),
108+
buildMaterializationEvent({timestamp: '1731688600000'}),
109+
buildMaterializationEvent({timestamp: '1731688600200'}),
110+
buildFailedToMaterializeEvent({timestamp: '1731688600500'}),
53111
]}
54112
/>
55113
);
56114

57115
// Interleaved materializations and observations — triggers the filter toggle
58116
export const MixedMaterializationsAndObservations = () => (
59-
<RecentUpdatesTimeline
60-
assetKey={buildAssetKey()}
61-
loading={false}
62-
events={[
63-
buildMaterializationEvent({timestamp: '1731685000000'}),
64-
buildObservationEvent({timestamp: '1731685005000'}),
65-
buildMaterializationEvent({timestamp: '1731685010000'}),
66-
buildObservationEvent({timestamp: '1731685015000'}),
67-
buildObservationEvent({timestamp: '1731685020000'}),
68-
buildMaterializationEvent({timestamp: '1731685025000'}),
69-
buildObservationEvent({timestamp: '1731685030000'}),
70-
buildMaterializationEvent({timestamp: '1731685035000'}),
71-
buildMaterializationEvent({timestamp: '1731685040000'}),
72-
buildObservationEvent({timestamp: '1731685045000'}),
73-
]}
74-
/>
117+
<TimelineStoryWrapper events={MixedMaterializationsAndObservationsEvents} now={STORY_NOW} />
75118
);
Lines changed: 39 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,54 @@
1-
.tickWrapper {
2-
position: absolute;
3-
height: 24px;
1+
/* Track layout sizing. Keep in sync with LABEL_HEIGHT_PX / TRACK_HEIGHT_PX. */
2+
.timelineContainer {
3+
--label-height: 18px;
4+
--track-height: 32px;
5+
width: 100%;
46
}
57

6-
.tickWrapper * {
7-
height: 24px;
8+
.timelineInner {
9+
position: relative;
10+
height: calc(var(--label-height) + var(--track-height));
811
}
912

10-
.tick {
13+
.track {
1114
position: absolute;
12-
width: 100%;
13-
top: 0;
14-
bottom: 0;
15+
left: 0;
16+
right: 0;
17+
top: var(--label-height);
18+
height: var(--track-height);
1519
overflow: hidden;
16-
background-color: transparent;
17-
cursor: pointer;
18-
border-radius: 2px;
1920
}
2021

21-
.tick:hover {
22-
background: var(--color-accent-green);
23-
}
24-
25-
.tick.error:hover {
26-
background: var(--color-accent-red);
27-
}
28-
29-
.tick.skipped:hover {
30-
background: var(--color-accent-gray);
22+
.tickWrapper {
23+
position: absolute;
24+
top: 0;
25+
bottom: 0;
26+
cursor: pointer;
27+
transform: translateX(-50%);
3128
}
3229

33-
.tick.mixed:hover {
34-
background: linear-gradient(90deg, var(--color-accent-red) 50%, var(--color-accent-green) 50%);
30+
.tickOuter {
31+
display: flex;
32+
flex-direction: column;
33+
gap: 2px;
34+
align-items: center;
3535
}
3636

37-
.tickText {
38-
position: absolute;
39-
top: 0;
40-
right: 0;
41-
left: 0;
42-
bottom: 0;
43-
display: grid;
44-
place-content: center;
45-
font-family: var(--font-mono);
46-
font-size: 12px;
47-
color: transparent;
48-
background: none;
49-
user-select: none;
37+
.tickLabel {
38+
height: 16px;
39+
line-height: 16px;
40+
display: flex;
41+
align-items: center;
42+
justify-content: center;
5043
}
5144

52-
.tickText:hover {
53-
user-select: initial;
54-
color: var(--color-accent-reversed);
45+
.tickWrapper:hover .innerTick {
46+
width: 3px;
5547
}
5648

5749
.innerTick {
58-
width: 8px;
59-
top: 0;
60-
bottom: 0;
61-
position: absolute;
62-
pointer-events: none;
50+
width: 1px;
51+
height: 30px;
6352
border-radius: 1px;
6453
background: var(--color-accent-green);
6554
}
@@ -76,19 +65,11 @@
7665
background: linear-gradient(90deg, var(--color-accent-red) 50%, var(--color-accent-green) 50%);
7766
}
7867

79-
.tickLines {
68+
.axisTick {
8069
pointer-events: none;
8170
position: absolute;
82-
left: 0;
83-
right: 0;
84-
bottom: -6px;
85-
top: -6px;
86-
background: repeating-linear-gradient(
87-
to right,
88-
var(--color-keyline-default) 0,
89-
var(--color-keyline-default) 2px,
90-
transparent 2px,
91-
transparent 5%
92-
);
71+
top: 0;
72+
bottom: 0;
73+
width: 1px;
74+
background: var(--color-keyline-default);
9375
}
94-

js_modules/ui-core/src/assets/overview/AssetNodeOverview.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {buildRepoAddress} from '../../workspace/buildRepoAddress';
3030
import {LargeCollapsibleSection} from '../LargeCollapsibleSection';
3131
import {MaterializationTag} from '../MaterializationTag';
3232
import {OverdueTag} from '../OverdueTag';
33-
import {RecentUpdatesTimelineForAssetKey} from '../RecentUpdatesTimeline';
33+
import {RecentUpdatesTimeline} from '../RecentUpdatesTimeline';
3434
import {SimpleStakeholderAssetStatus} from '../SimpleStakeholderAssetStatus';
3535
import {AssetChecksStatusSummary} from '../asset-checks/AssetChecksStatusSummary';
3636
import {buildConsolidatedColumnSchema} from '../buildConsolidatedColumnSchema';
@@ -200,7 +200,7 @@ export const AssetNodeOverview = ({
200200
) : undefined}
201201
</Box>
202202
{cachedOrLiveAssetNode.isPartitioned ? null : (
203-
<RecentUpdatesTimelineForAssetKey assetKey={cachedOrLiveAssetNode.assetKey} />
203+
<RecentUpdatesTimeline assetKey={cachedOrLiveAssetNode.assetKey} />
204204
)}
205205
</Box>
206206
);
@@ -373,7 +373,7 @@ export const AssetNodeOverviewNonSDA = ({
373373
</Text>
374374
)}
375375
</div>
376-
<RecentUpdatesTimelineForAssetKey assetKey={assetKey} />
376+
<RecentUpdatesTimeline assetKey={assetKey} />
377377
</Box>
378378
</LargeCollapsibleSection>
379379
}

js_modules/ui-core/src/instance/backfill/BackfillLogsTab.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {useMemo} from 'react';
44
import {BackfillLogsPageQuery, BackfillLogsPageQueryVariables} from './types/BackfillLogsTab.types';
55
import {BackfillDetailsBackfillFragment} from './types/useBackfillDetailsQuery.types';
66
import {gql} from '../../apollo-client';
7-
import {QueryRefreshCountdown} from '../../app/QueryRefresh';
7+
import {QueryRefreshCountdown, useRefreshAtInterval} from '../../app/QueryRefresh';
88
import {useCursorAccumulatedQuery} from '../../runs/useCursorAccumulatedQuery';
99
import {
1010
INSTIGATION_EVENT_LOG_FRAGMENT,
@@ -24,8 +24,8 @@ const getResultForBackfillLogsPage = (e: BackfillLogsPageQuery) => {
2424
export const BackfillLogsTab = ({backfill}: {backfill: BackfillDetailsBackfillFragment}) => {
2525
const {
2626
error,
27+
fetch,
2728
fetched: events,
28-
refreshState,
2929
} = useCursorAccumulatedQuery<
3030
BackfillLogsPageQuery,
3131
BackfillLogsPageQueryVariables,
@@ -37,6 +37,12 @@ export const BackfillLogsTab = ({backfill}: {backfill: BackfillDetailsBackfillFr
3737
getResult: getResultForBackfillLogsPage,
3838
});
3939

40+
const refreshState = useRefreshAtInterval({
41+
refresh: fetch,
42+
intervalMs: 10000,
43+
leading: true,
44+
});
45+
4046
const content = () => {
4147
if (error) {
4248
<Box flex={{justifyContent: 'center', alignItems: 'center'}} style={{flex: 1}}>

0 commit comments

Comments
 (0)