Skip to content

Commit 69671c7

Browse files
committed
[pac] UI refactor
1 parent 0b675d3 commit 69671c7

4 files changed

Lines changed: 217 additions & 139 deletions

File tree

Lines changed: 18 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,38 @@
1-
import {Box, Colors, MiddleTruncate} from '@dagster-io/ui-components';
2-
import {useVirtualizer} from '@tanstack/react-virtual';
3-
import {useEffect, useRef} from 'react';
4-
5-
import {AssetListContainer, AssetListRow} from './AssetEventList';
61
import {AssetPartitionStatus, assetPartitionStatusesToStyle} from './AssetPartitionStatus';
7-
import {Inner} from '../ui/VirtualizedTable';
2+
import {PartitionListSelector, PartitionStatusDot} from './PartitionListSelector';
83

94
export interface AssetPartitionListProps {
105
partitions: string[];
116
statusForPartition: (dimensionKey: string) => AssetPartitionStatus[];
127
focusedDimensionKey?: string;
138
setFocusedDimensionKey?: (dimensionKey: string | undefined) => void;
149
}
10+
11+
const STATUS_ORDER: AssetPartitionStatus[] = [
12+
AssetPartitionStatus.MISSING,
13+
AssetPartitionStatus.FAILED,
14+
AssetPartitionStatus.MATERIALIZING,
15+
AssetPartitionStatus.MATERIALIZED,
16+
];
17+
1518
export const AssetPartitionList = ({
1619
focusedDimensionKey,
1720
setFocusedDimensionKey,
1821
statusForPartition,
1922
partitions,
2023
}: AssetPartitionListProps) => {
21-
const parentRef = useRef<HTMLDivElement | null>(null);
22-
23-
const rowVirtualizer = useVirtualizer({
24-
count: partitions.length,
25-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
26-
getItemKey: (idx) => partitions[idx]!,
27-
getScrollElement: () => parentRef.current,
28-
estimateSize: () => 36,
29-
overscan: 10,
30-
});
31-
const totalHeight = rowVirtualizer.getTotalSize();
32-
const items = rowVirtualizer.getVirtualItems();
33-
34-
useEffect(() => {
35-
if (focusedDimensionKey && partitions.indexOf(focusedDimensionKey) !== -1) {
36-
rowVirtualizer.scrollToIndex(partitions.indexOf(focusedDimensionKey), {
37-
behavior: 'auto',
38-
align: 'auto',
39-
});
40-
}
41-
}, [focusedDimensionKey, rowVirtualizer, partitions]);
42-
4324
return (
44-
<AssetListContainer
45-
ref={parentRef}
46-
tabIndex={-1}
47-
onKeyDown={(e) => {
48-
const shift = {ArrowDown: 1, ArrowUp: -1}[e.key];
49-
if (!setFocusedDimensionKey || !shift || !focusedDimensionKey || e.isDefaultPrevented()) {
50-
return;
51-
}
52-
const nextIdx = partitions.indexOf(focusedDimensionKey) + shift;
53-
const next = partitions[nextIdx];
54-
if (next) {
55-
e.preventDefault();
56-
setFocusedDimensionKey(next);
57-
}
58-
}}
59-
>
60-
<Inner $totalHeight={totalHeight}>
61-
{items.map(({index, key, size, start}) => {
62-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
63-
const dimensionKey = partitions[index]!;
64-
const state = statusForPartition(dimensionKey);
65-
return (
66-
<AssetListRow
67-
key={key}
68-
$height={size}
69-
$start={start}
70-
$focused={dimensionKey === focusedDimensionKey}
71-
onClick={(e) => {
72-
// If you're interacting with something in the row, don't trigger a focus change.
73-
// Since focus is stored in the URL bar this overwrites any link click navigation.
74-
// We could alternatively e.preventDefault() on every link but it's easy to forget.
75-
if (e.target instanceof HTMLElement && e.target.closest('a')) {
76-
return;
77-
}
78-
setFocusedDimensionKey?.(
79-
focusedDimensionKey !== dimensionKey ? dimensionKey : undefined,
80-
);
81-
}}
82-
>
83-
<Box
84-
style={{height: size}}
85-
padding={{left: 24, right: 12}}
86-
flex={{direction: 'column', justifyContent: 'center', gap: 8}}
87-
border="bottom"
88-
>
89-
<Box flex={{gap: 4, direction: 'row', alignItems: 'center'}}>
90-
<div
91-
style={{flex: 1, minWidth: 0}}
92-
data-tooltip={dimensionKey}
93-
data-tooltip-style={PartitionTooltipStyle}
94-
>
95-
<MiddleTruncate text={dimensionKey} />
96-
</div>
97-
{/* Note: we could just state.map, but we want these in a particular order*/}
98-
{state.includes(AssetPartitionStatus.MISSING) && (
99-
<AssetPartitionStatusDot status={[AssetPartitionStatus.MISSING]} />
100-
)}
101-
{state.includes(AssetPartitionStatus.FAILED) && (
102-
<AssetPartitionStatusDot status={[AssetPartitionStatus.FAILED]} />
103-
)}
104-
{state.includes(AssetPartitionStatus.MATERIALIZING) && (
105-
<AssetPartitionStatusDot status={[AssetPartitionStatus.MATERIALIZING]} />
106-
)}
107-
{state.includes(AssetPartitionStatus.MATERIALIZED) && (
108-
<AssetPartitionStatusDot status={[AssetPartitionStatus.MATERIALIZED]} />
109-
)}
110-
</Box>
111-
</Box>
112-
</AssetListRow>
113-
);
114-
})}
115-
</Inner>
116-
</AssetListContainer>
25+
<PartitionListSelector
26+
partitions={partitions}
27+
statusForPartition={statusForPartition}
28+
focusedDimensionKey={focusedDimensionKey}
29+
setFocusedDimensionKey={setFocusedDimensionKey}
30+
statusesToStyle={assetPartitionStatusesToStyle}
31+
statusOrder={STATUS_ORDER}
32+
/>
11733
);
11834
};
11935

