Skip to content

Commit

Permalink
[2/2] AMP Timeline page - Evaluations table, Runs table + Detail moda…
Browse files Browse the repository at this point in the history
…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
salazarm authored Oct 9, 2023
1 parent 2567e2d commit 8b14987
Show file tree
Hide file tree
Showing 8 changed files with 573 additions and 114 deletions.
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}
`;
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);
}}
/>
);
}
Loading

1 comment on commit 8b14987

@github-actions
Copy link

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

Please sign in to comment.