diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckDetailDialog.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckDetailDialog.tsx index 5b97c77c92834..696c8e021c5b8 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckDetailDialog.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckDetailDialog.tsx @@ -90,6 +90,7 @@ export const ASSET_CHECK_EXECUTION_FRAGMENT = gql` metadataEntries { ...MetadataEntryFragment } + partition } } ${METADATA_ENTRY_FRAGMENT} diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckExecutionList.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckExecutionList.tsx index 6136710ab09f6..7ac1849ef2344 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckExecutionList.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckExecutionList.tsx @@ -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 ( @@ -43,6 +45,7 @@ export const AssetCheckExecutionList = ({ Evaluation result Timestamp Target materialization + {hasPartitions && Partition} Description @@ -95,6 +98,11 @@ export const AssetCheckExecutionList = ({ ' - ' )} + {hasPartitions && ( + + {execution.evaluation?.partition ? execution.evaluation.partition : ' - '} + + )} {execution.evaluation?.description ? ( diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckOverview.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckOverview.tsx index 9172b89d0a3fc..a4249bbebb6a8 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckOverview.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckOverview.tsx @@ -136,6 +136,18 @@ export const AssetCheckOverview = ({ ) : null} + {lastExecution ? ( + + Partition + {lastExecution.evaluation?.partition ? ( +
+ {lastExecution.evaluation.partition} +
+ ) : ( + <>— + )} +
+ ) : null} {lastExecution?.evaluation?.metadataEntries.length ? ( diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckPartitionDetail.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckPartitionDetail.tsx new file mode 100644 index 0000000000000..71a122c401282 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckPartitionDetail.tsx @@ -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 ( + + + + ); + } + + if (!partitionStatus) { + return ( + + + + ); + } + + const primaryStatus = partitionStatus; + + // Handle missing status with a blank state + if (primaryStatus === AssetCheckPartitionStatus.MISSING) { + return ( + + + {partitionKey} + + + + Status: + No execution attempted + + + + + This partition has not been executed for this asset check yet. + + + + + + ); + } + + return ( + + + {partitionKey} + + + + Status: + {latestExecution ? ( + + ) : ( + No execution data + )} + + + {executionLoading ? ( + + + Loading execution details... + + ) : latestExecution ? ( + + {/* Execution timestamp */} + {latestExecution.evaluation?.timestamp && ( + + Executed: + + + + + )} + + {/* Target materialization */} + {latestExecution.evaluation?.targetMaterialization && ( + + Target materialization: + + + + + )} + + {/* Description */} + {latestExecution.evaluation?.description && ( + + Description: + {latestExecution.evaluation.description} + + )} + + {/* Metadata */} + {latestExecution.evaluation?.metadataEntries && + latestExecution.evaluation.metadataEntries.length > 0 && ( + + Metadata: + + + )} + + ) : ( + + No execution data available for this partition. + + )} + + + + ); +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckPartitionStatus.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckPartitionStatus.tsx new file mode 100644 index 0000000000000..4597ed1d81abc --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckPartitionStatus.tsx @@ -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; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckPartitions.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckPartitions.tsx new file mode 100644 index 0000000000000..2e1af3474d0ac --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetCheckPartitions.tsx @@ -0,0 +1,185 @@ +import {Box, Colors, NonIdealState, TextInput} from '@dagster-io/ui-components'; +import {useMemo, useState} from 'react'; + +import {AssetCheckPartitionDetail} from './AssetCheckPartitionDetail'; +import { + AssetCheckPartitionStatus, + assetCheckPartitionStatusToText, + assetCheckPartitionStatusesToStyle, +} from './AssetCheckPartitionStatus'; +import {useAssetCheckPartitionData} from './useAssetCheckPartitionData'; +import {useQueryPersistedState} from '../../hooks/useQueryPersistedState'; +import {testId} from '../../testing/testId'; +import {PartitionListSelector} from '../PartitionListSelector'; +import {PartitionStatusCheckboxes} from '../PartitionStatusCheckboxes'; +import {AssetKey} from '../types'; + +interface AssetCheckPartitionsProps { + assetKey: AssetKey; + checkName: string; +} + +const DISPLAYED_STATUSES: AssetCheckPartitionStatus[] = [ + AssetCheckPartitionStatus.MISSING, + AssetCheckPartitionStatus.SKIPPED, + AssetCheckPartitionStatus.IN_PROGRESS, + AssetCheckPartitionStatus.SUCCEEDED, + AssetCheckPartitionStatus.FAILED, + AssetCheckPartitionStatus.EXECUTION_FAILED, +].sort(); + +const STATUS_ORDER: AssetCheckPartitionStatus[] = [ + AssetCheckPartitionStatus.MISSING, + AssetCheckPartitionStatus.FAILED, + AssetCheckPartitionStatus.EXECUTION_FAILED, + AssetCheckPartitionStatus.SKIPPED, + AssetCheckPartitionStatus.IN_PROGRESS, + AssetCheckPartitionStatus.SUCCEEDED, +]; + +export const AssetCheckPartitions = ({assetKey, checkName}: AssetCheckPartitionsProps) => { + const {data: partitionData, loading} = useAssetCheckPartitionData(assetKey, checkName); + const [focusedPartitionKey, setFocusedPartitionKey] = useState(); + const [searchValue, setSearchValue] = useState(''); + + const [statusFilters, setStatusFilters] = useQueryPersistedState({ + defaults: {status: [...DISPLAYED_STATUSES].sort().join(',')}, + encode: (val) => ({status: [...val].sort().join(',')}), + decode: (qs) => { + const status = qs.status; + if (typeof status === 'string') { + return status + .split(',') + .filter((s): s is AssetCheckPartitionStatus => + DISPLAYED_STATUSES.includes(s as AssetCheckPartitionStatus), + ); + } + return [...DISPLAYED_STATUSES]; + }, + }); + + // Calculate partition counts by status + const {filteredPartitions, countsByStatus} = useMemo(() => { + if (!partitionData) { + return {filteredPartitions: [], countsByStatus: {}}; + } + + const counts: {[key: string]: number} = { + [AssetCheckPartitionStatus.MISSING]: 0, + [AssetCheckPartitionStatus.SKIPPED]: 0, + [AssetCheckPartitionStatus.SUCCEEDED]: 0, + [AssetCheckPartitionStatus.IN_PROGRESS]: 0, + [AssetCheckPartitionStatus.FAILED]: 0, + [AssetCheckPartitionStatus.EXECUTION_FAILED]: 0, + }; + + const allPartitions = partitionData.partitions; + const filtered = allPartitions.filter((partition) => { + if (searchValue && !partition.toLowerCase().includes(searchValue.toLowerCase())) { + return false; + } + + const primaryStatus = partitionData.statusForPartition(partition); + + counts[primaryStatus] = !counts[primaryStatus] ? 1 : counts[primaryStatus] + 1; + return statusFilters.includes(primaryStatus); + }); + + return {filteredPartitions: filtered, countsByStatus: counts}; + }, [partitionData, statusFilters, searchValue]); + + if (loading) { + return ( + + + + ); + } + + if (!partitionData || partitionData.partitions.length === 0) { + return ( + + + + ); + } + + const statusForPartition = (dimensionKey: string): AssetCheckPartitionStatus[] => { + return [partitionData.statusForPartition(dimensionKey)]; + }; + + return ( + <> + +
+ {filteredPartitions.length.toLocaleString()} Partitions Selected +
+ +
+ + + + + setSearchValue(e.target.value)} + placeholder="Filter partitions" + /> + + + + + + + {focusedPartitionKey ? ( + + ) : ( + + Select a partition to view details + + )} + + + + ); +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecks.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecks.tsx index 0139460f0587e..3b2eb954bd049 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecks.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecks.tsx @@ -24,6 +24,7 @@ import { } from './AssetCheckDetailDialog'; import {AssetCheckExecutionList} from './AssetCheckExecutionList'; import {AssetCheckOverview} from './AssetCheckOverview'; +import {AssetCheckPartitions} from './AssetCheckPartitions'; import {ASSET_CHECKS_QUERY} from './AssetChecksQuery'; import {ExecuteChecksButton} from './ExecuteChecksButton'; import { @@ -103,6 +104,10 @@ export const AssetChecks = ({ return checks.find((check) => check.name === selectedCheckName) ?? checks[0]; }, [selectedCheckName, checks]); + const hasPartitions = React.useMemo(() => { + return !!(selectedCheck && selectedCheck.partitionDefinition); + }, [selectedCheck]); + const isSelectedCheckAutomated = !!selectedCheck?.automationCondition; const {paginationProps, executions, executionsLoading} = useHistoricalCheckExecutions( @@ -243,6 +248,7 @@ export const AssetChecks = ({ { setActiveTab(tab); }} @@ -258,11 +264,18 @@ export const AssetChecks = ({ /> ) : null} {activeTab === 'execution-history' ? ( - + ) : null} {activeTab === 'automation-history' ? ( ) : null} + {activeTab === 'partitions' ? ( + + ) : null}
diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecksQuery.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecksQuery.tsx index 1be396c818b8c..081c5e798260f 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecksQuery.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecksQuery.tsx @@ -14,6 +14,18 @@ const ASSET_CHECK_KEY_FRAGMENT = gql` } `; +export const ASSET_CHECK_PARTITION_FRAGMENT = gql` + fragment AssetCheckPartitionFragment on AssetCheck { + partitionDefinition { + description + dimensionTypes { + type + dynamicPartitionsDefinitionName + } + } + } +`; + export const ASSET_CHECKS_QUERY = gql` query AssetChecksQuery($assetKey: AssetKeyInput!) { assetNodeOrError(assetKey: $assetKey) { @@ -30,6 +42,7 @@ export const ASSET_CHECKS_QUERY = gql` ...AssetCheckKeyFragment ...ExecuteChecksButtonCheckFragment ...AssetCheckTableFragment + ...AssetCheckPartitionFragment } } } @@ -41,4 +54,5 @@ export const ASSET_CHECKS_QUERY = gql` ${EXECUTE_CHECKS_BUTTON_CHECK_FRAGMENT} ${ASSET_CHECK_TABLE_FRAGMENT} ${ASSET_CHECK_KEY_FRAGMENT} + ${ASSET_CHECK_PARTITION_FRAGMENT} `; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecksTabs.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecksTabs.tsx index d7264c45d3cdd..6ac395c86c009 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecksTabs.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/AssetChecksTabs.tsx @@ -1,14 +1,24 @@ import {Tab, Tabs, Tooltip} from '@dagster-io/ui-components'; -export type AssetChecksTabType = 'overview' | 'execution-history' | 'automation-history'; +export type AssetChecksTabType = + | 'overview' + | 'execution-history' + | 'automation-history' + | 'partitions'; interface Props { activeTab: AssetChecksTabType; enableAutomationHistory: boolean; onChange: (tabId: AssetChecksTabType) => void; + hasPartitions: boolean; } -export const AssetChecksTabs = ({activeTab, enableAutomationHistory, onChange}: Props) => { +export const AssetChecksTabs = ({ + activeTab, + enableAutomationHistory, + onChange, + hasPartitions, +}: Props) => { return ( @@ -26,6 +36,7 @@ export const AssetChecksTabs = ({activeTab, enableAutomationHistory, onChange}: } disabled={!enableAutomationHistory} /> + {hasPartitions && } ); }; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/AssetCheckDetailDialog.types.ts b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/AssetCheckDetailDialog.types.ts index 83eb2aec83a21..0f537457572ed 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/AssetCheckDetailDialog.types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/AssetCheckDetailDialog.types.ts @@ -20,6 +20,7 @@ export type AssetCheckExecutionFragment = { severity: Types.AssetCheckSeverity; timestamp: number; description: string | null; + partition: string | null; targetMaterialization: { __typename: 'AssetCheckEvaluationTargetMaterializationData'; timestamp: number; @@ -202,6 +203,7 @@ export type AssetCheckDetailsQuery = { severity: Types.AssetCheckSeverity; timestamp: number; description: string | null; + partition: string | null; targetMaterialization: { __typename: 'AssetCheckEvaluationTargetMaterializationData'; timestamp: number; @@ -364,4 +366,4 @@ export type AssetCheckDetailsQuery = { }>; }; -export const AssetCheckDetailsQueryVersion = 'dafb023319ef2c8aa8075c43686b318d3206f5781b8b87cf21130db3172a9f71'; +export const AssetCheckDetailsQueryVersion = 'f5e3bf07865d4121f8ab8e2e31c9af1519f915dfbfec0daa941de352610a4673'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/AssetChecksQuery.types.ts b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/AssetChecksQuery.types.ts index 3bdff0cf063f8..aca7bb9b58724 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/AssetChecksQuery.types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/AssetChecksQuery.types.ts @@ -8,6 +8,19 @@ export type AssetCheckKeyFragment = { assetKey: {__typename: 'AssetKey'; path: Array}; }; +export type AssetCheckPartitionFragment = { + __typename: 'AssetCheck'; + partitionDefinition: { + __typename: 'PartitionDefinition'; + description: string; + dimensionTypes: Array<{ + __typename: 'DimensionDefinitionType'; + type: Types.PartitionDefinitionType; + dynamicPartitionsDefinitionName: string | null; + }>; + } | null; +}; + export type AssetChecksQueryVariables = Types.Exact<{ assetKey: Types.AssetKeyInput; }>; @@ -50,6 +63,7 @@ export type AssetChecksQuery = { severity: Types.AssetCheckSeverity; timestamp: number; description: string | null; + partition: string | null; targetMaterialization: { __typename: 'AssetCheckEvaluationTargetMaterializationData'; timestamp: number; @@ -244,6 +258,15 @@ export type AssetChecksQuery = { >; } | null; } | null; + partitionDefinition: { + __typename: 'PartitionDefinition'; + description: string; + dimensionTypes: Array<{ + __typename: 'DimensionDefinitionType'; + type: Types.PartitionDefinitionType; + dynamicPartitionsDefinitionName: string | null; + }>; + } | null; }>; }; assetKey: {__typename: 'AssetKey'; path: Array}; @@ -257,4 +280,4 @@ export type AssetChecksQuery = { | {__typename: 'AssetNotFoundError'}; }; -export const AssetChecksQueryVersion = 'df2f0eba0bb9816ab3ffabe630b837f47a61ae541a1b543bd7abf365b0754094'; +export const AssetChecksQueryVersion = '0161481af04ecfb7dd909572994e065f7ec758af1b61e3bd825e494702063ecf'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/VirtualizedAssetCheckTable.types.ts b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/VirtualizedAssetCheckTable.types.ts index 0211161861c7a..53fe779e0c050 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/VirtualizedAssetCheckTable.types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/VirtualizedAssetCheckTable.types.ts @@ -26,6 +26,7 @@ export type AssetCheckTableFragment = { severity: Types.AssetCheckSeverity; timestamp: number; description: string | null; + partition: string | null; targetMaterialization: { __typename: 'AssetCheckEvaluationTargetMaterializationData'; timestamp: number; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/useAssetCheckPartitionData.types.ts b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/useAssetCheckPartitionData.types.ts new file mode 100644 index 0000000000000..2729cf6042a28 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/useAssetCheckPartitionData.types.ts @@ -0,0 +1,43 @@ +// Generated GraphQL types, do not edit manually. + +import * as Types from '../../../graphql/types'; + +export type AssetCheckPartitionHealthQueryVariables = Types.Exact<{ + assetKey: Types.AssetKeyInput; + checkName: Types.Scalars['String']['input']; +}>; + +export type AssetCheckPartitionHealthQuery = { + __typename: 'Query'; + assetNodeOrError: + | { + __typename: 'AssetNode'; + id: string; + assetCheckOrError: + | { + __typename: 'AssetCheck'; + name: string; + partitionKeysByDimension: Array<{ + __typename: 'DimensionPartitionKeys'; + name: string; + partitionKeys: Array; + }>; + partitionStatuses: { + __typename: 'AssetCheckPartitionStatuses'; + missing: Array; + succeeded: Array; + failed: Array; + inProgress: Array; + skipped: Array; + executionFailed: Array; + } | null; + } + | {__typename: 'AssetCheckNeedsAgentUpgradeError'} + | {__typename: 'AssetCheckNeedsMigrationError'} + | {__typename: 'AssetCheckNeedsUserCodeUpgrade'} + | {__typename: 'AssetCheckNotFoundError'}; + } + | {__typename: 'AssetNotFoundError'}; +}; + +export const AssetCheckPartitionHealthQueryVersion = '85d84ebb07a4f45fa3c774e3c2bbaa51eeeb4215cd7a95ea374bd2ce9e10656a'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/useAssetCheckPartitionDetail.types.ts b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/useAssetCheckPartitionDetail.types.ts new file mode 100644 index 0000000000000..8434ee3ad2a7c --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/types/useAssetCheckPartitionDetail.types.ts @@ -0,0 +1,189 @@ +// Generated GraphQL types, do not edit manually. + +import * as Types from '../../../graphql/types'; + +export type AssetCheckPartitionDetailQueryVariables = Types.Exact<{ + assetKey: Types.AssetKeyInput; + checkName: Types.Scalars['String']['input']; + partitionKey: Types.Scalars['String']['input']; +}>; + +export type AssetCheckPartitionDetailQuery = { + __typename: 'Query'; + assetCheckExecutions: Array<{ + __typename: 'AssetCheckExecution'; + id: string; + runId: string; + status: Types.AssetCheckExecutionResolvedStatus; + timestamp: number; + stepKey: string | null; + evaluation: { + __typename: 'AssetCheckEvaluation'; + timestamp: number; + severity: Types.AssetCheckSeverity; + description: string | null; + success: boolean; + partition: string | null; + targetMaterialization: { + __typename: 'AssetCheckEvaluationTargetMaterializationData'; + timestamp: number; + runId: string; + } | null; + metadataEntries: Array< + | { + __typename: 'AssetMetadataEntry'; + label: string; + description: string | null; + assetKey: {__typename: 'AssetKey'; path: Array}; + } + | { + __typename: 'BoolMetadataEntry'; + boolValue: boolean | null; + label: string; + description: string | null; + } + | { + __typename: 'CodeReferencesMetadataEntry'; + label: string; + description: string | null; + codeReferences: Array< + | { + __typename: 'LocalFileCodeReference'; + filePath: string; + lineNumber: number | null; + label: string | null; + } + | {__typename: 'UrlCodeReference'; url: string; label: string | null} + >; + } + | { + __typename: 'FloatMetadataEntry'; + floatValue: number | null; + label: string; + description: string | null; + } + | { + __typename: 'IntMetadataEntry'; + intValue: number | null; + intRepr: string; + label: string; + description: string | null; + } + | { + __typename: 'JobMetadataEntry'; + jobName: string; + repositoryName: string | null; + locationName: string; + label: string; + description: string | null; + } + | { + __typename: 'JsonMetadataEntry'; + jsonString: string; + label: string; + description: string | null; + } + | { + __typename: 'MarkdownMetadataEntry'; + mdStr: string; + label: string; + description: string | null; + } + | { + __typename: 'NotebookMetadataEntry'; + path: string; + label: string; + description: string | null; + } + | {__typename: 'NullMetadataEntry'; label: string; description: string | null} + | {__typename: 'PathMetadataEntry'; path: string; label: string; description: string | null} + | { + __typename: 'PipelineRunMetadataEntry'; + runId: string; + label: string; + description: string | null; + } + | {__typename: 'PoolMetadataEntry'; pool: string; label: string; description: string | null} + | { + __typename: 'PythonArtifactMetadataEntry'; + module: string; + name: string; + label: string; + description: string | null; + } + | { + __typename: 'TableColumnLineageMetadataEntry'; + label: string; + description: string | null; + lineage: Array<{ + __typename: 'TableColumnLineageEntry'; + columnName: string; + columnDeps: Array<{ + __typename: 'TableColumnDep'; + columnName: string; + assetKey: {__typename: 'AssetKey'; path: Array}; + }>; + }>; + } + | { + __typename: 'TableMetadataEntry'; + label: string; + description: string | null; + table: { + __typename: 'Table'; + records: Array; + schema: { + __typename: 'TableSchema'; + columns: Array<{ + __typename: 'TableColumn'; + name: string; + description: string | null; + type: string; + tags: Array<{__typename: 'DefinitionTag'; key: string; value: string}>; + constraints: { + __typename: 'TableColumnConstraints'; + nullable: boolean; + unique: boolean; + other: Array; + }; + }>; + constraints: {__typename: 'TableConstraints'; other: Array} | null; + }; + }; + } + | { + __typename: 'TableSchemaMetadataEntry'; + label: string; + description: string | null; + schema: { + __typename: 'TableSchema'; + columns: Array<{ + __typename: 'TableColumn'; + name: string; + description: string | null; + type: string; + tags: Array<{__typename: 'DefinitionTag'; key: string; value: string}>; + constraints: { + __typename: 'TableColumnConstraints'; + nullable: boolean; + unique: boolean; + other: Array; + }; + }>; + constraints: {__typename: 'TableConstraints'; other: Array} | null; + }; + } + | {__typename: 'TextMetadataEntry'; text: string; label: string; description: string | null} + | { + __typename: 'TimestampMetadataEntry'; + timestamp: number; + label: string; + description: string | null; + } + | {__typename: 'UrlMetadataEntry'; url: string; label: string; description: string | null} + >; + } | null; + }>; +}; + +export const AssetCheckPartitionDetailQueryVersion = '61a67dd1e835034edff4f8a722b9c6afb702eaaac5fdb2829f8e4a233a7832db'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/useAssetCheckPartitionData.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/useAssetCheckPartitionData.tsx new file mode 100644 index 0000000000000..41051b7e84f19 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/useAssetCheckPartitionData.tsx @@ -0,0 +1,182 @@ +import {useMemo, useState} from 'react'; + +import {AssetCheckPartitionStatus} from './AssetCheckPartitionStatus'; +import {gql, useApolloClient} from '../../apollo-client'; +import {usePartitionDataSubscriber} from '../PartitionSubscribers'; +import {AssetKey} from '../types'; +import { + AssetCheckPartitionHealthQuery, + AssetCheckPartitionHealthQueryVariables, +} from './types/useAssetCheckPartitionData.types'; + +export interface AssetCheckPartitionData { + assetKey: AssetKey; + checkName: string; + dimensions: AssetCheckPartitionDimension[]; + partitions: string[]; + statusForPartition: (dimensionKey: string) => AssetCheckPartitionStatus; +} + +export interface AssetCheckPartitionDimension { + name: string; + partitionKeys: string[]; +} + +export function useAssetCheckPartitionData(assetKey: AssetKey | null, checkName: string | null) { + const [partitionsLastUpdated, setPartitionsLastUpdatedAt] = useState(''); + usePartitionDataSubscriber(() => { + setPartitionsLastUpdatedAt(Date.now().toString()); + }); + + const cacheKey = `${JSON.stringify(assetKey)}-${checkName}-${partitionsLastUpdated}`; + const [result, setResult] = useState<(AssetCheckPartitionData & {fetchedAt: string}) | null>( + null, + ); + const client = useApolloClient(); + + const [loading, setLoading] = useState(true); + + // Fetch partition data for the asset check + useMemo(() => { + if (!assetKey || !checkName) { + setLoading(false); + setResult(null); + return; + } + + // Check if we already have this data cached + if ( + result && + result.assetKey === assetKey && + result.checkName === checkName && + result.fetchedAt === cacheKey + ) { + setLoading(false); + return; + } + + const run = async () => { + try { + const {data} = await client.query< + AssetCheckPartitionHealthQuery, + AssetCheckPartitionHealthQueryVariables + >({ + query: ASSET_CHECK_PARTITION_HEALTH_QUERY, + fetchPolicy: 'network-only', + variables: { + assetKey: {path: assetKey.path}, + checkName, + }, + }); + + const loaded = buildAssetCheckPartitionData(data, assetKey, checkName); + setResult(loaded ? {...loaded, fetchedAt: cacheKey} : null); + } catch (error) { + console.error('Failed to fetch asset check partition data:', error); + setResult(null); + } + setLoading(false); + }; + run(); + }, [client, assetKey, checkName, cacheKey, result]); + + return {data: result, loading}; +} + +function buildAssetCheckPartitionData( + data: AssetCheckPartitionHealthQuery, + assetKey: AssetKey, + checkName: string, +): AssetCheckPartitionData | null { + const assetNode = data.assetNodeOrError.__typename === 'AssetNode' ? data.assetNodeOrError : null; + + if (!assetNode?.assetCheckOrError || assetNode.assetCheckOrError.__typename !== 'AssetCheck') { + return null; + } + + const check = assetNode.assetCheckOrError; + + const dimensions = (check.partitionKeysByDimension || []).map((dim) => ({ + name: dim.name, + partitionKeys: dim.partitionKeys, + })); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const dim = dimensions[0]!; + const partitions = dimensions.length > 0 ? dim.partitionKeys : []; + + const partitionStatusMap = new Map(); + const partitionStatuses = check.partitionStatuses; + + // Initialize all partitions as missing + for (const partition of partitions) { + partitionStatusMap.set(partition, AssetCheckPartitionStatus.MISSING); + } + + // Update with actual statuses from GraphQL + if (partitionStatuses) { + // Process each status type + partitionStatuses.missing?.forEach((partition) => { + partitionStatusMap.set(partition, AssetCheckPartitionStatus.MISSING); + }); + + partitionStatuses.succeeded?.forEach((partition) => { + partitionStatusMap.set(partition, AssetCheckPartitionStatus.SUCCEEDED); + }); + + partitionStatuses.failed?.forEach((partition) => { + partitionStatusMap.set(partition, AssetCheckPartitionStatus.FAILED); + }); + + partitionStatuses.inProgress?.forEach((partition) => { + partitionStatusMap.set(partition, AssetCheckPartitionStatus.IN_PROGRESS); + }); + + partitionStatuses.skipped?.forEach((partition) => { + partitionStatusMap.set(partition, AssetCheckPartitionStatus.SKIPPED); + }); + + partitionStatuses.executionFailed?.forEach((partition) => { + partitionStatusMap.set(partition, AssetCheckPartitionStatus.EXECUTION_FAILED); + }); + } + + const statusForPartition = (dimensionKey: string): AssetCheckPartitionStatus => { + return partitionStatusMap.get(dimensionKey) || AssetCheckPartitionStatus.MISSING; + }; + + return { + assetKey, + checkName, + dimensions, + partitions, + statusForPartition, + }; +} + +export const ASSET_CHECK_PARTITION_HEALTH_QUERY = gql` + query AssetCheckPartitionHealthQuery($assetKey: AssetKeyInput!, $checkName: String!) { + assetNodeOrError(assetKey: $assetKey) { + ... on AssetNode { + id + assetCheckOrError(checkName: $checkName) { + ... on AssetCheck { + name + partitionKeysByDimension { + name + partitionKeys + } + partitionStatuses { + missing + succeeded + failed + inProgress + skipped + executionFailed + } + } + } + } + } + } +`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/useAssetCheckPartitionDetail.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/useAssetCheckPartitionDetail.tsx new file mode 100644 index 0000000000000..44cb862ab334a --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/asset-checks/useAssetCheckPartitionDetail.tsx @@ -0,0 +1,63 @@ +import {gql, useQuery} from '../../apollo-client'; +import {METADATA_ENTRY_FRAGMENT} from '../../metadata/MetadataEntryFragment'; +import {AssetKey} from '../types'; +import { + AssetCheckPartitionDetailQuery, + AssetCheckPartitionDetailQueryVariables, +} from './types/useAssetCheckPartitionDetail.types'; + +export function useAssetCheckPartitionDetail( + assetKey: AssetKey | null, + checkName: string | null, + partitionKey: string | null, +) { + const queryResult = useQuery< + AssetCheckPartitionDetailQuery, + AssetCheckPartitionDetailQueryVariables + >(ASSET_CHECK_PARTITION_DETAIL_QUERY, { + variables: { + assetKey: assetKey ? {path: assetKey.path} : {path: []}, + checkName: checkName || '', + partitionKey: partitionKey || '', + }, + skip: !assetKey || !checkName || !partitionKey, + }); + + return queryResult; +} + +export const ASSET_CHECK_PARTITION_DETAIL_QUERY = gql` + query AssetCheckPartitionDetailQuery( + $assetKey: AssetKeyInput! + $checkName: String! + $partitionKey: String! + ) { + assetCheckExecutions( + assetKey: $assetKey + checkName: $checkName + partition: $partitionKey + limit: 10 + ) { + id + runId + status + timestamp + stepKey + evaluation { + timestamp + severity + description + success + partition + targetMaterialization { + timestamp + runId + } + metadataEntries { + ...MetadataEntryFragment + } + } + } + } + ${METADATA_ENTRY_FRAGMENT} +`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/LogsRow.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/LogsRow.tsx index abfb0eabb2d70..5d6d20d4c2442 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/LogsRow.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/LogsRow.tsx @@ -243,6 +243,7 @@ export const LOGS_ROW_STRUCTURED_FRAGMENT = gql` metadataEntries { ...MetadataEntryFragment } + partition } } } diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsProvider.types.ts b/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsProvider.types.ts index 8bc94cb57fadb..7dc030828e60c 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsProvider.types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsProvider.types.ts @@ -60,6 +60,7 @@ export type PipelineRunLogsSubscription = { checkName: string; success: boolean; timestamp: number; + partition: string | null; assetKey: {__typename: 'AssetKey'; path: Array}; targetMaterialization: { __typename: 'AssetCheckEvaluationTargetMaterializationData'; @@ -3915,6 +3916,7 @@ export type RunLogsSubscriptionSuccessFragment = { checkName: string; success: boolean; timestamp: number; + partition: string | null; assetKey: {__typename: 'AssetKey'; path: Array}; targetMaterialization: { __typename: 'AssetCheckEvaluationTargetMaterializationData'; @@ -7705,6 +7707,7 @@ export type RunLogsQuery = { checkName: string; success: boolean; timestamp: number; + partition: string | null; assetKey: {__typename: 'AssetKey'; path: Array}; targetMaterialization: { __typename: 'AssetCheckEvaluationTargetMaterializationData'; @@ -11517,6 +11520,6 @@ export type RunLogsQuery = { | {__typename: 'RunNotFoundError'}; }; -export const PipelineRunLogsSubscriptionVersion = '929d969805de91b8890960433258984bbd32d2c08dbca55583da64c309fa3127'; +export const PipelineRunLogsSubscriptionVersion = '1fef76dfc3116df896a129a3e98e3071ced9a72b302b9e551dbcd5b28e7037cd'; -export const RunLogsQueryVersion = '811ef63a737599640cc1677f4a6c01c5193e5651e0b641680847abe613296deb'; +export const RunLogsQueryVersion = 'f81b9500cc7636e8bc477dbef44c955a449171ed951c2ffabd55c00f36aea8af'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsRow.types.ts b/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsRow.types.ts index 3ded08f9f5910..096afc7f153f0 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsRow.types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsRow.types.ts @@ -41,6 +41,7 @@ export type LogsRowStructuredFragment_AssetCheckEvaluationEvent = { checkName: string; success: boolean; timestamp: number; + partition: string | null; assetKey: {__typename: 'AssetKey'; path: Array}; targetMaterialization: { __typename: 'AssetCheckEvaluationTargetMaterializationData'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsScrollingTableMessageFragment.types.ts b/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsScrollingTableMessageFragment.types.ts index 1b46382c9c9af..8d86b475dc75b 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsScrollingTableMessageFragment.types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/types/LogsScrollingTableMessageFragment.types.ts @@ -41,6 +41,7 @@ export type LogsScrollingTableMessageFragment_AssetCheckEvaluationEvent = { checkName: string; success: boolean; timestamp: number; + partition: string | null; assetKey: {__typename: 'AssetKey'; path: Array}; targetMaterialization: { __typename: 'AssetCheckEvaluationTargetMaterializationData'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/types/RunFragments.types.ts b/js_modules/dagster-ui/packages/ui-core/src/runs/types/RunFragments.types.ts index 8ace6b46c7044..537e7ecfb655e 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/types/RunFragments.types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/types/RunFragments.types.ts @@ -92,6 +92,7 @@ export type RunDagsterRunEventFragment_AssetCheckEvaluationEvent = { checkName: string; success: boolean; timestamp: number; + partition: string | null; assetKey: {__typename: 'AssetKey'; path: Array}; targetMaterialization: { __typename: 'AssetCheckEvaluationTargetMaterializationData';