Skip to content

Commit

Permalink
[rfc] Add back "materialize changed and missing" as a dialog (#17115)
Browse files Browse the repository at this point in the history
## Summary & Motivation

We no longer fetch all of the asset status information in the graph up
front so we can't show a number on the option, instead we will lazily
fetch the stale status on click in a dialog:

<img width="300" alt="Screenshot 2023-10-10 at 10 11 42 AM"
src="https://github.com/dagster-io/dagster/assets/2286579/06a7e2c0-1115-4a45-a595-a3ae71035163">


<img width="606" alt="Screenshot 2023-10-10 at 9 00 10 AM"
src="https://github.com/dagster-io/dagster/assets/2286579/bd3091bc-597d-4f60-8cd5-972a954832b6">

<img width="545" alt="Screenshot 2023-10-10 at 10 19 22 AM"
src="https://github.com/dagster-io/dagster/assets/2286579/638c7550-a96c-4f2f-8f49-1ec8c4b6fc78">



<img width="560" alt="Screenshot 2023-10-10 at 8 50 23 AM"
src="https://github.com/dagster-io/dagster/assets/2286579/070016cd-cdb4-4819-9780-78de49fb3229">


## How I Tested These Changes

Locally
  • Loading branch information
salazarm authored Oct 10, 2023
1 parent 258d9ca commit ffda9b1
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import * as React from 'react';
import styled from 'styled-components';

import {GraphData, LiveData} from '../asset-graph/Utils';
import {GraphData} from '../asset-graph/Utils';
import {AssetGraphQueryItem, calculateGraphDistances} from '../asset-graph/useAssetGraphData';
import {AssetKeyInput} from '../graphql/types';

Expand All @@ -23,18 +23,9 @@ export const AssetNodeLineage: React.FC<{
setParams: (params: AssetViewParams) => void;
assetKey: AssetKeyInput;
assetGraphData: GraphData;
liveDataByNode: LiveData;
requestedDepth: number;
graphQueryItems: AssetGraphQueryItem[];
}> = ({
params,
setParams,
assetKey,
liveDataByNode,
assetGraphData,
graphQueryItems,
requestedDepth,
}) => {
}> = ({params, setParams, assetKey, assetGraphData, graphQueryItems, requestedDepth}) => {
const maxDistances = React.useMemo(
() => calculateGraphDistances(graphQueryItems, assetKey),
[graphQueryItems, assetKey],
Expand Down Expand Up @@ -76,7 +67,6 @@ export const AssetNodeLineage: React.FC<{
{Object.values(assetGraphData.nodes).length > 1 ? (
<LaunchAssetExecutionButton
intent="none"
liveDataForStale={liveDataByNode}
scope={{all: Object.values(assetGraphData.nodes).map((n) => n.definition)}}
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ export const AssetView = ({assetKey}: Props) => {
params={params}
setParams={setParams}
assetKey={assetKey}
liveDataByNode={liveDataByNode}
requestedDepth={visible.requestedDepth}
assetGraphData={visibleAssetGraph.assetGraphData}
graphQueryItems={visibleAssetGraph.graphQueryItems}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import {useQuery, gql} from '@apollo/client';
import {
Spinner,
Dialog,
DialogBody,
DialogFooter,
Button,
Box,
Icon,
Colors,
Checkbox,
MiddleTruncate,
} from '@dagster-io/ui-components';
import {useVirtualizer} from '@tanstack/react-virtual';
import React from 'react';
import {Link} from 'react-router-dom';
import styled from 'styled-components';

import {showCustomAlert} from '../app/CustomAlertProvider';
import {displayNameForAssetKey} from '../asset-graph/Utils';
import {Container, Inner, Row} from '../ui/VirtualizedTable';

import {isAssetStale, isAssetMissing} from './Stale';
import {asAssetKeyInput} from './asInput';
import {assetDetailsPathForKey} from './assetDetailsPathForKey';
import {AssetKey} from './types';
import {
AssetStaleStatusQuery,
AssetStaleStatusQueryVariables,
} from './types/CalculateChangedAndMissingDialog.types';

export const CalculateChangedAndMissingDialog = React.memo(
({
isOpen,
onClose,
assets,
onMaterializeAssets,
}: {
isOpen: boolean;
assets: {
assetKey: AssetKey;
}[];
onClose: () => void;
onMaterializeAssets: (assets: AssetKey[], e: React.MouseEvent<any>) => void;
}) => {
const {data, loading, error} = useQuery<AssetStaleStatusQuery, AssetStaleStatusQueryVariables>(
ASSET_STALE_STATUS_QUERY,
{
variables: {
assetKeys: assets.map(asAssetKeyInput),
},
skip: !isOpen,
},
);

const staleOrMissing = React.useMemo(
() =>
data?.assetNodes
.filter((node) => isAssetStale(node) || isAssetMissing(node))
.map(asAssetKeyInput),
[data],
);

React.useEffect(() => {
if (isOpen && !loading && (!data || error)) {
showCustomAlert({
title: 'Could not fetch stale status for assets',
body: 'An unknown error occurred.',
});
onClose();
}
}, [data, error, isOpen, loading, onClose]);

const containerRef = React.useRef<HTMLDivElement | null>(null);
const virtualizer = useVirtualizer({
count: staleOrMissing?.length ?? 0,
getScrollElement: () => containerRef.current,
estimateSize: () => 28,
});
const totalHeight = virtualizer.getTotalSize();
const items = virtualizer.getVirtualItems();

const [checked, setChecked] = React.useState<Set<AssetKey>>(new Set());
React.useLayoutEffect(() => {
setChecked(new Set(staleOrMissing));
}, [staleOrMissing]);

const content = () => {
if (!isOpen) {
return null;
}
if (loading) {
return (
<Box flex={{alignItems: 'center', gap: 8}}>
<Spinner purpose="body-text" /> Fetching asset statuses
</Box>
);
}
if (staleOrMissing?.length) {
return (
<>
<RowGrid border="bottom" padding={{bottom: 8}}>
<Checkbox
id="check-all"
checked={checked.size === staleOrMissing.length}
onChange={() => {
setChecked((checked) => {
if (checked.size === staleOrMissing.length) {
return new Set();
} else {
return new Set(staleOrMissing);
}
});
}}
/>
<label htmlFor="check-all" style={{color: Colors.Gray500, cursor: 'pointer'}}>
Asset Name
</label>
</RowGrid>
<Container ref={containerRef} style={{maxHeight: '400px'}}>
<Inner $totalHeight={totalHeight}>
{items.map(({index, key, size, start, measureElement}) => {
const item = staleOrMissing[index]!;
return (
<Row $height={size} $start={start} key={key} ref={measureElement}>
<RowGrid border="bottom">
<Checkbox
id={`checkbox-${key}`}
checked={checked.has(item)}
onChange={() => {
setChecked((checked) => {
const copy = new Set(checked);
if (copy.has(item)) {
copy.delete(item);
} else {
copy.add(item);
}
return copy;
});
}}
/>
<Box
as="label"
htmlFor={`checkbox-${key}`}
flex={{alignItems: 'center', gap: 4}}
style={{cursor: 'pointer'}}
>
<Box flex={{shrink: 1}}>
<MiddleTruncate text={displayNameForAssetKey(item)} />
</Box>
<Link to={assetDetailsPathForKey(item)} target="_blank">
<Icon name="open_in_new" color={Colors.Link} />
</Link>
</Box>
</RowGrid>
</Row>
);
})}
</Inner>
</Container>
</>
);
}
return (
<Box flex={{alignItems: 'center', gap: 8}}>
<Icon name="check_circle" color={Colors.Green500} />
<div>All assets are up to date</div>
</Box>
);
};
return (
<>
<Dialog isOpen={isOpen} onClose={onClose} title="Materialize changed and missing assets">
<DialogBody>{content()}</DialogBody>
<DialogFooter topBorder>
{loading ? (
<Button onClick={onClose}>Cancel</Button>
) : staleOrMissing?.length ? (
<Button
intent="primary"
onClick={(e) => {
onMaterializeAssets(Array.from(checked), e);
onClose();
}}
disabled={checked.size === 0}
>
Materialize {checked.size} assets
</Button>
) : (
<Button onClick={onClose}>Dismiss</Button>
)}
</DialogFooter>
</Dialog>
</>
);
},
);

const ASSET_STALE_STATUS_QUERY = gql`
query AssetStaleStatusQuery($assetKeys: [AssetKeyInput!]!) {
assetNodes(assetKeys: $assetKeys) {
id
assetKey {
path
}
staleStatus
}
}
`;

const TEMPLATE_COLUMNS = '20px minmax(0, 1fr)';

const RowGrid = styled(Box)`
display: grid;
grid-template-columns: ${TEMPLATE_COLUMNS};
gap: 8px;
height: 100%;
align-items: center;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ import {IExecutionSession} from '../app/ExecutionSessionStorage';
import {
displayNameForAssetKey,
isHiddenAssetGroupJob,
LiveData,
sortAssetKeys,
toGraphId,
tokenForAssetKey,
} from '../asset-graph/Utils';
import {useLaunchPadHooks} from '../launchpad/LaunchpadHooksContext';
Expand All @@ -35,9 +33,9 @@ import {RepoAddress} from '../workspace/types';

import {ASSET_NODE_CONFIG_FRAGMENT} from './AssetConfig';
import {MULTIPLE_DEFINITIONS_WARNING} from './AssetDefinedInMultipleReposNotice';
import {CalculateChangedAndMissingDialog} from './CalculateChangedAndMissingDialog';
import {LaunchAssetChoosePartitionsDialog} from './LaunchAssetChoosePartitionsDialog';
import {partitionDefinitionsEqual} from './MultipartitioningSupport';
import {isAssetMissing, isAssetStale} from './Stale';
import {asAssetKeyInput, getAssetCheckHandleInputs} from './asInput';
import {AssetKey} from './types';
import {
Expand Down Expand Up @@ -115,7 +113,7 @@ export const ERROR_INVALID_ASSET_SELECTION =
` the same code location and share a partition space, or form a connected` +
` graph in which root assets share the same partitioning.`;

function optionsForButton(scope: AssetsInScope, liveDataForStale?: LiveData): LaunchOption[] {
function optionsForButton(scope: AssetsInScope): LaunchOption[] {
// If you pass a set of selected assets, we always show just one option
// to materialize that selection.
if ('selected' in scope) {
Expand All @@ -142,20 +140,6 @@ function optionsForButton(scope: AssetsInScope, liveDataForStale?: LiveData): La
: `Materialize${isAnyPartitioned(executable) ? '…' : ''}`,
});

if (liveDataForStale) {
const missingOrStale = executable.filter(
(a) =>
isAssetMissing(liveDataForStale[toGraphId(a.assetKey)]) ||
isAssetStale(liveDataForStale[toGraphId(a.assetKey)]),
);

options.push({
assetKeys: missingOrStale.map((a) => a.assetKey),
label: `Materialize changed and missing${countOrBlank(missingOrStale)}`,
icon: <Icon name="changes_present" />,
});
}

return options;
}

Expand All @@ -173,27 +157,23 @@ export function executionDisabledMessageForAssets(

export const LaunchAssetExecutionButton: React.FC<{
scope: AssetsInScope;
liveDataForStale?: LiveData; // For "stale" dropdown options
intent?: 'primary' | 'none';
preferredJobName?: string;
additionalDropdownOptions?: {
label: string;
icon?: JSX.Element;
onClick: () => void;
}[];
}> = ({
scope,
liveDataForStale,
preferredJobName,
additionalDropdownOptions,
intent = 'primary',
}) => {
}> = ({scope, preferredJobName, additionalDropdownOptions, intent = 'primary'}) => {
const {onClick, loading, launchpadElement} = useMaterializationAction(preferredJobName);
const [isOpen, setIsOpen] = React.useState(false);

const {MaterializeButton} = useLaunchPadHooks();

const options = optionsForButton(scope, liveDataForStale);
const [showCalculatingChangedAndMissingDialog, setShowCalculatingChangedAndMissingDialog] =
React.useState<boolean>(false);

const options = optionsForButton(scope);
const firstOption = options[0]!;
if (!firstOption) {
return <span />;
Expand All @@ -219,6 +199,16 @@ export const LaunchAssetExecutionButton: React.FC<{

return (
<>
<CalculateChangedAndMissingDialog
isOpen={!!showCalculatingChangedAndMissingDialog}
onClose={() => {
setShowCalculatingChangedAndMissingDialog(false);
}}
assets={inScope}
onMaterializeAssets={(assets: AssetKey[], e: React.MouseEvent<any>) => {
onClick(assets, e);
}}
/>
<Box flex={{alignItems: 'center'}}>
<Tooltip
content="Shift+click to add configuration"
Expand Down Expand Up @@ -256,6 +246,13 @@ export const LaunchAssetExecutionButton: React.FC<{
onClick={(e) => onClick(option.assetKeys, e)}
/>
))}
{inScope.length && 'all' in scope ? (
<MenuItem
text="Materialize changed and missing"
icon="changes_present"
onClick={() => setShowCalculatingChangedAndMissingDialog(true)}
/>
) : null}
<MenuItem
text="Open launchpad"
icon={<Icon name="open_in_new" />}
Expand Down
4 changes: 2 additions & 2 deletions js_modules/dagster-ui/packages/ui-core/src/assets/Stale.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import {assetDetailsPathForKey} from './assetDetailsPathForKey';

type StaleDataForNode = Pick<LiveDataForNode, 'staleCauses' | 'staleStatus'>;

export const isAssetMissing = (liveData?: StaleDataForNode) =>
export const isAssetMissing = (liveData?: Pick<StaleDataForNode, 'staleStatus'>) =>
liveData && liveData.staleStatus === StaleStatus.MISSING;

export const isAssetStale = (liveData?: StaleDataForNode) =>
export const isAssetStale = (liveData?: Pick<StaleDataForNode, 'staleStatus'>) =>
liveData && liveData.staleStatus === StaleStatus.STALE;

const LABELS = {
Expand Down
Loading

1 comment on commit ffda9b1

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

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

Please sign in to comment.