-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[2/2] AMP Timeline page - Evaluations table, Runs table + Detail moda…
…l rows (#17054) ## Summary & Motivation Hooks up the Evaluations table, Runs table, and Detail modal rows ## How I Tested These Changes locally: <img width="1194" alt="Screenshot 2023-10-05 at 9 56 32 PM" src="https://github.com/dagster-io/dagster/assets/2286579/d717df90-f6a9-4a50-99d9-9ab22ffd5418"> <img width="1187" alt="Screenshot 2023-10-05 at 9 56 16 PM" src="https://github.com/dagster-io/dagster/assets/2286579/3d0f3391-d930-400f-a402-010cc0fc8da4"> <img width="820" alt="Screenshot 2023-10-05 at 9 54 29 PM" src="https://github.com/dagster-io/dagster/assets/2286579/59bf41a1-7103-4ea1-9f2a-d0408e209b92"> <img width="1166" alt="Screenshot 2023-10-05 at 9 54 19 PM" src="https://github.com/dagster-io/dagster/assets/2286579/62d434cd-3b32-49eb-96e2-bac5d946428e"> <img width="1178" alt="Screenshot 2023-10-05 at 9 54 10 PM" src="https://github.com/dagster-io/dagster/assets/2286579/773b3a7b-fd17-4aed-bd31-cd5844624f87">
- Loading branch information
Showing
8 changed files
with
573 additions
and
114 deletions.
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
...les/dagster-ui/packages/ui-core/src/assets/auto-materialization/AssetDaemonTicksQuery.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import {gql} from '@apollo/client'; | ||
|
||
import {PYTHON_ERROR_FRAGMENT} from '../../app/PythonErrorFragment'; | ||
|
||
export const ASSET_DAMEON_TICKS_QUERY = gql` | ||
query AssetDaemonTicksQuery( | ||
$dayRange: Int | ||
$dayOffset: Int | ||
$statuses: [InstigationTickStatus!] | ||
$limit: Int | ||
$cursor: String | ||
) { | ||
autoMaterializeTicks( | ||
dayRange: $dayRange | ||
dayOffset: $dayOffset | ||
statuses: $statuses | ||
limit: $limit | ||
cursor: $cursor | ||
) { | ||
id | ||
...AssetDaemonTickFragment | ||
} | ||
} | ||
fragment AssetDaemonTickFragment on InstigationTick { | ||
id | ||
timestamp | ||
endTimestamp | ||
status | ||
instigationType | ||
error { | ||
...PythonErrorFragment | ||
} | ||
requestedAssetKeys { | ||
path | ||
} | ||
requestedAssetMaterializationCount | ||
autoMaterializeAssetEvaluationId | ||
requestedMaterializationsForAssets { | ||
assetKey { | ||
path | ||
} | ||
partitionKeys | ||
} | ||
} | ||
${PYTHON_ERROR_FRAGMENT} | ||
`; |
301 changes: 267 additions & 34 deletions
301
...ges/ui-core/src/assets/auto-materialization/AutomaterializationEvaluationHistoryTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,275 @@ | ||
import {Body2, Colors, Table} from '@dagster-io/ui-components'; | ||
import { | ||
BaseTag, | ||
Body2, | ||
Box, | ||
Button, | ||
ButtonGroup, | ||
ButtonLink, | ||
Checkbox, | ||
Colors, | ||
CursorHistoryControls, | ||
Dialog, | ||
DialogBody, | ||
DialogFooter, | ||
Spinner, | ||
Table, | ||
Tag, | ||
} from '@dagster-io/ui-components'; | ||
import React from 'react'; | ||
|
||
import {PythonErrorInfo} from '../../app/PythonErrorInfo'; | ||
import {useQueryRefreshAtInterval} from '../../app/QueryRefresh'; | ||
import {Timestamp} from '../../app/time/Timestamp'; | ||
import {InstigationTickStatus} from '../../graphql/types'; | ||
import {useQueryPersistedState} from '../../hooks/useQueryPersistedState'; | ||
import {TimeElapsed} from '../../runs/TimeElapsed'; | ||
import {AnchorButton} from '../../ui/AnchorButton'; | ||
import {useCursorPaginatedQuery} from '../../runs/useCursorPaginatedQuery'; | ||
|
||
import {ASSET_DAMEON_TICKS_QUERY} from './AssetDaemonTicksQuery'; | ||
import { | ||
AssetDaemonTicksQuery, | ||
AssetDaemonTicksQueryVariables, | ||
AssetDaemonTickFragment, | ||
} from './types/AssetDaemonTicksQuery.types'; | ||
|
||
const PAGE_SIZE = 15; | ||
|
||
export const AutomaterializationEvaluationHistoryTable = ({ | ||
setSelectedTick, | ||
setTableView, | ||
}: { | ||
setSelectedTick: (tick: AssetDaemonTickFragment | null) => void; | ||
setTableView: (view: 'evaluations' | 'runs') => void; | ||
}) => { | ||
const [statuses, setStatuses] = useQueryPersistedState<Set<InstigationTickStatus>>({ | ||
queryKey: 'statuses', | ||
decode: React.useCallback(({statuses}: {statuses?: string}) => { | ||
return new Set<InstigationTickStatus>( | ||
statuses | ||
? JSON.parse(statuses) | ||
: [ | ||
InstigationTickStatus.STARTED, | ||
InstigationTickStatus.SUCCESS, | ||
InstigationTickStatus.FAILURE, | ||
InstigationTickStatus.SKIPPED, | ||
], | ||
); | ||
}, []), | ||
encode: React.useCallback((raw: Set<InstigationTickStatus>) => { | ||
return {statuses: JSON.stringify(Array.from(raw))}; | ||
}, []), | ||
}); | ||
|
||
const {queryResult, paginationProps} = useCursorPaginatedQuery< | ||
AssetDaemonTicksQuery, | ||
AssetDaemonTicksQueryVariables | ||
>({ | ||
query: ASSET_DAMEON_TICKS_QUERY, | ||
variables: { | ||
statuses: React.useMemo(() => Array.from(statuses), [statuses]), | ||
}, | ||
nextCursorForResult: (data) => { | ||
const ticks = data.autoMaterializeTicks; | ||
if (!ticks.length) { | ||
return undefined; | ||
} | ||
return ticks[PAGE_SIZE - 1]?.id; | ||
}, | ||
getResultArray: (data) => { | ||
if (!data?.autoMaterializeTicks) { | ||
return []; | ||
} | ||
return data.autoMaterializeTicks; | ||
}, | ||
pageSize: PAGE_SIZE, | ||
}); | ||
// Only refresh if we're on the first page | ||
useQueryRefreshAtInterval(queryResult, !paginationProps.hasPrevCursor ? 10000 : 60 * 60 * 1000); | ||
|
||
return ( | ||
<Box> | ||
<Box | ||
flex={{justifyContent: 'space-between', alignItems: 'center'}} | ||
padding={{vertical: 12, horizontal: 24}} | ||
margin={{top: 32}} | ||
border="top" | ||
> | ||
<Box flex={{direction: 'row', gap: 8}}> | ||
<ButtonGroup | ||
activeItems={new Set(['evaluations'])} | ||
buttons={[ | ||
{id: 'evaluations', label: 'Evaluations'}, | ||
{id: 'runs', label: 'Runs'}, | ||
]} | ||
onClick={(id: 'evaluations' | 'runs') => { | ||
setTableView(id); | ||
}} | ||
/> | ||
{!queryResult.data ? <Spinner purpose="body-text" /> : null} | ||
</Box> | ||
<Box flex={{direction: 'row', gap: 12, alignItems: 'center'}}> | ||
<StatusCheckbox | ||
statuses={statuses} | ||
setStatuses={setStatuses} | ||
status={InstigationTickStatus.STARTED} | ||
/> | ||
<StatusCheckbox | ||
statuses={statuses} | ||
setStatuses={setStatuses} | ||
status={InstigationTickStatus.SUCCESS} | ||
/> | ||
<StatusCheckbox | ||
statuses={statuses} | ||
setStatuses={setStatuses} | ||
status={InstigationTickStatus.FAILURE} | ||
/> | ||
<StatusCheckbox | ||
statuses={statuses} | ||
setStatuses={setStatuses} | ||
status={InstigationTickStatus.SKIPPED} | ||
/> | ||
</Box> | ||
</Box> | ||
<Table> | ||
<thead> | ||
<tr> | ||
<th>Timestamp</th> | ||
<th>Status</th> | ||
<th>Duration</th> | ||
<th>Result</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{/* Use previous data to stop page from jumping while new data loads */} | ||
{(queryResult.data || queryResult.previousData)?.autoMaterializeTicks.map((tick) => ( | ||
<tr key={tick.id}> | ||
<td> | ||
<Timestamp timestamp={{unix: tick.timestamp}} /> | ||
</td> | ||
<td> | ||
<StatusTag tick={tick} /> | ||
</td> | ||
<td> | ||
<TimeElapsed | ||
startUnix={tick.timestamp} | ||
endUnix={tick.endTimestamp || Date.now() / 1000} | ||
/> | ||
</td> | ||
<td> | ||
{[InstigationTickStatus.SKIPPED, InstigationTickStatus.SUCCESS].includes( | ||
tick.status, | ||
) ? ( | ||
<ButtonLink | ||
onClick={() => { | ||
setSelectedTick(tick); | ||
}} | ||
> | ||
<Body2> | ||
{tick.requestedAssetMaterializationCount} materializations requested | ||
</Body2> | ||
</ButtonLink> | ||
) : ( | ||
' - ' | ||
)} | ||
</td> | ||
</tr> | ||
))} | ||
</tbody> | ||
</Table> | ||
<div style={{paddingBottom: '16px'}}> | ||
<CursorHistoryControls {...paginationProps} /> | ||
</div> | ||
</Box> | ||
); | ||
}; | ||
|
||
const StatusTag = ({tick}: {tick: AssetDaemonTickFragment}) => { | ||
const {status, error, requestedAssetMaterializationCount} = tick; | ||
const count = requestedAssetMaterializationCount; | ||
const [showErrors, setShowErrors] = React.useState(false); | ||
const tag = React.useMemo(() => { | ||
switch (status) { | ||
case InstigationTickStatus.STARTED: | ||
return ( | ||
<Tag intent="primary" icon="spinner"> | ||
Evaluating | ||
</Tag> | ||
); | ||
case InstigationTickStatus.SKIPPED: | ||
return <BaseTag fillColor={Colors.Olive50} label="0 requested" />; | ||
case InstigationTickStatus.FAILURE: | ||
return ( | ||
<Box flex={{direction: 'row', alignItems: 'center', gap: 6}}> | ||
<Tag intent="danger">Failure</Tag> | ||
{error ? ( | ||
<ButtonLink | ||
onClick={() => { | ||
setShowErrors(true); | ||
}} | ||
> | ||
View | ||
</ButtonLink> | ||
) : null} | ||
</Box> | ||
); | ||
case InstigationTickStatus.SUCCESS: | ||
return <Tag intent="success">{count} requested</Tag>; | ||
} | ||
}, [error, count, status]); | ||
|
||
export const AutomaterializationEvaluationHistoryTable = () => { | ||
// TODO | ||
return ( | ||
<Table> | ||
<thead> | ||
<tr> | ||
<th>Timestamp</th> | ||
<th>Status</th> | ||
<th>Duration</th> | ||
<th>Result</th> | ||
<th></th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr> | ||
<td> | ||
<Timestamp timestamp={{unix: 0}} /> | ||
</td> | ||
<td> | ||
<div /> | ||
</td> | ||
<td> | ||
<TimeElapsed startUnix={0} endUnix={10000} /> | ||
</td> | ||
<td> | ||
<Body2 color={Colors.Gray700}>No runs launched</Body2> | ||
</td> | ||
<td> | ||
<AnchorButton to="/">View details</AnchorButton> | ||
</td> | ||
</tr> | ||
</tbody> | ||
</Table> | ||
<> | ||
{tag} | ||
{error ? ( | ||
<Dialog isOpen={showErrors} title="Error" style={{width: '80vw'}}> | ||
<DialogBody> | ||
<PythonErrorInfo error={error} /> | ||
</DialogBody> | ||
<DialogFooter topBorder> | ||
<Button | ||
intent="primary" | ||
onClick={() => { | ||
setShowErrors(false); | ||
}} | ||
> | ||
Close | ||
</Button> | ||
</DialogFooter> | ||
</Dialog> | ||
) : null} | ||
</> | ||
); | ||
}; | ||
|
||
const StatusLabels = { | ||
[InstigationTickStatus.SKIPPED]: 'None requested', | ||
[InstigationTickStatus.STARTED]: 'Started', | ||
[InstigationTickStatus.FAILURE]: 'Failed', | ||
[InstigationTickStatus.SUCCESS]: 'Requested', | ||
}; | ||
|
||
function StatusCheckbox({ | ||
status, | ||
statuses, | ||
setStatuses, | ||
}: { | ||
status: InstigationTickStatus; | ||
statuses: Set<InstigationTickStatus>; | ||
setStatuses: (statuses: Set<InstigationTickStatus>) => void; | ||
}) { | ||
return ( | ||
<Checkbox | ||
label={StatusLabels[status]} | ||
checked={statuses.has(status)} | ||
onChange={() => { | ||
const newStatuses = new Set(statuses); | ||
if (statuses.has(status)) { | ||
newStatuses.delete(status); | ||
} else { | ||
newStatuses.add(status); | ||
} | ||
setStatuses(newStatuses); | ||
}} | ||
/> | ||
); | ||
} |
Oops, something went wrong.
8b14987
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deploy preview for dagit-core-storybook ready!
✅ Preview
https://dagit-core-storybook-o8os18qdk-elementl.vercel.app
Built with commit 8b14987.
This pull request is being automatically deployed with vercel-action