12036
export const AssetPartitionStatusDot = ({status}: {status: AssetPartitionStatus[]}) => (
121-
<div
122-
style={{
123-
width: 10,
124-
height: 10,
125-
borderRadius: '100%',
126-
flexShrink: 0,
127-
...assetPartitionStatusesToStyle(status),
128-
}}
129-
/>
37+
<PartitionStatusDot status={status} statusesToStyle={assetPartitionStatusesToStyle} />
13038
);
131-
132-
const PartitionTooltipStyle = JSON.stringify({
133-
background: Colors.backgroundLight(),
134-
border: `1px solid ${Colors.borderDefault()}`,
135-
color: Colors.textDefault(),
136-
fontSize: '14px',
137-
top: 0,
138-
left: 0,
139-
});
Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import {Box, Checkbox} from '@dagster-io/ui-components';
2-
31
import {AssetPartitionStatus, assetPartitionStatusToText} from './AssetPartitionStatus';
4-
import {testId} from '../testing/testId';
2+
import {PartitionStatusCheckboxes} from './PartitionStatusCheckboxes';
53

64
export const AssetPartitionStatusCheckboxes = ({
75
counts,
@@ -17,22 +15,13 @@ export const AssetPartitionStatusCheckboxes = ({
1715
disabled?: boolean;
1816
}) => {
1917
return (
20-
<Box flex={{direction: 'row', alignItems: 'center', gap: 12}} style={{overflow: 'hidden'}}>
21-
{allowed.map((status) => (
22-
<Checkbox
23-
key={status}
24-
data-testid={testId(`partition-status-${status}-checkbox`)}
25-
disabled={disabled}
26-
style={{marginBottom: 0, marginLeft: 10, minWidth: 200}}
27-
checked={value.includes(status) && !disabled}
28-
label={`${assetPartitionStatusToText(status)} (${counts[status]})`}
29-
onChange={() =>
30-
onChange(
31-
value.includes(status) ? value.filter((v) => v !== status) : [...value, status],
32-
)
33-
}
34-
/>
35-
))}
36-
</Box>
18+
<PartitionStatusCheckboxes
19+
counts={counts}
20+
value={value}
21+
onChange={onChange}
22+
allowed={allowed}
23+
statusToText={assetPartitionStatusToText}
24+
disabled={disabled}
25+
/>
3726
);
3827
};
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import {Box, Colors, MiddleTruncate} from '@dagster-io/ui-components';
2+
import {useVirtualizer} from '@tanstack/react-virtual';
3+
import {CSSProperties, useEffect, useRef} from 'react';
4+
5+
import {AssetListContainer, AssetListRow} from './AssetEventList';
6+
import {Inner} from '../ui/VirtualizedTable';
7+
8+
export interface PartitionListSelectorProps<TStatus> {
9+
partitions: string[];
10+
statusForPartition: (dimensionKey: string) => TStatus[];
11+
focusedDimensionKey?: string;
12+
setFocusedDimensionKey?: (dimensionKey: string | undefined) => void;
13+
statusesToStyle: (statuses: TStatus[]) => CSSProperties;
14+
statusOrder: TStatus[];
15+
}
16+
17+
export function PartitionListSelector<TStatus>({
18+
focusedDimensionKey,
19+
setFocusedDimensionKey,
20+
statusForPartition,
21+
partitions,
22+
statusesToStyle,
23+
statusOrder,
24+
}: PartitionListSelectorProps<TStatus>) {
25+
const parentRef = useRef<HTMLDivElement | null>(null);
26+
27+
const rowVirtualizer = useVirtualizer({
28+
count: partitions.length,
29+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
30+
getItemKey: (idx) => partitions[idx]!,
31+
getScrollElement: () => parentRef.current,
32+
estimateSize: () => 36,
33+
overscan: 10,
34+
});
35+
const totalHeight = rowVirtualizer.getTotalSize();
36+
const items = rowVirtualizer.getVirtualItems();
37+
38+
useEffect(() => {
39+
if (focusedDimensionKey && partitions.indexOf(focusedDimensionKey) !== -1) {
40+
rowVirtualizer.scrollToIndex(partitions.indexOf(focusedDimensionKey), {
41+
behavior: 'auto',
42+
align: 'auto',
43+
});
44+
}
45+
}, [focusedDimensionKey, rowVirtualizer, partitions]);
46+
47+
return (
48+
<AssetListContainer
49+
ref={parentRef}
50+
tabIndex={-1}
51+
onKeyDown={(e) => {
52+
const shift = {ArrowDown: 1, ArrowUp: -1}[e.key];
53+
if (!setFocusedDimensionKey || !shift || !focusedDimensionKey || e.isDefaultPrevented()) {
54+
return;
55+
}
56+
const nextIdx = partitions.indexOf(focusedDimensionKey) + shift;
57+
const next = partitions[nextIdx];
58+
if (next) {
59+
e.preventDefault();
60+
setFocusedDimensionKey(next);
61+
}
62+
}}
63+
>
64+
<Inner $totalHeight={totalHeight}>
65+
{items.map(({index, key, size, start}) => {
66+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
67+
const dimensionKey = partitions[index]!;
68+
const state = statusForPartition(dimensionKey);
69+
return (
70+
<AssetListRow
71+
key={key}
72+
$height={size}
73+
$start={start}
74+
$focused={dimensionKey === focusedDimensionKey}
75+
onClick={(e) => {
76+
// If you're interacting with something in the row, don't trigger a focus change.
77+
// Since focus is stored in the URL bar this overwrites any link click navigation.
78+
// We could alternatively e.preventDefault() on every link but it's easy to forget.
79+
if (e.target instanceof HTMLElement && e.target.closest('a')) {
80+
return;
81+
}
82+
setFocusedDimensionKey?.(
83+
focusedDimensionKey !== dimensionKey ? dimensionKey : undefined,
84+
);
85+
}}
86+
>
87+
<Box
88+
style={{height: size}}
89+
padding={{left: 24, right: 12}}
90+
flex={{direction: 'column', justifyContent: 'center', gap: 8}}
91+
border="bottom"
92+
>
93+
<Box flex={{gap: 4, direction: 'row', alignItems: 'center'}}>
94+
<div
95+
style={{flex: 1, minWidth: 0}}
96+
data-tooltip={dimensionKey}
97+
data-tooltip-style={PartitionTooltipStyle}
98+
>
99+
<MiddleTruncate text={dimensionKey} />
100+
</div>
101+
{/* Show status dots in the specified order */}
102+
{statusOrder.map((status) => {
103+
if (state.includes(status)) {
104+
return (
105+
<PartitionStatusDot
106+
key={String(status)}
107+
status={[status]}
108+
statusesToStyle={statusesToStyle}
109+
/>
110+
);
111+
}
112+
return null;
113+
})}
114+
</Box>
115+
</Box>
116+
</AssetListRow>
117+
);
118+
})}
119+
</Inner>
120+
</AssetListContainer>
121+
);
122+
}
123+
124+
export const PartitionStatusDot = <TStatus,>({
125+
status,
126+
statusesToStyle,
127+
}: {
128+
status: TStatus[];
129+
statusesToStyle: (statuses: TStatus[]) => CSSProperties;
130+
}) => (
131+
<div
132+
style={{
133+
width: 10,
134+
height: 10,
135+
borderRadius: '100%',
136+
flexShrink: 0,
137+
...statusesToStyle(status),
138+
}}
139+
/>
140+
);
141+
142+
const PartitionTooltipStyle = JSON.stringify({
143+
background: Colors.backgroundLight(),
144+
border: `1px solid ${Colors.borderDefault()}`,
145+
color: Colors.textDefault(),
146+
fontSize: '14px',
147+
top: 0,
148+
left: 0,
149+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {Box, Checkbox} from '@dagster-io/ui-components';
2+
3+
import {testId} from '../testing/testId';
4+
5+
export interface PartitionStatusCheckboxesProps<TStatus> {
6+
counts: {[status: string]: number};
7+
value: TStatus[];
8+
onChange: (selected: TStatus[]) => void;
9+
allowed: TStatus[];
10+
statusToText: (status: TStatus) => string;
11+
disabled?: boolean;
12+
}
13+
14+
export function PartitionStatusCheckboxes<TStatus>({
15+
counts,
16+
value,
17+
onChange,
18+
allowed,
19+
statusToText,
20+
disabled,
21+
}: PartitionStatusCheckboxesProps<TStatus>) {
22+
return (
23+
<Box flex={{direction: 'row', alignItems: 'center', gap: 12}} style={{overflow: 'hidden'}}>
24+
{allowed.map((status) => (
25+
<Checkbox
26+
key={String(status)}
27+
data-testid={testId(`partition-status-${String(status)}-checkbox`)}
28+
disabled={disabled}
29+
style={{marginBottom: 0, marginLeft: 10, minWidth: 200}}
30+
checked={value.includes(status) && !disabled}
31+
label={`${statusToText(status)} (${counts[String(status)] || 0})`}
32+
onChange={() =>
33+
onChange(
34+
value.includes(status) ? value.filter((v) => v !== status) : [...value, status],
35+
)
36+
}
37+
/>
38+
))}
39+
</Box>
40+
);
41+
}

0 commit comments

Comments
 (0)