Skip to content

Commit

Permalink
[ui] Show one asset run tag or “4 assets”, hover action for lineage (#…
Browse files Browse the repository at this point in the history
…16449)

## Summary & Motivation

This PR changes the asset tags that appear in the run table and on the
run details page:

1) Rather than show 1-3 individual assets or a "6 assets" link in the
run table or run details header, we now show either a single asset or "6
assets". This means the rendering is more compact in the 2-3 asset case
since the tags are not rendered individually.

2) Instead of being directly clickable, hovering over the assets gives
you the option to jump to a lineage view OR open the list of assets. For
>1 asset, the downstream lineage option goes to the global asset graph
with the "assetA*, assetB*" query. It's actually pretty slick.

Right now in Dagit we don't have a "default behavior" for clicking tags
that have tag actions -- you have to choose an action. I could see
keeping the default "link" behavior we had before though...

Before:

![image](https://github.com/dagster-io/dagster/assets/1037212/e09c7998-d92a-496f-96b4-bcdd7d48c253)

![image](https://github.com/dagster-io/dagster/assets/1037212/ea86bac2-bf6a-4c03-84b6-3c134c01db5f)

![image](https://github.com/dagster-io/dagster/assets/1037212/44c711e0-dfef-4213-acaf-deff4f70b489)

After:

![image](https://github.com/dagster-io/dagster/assets/1037212/f81a4178-c3af-437a-bcba-1db760750244)

![image](https://github.com/dagster-io/dagster/assets/1037212/845dfd0b-b1b7-438b-8d70-b9130fba9fb1)

![image](https://github.com/dagster-io/dagster/assets/1037212/836cbb21-f485-4555-b82a-dfe6303cbeec)

Also verified with multi-component asset keys:

![image](https://github.com/dagster-io/dagster/assets/1037212/882bced3-382c-4bdb-b420-93e32be2038e)

![image](https://github.com/dagster-io/dagster/assets/1037212/da6b77b9-6ff9-40ef-aced-235fb6677439)


## How I Tested These Changes

I viewed the runs list and run details for a wide range of runs.

---------

Co-authored-by: bengotow <[email protected]>
  • Loading branch information
bengotow and bengotow authored Sep 13, 2023
1 parent 2355f27 commit 7a0617c
Show file tree
Hide file tree
Showing 13 changed files with 377 additions and 278 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,21 @@ import {AssetGroupSelector} from '../graphql/types';
import {useDocumentTitle} from '../hooks/useDocumentTitle';
import {useQueryPersistedState} from '../hooks/useQueryPersistedState';
import {RepoFilterButton} from '../instance/RepoFilterButton';
import {
ExplorerPath,
explorerPathFromString,
explorerPathToString,
} from '../pipelines/PipelinePathUtils';
import {ExplorerPath} from '../pipelines/PipelinePathUtils';
import {ReloadAllButton} from '../workspace/ReloadAllButton';
import {WorkspaceContext} from '../workspace/WorkspaceContext';

import {AssetGroupSuggest, buildAssetGroupSelector} from './AssetGroupSuggest';
import {assetDetailsPathForKey} from './assetDetailsPathForKey';
import {
globalAssetGraphPathFromString,
globalAssetGraphPathToString,
} from './globalAssetGraphPathToString';

interface AssetGroupRootParams {
0: string;
}

const __GLOBAL__ = '__GLOBAL__';

export const AssetsGroupsGlobalGraphRoot: React.FC = () => {
const {0: path} = useParams<AssetGroupRootParams>();
const {allRepos, visibleRepos} = React.useContext(WorkspaceContext);
Expand All @@ -41,8 +39,10 @@ export const AssetsGroupsGlobalGraphRoot: React.FC = () => {

const onChangeExplorerPath = React.useCallback(
(path: ExplorerPath, mode: 'push' | 'replace') => {
const str = explorerPathToString({...path, pipelineName: __GLOBAL__}).replace(__GLOBAL__, '');
history[mode]({pathname: `/asset-groups${str}`, search: history.location.search});
history[mode]({
pathname: globalAssetGraphPathToString(path),
search: history.location.search,
});
},
[history],
);
Expand Down Expand Up @@ -113,7 +113,7 @@ export const AssetsGroupsGlobalGraphRoot: React.FC = () => {
</>
}
options={{preferAssetRendering: true, explodeComposites: true}}
explorerPath={explorerPathFromString(__GLOBAL__ + path || '/')}
explorerPath={globalAssetGraphPathFromString(path)}
onChangeExplorerPath={onChangeExplorerPath}
onNavigateToSourceAssetNode={onNavigateToSourceAssetNode}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
globalAssetGraphPathToString,
globalAssetGraphPathForAssetsAndDescendants,
} from '../globalAssetGraphPathToString';

// This file must be mocked because Jest can't handle `import.meta.url`.
jest.mock('../../graph/asyncGraphLayout', () => ({}));

describe('Global Graph URLs', () => {
describe('globalAssetGraphPathToString', () => {
it('should return a valid path given a selection and query', async () => {
const url = globalAssetGraphPathToString({
opNames: ['foo_bar'],
opsQuery: `foo*, bar, foo_bar++`,
});
expect(url).toEqual(`/asset-groups~foo*%2C%20bar%2C%20foo_bar%2B%2B/foo_bar`);
});
});

describe('globalAssetGraphPathForAssetsAndDescendants', () => {
it('should return a valid path for a single asset', async () => {
const url = globalAssetGraphPathForAssetsAndDescendants([{path: ['asset_0']}]);
expect(url).toEqual(`/asset-groups~asset_0*/asset_0`);
});

it('should avoid exceeding a 32k character URL', async () => {
const keysLarge = new Array(900).fill(0).map((_, idx) => ({path: [`asset_${idx}`]}));
const urlLarge = globalAssetGraphPathForAssetsAndDescendants(keysLarge);
expect(urlLarge.length).toEqual(24986);

const keysHuge = new Array(1500).fill(0).map((_, idx) => ({path: [`asset_${idx}`]}));
const urlHuge = globalAssetGraphPathForAssetsAndDescendants(keysHuge);
expect(urlHuge.length).toEqual(24399); // smaller because the selection is not passed
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {tokenForAssetKey} from '../asset-graph/Utils';
import {AssetKeyInput} from '../graphql/types';
import {
ExplorerPath,
explorerPathFromString,
explorerPathToString,
} from '../pipelines/PipelinePathUtils';

// https://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
const URL_MAX_LENGTH = 32000;
const __GLOBAL__ = '__GLOBAL__';

export function globalAssetGraphPathToString(path: Omit<ExplorerPath, 'pipelineName'>) {
const str = explorerPathToString({...path, pipelineName: __GLOBAL__}).replace(__GLOBAL__, '');
return `/asset-groups${str}`;
}

export function globalAssetGraphPathFromString(pathName: string) {
return explorerPathFromString(__GLOBAL__ + pathName || '/');
}

export function globalAssetGraphPathForAssetsAndDescendants(assetKeys: AssetKeyInput[]) {
// In a perfect world we populate ops query to "asset1*,asset2*" and then select the roots
// by passing opNames. If we don't have enough characters to do both, just populate the ops
// query. It might still be too long, but we tried.
const opsQuery = assetKeys.map((a) => `${tokenForAssetKey(a)}*`).join(', ');
const opNames =
opsQuery.length > URL_MAX_LENGTH / 2 ? [] : [assetKeys.map(tokenForAssetKey).join(',')];
return globalAssetGraphPathToString({opNames, opsQuery});
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import {Link} from 'react-router-dom';

import {useQueryRefreshAtInterval, FIFTEEN_SECONDS} from '../app/QueryRefresh';
import {RunStatus} from '../graphql/types';
import {timingStringForStatus} from '../runs/RunDetails';
import {RunStatusIndicator} from '../runs/RunStatusDots';
import {DagsterTag} from '../runs/RunTag';
import {timingStringForStatus} from '../runs/RunTimingDetails';
import {RunTime, RUN_TIME_FRAGMENT} from '../runs/RunUtils';
import {TimestampDisplay} from '../schedules/TimestampDisplay';
import {repoAddressAsTag} from '../workspace/repoAddressAsString';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {
import * as React from 'react';
import {Link} from 'react-router-dom';

import {displayNameForAssetKey, tokenForAssetKey} from '../asset-graph/Utils';
import {displayNameForAssetKey} from '../asset-graph/Utils';
import {assetDetailsPathForKey} from '../assets/assetDetailsPathForKey';
import {globalAssetGraphPathForAssetsAndDescendants} from '../assets/globalAssetGraphPathToString';
import {AssetKey} from '../assets/types';
import {TagActionsPopover} from '../ui/TagActions';
import {VirtualizedItemListForDialog} from '../ui/VirtualizedItemListForDialog';

const MAX_ASSET_TAGS = 3;

export const AssetKeyTagCollection: React.FC<{
assetKeys: AssetKey[] | null;
modalTitle?: string;
Expand All @@ -29,12 +29,8 @@ export const AssetKeyTagCollection: React.FC<{
return null;
}

const assetCount = assetKeys.length;
const displayed = assetCount <= MAX_ASSET_TAGS ? assetKeys : [];
const hidden = assetCount - displayed.length;

const showMoreDialog =
hidden > 0 ? (
assetKeys.length > 1 ? (
<Dialog
title={modalTitle}
onClose={() => setShowMore(false)}
Expand All @@ -57,49 +53,76 @@ export const AssetKeyTagCollection: React.FC<{
</Dialog>
) : undefined;

if (useTags) {
if (assetKeys.length === 1) {
// Outer span ensures the popover target is in the right place if the
// parent is a flexbox.
const assetKey = assetKeys[0]!;
return (
<>
{displayed.map((assetKey, ii) => (
<Link to={assetDetailsPathForKey(assetKey)} key={`${tokenForAssetKey(assetKey)}-${ii}`}>
<span style={useTags ? {} : {marginBottom: -4}}>
<TagActionsPopover
data={{key: '', value: ''}}
actions={[
{
label: 'View asset',
to: assetDetailsPathForKey(assetKey),
},
{
label: 'View downstream lineage',
to: assetDetailsPathForKey(assetKey, {
view: 'lineage',
lineageScope: 'downstream',
}),
},
]}
>
{useTags ? (
<Tag intent="none" interactive icon="asset">
{displayNameForAssetKey(assetKey)}
</Tag>
</Link>
))}
{hidden > 0 && (
<ButtonLink onClick={() => setShowMore(true)}>
<Tag intent="none" icon="asset">
{hidden} assets
</Tag>
</ButtonLink>
)}
{showMoreDialog}
</>
) : (
<Link to={assetDetailsPathForKey(assetKey)}>
<Box flex={{direction: 'row', gap: 8, alignItems: 'center'}}>
<Icon color={Colors.Gray400} name="asset" size={16} />
{displayNameForAssetKey(assetKey)}
</Box>
</Link>
)}
</TagActionsPopover>
</span>
);
}

return (
<Box flex={{direction: 'row', gap: 8, alignItems: 'center'}}>
<Icon color={Colors.Gray400} name="asset" size={16} />
<Box style={{flex: 1}} flex={{wrap: 'wrap', display: 'inline-flex'}}>
{displayed.map((assetKey, idx) => (
<Link
to={assetDetailsPathForKey(assetKey)}
key={tokenForAssetKey(assetKey)}
style={{marginRight: 4}}
>
{`${displayNameForAssetKey(assetKey)}${idx < displayed.length - 1 ? ',' : ''}`}
</Link>
))}

{hidden > 0 && displayed.length > 0 ? (
<ButtonLink onClick={() => setShowMore(true)}>{` + ${hidden} more`}</ButtonLink>
) : hidden > 0 ? (
<ButtonLink onClick={() => setShowMore(true)}>{`${hidden} assets`}</ButtonLink>
) : undefined}
</Box>
<span style={useTags ? {} : {marginBottom: -4}}>
<TagActionsPopover
data={{key: '', value: ''}}
actions={[
{
label: 'View list',
onClick: () => setShowMore(true),
},
{
label: 'View downstream lineage',
to: globalAssetGraphPathForAssetsAndDescendants(assetKeys),
},
]}
>
{useTags ? (
<Tag intent="none" icon="asset">
{assetKeys.length} assets
</Tag>
) : (
<ButtonLink onClick={() => setShowMore(true)}>
<Box flex={{direction: 'row', gap: 8, alignItems: 'center', display: 'inline-flex'}}>
<Icon color={Colors.Gray400} name="asset" size={16} />
<Box style={{flex: 1}} flex={{wrap: 'wrap', display: 'inline-flex'}}>
{`${assetKeys.length} assets`}
</Box>
</Box>
</ButtonLink>
)}
</TagActionsPopover>
{showMoreDialog}
</Box>
</span>
);
});
Loading

1 comment on commit 7a0617c

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

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

Please sign in to comment.