Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const ASSET_CHECK_EXECUTION_FRAGMENT = gql`
metadataEntries {
...MetadataEntryFragment
}
partition
}
}
${METADATA_ENTRY_FRAGMENT}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ type Execution = AssetCheckDetailsQuery['assetCheckExecutions'][0];
export const AssetCheckExecutionList = ({
executions,
paginationProps,
hasPartitions = false,
}: {
executions: Execution[];
paginationProps: CursorPaginationProps;
hasPartitions?: boolean;
}) => {
if (!executions) {
return (
Expand All @@ -43,6 +45,7 @@ export const AssetCheckExecutionList = ({
<th style={{width: '160px'}}>Evaluation result</th>
<th style={{width: '200px'}}>Timestamp</th>
<th style={{width: '200px'}}>Target materialization</th>
{hasPartitions && <th style={{width: '150px'}}>Partition</th>}
<th>
<Box flex={{gap: 4}}>
Description
Expand Down Expand Up @@ -95,6 +98,11 @@ export const AssetCheckExecutionList = ({
' - '
)}
</td>
{hasPartitions && (
<td>
{execution.evaluation?.partition ? execution.evaluation.partition : ' - '}
</td>
)}
<td>
{execution.evaluation?.description ? (
<Description fontSize="14px" description={execution.evaluation.description} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ export const AssetCheckOverview = ({
</Link>
</Box>
) : null}
{lastExecution ? (
<Box flex={{direction: 'column', gap: 8}}>
<Subtitle2>Partition</Subtitle2>
{lastExecution.evaluation?.partition ? (
<div style={{fontFamily: FontFamily.monospace, fontSize: 12}}>
{lastExecution.evaluation.partition}
</div>
) : (
<>&mdash;</>
)}
</Box>
) : null}
</Box>
{lastExecution?.evaluation?.metadataEntries.length ? (
<Box flex={{direction: 'column', gap: 8}}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {Box, Heading, NonIdealState, Spinner, Tag} from '@dagster-io/ui-components';
import {useMemo} from 'react';
import {Link} from 'react-router-dom';

import {AssetCheckPartitionStatus} from './AssetCheckPartitionStatus';
import {AssetCheckStatusTag} from './AssetCheckStatusTag';
import {useAssetCheckPartitionDetail} from './useAssetCheckPartitionDetail';
import {MetadataEntries} from '../../metadata/MetadataEntry';
import {linkToRunEvent} from '../../runs/RunUtils';
import {TimestampDisplay} from '../../schedules/TimestampDisplay';
import {AssetKey} from '../types';

interface AssetCheckPartitionDetailProps {
assetKey: AssetKey;
checkName: string;
partitionKey: string;
}

export const AssetCheckPartitionDetail = ({
assetKey,
checkName,
partitionKey,
}: AssetCheckPartitionDetailProps) => {
const {data: executionData, loading: executionLoading} = useAssetCheckPartitionDetail(
assetKey,
checkName,
partitionKey,
);

const latestExecution = useMemo(() => {
if (!executionData?.assetCheckExecutions?.length) {
return null;
}

return executionData.assetCheckExecutions[0];
}, [executionData]);

const partitionStatus = useMemo(() => {
if (!latestExecution) {
return AssetCheckPartitionStatus.MISSING;
}
return latestExecution.status;
}, [latestExecution]);

if (executionLoading || !executionData) {
return (
<Box
flex={{direction: 'column', justifyContent: 'center', alignItems: 'center'}}
style={{height: '100%'}}
>
<Spinner purpose="section" />
</Box>
);
}

if (!partitionStatus) {
return (
<Box padding={24}>
<NonIdealState
title="Partition not found"
description={`No data available for partition "${partitionKey}"`}
icon="partition"
/>
</Box>
);
}

const primaryStatus = partitionStatus;

// Handle missing status with a blank state
if (primaryStatus === AssetCheckPartitionStatus.MISSING) {
return (
<Box padding={24}>
<Box flex={{direction: 'column', gap: 16}}>
<Heading>{partitionKey}</Heading>

<Box flex={{direction: 'column', gap: 12}}>
<Box flex={{direction: 'row', gap: 8, alignItems: 'center'}}>
<strong>Status:</strong>
<Tag intent="none">No execution attempted</Tag>
</Box>

<Box flex={{direction: 'column', gap: 8}}>
<Box style={{color: '#666', fontSize: '14px'}}>
This partition has not been executed for this asset check yet.
</Box>
</Box>
</Box>
</Box>
</Box>
);
}

return (
<Box padding={24}>
<Box flex={{direction: 'column', gap: 16}}>
<Heading>{partitionKey}</Heading>

<Box flex={{direction: 'column', gap: 12}}>
<Box flex={{direction: 'row', gap: 8, alignItems: 'center'}}>
<strong>Status:</strong>
{latestExecution ? (
<AssetCheckStatusTag execution={latestExecution} />
) : (
<Tag intent="none">No execution data</Tag>
)}
</Box>

{executionLoading ? (
<Box flex={{direction: 'row', alignItems: 'center', gap: 8}}>
<Spinner purpose="body-text" />
<span>Loading execution details...</span>
</Box>
) : latestExecution ? (
<Box flex={{direction: 'column', gap: 12}}>
{/* Execution timestamp */}
{latestExecution.evaluation?.timestamp && (
<Box flex={{direction: 'row', gap: 8, alignItems: 'center'}}>
<strong>Executed:</strong>
<Link
to={linkToRunEvent(
{id: latestExecution.runId},
{stepKey: latestExecution.stepKey, timestamp: latestExecution.timestamp},
)}
>
<TimestampDisplay timestamp={latestExecution.evaluation.timestamp} />
</Link>
</Box>
)}

{/* Target materialization */}
{latestExecution.evaluation?.targetMaterialization && (
<Box flex={{direction: 'row', gap: 8, alignItems: 'center'}}>
<strong>Target materialization:</strong>
<Link to={`/runs/${latestExecution.evaluation.targetMaterialization.runId}`}>
<TimestampDisplay
timestamp={latestExecution.evaluation.targetMaterialization.timestamp}
/>
</Link>
</Box>
)}

{/* Description */}
{latestExecution.evaluation?.description && (
<Box flex={{direction: 'column', gap: 4}}>
<strong>Description:</strong>
<Box style={{color: '#666'}}>{latestExecution.evaluation.description}</Box>
</Box>
)}

{/* Metadata */}
{latestExecution.evaluation?.metadataEntries &&
latestExecution.evaluation.metadataEntries.length > 0 && (
<Box flex={{direction: 'column', gap: 4}}>
<strong>Metadata:</strong>
<MetadataEntries entries={latestExecution.evaluation.metadataEntries} />
</Box>
)}
</Box>
) : (
<Box style={{color: '#666', fontSize: '14px'}}>
No execution data available for this partition.
</Box>
)}
</Box>
</Box>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {Colors} from '@dagster-io/ui-components';
import {CSSProperties} from 'react';

import {assertUnreachable} from '../../app/Util';
import {AssetCheckExecutionResolvedStatus} from '../../graphql/types';

// Asset Check Partition Status includes all execution statuses plus MISSING for unattempted partitions
export enum AssetCheckPartitionStatus {
MISSING = 'MISSING',
IN_PROGRESS = 'IN_PROGRESS',
SUCCEEDED = 'SUCCEEDED',
FAILED = 'FAILED',
EXECUTION_FAILED = 'EXECUTION_FAILED',
SKIPPED = 'SKIPPED',
}

export const emptyAssetCheckPartitionStatusCounts = () => ({
[AssetCheckPartitionStatus.MISSING]: 0,
[AssetCheckPartitionStatus.IN_PROGRESS]: 0,
[AssetCheckPartitionStatus.SUCCEEDED]: 0,
[AssetCheckPartitionStatus.FAILED]: 0,
[AssetCheckPartitionStatus.EXECUTION_FAILED]: 0,
[AssetCheckPartitionStatus.SKIPPED]: 0,
});

export const assetCheckPartitionStatusToText = (status: AssetCheckPartitionStatus) => {
switch (status) {
case AssetCheckPartitionStatus.MISSING:
return 'Missing';
case AssetCheckPartitionStatus.SUCCEEDED:
return 'Passed';
case AssetCheckPartitionStatus.IN_PROGRESS:
return 'In Progress';
case AssetCheckPartitionStatus.FAILED:
return 'Failed';
case AssetCheckPartitionStatus.EXECUTION_FAILED:
return 'Execution Failed';
case AssetCheckPartitionStatus.SKIPPED:
return 'Skipped';
default:
assertUnreachable(status);
}
};

const assetCheckPartitionStatusToColor = (status: AssetCheckPartitionStatus) => {
switch (status) {
case AssetCheckPartitionStatus.MISSING:
return Colors.accentGray();
case AssetCheckPartitionStatus.SUCCEEDED:
return Colors.accentGreen();
case AssetCheckPartitionStatus.IN_PROGRESS:
return Colors.accentBlue();
case AssetCheckPartitionStatus.FAILED:
case AssetCheckPartitionStatus.EXECUTION_FAILED:
return Colors.accentRed();
case AssetCheckPartitionStatus.SKIPPED:
return Colors.accentYellow();
default:
assertUnreachable(status);
}
};

export function assetCheckPartitionStatusesToStyle(
statuses: AssetCheckPartitionStatus[],
): CSSProperties {
if (statuses.length === 1) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return {backgroundColor: assetCheckPartitionStatusToColor(statuses[0]!)};
}
const colors = statuses.map(assetCheckPartitionStatusToColor);
const pct = 100 / colors.length;
const stops = colors.map((c, idx) => `${c} ${idx * pct}%, ${c} ${(idx + 1) * pct}%`);
return {
background: `linear-gradient(to right, ${stops.join(', ')})`,
};
}

// Helper function to convert AssetCheckExecutionResolvedStatus to AssetCheckPartitionStatus
export function executionStatusToPartitionStatus(
status: AssetCheckExecutionResolvedStatus,
): AssetCheckPartitionStatus {
switch (status) {
case AssetCheckExecutionResolvedStatus.IN_PROGRESS:
return AssetCheckPartitionStatus.IN_PROGRESS;
case AssetCheckExecutionResolvedStatus.SUCCEEDED:
return AssetCheckPartitionStatus.SUCCEEDED;
case AssetCheckExecutionResolvedStatus.FAILED:
return AssetCheckPartitionStatus.FAILED;
case AssetCheckExecutionResolvedStatus.EXECUTION_FAILED:
return AssetCheckPartitionStatus.EXECUTION_FAILED;
case AssetCheckExecutionResolvedStatus.SKIPPED:
return AssetCheckPartitionStatus.SKIPPED;
default:
assertUnreachable(status);
}
}

// Legacy exports for backward compatibility
export const assetCheckExecutionStatusToText = assetCheckPartitionStatusToText;
export const assetCheckExecutionStatusesToStyle = assetCheckPartitionStatusesToStyle;
Loading