-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First stab at displaying the new asset catalog page. Relies on the existing `assetsOrError` query to fetch asset definition info, and adds logic to group assets by code location / owner / etc. This will enable changes to this view, i.e. displaying a tooltip of the asset keys per group, if desired. Tagging others on this PR to get thoughts so far on the existing implementation. Next steps: - Add tests for this component - Unify styling for `UserDisplay` component across core and cloud. Currently they display differently. - Supporting asset search - Displaying a background for the search section Outstanding questions: - How do we handle large numbers of assets on this page? Do we need to implement some way to minimize each section, or paginate? <img width="1457" alt="image" src="https://github.com/dagster-io/dagster/assets/29110579/de586d6b-8ee9-4f62-9891-b491e937f562">
- Loading branch information
1 parent
c1f62a1
commit 258bc71
Showing
8 changed files
with
388 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
331 changes: 331 additions & 0 deletions
331
js_modules/dagster-ui/packages/ui-core/src/assets/AssetsOverview.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,331 @@ | ||
import {useQuery} from '@apollo/client'; | ||
import {Box, Colors, Heading, Icon, Page, Spinner, TextInput} from '@dagster-io/ui-components'; | ||
import qs from 'qs'; | ||
import {useContext} from 'react'; | ||
import {Link, useParams} from 'react-router-dom'; | ||
import styled from 'styled-components'; | ||
|
||
import {AssetGlobalLineageButton, AssetPageHeader} from './AssetPageHeader'; | ||
import {ASSET_CATALOG_TABLE_QUERY} from './AssetsCatalogTable'; | ||
import {AssetTableFragment} from './types/AssetTableFragment.types'; | ||
import { | ||
AssetCatalogTableQuery, | ||
AssetCatalogTableQueryVariables, | ||
} from './types/AssetsCatalogTable.types'; | ||
import {useTrackPageView} from '../app/analytics'; | ||
import {TimeContext} from '../app/time/TimeContext'; | ||
import {browserTimezone} from '../app/time/browserTimezone'; | ||
import {TagIcon} from '../graph/OpTags'; | ||
import {useDocumentTitle} from '../hooks/useDocumentTitle'; | ||
import {useLaunchPadHooks} from '../launchpad/LaunchpadHooksContext'; | ||
import {ReloadAllButton} from '../workspace/ReloadAllButton'; | ||
import {buildRepoPathForHuman} from '../workspace/buildRepoAddress'; | ||
import {repoAddressAsHumanString, repoAddressAsURLString} from '../workspace/repoAddressAsString'; | ||
import {repoAddressFromPath} from '../workspace/repoAddressFromPath'; | ||
import {RepoAddress} from '../workspace/types'; | ||
|
||
type AssetCountsResult = { | ||
countsByOwner: Record<string, number>; | ||
countsByComputeKind: Record<string, number>; | ||
countPerAssetGroup: CountPerGroupName[]; | ||
countPerCodeLocation: CountPerCodeLocation[]; | ||
}; | ||
|
||
type GroupMetadata = { | ||
groupName: string; | ||
repositoryLocationName: string; | ||
repositoryName: string; | ||
}; | ||
|
||
type CountPerGroupName = { | ||
assetCount: number; | ||
groupMetadata: GroupMetadata; | ||
}; | ||
|
||
type CountPerCodeLocation = { | ||
repoAddress: RepoAddress; | ||
assetCount: number; | ||
}; | ||
|
||
function buildAssetCountBySection(assets: AssetTableFragment[]): AssetCountsResult { | ||
const assetCountByOwner: Record<string, number> = {}; | ||
const assetCountByComputeKind: Record<string, number> = {}; | ||
const assetCountByGroup: Record<string, number> = {}; | ||
const assetCountByCodeLocation: Record<string, number> = {}; | ||
|
||
assets | ||
.filter((asset) => asset.definition) | ||
.forEach((asset) => { | ||
const assetDefinition = asset.definition!; | ||
assetDefinition.owners.forEach((owner) => { | ||
const ownerKey = owner.__typename === 'UserAssetOwner' ? owner.email : owner.team; | ||
assetCountByOwner[ownerKey] = (assetCountByOwner[ownerKey] || 0) + 1; | ||
}); | ||
|
||
const computeKind = assetDefinition.computeKind; | ||
if (computeKind) { | ||
assetCountByComputeKind[computeKind] = (assetCountByComputeKind[computeKind] || 0) + 1; | ||
} | ||
|
||
const groupName = assetDefinition.groupName; | ||
const locationName = assetDefinition.repository.location.name; | ||
const repositoryName = assetDefinition.repository.name; | ||
|
||
if (groupName) { | ||
const metadata: GroupMetadata = { | ||
groupName, | ||
repositoryLocationName: locationName, | ||
repositoryName, | ||
}; | ||
const groupIdentifier = JSON.stringify(metadata); | ||
assetCountByGroup[groupIdentifier] = (assetCountByGroup[groupIdentifier] || 0) + 1; | ||
} | ||
|
||
const stringifiedCodeLocation = buildRepoPathForHuman(repositoryName, locationName); | ||
assetCountByCodeLocation[stringifiedCodeLocation] = | ||
(assetCountByCodeLocation[stringifiedCodeLocation] || 0) + 1; | ||
}); | ||
|
||
const countPerAssetGroup = Object.entries(assetCountByGroup).map(([groupIdentifier, count]) => ({ | ||
assetCount: count, | ||
groupMetadata: JSON.parse(groupIdentifier), | ||
})); | ||
|
||
return { | ||
countsByOwner: assetCountByOwner, | ||
countsByComputeKind: assetCountByComputeKind, | ||
countPerAssetGroup, | ||
countPerCodeLocation: Object.entries(assetCountByCodeLocation).map(([key, count]) => { | ||
return {repoAddress: repoAddressFromPath(key)!, assetCount: count}; | ||
}), | ||
}; | ||
} | ||
|
||
interface AssetOverviewCategoryProps { | ||
children: React.ReactNode; | ||
assetsCount: number; | ||
} | ||
|
||
function getGreeting(timezone: string) { | ||
const hour = Number( | ||
new Date().toLocaleTimeString('en-US', { | ||
hour: '2-digit', | ||
hourCycle: 'h23', | ||
timeZone: timezone === 'Automatic' ? browserTimezone() : timezone, | ||
}), | ||
); | ||
if (hour < 4) { | ||
return 'Good evening'; | ||
} else if (hour < 12) { | ||
return 'Good morning'; | ||
} else if (hour < 18) { | ||
return 'Good afternoon'; | ||
} else { | ||
return 'Good evening'; | ||
} | ||
} | ||
|
||
const CountForAssetType = ({children, assetsCount}: AssetOverviewCategoryProps) => { | ||
return ( | ||
<Box | ||
flex={{direction: 'row', justifyContent: 'space-between'}} | ||
style={{width: 'calc(33% - 16px)'}} | ||
> | ||
<div>{children}</div> | ||
<AssetCount>{assetsCount} assets</AssetCount> | ||
</Box> | ||
); | ||
}; | ||
|
||
const SectionHeader = ({sectionName}: {sectionName: string}) => { | ||
return ( | ||
<Box | ||
flex={{alignItems: 'center', justifyContent: 'space-between'}} | ||
padding={{horizontal: 24, vertical: 8}} | ||
border="top-and-bottom" | ||
> | ||
<SectionName>{sectionName}</SectionName> | ||
</Box> | ||
); | ||
}; | ||
|
||
const SectionBody = ({children}: {children: React.ReactNode}) => { | ||
return ( | ||
<Box | ||
padding={{horizontal: 24, vertical: 16}} | ||
flex={{wrap: 'wrap'}} | ||
style={{rowGap: '14px', columnGap: '24px'}} | ||
> | ||
{children} | ||
</Box> | ||
); | ||
}; | ||
|
||
const linkToAssetGraphGroup = (groupMetadata: GroupMetadata) => { | ||
return `/asset-groups?${qs.stringify({groups: JSON.stringify([groupMetadata])})}`; | ||
}; | ||
|
||
const linkToAssetGraphComputeKind = (computeKind: string) => { | ||
return `/asset-groups?${qs.stringify({ | ||
computeKindTags: JSON.stringify([computeKind]), | ||
})}`; | ||
}; | ||
|
||
const linkToCodeLocation = (repoAddress: RepoAddress) => { | ||
return `/locations/${repoAddressAsURLString(repoAddress)}/assets`; | ||
}; | ||
|
||
export const AssetsOverview = ({viewerName}: {viewerName?: string}) => { | ||
useTrackPageView(); | ||
|
||
const params = useParams(); | ||
const currentPath: string[] = ((params as any)['0'] || '') | ||
.split('/') | ||
.filter((x: string) => x) | ||
.map(decodeURIComponent); | ||
|
||
useDocumentTitle('Assets'); | ||
|
||
const assetsQuery = useQuery<AssetCatalogTableQuery, AssetCatalogTableQueryVariables>( | ||
ASSET_CATALOG_TABLE_QUERY, | ||
{ | ||
notifyOnNetworkStatusChange: true, | ||
}, | ||
); | ||
const assetCountBySection = buildAssetCountBySection( | ||
assetsQuery.data?.assetsOrError.__typename === 'AssetConnection' | ||
? assetsQuery.data.assetsOrError.nodes | ||
: [], | ||
); | ||
const {UserDisplay} = useLaunchPadHooks(); | ||
const { | ||
timezone: [timezone], | ||
} = useContext(TimeContext); | ||
|
||
if (assetsQuery.loading) { | ||
return ( | ||
<Page> | ||
<AssetPageHeader assetKey={{path: currentPath}} /> | ||
<Box flex={{direction: 'row', justifyContent: 'center'}} style={{paddingTop: '100px'}}> | ||
<Box flex={{direction: 'row', alignItems: 'center', gap: 16}}> | ||
<Spinner purpose="body-text" /> | ||
<div style={{color: Colors.textLight()}}>Loading assets…</div> | ||
</Box> | ||
</Box> | ||
</Page> | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
<AssetPageHeader | ||
assetKey={{path: currentPath}} | ||
right={<ReloadAllButton label="Reload definitions" />} | ||
/> | ||
<Box flex={{direction: 'column'}} style={{height: '100%', overflow: 'auto'}}> | ||
<Box padding={64} flex={{justifyContent: 'center', alignItems: 'center'}}> | ||
<Box style={{width: '60%'}} flex={{direction: 'column', gap: 16}}> | ||
<Box flex={{direction: 'row', alignItems: 'center', justifyContent: 'space-between'}}> | ||
<Heading> | ||
{getGreeting(timezone)} | ||
{viewerName ? `, ${viewerName}` : ''} | ||
</Heading> | ||
<Box flex={{direction: 'row', gap: 16, alignItems: 'center'}}> | ||
<Link to="/assets">View all</Link> | ||
<AssetGlobalLineageButton /> | ||
</Box> | ||
</Box> | ||
<TextInput /> | ||
</Box> | ||
</Box> | ||
<Box flex={{direction: 'column'}}> | ||
{Object.keys(assetCountBySection.countsByOwner).length > 0 && ( | ||
<> | ||
<SectionHeader sectionName="Owner" /> | ||
<SectionBody> | ||
{Object.entries(assetCountBySection.countsByOwner).map(([label, count]) => ( | ||
<CountForAssetType key={label} assetsCount={count}> | ||
<UserDisplay email={label} /> | ||
</CountForAssetType> | ||
))} | ||
</SectionBody> | ||
</> | ||
)} | ||
{Object.keys(assetCountBySection.countsByComputeKind).length > 0 && ( | ||
<> | ||
<SectionHeader sectionName="Compute kind" /> | ||
<SectionBody> | ||
{Object.entries(assetCountBySection.countsByComputeKind).map(([label, count]) => ( | ||
<CountForAssetType key={label} assetsCount={count}> | ||
<Box flex={{direction: 'row', gap: 4, alignItems: 'center'}}> | ||
<TagIcon label={label} /> | ||
<Link to={linkToAssetGraphComputeKind(label)}>{label}</Link> | ||
</Box> | ||
</CountForAssetType> | ||
))} | ||
</SectionBody> | ||
</> | ||
)} | ||
{assetCountBySection.countPerAssetGroup.length > 0 && ( | ||
<> | ||
<SectionHeader sectionName="Asset groups" /> | ||
<SectionBody> | ||
{assetCountBySection.countPerAssetGroup.map((assetGroupCount) => ( | ||
<CountForAssetType | ||
key={JSON.stringify(assetGroupCount.groupMetadata)} | ||
assetsCount={assetGroupCount.assetCount} | ||
> | ||
<Box flex={{direction: 'row', gap: 4, alignItems: 'center'}}> | ||
<Icon name="asset_group" /> | ||
<Link to={linkToAssetGraphGroup(assetGroupCount.groupMetadata)}> | ||
{assetGroupCount.groupMetadata.groupName} | ||
</Link> | ||
<span style={{color: Colors.textLighter()}}> | ||
{assetGroupCount.groupMetadata.repositoryLocationName} | ||
</span> | ||
</Box> | ||
</CountForAssetType> | ||
))} | ||
</SectionBody> | ||
</> | ||
)} | ||
{assetCountBySection.countPerCodeLocation.length > 0 && ( | ||
<> | ||
<SectionHeader sectionName="Code locations" /> | ||
<SectionBody> | ||
{assetCountBySection.countPerCodeLocation.map((countPerCodeLocation) => ( | ||
<CountForAssetType | ||
key={repoAddressAsHumanString(countPerCodeLocation.repoAddress)} | ||
assetsCount={countPerCodeLocation.assetCount} | ||
> | ||
<Box flex={{direction: 'row', gap: 4, alignItems: 'center'}}> | ||
<Icon name="folder" /> | ||
<Link to={linkToCodeLocation(countPerCodeLocation.repoAddress)}> | ||
{repoAddressAsHumanString(countPerCodeLocation.repoAddress)} | ||
</Link> | ||
</Box> | ||
</CountForAssetType> | ||
))} | ||
</SectionBody> | ||
</> | ||
)} | ||
</Box> | ||
</Box> | ||
</> | ||
); | ||
}; | ||
|
||
// Imported via React.lazy, which requires a default export. | ||
// eslint-disable-next-line import/no-default-export | ||
export default AssetsOverview; | ||
|
||
const SectionName = styled.span` | ||
font-weight: 600; | ||
color: ${Colors.textLight()}; | ||
font-size: 12px; | ||
`; | ||
|
||
const AssetCount = styled.span` | ||
color: ${Colors.textLight()}; | ||
font-size: 14px; | ||
`; |
6 changes: 6 additions & 0 deletions
6
js_modules/dagster-ui/packages/ui-core/src/assets/types/AssetTableFragment.types.ts
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
258bc71
There was a problem hiding this comment.
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-djeedgyeo-elementl.vercel.app
Built with commit 258bc71.
This pull request is being automatically deployed with vercel-action