diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeOverview.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeOverview.tsx
deleted file mode 100644
index f3f25d26fcbd8..0000000000000
--- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetNodeOverview.tsx
+++ /dev/null
@@ -1,925 +0,0 @@
-// eslint-disable-next-line no-restricted-imports
-import {
- Body,
- Body2,
- Box,
- Button,
- ButtonLink,
- Caption,
- Colors,
- ConfigTypeSchema,
- Icon,
- MiddleTruncate,
- NonIdealState,
- Skeleton,
- Subtitle2,
- Tag,
- Tooltip,
-} from '@dagster-io/ui-components';
-import dayjs from 'dayjs';
-import React, {useMemo, useState} from 'react';
-import {Link} from 'react-router-dom';
-import {UserDisplay} from 'shared/runs/UserDisplay.oss';
-import styled from 'styled-components';
-
-import {AssetDefinedInMultipleReposNotice} from './AssetDefinedInMultipleReposNotice';
-import {AssetEventMetadataEntriesTable} from './AssetEventMetadataEntriesTable';
-import {metadataForAssetNode} from './AssetMetadata';
-import {insitigatorsByType} from './AssetNodeInstigatorTag';
-import {EvaluationUserLabel} from './AutoMaterializePolicyPage/EvaluationConditionalLabel';
-import {DependsOnSelfBanner} from './DependsOnSelfBanner';
-import {LargeCollapsibleSection} from './LargeCollapsibleSection';
-import {MaterializationTag} from './MaterializationTag';
-import {OverdueTag, freshnessPolicyDescription} from './OverdueTag';
-import {RecentUpdatesTimeline} from './RecentUpdatesTimeline';
-import {SimpleStakeholderAssetStatus} from './SimpleStakeholderAssetStatus';
-import {UnderlyingOpsOrGraph} from './UnderlyingOpsOrGraph';
-import {AssetChecksStatusSummary} from './asset-checks/AssetChecksStatusSummary';
-import {assetDetailsPathForKey} from './assetDetailsPathForKey';
-import {buildConsolidatedColumnSchema} from './buildConsolidatedColumnSchema';
-import {globalAssetGraphPathForAssetsAndDescendants} from './globalAssetGraphPathToString';
-import {AssetKey} from './types';
-import {AssetNodeDefinitionFragment} from './types/AssetNodeDefinition.types';
-import {AssetTableDefinitionFragment} from './types/AssetTableFragment.types';
-import {useLatestPartitionEvents} from './useLatestPartitionEvents';
-import {useRecentAssetEvents} from './useRecentAssetEvents';
-import {showCustomAlert} from '../app/CustomAlertProvider';
-import {showSharedToaster} from '../app/DomUtils';
-import {COMMON_COLLATOR} from '../app/Util';
-import {useCopyToClipboard} from '../app/browser';
-import {useAssetsLiveData} from '../asset-data/AssetLiveDataProvider';
-import {
- LiveDataForNode,
- displayNameForAssetKey,
- isHiddenAssetGroupJob,
- sortAssetKeys,
- tokenForAssetKey,
-} from '../asset-graph/Utils';
-import {StatusDot} from '../asset-graph/sidebar/StatusDot';
-import {AssetNodeForGraphQueryFragment} from '../asset-graph/types/useAssetGraphData.types';
-import {CodeLink, getCodeReferenceKey} from '../code-links/CodeLink';
-import {DagsterTypeSummary} from '../dagstertype/DagsterType';
-import {AssetKind, isCanonicalStorageKindTag, isSystemTag} from '../graph/KindTags';
-import {IntMetadataEntry} from '../graphql/types';
-import {useStateWithStorage} from '../hooks/useStateWithStorage';
-import {isCanonicalRowCountMetadataEntry} from '../metadata/MetadataEntry';
-import {
- TableSchema,
- TableSchemaAssetContext,
- isCanonicalCodeSourceEntry,
- isCanonicalTableNameEntry,
- isCanonicalUriEntry,
-} from '../metadata/TableSchema';
-import {RepositoryLink} from '../nav/RepositoryLink';
-import {ScheduleOrSensorTag} from '../nav/ScheduleOrSensorTag';
-import {useRepositoryLocationForAddress} from '../nav/useRepositoryLocationForAddress';
-import {Description} from '../pipelines/Description';
-import {PipelineTag} from '../pipelines/PipelineReference';
-import {numberFormatter} from '../ui/formatters';
-import {buildTagString} from '../ui/tagAsString';
-import {buildRepoAddress} from '../workspace/buildRepoAddress';
-import {workspacePathFromAddress} from '../workspace/workspacePath';
-
-const SystemTagsToggle = ({tags}: {tags: Array<{key: string; value: string}>}) => {
- const [shown, setShown] = useStateWithStorage('show-asset-definition-system-tags', Boolean);
-
- if (!shown) {
- return (
-
- setShown(true)}>
-
- Show system tags ({tags.length || 0})
-
-
-
-
- );
- } else {
- return (
-
-
- {tags.map((tag, idx) => (
- {buildTagString(tag)}
- ))}
-
-
- setShown(false)}>
-
- Hide system tags
-
-
-
-
-
- );
- }
-};
-
-export const AssetNodeOverview = ({
- assetKey,
- assetNode,
- cachedAssetNode,
- upstream,
- downstream,
- liveData,
- dependsOnSelf,
-}: {
- assetKey: AssetKey;
- assetNode: AssetNodeDefinitionFragment | undefined | null;
- cachedAssetNode: AssetTableDefinitionFragment | undefined | null;
- upstream: AssetNodeForGraphQueryFragment[] | null;
- downstream: AssetNodeForGraphQueryFragment[] | null;
- liveData: LiveDataForNode | undefined;
- dependsOnSelf: boolean;
-}) => {
- const cachedOrLiveAssetNode = assetNode ?? cachedAssetNode;
- const repoAddress = cachedOrLiveAssetNode
- ? buildRepoAddress(
- cachedOrLiveAssetNode.repository.name,
- cachedOrLiveAssetNode.repository.location.name,
- )
- : null;
- const location = useRepositoryLocationForAddress(repoAddress);
-
- const {assetType, assetMetadata} = metadataForAssetNode(assetNode);
- const {schedules, sensors} = useMemo(() => insitigatorsByType(assetNode), [assetNode]);
- const configType = assetNode?.configField?.configType;
- const assetConfigSchema = configType && configType.key !== 'Any' ? configType : null;
- const visibleJobNames =
- cachedOrLiveAssetNode?.jobNames.filter((jobName) => !isHiddenAssetGroupJob(jobName)) || [];
-
- const assetNodeLoadTimestamp = location ? location.updatedTimestamp * 1000 : undefined;
-
- const {materialization, observation, loading} = useLatestPartitionEvents(
- assetKey,
- assetNodeLoadTimestamp,
- liveData,
- );
-
- const {
- materializations,
- observations,
- loading: materializationsLoading,
- } = useRecentAssetEvents(
- cachedOrLiveAssetNode?.partitionDefinition ? undefined : cachedOrLiveAssetNode?.assetKey,
- {},
- {assetHasDefinedPartitions: false},
- );
-
- // Start loading neighboring assets data immediately to avoid waterfall.
- useAssetsLiveData(
- useMemo(
- () => [
- ...(downstream || []).map((node) => node.assetKey),
- ...(upstream || []).map((node) => node.assetKey),
- ],
- [downstream, upstream],
- ),
- );
-
- if (loading || !cachedOrLiveAssetNode) {
- return ;
- }
-
- const {tableSchema, tableSchemaLoadTimestamp} = buildConsolidatedColumnSchema({
- materialization,
- definition: assetNode,
- definitionLoadTimestamp: assetNodeLoadTimestamp,
- });
-
- const rowCountMeta: IntMetadataEntry | undefined = materialization?.metadataEntries.find(
- (entry) => isCanonicalRowCountMetadataEntry(entry),
- ) as IntMetadataEntry | undefined;
-
- const renderStatusSection = () => (
-
-
-
-
- Latest {assetNode?.isObservable ? 'observation' : 'materialization'}
-
-
- {liveData ? (
-
- ) : (
-
- )}
- {assetNode && assetNode.freshnessPolicy && (
-
- )}
-
-
- {liveData?.assetChecks.length ? (
-
- Check results
-
-
- ) : undefined}
- {rowCountMeta?.intValue ? (
-
- Row count
-
- {numberFormatter.format(rowCountMeta.intValue)}
-
-
- ) : undefined}
-
- {cachedOrLiveAssetNode.isPartitioned ? null : (
-
- )}
-
- );
-
- const renderDescriptionSection = () =>
- cachedOrLiveAssetNode.description ? (
-
- ) : (
-
- );
-
- const renderLineageSection = () => (
- <>
- {dependsOnSelf && (
-
-
-
- )}
-
-
-
- Upstream assets
- {upstream?.length ? (
-
- ) : (
-
-
-
- )}
-
-
- Downstream assets
- {downstream?.length ? (
-
- ) : (
-
-
-
- )}
-
-
- >
- );
-
- const storageKindTag = cachedOrLiveAssetNode.tags?.find(isCanonicalStorageKindTag);
- const filteredTags = cachedOrLiveAssetNode.tags?.filter(
- (tag) => tag.key !== 'dagster/storage_kind',
- );
-
- const nonSystemTags = filteredTags?.filter((tag) => !isSystemTag(tag));
- const systemTags = filteredTags?.filter(isSystemTag);
-
- const tableNameMetadata = assetNode?.metadataEntries?.find(isCanonicalTableNameEntry);
- const uriMetadata = assetNode?.metadataEntries?.find(isCanonicalUriEntry);
- const codeSource = assetNode?.metadataEntries?.find(isCanonicalCodeSourceEntry);
-
- const renderDefinitionSection = () => (
-
-
-
-
- {cachedOrLiveAssetNode.groupName}
-
-
-
-
-
-
-
-
-
- {location && (
-
- Loaded {dayjs.unix(location.updatedTimestamp).fromNow()}
-
- )}
-
-
-
-
- {cachedOrLiveAssetNode.owners &&
- cachedOrLiveAssetNode.owners.length > 0 &&
- cachedOrLiveAssetNode.owners.map((owner, idx) =>
- owner.__typename === 'UserAssetOwner' ? (
-
-
-
- ) : (
-
- {owner.team}
-
- ),
- )}
-
-
- {cachedOrLiveAssetNode.computeKind && (
-
- )}
-
-
- {(cachedOrLiveAssetNode.kinds.length > 1 || !cachedOrLiveAssetNode.computeKind) &&
- cachedOrLiveAssetNode.kinds.map((kind) => (
-
- ))}
-
-
- {(tableNameMetadata || uriMetadata || storageKindTag) && (
-
- {tableNameMetadata && (
-
-
-
-
- )}
- {uriMetadata && (
-
- {uriMetadata.__typename === 'TextMetadataEntry' ? (
- uriMetadata.text
- ) : (
-
- {uriMetadata.url}
-
- )}
-
-
- )}
- {storageKindTag && (
-
- )}
-
- )}
-
-
- {filteredTags && filteredTags.length > 0 && (
-
-
- {nonSystemTags.map((tag, idx) => (
- {buildTagString(tag)}
- ))}
-
- {systemTags.length > 0 && }
-
- )}
-
-
- {codeSource &&
- codeSource.codeReferences &&
- codeSource.codeReferences.map((ref) => (
-
- ))}
-
-
- );
-
- const renderAutomationDetailsSection = () => {
- const attributes = [
- {
- label: 'Jobs',
- children: visibleJobNames.map((jobName) => (
-
- )),
- },
- {
- label: 'Sensors',
- children: assetNode ? (
- sensors.length > 0 ? (
-
- ) : null
- ) : (
-
- ),
- },
- {
- label: 'Schedules',
- children: assetNode ? (
- schedules.length > 0 && (
-
- )
- ) : (
-
- ),
- },
- {
- label: 'Freshness policy',
- children: assetNode ? (
- assetNode?.freshnessPolicy && (
- {freshnessPolicyDescription(assetNode.freshnessPolicy)}
- )
- ) : (
-
- ),
- },
- ];
-
- if (
- attributes.every((props) => isEmptyChildren(props.children)) &&
- !cachedOrLiveAssetNode.automationCondition
- ) {
- return (
-
- );
- } else {
- if (assetNode?.automationCondition && assetNode?.automationCondition.label) {
- return (
-
- );
- }
- }
-
- return (
-
- {attributes.map((props) => (
-
- ))}
-
- );
- };
-
- const renderComputeDetailsSection = () => {
- if (!assetNode) {
- return ;
- }
- return (
-
-
-
-
-
-
-
- {assetNode.opVersion}
-
-
- {[...assetNode.requiredResources]
- .sort((a, b) => COMMON_COLLATOR.compare(a.resourceKey, b.resourceKey))
- .map((resource) => (
-
-
-
- {repoAddress ? (
-
- {resource.resourceKey}
-
- ) : (
- resource.resourceKey
- )}
-
-
- ))}
-
-
-
- {assetConfigSchema && (
- {
- showCustomAlert({
- title: 'Config schema',
- body: (
-
- ),
- });
- }}
- >
- View config details
-
- )}
-
-
-
- {assetType && assetType.displayName !== 'Any' && (
- {
- showCustomAlert({
- title: 'Type summary',
- body: ,
- });
- }}
- >
- View type details
-
- )}
-
-
-
- {assetNode.backfillPolicy?.description}
-
-
- );
- };
-
- return (
-
-
- {renderStatusSection()}
-
-
- {renderDescriptionSection()}
-
- {tableSchema && (
-
-
-
-
-
- )}
-
-
- }
- />
-
- e.stopPropagation()}
- >
- View in graph
-
- }
- >
- {renderLineageSection()}
-
- >
- }
- right={
- <>
-
- {renderDefinitionSection()}
-
-
- {renderAutomationDetailsSection()}
-
- {cachedOrLiveAssetNode.isExecutable ? (
-
- {renderComputeDetailsSection()}
-
- ) : null}
- >
- }
- />
- );
-};
-
-const AssetNodeOverviewContainer = ({
- left,
- right,
-}: {
- left: React.ReactNode;
- right: React.ReactNode;
-}) => (
-
-
- {left}
-
-
- {right}
-
-
-);
-
-const isEmptyChildren = (children: React.ReactNode) =>
- !children || (children instanceof Array && children.length === 0);
-
-const CopyButton = ({value}: {value: string}) => {
- const copy = useCopyToClipboard();
- const onCopy = async () => {
- copy(value);
- await showSharedToaster({
- intent: 'success',
- icon: 'copy_to_clipboard_done',
- message: 'Copied!',
- });
- };
-
- return (
-
-
-
-
-
- );
-};
-
-const AttributeAndValue = ({
- label,
- children,
-}: {
- label: React.ReactNode;
- children: React.ReactNode;
-}) => {
- if (isEmptyChildren(children)) {
- return null;
- }
-
- return (
-
- {label}
-
- {children}
-
-
- );
-};
-
-const NoValue = () => –;
-
-export const AssetNodeOverviewNonSDA = ({
- assetKey,
- lastMaterialization,
-}: {
- assetKey: AssetKey;
- lastMaterialization: {timestamp: string; runId: string} | null | undefined;
-}) => {
- const {materializations, observations, loading} = useRecentAssetEvents(
- assetKey,
- {},
- {assetHasDefinedPartitions: false},
- );
-
- return (
-
-
-
- {lastMaterialization ? (
-
- ) : (
-
Never materialized
- )}
-
-
-
-
- }
- right={
-
-
-
-
-
- }
- />
- );
-};
-
-export const AssetNodeOverviewLoading = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- }
- right={
-
-
- }>
-
-
- }>
-
-
- }>
-
-
-
-
- }
- />
-);
-
-const SectionEmptyState = ({
- title,
- description,
- learnMoreLink,
-}: {
- title: string;
- description: string;
- learnMoreLink: string;
-}) => (
-
- {title}
- {description}
- {learnMoreLink ? (
-
- Learn more
-
- ) : undefined}
-
-);
-
-const AssetLinksWithStatus = ({
- assets,
- displayedByDefault = 20,
-}: {
- assets: AssetNodeForGraphQueryFragment[];
- displayedByDefault?: number;
-}) => {
- const [displayedCount, setDisplayedCount] = useState(displayedByDefault);
-
- const displayed = React.useMemo(
- () => assets.sort((a, b) => sortAssetKeys(a.assetKey, b.assetKey)).slice(0, displayedCount),
- [assets, displayedCount],
- );
-
- return (
-
- {displayed.map((asset) => (
-
-
-
-
-
-
- ))}
-
- {displayed.length < assets.length ? (
-
- ) : displayed.length > displayedByDefault ? (
-
- ) : undefined}
-
-
- );
-};
-
-const UserAssetOwnerWrapper = styled.div`
- > div {
- background-color: ${Colors.backgroundGray()};
- }
-`;
-
-const SectionSkeleton = () => (
-
-
-
-
-
-);
diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetView.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetView.tsx
index 161e809cac044..4e0e1888700b7 100644
--- a/js_modules/dagster-ui/packages/ui-core/src/assets/AssetView.tsx
+++ b/js_modules/dagster-ui/packages/ui-core/src/assets/AssetView.tsx
@@ -11,7 +11,6 @@ import {AssetFeatureContext} from './AssetFeatureContext';
import {ASSET_NODE_DEFINITION_FRAGMENT, AssetNodeDefinition} from './AssetNodeDefinition';
import {ASSET_NODE_INSTIGATORS_FRAGMENT} from './AssetNodeInstigatorTag';
import {AssetNodeLineage} from './AssetNodeLineage';
-import {AssetNodeOverview, AssetNodeOverviewNonSDA} from './AssetNodeOverview';
import {AssetPartitions} from './AssetPartitions';
import {AssetPlotsPage} from './AssetPlotsPage';
import {AssetTabs} from './AssetTabs';
@@ -22,8 +21,8 @@ import {LaunchAssetExecutionButton} from './LaunchAssetExecutionButton';
import {UNDERLYING_OPS_ASSET_NODE_FRAGMENT} from './UnderlyingOpsOrGraph';
import {AssetChecks} from './asset-checks/AssetChecks';
import {assetDetailsPathForKey} from './assetDetailsPathForKey';
+import {AssetNodeOverview, AssetNodeOverviewNonSDA} from './overview/AssetNodeOverview';
import {AssetKey, AssetViewParams} from './types';
-import {AssetTableDefinitionFragment} from './types/AssetTableFragment.types';
import {
AssetViewDefinitionNodeFragment,
AssetViewDefinitionQuery,
@@ -34,6 +33,7 @@ import {healthRefreshHintFromLiveData} from './usePartitionHealthData';
import {useReportEventsModal} from './useReportEventsModal';
import {useWipeModal} from './useWipeModal';
import {gql, useQuery} from '../apollo-client';
+import {AssetTableDefinitionFragment} from './types/AssetTableFragment.types';
import {currentPageAtom} from '../app/analytics';
import {Timestamp} from '../app/time/Timestamp';
import {AssetLiveDataRefreshButton, useAssetLiveData} from '../asset-data/AssetLiveDataProvider';
diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/overview/AssetNodeOverview.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/AssetNodeOverview.tsx
new file mode 100644
index 0000000000000..8c84edda92807
--- /dev/null
+++ b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/AssetNodeOverview.tsx
@@ -0,0 +1,388 @@
+import {
+ Box,
+ Caption,
+ Colors,
+ NonIdealState,
+ Skeleton,
+ Subtitle2,
+ Tag,
+} from '@dagster-io/ui-components';
+import React, {useMemo} from 'react';
+import {Link} from 'react-router-dom';
+
+import {AssetEventMetadataEntriesTable} from '../AssetEventMetadataEntriesTable';
+import {metadataForAssetNode} from '../AssetMetadata';
+import {AutomationDetailsSection} from './AutomationDetailsSection';
+import {AttributeAndValue, NoValue, SectionEmptyState} from './Common';
+import {ComputeDetailsSection} from './ComputeDetailsSection';
+import {DefinitionSection} from './DefinitionSection';
+import {LineageSection} from './LineageSection';
+import {useAssetsLiveData} from '../../asset-data/AssetLiveDataProvider';
+import {LiveDataForNode} from '../../asset-graph/Utils';
+import {AssetNodeForGraphQueryFragment} from '../../asset-graph/types/useAssetGraphData.types';
+import {IntMetadataEntry} from '../../graphql/types';
+import {isCanonicalRowCountMetadataEntry} from '../../metadata/MetadataEntry';
+import {TableSchema, TableSchemaAssetContext} from '../../metadata/TableSchema';
+import {useRepositoryLocationForAddress} from '../../nav/useRepositoryLocationForAddress';
+import {Description} from '../../pipelines/Description';
+import {numberFormatter} from '../../ui/formatters';
+import {buildRepoAddress} from '../../workspace/buildRepoAddress';
+import {LargeCollapsibleSection} from '../LargeCollapsibleSection';
+import {MaterializationTag} from '../MaterializationTag';
+import {OverdueTag} from '../OverdueTag';
+import {RecentUpdatesTimeline} from '../RecentUpdatesTimeline';
+import {SimpleStakeholderAssetStatus} from '../SimpleStakeholderAssetStatus';
+import {AssetChecksStatusSummary} from '../asset-checks/AssetChecksStatusSummary';
+import {buildConsolidatedColumnSchema} from '../buildConsolidatedColumnSchema';
+import {globalAssetGraphPathForAssetsAndDescendants} from '../globalAssetGraphPathToString';
+import {AssetKey} from '../types';
+import {AssetNodeDefinitionFragment} from '../types/AssetNodeDefinition.types';
+import {AssetTableDefinitionFragment} from '../types/AssetTableFragment.types';
+import {useLatestPartitionEvents} from '../useLatestPartitionEvents';
+import {useRecentAssetEvents} from '../useRecentAssetEvents';
+
+export const AssetNodeOverview = ({
+ assetKey,
+ assetNode,
+ cachedAssetNode,
+ upstream,
+ downstream,
+ liveData,
+ dependsOnSelf,
+}: {
+ assetKey: AssetKey;
+ assetNode: AssetNodeDefinitionFragment | undefined | null;
+ cachedAssetNode: AssetTableDefinitionFragment | undefined | null;
+ upstream: AssetNodeForGraphQueryFragment[] | null;
+ downstream: AssetNodeForGraphQueryFragment[] | null;
+ liveData: LiveDataForNode | undefined;
+ dependsOnSelf: boolean;
+}) => {
+ const cachedOrLiveAssetNode = assetNode ?? cachedAssetNode;
+ const repoAddress = cachedOrLiveAssetNode
+ ? buildRepoAddress(
+ cachedOrLiveAssetNode.repository.name,
+ cachedOrLiveAssetNode.repository.location.name,
+ )
+ : null;
+ const location = useRepositoryLocationForAddress(repoAddress);
+
+ const {assetMetadata} = metadataForAssetNode(assetNode);
+
+ const assetNodeLoadTimestamp = location ? location.updatedTimestamp * 1000 : undefined;
+
+ const {materialization, observation, loading} = useLatestPartitionEvents(
+ assetKey,
+ assetNodeLoadTimestamp,
+ liveData,
+ );
+
+ const {
+ materializations,
+ observations,
+ loading: materializationsLoading,
+ } = useRecentAssetEvents(
+ cachedOrLiveAssetNode?.partitionDefinition ? undefined : cachedOrLiveAssetNode?.assetKey,
+ {},
+ {assetHasDefinedPartitions: false},
+ );
+
+ // Start loading neighboring assets data immediately to avoid waterfall.
+ useAssetsLiveData(
+ useMemo(
+ () => [
+ ...(downstream || []).map((node) => node.assetKey),
+ ...(upstream || []).map((node) => node.assetKey),
+ ],
+ [downstream, upstream],
+ ),
+ );
+
+ if (loading || !cachedOrLiveAssetNode) {
+ return ;
+ }
+
+ const {tableSchema, tableSchemaLoadTimestamp} = buildConsolidatedColumnSchema({
+ materialization,
+ definition: assetNode,
+ definitionLoadTimestamp: assetNodeLoadTimestamp,
+ });
+
+ const rowCountMeta: IntMetadataEntry | undefined = materialization?.metadataEntries.find(
+ (entry) => isCanonicalRowCountMetadataEntry(entry),
+ ) as IntMetadataEntry | undefined;
+
+ const renderStatusSection = () => (
+
+
+
+
+ Latest {assetNode?.isObservable ? 'observation' : 'materialization'}
+
+
+ {liveData ? (
+
+ ) : (
+
+ )}
+ {assetNode && assetNode.freshnessPolicy && (
+
+ )}
+
+
+ {liveData?.assetChecks.length ? (
+
+ Check results
+
+
+ ) : undefined}
+ {rowCountMeta?.intValue ? (
+
+ Row count
+
+ {numberFormatter.format(rowCountMeta.intValue)}
+
+
+ ) : undefined}
+
+ {cachedOrLiveAssetNode.isPartitioned ? null : (
+
+ )}
+
+ );
+
+ return (
+
+
+ {renderStatusSection()}
+
+
+ {cachedOrLiveAssetNode.description ? (
+
+ ) : (
+
+ )}
+
+ {tableSchema && (
+
+
+
+
+
+ )}
+
+
+ }
+ />
+
+ e.stopPropagation()}
+ >
+ View in graph
+
+ }
+ >
+
+
+ >
+ }
+ right={
+ <>
+
+
+
+
+
+
+ {cachedOrLiveAssetNode.isExecutable ? (
+
+
+
+ ) : null}
+ >
+ }
+ />
+ );
+};
+
+const AssetNodeOverviewContainer = ({
+ left,
+ right,
+}: {
+ left: React.ReactNode;
+ right: React.ReactNode;
+}) => (
+
+
+ {left}
+
+
+ {right}
+
+
+);
+
+export const AssetNodeOverviewNonSDA = ({
+ assetKey,
+ lastMaterialization,
+}: {
+ assetKey: AssetKey;
+ lastMaterialization: {timestamp: string; runId: string} | null | undefined;
+}) => {
+ const {materializations, observations, loading} = useRecentAssetEvents(
+ assetKey,
+ {},
+ {assetHasDefinedPartitions: false},
+ );
+
+ return (
+
+
+
+ {lastMaterialization ? (
+
+ ) : (
+
Never materialized
+ )}
+
+
+
+
+ }
+ right={
+
+
+
+
+
+ }
+ />
+ );
+};
+
+export const AssetNodeOverviewLoading = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ }
+ right={
+
+
+ }>
+
+
+ }>
+
+
+ }>
+
+
+
+
+ }
+ />
+);
diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/overview/AutomationDetailsSection.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/AutomationDetailsSection.tsx
new file mode 100644
index 0000000000000..60f730808072b
--- /dev/null
+++ b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/AutomationDetailsSection.tsx
@@ -0,0 +1,106 @@
+import {Body, Box} from '@dagster-io/ui-components';
+import React, {useMemo} from 'react';
+
+import {insitigatorsByType} from '../AssetNodeInstigatorTag';
+import {AttributeAndValue, SectionEmptyState, SectionSkeleton, isEmptyChildren} from './Common';
+import {isHiddenAssetGroupJob} from '../../asset-graph/Utils';
+import {ScheduleOrSensorTag} from '../../nav/ScheduleOrSensorTag';
+import {PipelineTag} from '../../pipelines/PipelineReference';
+import {RepoAddress} from '../../workspace/types';
+import {EvaluationUserLabel} from '../AutoMaterializePolicyPage/EvaluationConditionalLabel';
+import {freshnessPolicyDescription} from '../OverdueTag';
+import {AssetNodeDefinitionFragment} from '../types/AssetNodeDefinition.types';
+import {AssetTableDefinitionFragment} from '../types/AssetTableFragment.types';
+
+export const AutomationDetailsSection = ({
+ repoAddress,
+ assetNode,
+ cachedOrLiveAssetNode,
+}: {
+ repoAddress: RepoAddress | null;
+ assetNode: AssetNodeDefinitionFragment | null | undefined;
+ cachedOrLiveAssetNode: AssetNodeDefinitionFragment | AssetTableDefinitionFragment;
+}) => {
+ const {schedules, sensors} = useMemo(() => insitigatorsByType(assetNode), [assetNode]);
+ const visibleJobNames =
+ cachedOrLiveAssetNode?.jobNames.filter((jobName) => !isHiddenAssetGroupJob(jobName)) || [];
+
+ const attributes = [
+ {
+ label: 'Jobs',
+ children: visibleJobNames.map((jobName) => (
+
+ )),
+ },
+ {
+ label: 'Sensors',
+ children: assetNode ? (
+ sensors.length > 0 ? (
+
+ ) : null
+ ) : (
+
+ ),
+ },
+ {
+ label: 'Schedules',
+ children: assetNode ? (
+ schedules.length > 0 && (
+
+ )
+ ) : (
+
+ ),
+ },
+ {
+ label: 'Freshness policy',
+ children: assetNode ? (
+ assetNode?.freshnessPolicy && (
+ {freshnessPolicyDescription(assetNode.freshnessPolicy)}
+ )
+ ) : (
+
+ ),
+ },
+ ];
+
+ if (
+ attributes.every((props) => isEmptyChildren(props.children)) &&
+ !cachedOrLiveAssetNode.automationCondition
+ ) {
+ return (
+
+ );
+ } else {
+ if (assetNode?.automationCondition && assetNode?.automationCondition.label) {
+ return (
+
+ );
+ }
+ }
+
+ return (
+
+ {attributes.map((props) => (
+
+ ))}
+
+ );
+};
diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/overview/Common.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/Common.tsx
new file mode 100644
index 0000000000000..b5f78d7104072
--- /dev/null
+++ b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/Common.tsx
@@ -0,0 +1,59 @@
+import {Body2, Box, Colors, Skeleton, Subtitle2} from '@dagster-io/ui-components';
+
+export const isEmptyChildren = (children: React.ReactNode) =>
+ !children || (children instanceof Array && children.length === 0);
+
+export const AttributeAndValue = ({
+ label,
+ children,
+}: {
+ label: React.ReactNode;
+ children: React.ReactNode;
+}) => {
+ if (isEmptyChildren(children)) {
+ return null;
+ }
+
+ return (
+
+ {label}
+
+ {children}
+
+
+ );
+};
+
+export const NoValue = () => –;
+
+export const SectionSkeleton = () => (
+
+
+
+
+
+);
+
+export const SectionEmptyState = ({
+ title,
+ description,
+ learnMoreLink,
+}: {
+ title: string;
+ description: string;
+ learnMoreLink: string;
+}) => (
+
+ {title}
+ {description}
+ {learnMoreLink ? (
+
+ Learn more
+
+ ) : undefined}
+
+);
diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/overview/ComputeDetailsSection.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/ComputeDetailsSection.tsx
new file mode 100644
index 0000000000000..8fe2ff1e87552
--- /dev/null
+++ b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/ComputeDetailsSection.tsx
@@ -0,0 +1,104 @@
+import {Box, ButtonLink, Colors, ConfigTypeSchema, Icon, Tag} from '@dagster-io/ui-components';
+import React from 'react';
+import {Link} from 'react-router-dom';
+
+import {metadataForAssetNode} from '../AssetMetadata';
+import {AttributeAndValue, SectionSkeleton} from './Common';
+import {showCustomAlert} from '../../app/CustomAlertProvider';
+import {COMMON_COLLATOR} from '../../app/Util';
+import {DagsterTypeSummary} from '../../dagstertype/DagsterType';
+import {RepoAddress} from '../../workspace/types';
+import {workspacePathFromAddress} from '../../workspace/workspacePath';
+import {UnderlyingOpsOrGraph} from '../UnderlyingOpsOrGraph';
+import {AssetNodeDefinitionFragment} from '../types/AssetNodeDefinition.types';
+
+export const ComputeDetailsSection = ({
+ repoAddress,
+ assetNode,
+}: {
+ repoAddress: RepoAddress | null;
+ assetNode: AssetNodeDefinitionFragment | null | undefined;
+}) => {
+ if (!assetNode) {
+ return ;
+ }
+ const {assetType} = metadataForAssetNode(assetNode);
+ const configType = assetNode?.configField?.configType;
+ const assetConfigSchema = configType && configType.key !== 'Any' ? configType : null;
+
+ return (
+
+
+
+
+
+
+
+ {assetNode.opVersion}
+
+
+ {[...assetNode.requiredResources]
+ .sort((a, b) => COMMON_COLLATOR.compare(a.resourceKey, b.resourceKey))
+ .map((resource) => (
+
+
+
+ {repoAddress ? (
+
+ {resource.resourceKey}
+
+ ) : (
+ resource.resourceKey
+ )}
+
+
+ ))}
+
+
+
+ {assetConfigSchema && (
+ {
+ showCustomAlert({
+ title: 'Config schema',
+ body: (
+
+ ),
+ });
+ }}
+ >
+ View config details
+
+ )}
+
+
+
+ {assetType && assetType.displayName !== 'Any' && (
+ {
+ showCustomAlert({
+ title: 'Type summary',
+ body: ,
+ });
+ }}
+ >
+ View type details
+
+ )}
+
+
+
+ {assetNode.backfillPolicy?.description}
+
+
+ );
+};
diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/overview/DefinitionSection.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/DefinitionSection.tsx
new file mode 100644
index 0000000000000..c5d53a674c4ac
--- /dev/null
+++ b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/DefinitionSection.tsx
@@ -0,0 +1,246 @@
+import {
+ Box,
+ ButtonLink,
+ Caption,
+ Colors,
+ Icon,
+ MiddleTruncate,
+ Tag,
+ Tooltip,
+} from '@dagster-io/ui-components';
+import dayjs from 'dayjs';
+import React from 'react';
+import {Link} from 'react-router-dom';
+import {UserDisplay} from 'shared/runs/UserDisplay.oss';
+import styled from 'styled-components';
+
+import {AssetDefinedInMultipleReposNotice} from '../AssetDefinedInMultipleReposNotice';
+import {AttributeAndValue} from './Common';
+import {showSharedToaster} from '../../app/DomUtils';
+import {useCopyToClipboard} from '../../app/browser';
+import {CodeLink, getCodeReferenceKey} from '../../code-links/CodeLink';
+import {AssetKind, isCanonicalStorageKindTag, isSystemTag} from '../../graph/KindTags';
+import {useStateWithStorage} from '../../hooks/useStateWithStorage';
+import {
+ isCanonicalCodeSourceEntry,
+ isCanonicalTableNameEntry,
+ isCanonicalUriEntry,
+} from '../../metadata/TableSchema';
+import {RepositoryLink} from '../../nav/RepositoryLink';
+import {buildTagString} from '../../ui/tagAsString';
+import {WorkspaceLocationNodeFragment} from '../../workspace/WorkspaceContext/types/WorkspaceQueries.types';
+import {RepoAddress} from '../../workspace/types';
+import {workspacePathFromAddress} from '../../workspace/workspacePath';
+import {AssetNodeDefinitionFragment} from '../types/AssetNodeDefinition.types';
+import {AssetTableDefinitionFragment} from '../types/AssetTableFragment.types';
+
+export const DefinitionSection = ({
+ repoAddress,
+ location,
+ assetNode,
+ cachedOrLiveAssetNode,
+}: {
+ repoAddress: RepoAddress | null;
+ location: WorkspaceLocationNodeFragment | undefined;
+ assetNode: AssetNodeDefinitionFragment | null | undefined;
+ cachedOrLiveAssetNode: AssetNodeDefinitionFragment | AssetTableDefinitionFragment;
+}) => {
+ const storageKindTag = cachedOrLiveAssetNode.tags?.find(isCanonicalStorageKindTag);
+ const filteredTags = cachedOrLiveAssetNode.tags?.filter(
+ (tag) => tag.key !== 'dagster/storage_kind',
+ );
+
+ const nonSystemTags = filteredTags?.filter((tag) => !isSystemTag(tag));
+ const systemTags = filteredTags?.filter(isSystemTag);
+
+ const tableNameMetadata = assetNode?.metadataEntries?.find(isCanonicalTableNameEntry);
+ const uriMetadata = assetNode?.metadataEntries?.find(isCanonicalUriEntry);
+ const codeSource = assetNode?.metadataEntries?.find(isCanonicalCodeSourceEntry);
+
+ return (
+
+
+
+
+ {cachedOrLiveAssetNode.groupName}
+
+
+
+
+
+
+
+
+ {location && (
+
+ Loaded {dayjs.unix(location.updatedTimestamp).fromNow()}
+
+ )}
+
+
+
+ {cachedOrLiveAssetNode.owners &&
+ cachedOrLiveAssetNode.owners.length > 0 &&
+ cachedOrLiveAssetNode.owners.map((owner, idx) =>
+ owner.__typename === 'UserAssetOwner' ? (
+
+
+
+ ) : (
+
+ {owner.team}
+
+ ),
+ )}
+
+
+ {cachedOrLiveAssetNode.computeKind && (
+
+ )}
+
+
+ {(cachedOrLiveAssetNode.kinds.length > 1 || !cachedOrLiveAssetNode.computeKind) &&
+ cachedOrLiveAssetNode.kinds.map((kind) => (
+
+ ))}
+
+
+ {(tableNameMetadata || uriMetadata || storageKindTag) && (
+
+ {tableNameMetadata && (
+
+
+
+
+ )}
+ {uriMetadata && (
+
+ {uriMetadata.__typename === 'TextMetadataEntry' ? (
+ uriMetadata.text
+ ) : (
+
+ {uriMetadata.url}
+
+ )}
+
+
+ )}
+ {storageKindTag && (
+
+ )}
+
+ )}
+
+
+ {filteredTags && filteredTags.length > 0 && (
+
+
+ {nonSystemTags.map((tag, idx) => (
+ {buildTagString(tag)}
+ ))}
+
+ {systemTags.length > 0 && }
+
+ )}
+
+
+ {codeSource &&
+ codeSource.codeReferences &&
+ codeSource.codeReferences.map((ref) => (
+
+ ))}
+
+
+ );
+};
+
+const SystemTagsToggle = ({tags}: {tags: Array<{key: string; value: string}>}) => {
+ const [shown, setShown] = useStateWithStorage('show-asset-definition-system-tags', Boolean);
+
+ if (!shown) {
+ return (
+
+ setShown(true)}>
+
+ Show system tags ({tags.length || 0})
+
+
+
+
+ );
+ } else {
+ return (
+
+
+ {tags.map((tag, idx) => (
+ {buildTagString(tag)}
+ ))}
+
+
+ setShown(false)}>
+
+ Hide system tags
+
+
+
+
+
+ );
+ }
+};
+
+const CopyButton = ({value}: {value: string}) => {
+ const copy = useCopyToClipboard();
+ const onCopy = async () => {
+ copy(value);
+ await showSharedToaster({
+ intent: 'success',
+ icon: 'copy_to_clipboard_done',
+ message: 'Copied!',
+ });
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+const UserAssetOwnerWrapper = styled.div`
+ > div {
+ background-color: ${Colors.backgroundGray()};
+ }
+`;
diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/overview/LineageSection.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/LineageSection.tsx
new file mode 100644
index 0000000000000..58180ae2cc721
--- /dev/null
+++ b/js_modules/dagster-ui/packages/ui-core/src/assets/overview/LineageSection.tsx
@@ -0,0 +1,99 @@
+import {Box, Button, MiddleTruncate, Subtitle2} from '@dagster-io/ui-components';
+import React, {useState} from 'react';
+import {Link} from 'react-router-dom';
+
+import {NoValue} from './Common';
+import {displayNameForAssetKey, sortAssetKeys, tokenForAssetKey} from '../../asset-graph/Utils';
+import {StatusDot} from '../../asset-graph/sidebar/StatusDot';
+import {AssetNodeForGraphQueryFragment} from '../../asset-graph/types/useAssetGraphData.types';
+import {DependsOnSelfBanner} from '../DependsOnSelfBanner';
+import {assetDetailsPathForKey} from '../assetDetailsPathForKey';
+
+export const LineageSection = ({
+ dependsOnSelf,
+ upstream,
+ downstream,
+}: {
+ upstream: AssetNodeForGraphQueryFragment[] | null;
+ downstream: AssetNodeForGraphQueryFragment[] | null;
+ dependsOnSelf: boolean;
+}) => {
+ return (
+ <>
+ {dependsOnSelf && (
+
+
+
+ )}
+
+
+
+ Upstream assets
+ {upstream?.length ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ Downstream assets
+ {downstream?.length ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ >
+ );
+};
+
+const AssetLinksWithStatus = ({
+ assets,
+ displayedByDefault = 20,
+}: {
+ assets: AssetNodeForGraphQueryFragment[];
+ displayedByDefault?: number;
+}) => {
+ const [displayedCount, setDisplayedCount] = useState(displayedByDefault);
+
+ const displayed = React.useMemo(
+ () => assets.sort((a, b) => sortAssetKeys(a.assetKey, b.assetKey)).slice(0, displayedCount),
+ [assets, displayedCount],
+ );
+
+ return (
+
+ {displayed.map((asset) => (
+
+
+
+
+
+
+ ))}
+
+ {displayed.length < assets.length ? (
+
+ ) : displayed.length > displayedByDefault ? (
+
+ ) : undefined}
+
+
+ );
+};