Skip to content

Commit

Permalink
[ui] Open evaluation detail dialog from automaterialize tag (#26963)
Browse files Browse the repository at this point in the history
## Summary & Motivation

Fix "View evaluation" links from `AutomaterializeTagWithEvaluation`, which shows asset keys and evaluation links in a popover, e.g. on a Run. When clicking the link, open the evaluation detail dialog.

Additionally:

- Added the asset (or asset check) information in a tag in the dialog header, because otherwise it's not clear what asset you're looking at.
- Added a footer button to link to the asset's Automation tab, cursored to the currently viewed evaluation ID. This is to allow a look at recent history.

## How I Tested These Changes

View a run that has this tag. Verify that the popover link opens the dialog correctly, and that the asset tag renders in the header.

## Changelog

[ui] Open the evaluation detail dialog from the "Automation condition" tag popover on Runs.
  • Loading branch information
hellendag authored Jan 9, 2025
1 parent 7eeca80 commit 9a8a7e9
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import {
Button,
Dialog,
DialogFooter,
DialogHeader,
Icon,
Mono,
NonIdealState,
SpinnerWithText,
Tag,
} from '@dagster-io/ui-components';
import {ReactNode, useState} from 'react';
import {ReactNode, useMemo, useState} from 'react';

import {GET_SLIM_EVALUATIONS_QUERY} from './GetEvaluationsQuery';
import {PartitionTagSelector} from './PartitionTagSelector';
Expand All @@ -21,6 +22,7 @@ import {usePartitionsForAssetKey} from './usePartitionsForAssetKey';
import {useQuery} from '../../apollo-client';
import {DEFAULT_TIME_FORMAT} from '../../app/time/TimestampFormat';
import {TimestampDisplay} from '../../schedules/TimestampDisplay';
import {AnchorButton} from '../../ui/AnchorButton';

interface Props {
isOpen: boolean;
Expand Down Expand Up @@ -81,10 +83,24 @@ const EvaluationDetailDialogContents = ({
const {partitions: allPartitions, loading: partitionsLoading} =
usePartitionsForAssetKey(assetKeyPath);

const viewAllPath = useMemo(() => {
// todo dish: I don't think the asset check evaluations list is permalinkable yet.
if (assetCheckName) {
return null;
}

const queryString = new URLSearchParams({
view: 'automation',
evaluation: evaluationID,
}).toString();

return `/assets/${assetKeyPath.join('/')}?${queryString}`;
}, [assetCheckName, evaluationID, assetKeyPath]);

if (loading || partitionsLoading) {
return (
<DialogContents
header={<DialogHeader icon="automation" label="Evaluation details" />}
header={<DialogHeader assetKeyPath={assetKeyPath} assetCheckName={assetCheckName} />}
body={
<Box padding={{top: 64}} flex={{direction: 'row', justifyContent: 'center'}}>
<SpinnerWithText label="Loading evaluation details..." />
Expand All @@ -100,7 +116,7 @@ const EvaluationDetailDialogContents = ({
if (record?.__typename === 'AutoMaterializeAssetEvaluationNeedsMigrationError') {
return (
<DialogContents
header={<DialogHeader icon="automation" label="Evaluation details" />}
header={<DialogHeader assetKeyPath={assetKeyPath} assetCheckName={assetCheckName} />}
body={
<Box margin={{top: 64}}>
<NonIdealState
Expand All @@ -120,7 +136,7 @@ const EvaluationDetailDialogContents = ({
if (!evaluation) {
return (
<DialogContents
header={<DialogHeader icon="automation" label="Evaluation details" />}
header={<DialogHeader assetKeyPath={assetKeyPath} assetCheckName={assetCheckName} />}
body={
<Box margin={{top: 64}}>
<NonIdealState
Expand All @@ -144,16 +160,9 @@ const EvaluationDetailDialogContents = ({
header={
<>
<DialogHeader
icon="automation"
label={
<div>
Evaluation details:{' '}
<TimestampDisplay
timestamp={evaluation.timestamp}
timeFormat={{...DEFAULT_TIME_FORMAT, showSeconds: true}}
/>
</div>
}
assetKeyPath={assetKeyPath}
assetCheckName={assetCheckName}
timestamp={evaluation.timestamp}
/>
{allPartitions.length > 0 && evaluation.isLegacy ? (
<Box padding={{vertical: 12, right: 20}} flex={{justifyContent: 'flex-end'}}>
Expand All @@ -174,25 +183,76 @@ const EvaluationDetailDialogContents = ({
setSelectedPartition={setSelectedPartition}
/>
}
viewAllButton={
viewAllPath ? (
<AnchorButton to={viewAllPath} icon={<Icon name="automation_condition" />}>
View evaluations for this asset
</AnchorButton>
) : null
}
onDone={onClose}
/>
);
};

const DialogHeader = ({
assetKeyPath,
assetCheckName,
timestamp,
}: {
assetKeyPath: string[];
assetCheckName?: string;
timestamp?: number;
}) => {
const assetKeyPathString = assetKeyPath.join('/');
const assetDetailsTag = assetCheckName ? (
<Tag icon="asset_check">
{assetCheckName} on {assetKeyPathString}
</Tag>
) : (
<Tag icon="asset">{assetKeyPathString}</Tag>
);

const timestampDisplay = timestamp ? (
<TimestampDisplay
timestamp={timestamp}
timeFormat={{...DEFAULT_TIME_FORMAT, showSeconds: true}}
/>
) : null;

return (
<Box
padding={{vertical: 16, horizontal: 20}}
flex={{direction: 'row', alignItems: 'center', justifyContent: 'space-between'}}
border="bottom"
>
<Box flex={{direction: 'row', alignItems: 'center', gap: 8}}>
<Icon name="automation" />
<strong>
<span>Evaluation details</span>
{timestampDisplay ? <span>: {timestampDisplay}</span> : ''}
</strong>
</Box>
{assetDetailsTag}
</Box>
);
};

interface BasicContentProps {
header: ReactNode;
body: ReactNode;
viewAllButton?: ReactNode;
onDone: () => void;
}

// Dialog contents for which the body container is scrollable and expands to fill the height.
const DialogContents = ({header, body, onDone}: BasicContentProps) => {
const DialogContents = ({header, body, onDone, viewAllButton}: BasicContentProps) => {
return (
<Box flex={{direction: 'column'}} style={{height: '100%'}}>
{header}
<div style={{flex: 1, overflowY: 'auto'}}>{body}</div>
<div style={{flexGrow: 0}}>
<DialogFooter topBorder>
<DialogFooter topBorder left={viewAllButton}>
<Button onClick={onDone}>Done</Button>
</DialogFooter>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,71 +1,89 @@
import {Box, Icon, MiddleTruncate, Popover, Tag} from '@dagster-io/ui-components';
import {useMemo} from 'react';
import {Link} from 'react-router-dom';
import {Box, ButtonLink, Icon, MiddleTruncate, Popover, Tag} from '@dagster-io/ui-components';
import {useMemo, useState} from 'react';

import {EvaluationDetailDialog} from './AutoMaterializePolicyPage/EvaluationDetailDialog';
import {assetDetailsPathForKey} from './assetDetailsPathForKey';
import {AssetKey} from './types';

const COLLATOR = new Intl.Collator(navigator.language, {sensitivity: 'base'});

type OpenEvaluation = {
assetKeyPath: string[];
evaluationId: string;
};

interface Props {
assetKeys: AssetKey[];
evaluationId: string;
}

export const AutomaterializeTagWithEvaluation = ({assetKeys, evaluationId}: Props) => {
const [openEvaluation, setOpenEvaluation] = useState<OpenEvaluation | null>(null);

const sortedKeys = useMemo(() => {
return [...assetKeys].sort((a, b) => COLLATOR.compare(a.path.join('/'), b.path.join('/')));
}, [assetKeys]);

return (
<Popover
placement="bottom"
content={
<div style={{width: '340px'}}>
<Box padding={{vertical: 8, horizontal: 12}} border="bottom" style={{fontWeight: 600}}>
Automation condition
</Box>
<Box
flex={{direction: 'column', gap: 12}}
padding={{vertical: 12}}
style={{maxHeight: '220px', overflowY: 'auto'}}
>
{sortedKeys.map((assetKey) => {
const url = assetDetailsPathForKey(assetKey, {
view: 'automation',
evaluation: evaluationId,
});
return (
<Box
key={url}
padding={{vertical: 8, left: 12, right: 16}}
flex={{
direction: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8,
}}
style={{overflow: 'hidden'}}
>
<>
<Popover
placement="bottom"
content={
<div style={{width: '400px'}}>
<Box padding={{vertical: 8, horizontal: 12}} border="bottom" style={{fontWeight: 600}}>
Automation condition
</Box>
<Box
flex={{direction: 'column', gap: 16}}
padding={{vertical: 12}}
style={{maxHeight: '220px', overflowY: 'auto'}}
>
{sortedKeys.map((assetKey) => {
const url = assetDetailsPathForKey(assetKey, {
view: 'automation',
evaluation: evaluationId,
});
return (
<Box
flex={{direction: 'row', alignItems: 'center', gap: 8}}
key={url}
padding={{vertical: 8, left: 12, right: 16}}
flex={{
direction: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 16,
}}
style={{overflow: 'hidden'}}
>
<Icon name="asset" />
<MiddleTruncate text={assetKey.path.join('/')} />
<Box
flex={{direction: 'row', alignItems: 'center', gap: 8}}
style={{overflow: 'hidden'}}
>
<Icon name="asset" />
<MiddleTruncate text={assetKey.path.join('/')} />
</Box>
<ButtonLink
onClick={() => setOpenEvaluation({assetKeyPath: assetKey.path, evaluationId})}
style={{whiteSpace: 'nowrap'}}
>
View evaluation
</ButtonLink>
</Box>
<Link to={url} style={{whiteSpace: 'nowrap'}}>
View evaluation
</Link>
</Box>
);
})}
</Box>
</div>
}
interactionKind="hover"
>
<Tag icon="automation_condition">Automation condition</Tag>
</Popover>
);
})}
</Box>
</div>
}
interactionKind="hover"
>
<Tag icon="automation_condition">Automation condition</Tag>
</Popover>
<EvaluationDetailDialog
assetKeyPath={openEvaluation?.assetKeyPath ?? []}
isOpen={!!openEvaluation}
onClose={() => setOpenEvaluation(null)}
evaluationID={openEvaluation?.evaluationId ?? ''}
/>
</>
);
};

1 comment on commit 9a8a7e9

@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-j2cm30qdn-elementl.vercel.app

Built with commit 9a8a7e9.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.