Skip to content

Commit

Permalink
[dagit] Add asset group filter to the Asset Catalog (#8204)
Browse files Browse the repository at this point in the history
Co-authored-by: bengotow <bgotow@elementl.com>
  • Loading branch information
bengotow and bengotow committed Jun 7, 2022
1 parent 8cc09fe commit cc31c77
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 48 deletions.
2 changes: 2 additions & 0 deletions js_modules/dagit/packages/core/src/assets/AssetGroupRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {useQueryPersistedState} from '../hooks/useQueryPersistedState';
import {RepositoryLink} from '../nav/RepositoryLink';
import {explorerPathFromString, explorerPathToString} from '../pipelines/PipelinePathUtils';
import {TabLink} from '../ui/TabLink';
import {ReloadAllButton} from '../workspace/ReloadAllButton';
import {RepoAddress} from '../workspace/types';
import {workspacePathFromAddress} from '../workspace/workspacePath';

Expand Down Expand Up @@ -35,6 +36,7 @@ export const AssetGroupRoot: React.FC<{repoAddress: RepoAddress}> = ({repoAddres
<Page style={{display: 'flex', flexDirection: 'column', paddingBottom: 0}}>
<PageHeader
title={<Heading>{groupName}</Heading>}
right={<ReloadAllButton label="Reload definitions" />}
tags={
<Tag icon="asset_group">
Asset Group in <RepositoryLink repoAddress={repoAddress} />
Expand Down
7 changes: 5 additions & 2 deletions js_modules/dagit/packages/core/src/assets/AssetTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ import {workspacePathFromAddress} from '../workspace/workspacePath';
import {AssetLink} from './AssetLink';
import {AssetWipeDialog} from './AssetWipeDialog';
import {AssetTableFragment as Asset} from './types/AssetTableFragment';
import {AssetViewType} from './useAssetView';

type AssetKey = {path: string[]};

export const AssetTable = ({
view,
assets,
actionBarComponents,
liveDataByNode,
Expand All @@ -41,6 +43,7 @@ export const AssetTable = ({
maxDisplayCount,
requery,
}: {
view: AssetViewType;
assets: Asset[];
actionBarComponents: React.ReactNode;
liveDataByNode: LiveData;
Expand Down Expand Up @@ -100,7 +103,7 @@ export const AssetTable = ({
}}
/>
</th>
<th>Asset Key</th>
<th>{view === 'directory' ? 'Asset Key Prefix' : 'Asset Key'}</th>
<th style={{width: 340}}>Defined In</th>
<th style={{width: 200}}>Materialized</th>
<th style={{width: 100}}>Latest Run</th>
Expand Down Expand Up @@ -189,7 +192,7 @@ const AssetEntryRow: React.FC<{

return (
<tr>
<td style={{paddingRight: '4px'}}>
<td style={{paddingRight: 8}}>
<Checkbox checked={isSelected} onChange={onChange} />
</td>
<td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ import {AssetViewType, useAssetView} from './useAssetView';

export const AssetViewModeSwitch = () => {
const history = useHistory();
const [view, _setView] = useAssetView();
const [view, setView] = useAssetView();

const buttons: ButtonGroupItem<AssetViewType>[] = [
{id: 'flat', icon: 'view_list', tooltip: 'List view'},
{id: 'directory', icon: 'folder', tooltip: 'Folder view'},
];

const setView = (view: AssetViewType) => {
_setView(view);
const onClick = (view: AssetViewType) => {
setView(view);
if (history.location.pathname !== '/instance/assets') {
history.push('/instance/assets');
}
};

return <ButtonGroup activeItems={new Set([view])} buttons={buttons} onClick={setView} />;
return <ButtonGroup activeItems={new Set([view])} buttons={buttons} onClick={onClick} />;
};
121 changes: 108 additions & 13 deletions js_modules/dagit/packages/core/src/assets/AssetsCatalogTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import {gql, QueryResult, useQuery} from '@apollo/client';
import {
Box,
CursorPaginationControls,
CursorPaginationProps,
TextInput,
Suggest,
MenuItem,
Icon,
} from '@dagster-io/ui';
import isEqual from 'lodash/isEqual';
import uniqBy from 'lodash/uniqBy';
import * as React from 'react';
import styled from 'styled-components/macro';

import {Box, CursorPaginationControls, CursorPaginationProps, TextInput} from '../../../ui/src';
import {PythonErrorInfo, PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorInfo';
import {
FIFTEEN_SECONDS,
Expand All @@ -15,8 +25,10 @@ import {toGraphId, tokenForAssetKey} from '../asset-graph/Utils';
import {useLiveDataForAssetKeys} from '../asset-graph/useLiveDataForAssetKeys';
import {useQueryPersistedState} from '../hooks/useQueryPersistedState';
import {AssetGroupSelector} from '../types/globalTypes';
import {ClearButton} from '../ui/ClearButton';
import {LoadingSpinner} from '../ui/Loading';
import {StickyTableContainer} from '../ui/StickyTableContainer';
import {buildRepoPath} from '../workspace/buildRepoAddress';

import {AssetTable, ASSET_TABLE_DEFINITION_FRAGMENT, ASSET_TABLE_FRAGMENT} from './AssetTable';
import {AssetViewModeSwitch} from './AssetViewModeSwitch';
Expand Down Expand Up @@ -86,24 +98,29 @@ export const AssetsCatalogTable: React.FC<AssetCatalogTableProps> = ({
prefixPath = [],
groupSelector,
}) => {
const [view, setView] = useAssetView();
const [cursor, setCursor] = useQueryPersistedState<string | undefined>({queryKey: 'cursor'});
const [search, setSearch] = useQueryPersistedState<string | undefined>({queryKey: 'q'});
const [view, _setView] = useAssetView();
const [searchGroup, setSearchGroup] = useQueryPersistedState<AssetGroupSelector | null>({
queryKey: 'g',
decode: (qs) => (qs.group ? JSON.parse(qs.group) : null),
encode: (group) => ({group: group ? JSON.stringify(group) : undefined}),
});

const searchSeparatorAgnostic = (search || '')
const searchPath = (search || '')
.replace(/(( ?> ?)|\.|\/)/g, '/')
.toLowerCase()
.trim();

const {assets, query, error} = useAllAssets(groupSelector);
const filtered = React.useMemo(
() =>
(assets || []).filter(
(a) =>
!searchSeparatorAgnostic ||
tokenForAssetKey(a.key).toLowerCase().includes(searchSeparatorAgnostic),
),
[assets, searchSeparatorAgnostic],
(assets || []).filter((a) => {
const groupMatch = !searchGroup || isEqual(buildAssetGroupSelector(a), searchGroup);
const pathMatch = !searchPath || tokenForAssetKey(a.key).toLowerCase().includes(searchPath);
return groupMatch && pathMatch;
}),
[assets, searchPath, searchGroup],
);

const {displayPathForAsset, displayed, nextCursor, prevCursor} =
Expand Down Expand Up @@ -137,9 +154,9 @@ export const AssetsCatalogTable: React.FC<AssetCatalogTableProps> = ({

React.useEffect(() => {
if (view !== 'directory' && prefixPath.length) {
_setView('directory');
setView('directory');
}
}, [view, _setView, prefixPath]);
}, [view, setView, prefixPath]);

if (error) {
return <PythonErrorInfo error={error} />;
Expand Down Expand Up @@ -169,6 +186,7 @@ export const AssetsCatalogTable: React.FC<AssetCatalogTableProps> = ({
<Wrapper>
<StickyTableContainer $top={0}>
<AssetTable
view={view}
assets={displayed}
liveDataByNode={liveDataByNode}
actionBarComponents={
Expand All @@ -179,11 +197,14 @@ export const AssetsCatalogTable: React.FC<AssetCatalogTableProps> = ({
style={{width: '30vw', minWidth: 150, maxWidth: 400}}
placeholder={
prefixPath.length
? `Search asset_keys in ${prefixPath.join('/')}…`
: `Search all asset_keys…`
? `Filter asset_keys in ${prefixPath.join('/')}…`
: `Filter all asset_keys…`
}
onChange={(e: React.ChangeEvent<any>) => setSearch(e.target.value)}
/>
{!groupSelector ? (
<AssetGroupSuggest assets={assets} value={searchGroup} onChange={setSearchGroup} />
) : undefined}
<QueryRefreshCountdown refreshState={refreshState} />
</>
}
Expand All @@ -200,6 +221,70 @@ export const AssetsCatalogTable: React.FC<AssetCatalogTableProps> = ({
);
};

const AssetGroupSuggest: React.FC<{
assets: Asset[];
value: AssetGroupSelector | null;
onChange: (g: AssetGroupSelector | null) => void;
}> = ({assets, value, onChange}) => {
const assetGroups = React.useMemo(
() =>
uniqBy(
(assets || []).map(buildAssetGroupSelector).filter((a) => !!a) as AssetGroupSelector[],
(a) => JSON.stringify(a),
).sort((a, b) => a.groupName.localeCompare(b.groupName)),
[assets],
);

const repoContextNeeded = React.useMemo(() => {
// This is a bit tricky - the first time we find a groupName it sets the key to `false`.
// The second time, it sets the value to `true` + tells use we need to show the repo name
const result: {[groupName: string]: boolean} = {};
assetGroups.forEach(
(group) => (result[group.groupName] = result.hasOwnProperty(group.groupName)),
);
return result;
}, [assetGroups]);

return (
<Suggest<AssetGroupSelector>
selectedItem={value}
items={assetGroups}
inputProps={{
style: {width: 220},
placeholder: 'Filter asset groups…',
rightElement: value ? (
<ClearButton onClick={() => onChange(null)} style={{marginTop: 5, marginRight: 4}}>
<Icon name="cancel" />
</ClearButton>
) : undefined,
}}
inputValueRenderer={(partition) => partition.groupName}
itemPredicate={(query, partition) =>
query.length === 0 || partition.groupName.includes(query)
}
itemsEqual={isEqual}
itemRenderer={(assetGroup, props) => (
<MenuItem
active={props.modifiers.active}
onClick={props.handleClick}
key={JSON.stringify(assetGroup)}
text={
<>
{assetGroup.groupName}
{repoContextNeeded[assetGroup.groupName] ? (
<span style={{opacity: 0.5, paddingLeft: 4}}>
{buildRepoPath(assetGroup.repositoryName, assetGroup.repositoryLocationName)}
</span>
) : undefined}
</>
}
/>
)}
noResults={<MenuItem disabled={true} text="No asset groups" />}
onItemSelect={onChange}
/>
);
};
const Wrapper = styled.div`
flex: 1 1;
display: flex;
Expand Down Expand Up @@ -251,6 +336,16 @@ function definitionToAssetTableFragment(
return {__typename: 'Asset', id: definition.id, key: definition.assetKey, definition};
}

function buildAssetGroupSelector(a: Asset) {
return a.definition && a.definition.groupName
? {
groupName: a.definition.groupName,
repositoryName: a.definition.repository.name,
repositoryLocationName: a.definition.repository.location.name,
}
: null;
}

function buildFlatProps(assets: Asset[], prefixPath: string[], cursor: string | undefined) {
const cursorValue = (asset: Asset) => JSON.stringify([...prefixPath, ...asset.key.path]);
const cursorIndex = cursor ? assets.findIndex((ns) => cursor <= cursorValue(ns)) : 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,6 @@ const ConfigEditorPartitionPicker: React.FC<ConfigEditorPartitionPickerProps> =
onItemSelect={(item) => {
onSelect(repositorySelector, partitionSetName, item.name);
}}
popoverProps={{modifiers: {offset: {enabled: true, offset: '-5px 8px'}}}}
/>
);
},
Expand Down
28 changes: 2 additions & 26 deletions js_modules/dagit/packages/core/src/runs/LogsFilterInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import {
SuggestionProvider,
useSuggestionsForString,
Icon,
IconWrapper,
} from '@dagster-io/ui';
import Fuse from 'fuse.js';
import * as React from 'react';
import styled from 'styled-components/macro';

import {ClearButton} from '../ui/ClearButton';

interface Props {
value: string;
onChange: (value: string) => void;
Expand Down Expand Up @@ -217,31 +218,6 @@ const ResultItem: React.FC<{
);
};

const ClearButton = styled.button`
background: transparent;
border: none;
cursor: pointer;
margin: 0 -2px 0 0;
padding: 2px;
${IconWrapper} {
background-color: ${Colors.Gray400};
transition: background-color 100ms linear;
}
:hover ${IconWrapper}, :focus ${IconWrapper} {
background-color: ${Colors.Gray700};
}
:active ${IconWrapper} {
background-color: ${Colors.Dark};
}
:focus {
outline: none;
}
`;

const FilterInput = styled(TextInput)`
width: 300px;
`;
Expand Down
27 changes: 27 additions & 0 deletions js_modules/dagit/packages/core/src/ui/ClearButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {Colors, IconWrapper} from '@dagster-io/ui';
import styled from 'styled-components/macro';

export const ClearButton = styled.button`
background: transparent;
border: none;
cursor: pointer;
margin: 0 -2px 0 0;
padding: 2px;
${IconWrapper} {
background-color: ${Colors.Gray400};
transition: background-color 100ms linear;
}
:hover ${IconWrapper}, :focus ${IconWrapper} {
background-color: ${Colors.Gray700};
}
:active ${IconWrapper} {
background-color: ${Colors.Dark};
}
:focus {
outline: none;
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export const Default = () => {
)}
noResults={<MenuItem disabled={true} text="No presets." />}
onItemSelect={(item) => setSelectedItem(item)}
popoverProps={{modifiers: {offset: {enabled: true, offset: '-2px 8px'}}}}
selectedItem={selectedItem}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion js_modules/dagit/packages/ui/src/components/Suggest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const Suggest = <T,>(props: React.PropsWithChildren<SuggestProps<T>>) =>
...props.popoverProps,
minimal: true,
modifiers: deepmerge(
{offset: {enabled: true, offset: '0, 8px'}},
{offset: {enabled: true, offset: '0 8px'}},
props.popoverProps?.modifiers || {},
),
popoverClassName: `dagit-popover ${props.popoverProps?.className || ''}`,
Expand Down

1 comment on commit cc31c77

@vercel
Copy link

@vercel vercel bot commented on cc31c77 Jun 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

dagit-storybook – ./js_modules/dagit/packages/ui

dagit-storybook-git-master-elementl.vercel.app
dagit-storybook-elementl.vercel.app
dagit-storybook.vercel.app

Please sign in to comment.