From 0474dbd7ec7764e38f67bdbd7b0ca892cff3ff32 Mon Sep 17 00:00:00 2001 From: bengotow Date: Mon, 9 Oct 2023 11:08:42 -0500 Subject: [PATCH] [ui] Support for manually reporting materialization events from asset details --- .../ui-core/src/assets/AssetEventList.tsx | 6 +- .../packages/ui-core/src/assets/AssetView.tsx | 18 +- .../LaunchAssetChoosePartitionsDialog.tsx | 37 +-- .../src/assets/LaunchAssetExecutionButton.tsx | 21 +- .../types/useReportEventsModal.types.ts | 27 ++ .../src/assets/useReportEventsModal.tsx | 259 ++++++++++++++++++ .../ui-core/src/ui/ToggleableSection.tsx | 39 +++ 7 files changed, 367 insertions(+), 40 deletions(-) create mode 100644 js_modules/dagster-ui/packages/ui-core/src/assets/types/useReportEventsModal.types.ts create mode 100644 js_modules/dagster-ui/packages/ui-core/src/assets/useReportEventsModal.tsx create mode 100644 js_modules/dagster-ui/packages/ui-core/src/ui/ToggleableSection.tsx diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetEventList.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetEventList.tsx index 2d8933a620580..56b6d0ddeaba3 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetEventList.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetEventList.tsx @@ -149,7 +149,7 @@ const AssetEventListEventRow: React.FC<{group: AssetEventGroup}> = ({group}) => {partition && {partition}} - {latest && run && ( + {latest && run ? ( = ({group}) => - )} + ) : latest && latest.runId === '' ? ( + Reported + ) : undefined} ); diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetView.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetView.tsx index 3c215683d24e8..214882dbad11f 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetView.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetView.tsx @@ -49,6 +49,7 @@ import { AssetViewDefinitionNodeFragment, } from './types/AssetView.types'; import {healthRefreshHintFromLiveData} from './usePartitionHealthData'; +import {useReportEventsModal} from './useReportEventsModal'; interface Props { assetKey: AssetKey; @@ -239,6 +240,17 @@ export const AssetView = ({assetKey}: Props) => { } }; + const reportEvents = useReportEventsModal( + definition + ? { + assetKey: definition.assetKey, + isPartitioned: definition.isPartitioned, + repository: definition.repository, + } + : null, + refreshState.refetch, + ); + return ( { scope={{all: [definition], skipAllTerm: true}} /> ) : definition && definition.jobNames.length > 0 && upstream ? ( - + ) : undefined} + {reportEvents.element} } /> diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetChoosePartitionsDialog.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetChoosePartitionsDialog.tsx index 5e7b9738f3dc4..e14c6513399cc 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetChoosePartitionsDialog.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetChoosePartitionsDialog.tsx @@ -56,6 +56,7 @@ import {DimensionRangeWizard} from '../partitions/DimensionRangeWizard'; import {assembleIntoSpans, stringForSpan} from '../partitions/SpanRepresentation'; import {DagsterTag} from '../runs/RunTag'; import {testId} from '../testing/testId'; +import {ToggleableSection} from '../ui/ToggleableSection'; import {useFeatureFlagForCodeLocation} from '../workspace/WorkspaceContext'; import {RepoAddress} from '../workspace/types'; @@ -868,39 +869,3 @@ const Warnings: React.FC<{ ); }; - -const ToggleableSection = ({ - isInitiallyOpen, - title, - children, - background, -}: { - isInitiallyOpen: boolean; - title: React.ReactNode; - children: React.ReactNode; - background?: string; -}) => { - const [isOpen, setIsOpen] = React.useState(isInitiallyOpen); - return ( - - setIsOpen(!isOpen)} - background={background ?? Colors.Gray50} - border="bottom" - flex={{alignItems: 'center', direction: 'row'}} - padding={{vertical: 12, horizontal: 24}} - style={{cursor: 'pointer'}} - > - - - -
{title}
-
- {isOpen && {children}} -
- ); -}; - -const Rotateable = styled.span<{$rotate: boolean}>` - ${({$rotate}) => ($rotate ? 'transform: rotate(-90deg);' : '')} -`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetExecutionButton.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetExecutionButton.tsx index 3c56cad640fe0..2d7dcc33b7b9b 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetExecutionButton.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetExecutionButton.tsx @@ -175,7 +175,18 @@ export const LaunchAssetExecutionButton: React.FC<{ liveDataForStale?: LiveData; // For "stale" dropdown options intent?: 'primary' | 'none'; preferredJobName?: string; -}> = ({scope, liveDataForStale, preferredJobName, intent = 'primary'}) => { + additionalDropdownOptions?: { + label: string; + icon?: JSX.Element; + onClick: () => void; + }[]; +}> = ({ + scope, + liveDataForStale, + preferredJobName, + additionalDropdownOptions, + intent = 'primary', +}) => { const {onClick, loading, launchpadElement} = useMaterializationAction(preferredJobName); const [isOpen, setIsOpen] = React.useState(false); @@ -251,6 +262,14 @@ export const LaunchAssetExecutionButton: React.FC<{ onClick(firstOption.assetKeys, e, true); }} /> + {additionalDropdownOptions?.map((option) => ( + + ))} } > diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/types/useReportEventsModal.types.ts b/js_modules/dagster-ui/packages/ui-core/src/assets/types/useReportEventsModal.types.ts new file mode 100644 index 0000000000000..565511aeb7beb --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/types/useReportEventsModal.types.ts @@ -0,0 +1,27 @@ +// Generated GraphQL types, do not edit manually. + +import * as Types from '../../graphql/types'; + +export type ReportEventMutationVariables = Types.Exact<{ + eventParams: Types.ReportRunlessAssetEventsParams; +}>; + +export type ReportEventMutation = { + __typename: 'Mutation'; + reportRunlessAssetEvents: + | { + __typename: 'PythonError'; + message: string; + stack: Array; + errorChain: Array<{ + __typename: 'ErrorChainLink'; + isExplicitLink: boolean; + error: {__typename: 'PythonError'; message: string; stack: Array}; + }>; + } + | { + __typename: 'ReportRunlessAssetEventsSuccess'; + assetKey: {__typename: 'AssetKey'; path: Array}; + } + | {__typename: 'UnauthorizedError'; message: string}; +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/useReportEventsModal.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/useReportEventsModal.tsx new file mode 100644 index 0000000000000..184ec36939cb4 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/useReportEventsModal.tsx @@ -0,0 +1,259 @@ +import {gql, useMutation} from '@apollo/client'; +import { + Body2, + Box, + Button, + Caption, + Dialog, + DialogFooter, + DialogHeader, + Icon, + Subheading, + TextInput, +} from '@dagster-io/ui-components'; +import React from 'react'; + +import {showCustomAlert} from '../app/CustomAlertProvider'; +import {showSharedToaster} from '../app/DomUtils'; +import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorFragment'; +import {PythonErrorInfo} from '../app/PythonErrorInfo'; +import {AssetEventType, AssetKeyInput, PartitionDefinitionType} from '../graphql/types'; +import {DimensionRangeWizard} from '../partitions/DimensionRangeWizard'; +import {ToggleableSection} from '../ui/ToggleableSection'; +import {buildRepoAddress} from '../workspace/buildRepoAddress'; +import {RepoAddress} from '../workspace/types'; + +import {partitionCountString} from './AssetNodePartitionCounts'; +import { + explodePartitionKeysInSelectionMatching, + mergedAssetHealth, +} from './MultipartitioningSupport'; +import { + ReportEventMutation, + ReportEventMutationVariables, +} from './types/useReportEventsModal.types'; +import {usePartitionDimensionSelections} from './usePartitionDimensionSelections'; +import {keyCountInSelections, usePartitionHealthData} from './usePartitionHealthData'; + +type Asset = { + isPartitioned: boolean; + assetKey: AssetKeyInput; + repository: {name: string; location: {name: string}}; +}; + +export function useReportEventsModal(asset: Asset | null, onEventReported: () => void) { + const [recordEventOpen, setRecordEventOpen] = React.useState(false); + const dropdownOptions = React.useMemo( + () => [ + { + label: 'Record materialization event', + icon: , + onClick: () => setRecordEventOpen(true), + }, + ], + [], + ); + + const element = asset ? ( + + ) : undefined; + return { + dropdownOptions, + element, + }; +} + +const ReportEventDialogBody: React.FC<{ + asset: Asset; + repoAddress: RepoAddress; + isOpen: boolean; + setIsOpen: (open: boolean) => void; + onEventReported: () => void; +}> = ({asset, repoAddress, isOpen, setIsOpen, onEventReported}) => { + const [description, setDescription] = React.useState(''); + + const [mutation] = useMutation( + REPORT_EVENT_MUTATION, + ); + + const [lastRefresh, setLastRefresh] = React.useState(Date.now()); + const assetHealth = mergedAssetHealth( + usePartitionHealthData( + asset.isPartitioned ? [asset.assetKey] : [], + lastRefresh.toString(), + 'background', + ), + ); + const isDynamic = assetHealth.dimensions.some((d) => d.type === PartitionDefinitionType.DYNAMIC); + const [selections, setSelections] = usePartitionDimensionSelections({ + assetHealth, + modifyQueryString: false, + skipPartitionKeyValidation: isDynamic, + shouldReadPartitionQueryStringParam: true, + }); + + const keysFiltered = React.useMemo(() => { + return explodePartitionKeysInSelectionMatching(selections, () => true); + }, [selections]); + + const onReportEvent = async () => { + const result = await mutation({ + variables: { + eventParams: { + eventType: AssetEventType.ASSET_MATERIALIZATION, + partitionKeys: asset.isPartitioned ? keysFiltered : undefined, + assetKey: {path: asset.assetKey.path}, + description, + }, + }, + }); + const data = result.data?.reportRunlessAssetEvents; + + if (!data || data.__typename === 'PythonError') { + await showSharedToaster({ + message:
An unexpected error occurred. This event was not recorded.
, + icon: 'error', + intent: 'danger', + action: data + ? { + text: 'View error', + onClick: () => showCustomAlert({body: }), + } + : undefined, + }); + } else if (data.__typename === 'UnauthorizedError') { + await showSharedToaster({ + message:
{data.message}
, + icon: 'error', + intent: 'danger', + }); + } else { + await showSharedToaster({ + message: + keysFiltered.length > 1 ? ( +
Your events have been recorded.
+ ) : ( +
Your event has been recorded.
+ ), + icon: 'materialization', + intent: 'success', + }); + onEventReported(); + setIsOpen(false); + } + }; + + return ( + setIsOpen(false)} + > + + + + Let Dagster know about a materialization that happened outside of Dagster. This is + typically only needed in exceptional circumstances. + + + + {asset.isPartitioned ? ( + + Partition selection + {partitionCountString(keyCountInSelections(selections))} + + } + > + {selections.map((range, idx) => ( + + + + {range.dimension.name} + + + Select partitions to materialize.{' '} + {range.dimension.type === PartitionDefinitionType.TIME_WINDOW + ? 'Click and drag to select a range on the timeline.' + : null} + + + + setSelections((selections) => + selections.map((r) => + r.dimension === range.dimension ? {...r, selectedKeys} : r, + ), + ) + } + partitionDefinitionName={range.dimension.name} + repoAddress={repoAddress} + refetch={async () => setLastRefresh(Date.now())} + /> + + ))} + + ) : undefined} + + + + Description + setDescription(e.target.value)} + placeholder="Add a description" + /> + + + + + + + + ); +}; + +const REPORT_EVENT_MUTATION = gql` + mutation ReportEventMutation($eventParams: ReportRunlessAssetEventsParams!) { + reportRunlessAssetEvents(eventParams: $eventParams) { + ...PythonErrorFragment + ... on UnauthorizedError { + message + } + ... on ReportRunlessAssetEventsSuccess { + assetKey { + path + } + } + } + } + ${PYTHON_ERROR_FRAGMENT} +`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/ui/ToggleableSection.tsx b/js_modules/dagster-ui/packages/ui-core/src/ui/ToggleableSection.tsx new file mode 100644 index 0000000000000..874377d6c917d --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/ui/ToggleableSection.tsx @@ -0,0 +1,39 @@ +import {Box, Colors, Icon} from '@dagster-io/ui-components'; +import React from 'react'; +import styled from 'styled-components'; + +export const ToggleableSection = ({ + isInitiallyOpen, + title, + children, + background, +}: { + isInitiallyOpen: boolean; + title: React.ReactNode; + children: React.ReactNode; + background?: string; +}) => { + const [isOpen, setIsOpen] = React.useState(isInitiallyOpen); + return ( + + setIsOpen(!isOpen)} + background={background ?? Colors.Gray50} + border="bottom" + flex={{alignItems: 'center', direction: 'row'}} + padding={{vertical: 12, horizontal: 24}} + style={{cursor: 'pointer'}} + > + + + +
{title}
+
+ {isOpen && {children}} +
+ ); +}; + +const Rotateable = styled.span<{$rotate: boolean}>` + ${({$rotate}) => ($rotate ? 'transform: rotate(-90deg);' : '')} +`;