Skip to content

Commit

Permalink
[Sensors] Show targeted assets (#19674)
Browse files Browse the repository at this point in the history
## Summary & Motivation
Use the `assetSelectionString` from the backend to describe the targeted
assets and show a full list of the targeted assets in a modal.


## How I Tested These Changes
<img width="841" alt="Screenshot 2024-02-13 at 10 46 27 AM"
src="https://github.com/dagster-io/dagster/assets/2286579/38e55b04-85b4-4cfb-bdd1-849d3d90d99b">



<img width="834" alt="Screenshot 2024-02-08 at 10 14 40 AM"
src="https://github.com/dagster-io/dagster/assets/2286579/611951b9-5ed2-4dce-aa98-fe1932b22869">
<img width="667" alt="Screenshot 2024-02-08 at 10 14 30 AM"
src="https://github.com/dagster-io/dagster/assets/2286579/c0baeb53-85cb-405d-971e-f6e0713ac664">
<img width="533" alt="Screenshot 2024-02-08 at 10 14 21 AM"
src="https://github.com/dagster-io/dagster/assets/2286579/383d8b3f-e6f9-4ee7-9a40-7b88c628931d">
  • Loading branch information
salazarm authored Feb 13, 2024
1 parent 7db9001 commit 45423fe
Show file tree
Hide file tree
Showing 10 changed files with 414 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {Icon} from './Icon';
import {UnstyledButton} from './UnstyledButton';

type Props = {
isOpen: boolean;
onToggle: (e: React.MouseEvent<HTMLButtonElement>) => void;
};
export const DisclosureTriangleButton = ({isOpen, onToggle}: Props) => {
return (
<UnstyledButton
onClick={(e) => {
onToggle(e);
}}
style={{cursor: 'pointer', width: 18}}
>
<Icon
name="arrow_drop_down"
style={{transform: isOpen ? 'rotate(0deg)' : 'rotate(-90deg)'}}
/>
</UnstyledButton>
);
};
1 change: 1 addition & 0 deletions js_modules/dagster-ui/packages/ui-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export * from './components/TagSelector';
export * from './components/Text';
export * from './components/TextInput';
export * from './components/Toaster';
export * from './components/DisclosureTriangleButton';
export * from './components/TokenizingField';
export * from './components/Tooltip';
export * from './components/Trace';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ function optionsForButton(scope: AssetsInScope): LaunchOption[] {
export function executionDisabledMessageForAssets(
assets: {isSource: boolean; isExecutable: boolean; hasMaterializePermission: boolean}[],
) {
if (!assets.length) {
return null;
}
return assets.some((a) => !a.hasMaterializePermission)
? 'You do not have permission to materialize assets'
: assets.every((a) => a.isSource)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {QueryResult} from '@apollo/client';
import {
Box,
Button,
Expand All @@ -16,6 +17,10 @@ import {SensorResetButton} from './SensorResetButton';
import {SensorSwitch} from './SensorSwitch';
import {SensorTargetList} from './SensorTargetList';
import {SensorFragment} from './types/SensorFragment.types';
import {
SensorAssetSelectionQuery,
SensorAssetSelectionQueryVariables,
} from './types/SensorRoot.types';
import {QueryRefreshCountdown, QueryRefreshState} from '../app/QueryRefresh';
import {InstigationStatus, SensorType} from '../graphql/types';
import {RepositoryLink} from '../nav/RepositoryLink';
Expand Down Expand Up @@ -53,11 +58,13 @@ export const SensorDetails = ({
repoAddress,
daemonHealth,
refreshState,
selectionQueryResult,
}: {
sensor: SensorFragment;
repoAddress: RepoAddress;
daemonHealth: boolean | null;
refreshState: QueryRefreshState;
selectionQueryResult: QueryResult<SensorAssetSelectionQuery, SensorAssetSelectionQueryVariables>;
}) => {
const {
name,
Expand Down Expand Up @@ -154,7 +161,12 @@ export const SensorDetails = ({
<tr>
<td>Target</td>
<td>
<SensorTargetList targets={sensor.targets} repoAddress={repoAddress} />
<SensorTargetList
targets={sensor.targets}
repoAddress={repoAddress}
selectionQueryResult={selectionQueryResult}
sensorType={sensor.sensorType}
/>
</td>
</tr>
) : null}
Expand Down
54 changes: 51 additions & 3 deletions js_modules/dagster-ui/packages/ui-core/src/sensors/SensorRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import {SENSOR_FRAGMENT} from './SensorFragment';
import {SensorInfo} from './SensorInfo';
import {SensorPageAutomaterialize} from './SensorPageAutomaterialize';
import {SensorPreviousRuns} from './SensorPreviousRuns';
import {SensorRootQuery, SensorRootQueryVariables} from './types/SensorRoot.types';
import {
SensorAssetSelectionQuery,
SensorAssetSelectionQueryVariables,
SensorRootQuery,
SensorRootQueryVariables,
} from './types/SensorRoot.types';
import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorFragment';
import {PythonErrorInfo} from '../app/PythonErrorInfo';
import {FIFTEEN_SECONDS, useQueryRefreshAtInterval} from '../app/QueryRefresh';
import {FIFTEEN_SECONDS, useMergedRefresh, useQueryRefreshAtInterval} from '../app/QueryRefresh';
import {useTrackPageView} from '../app/analytics';
import {InstigationTickStatus, SensorType} from '../graphql/types';
import {useDocumentTitle} from '../hooks/useDocumentTitle';
Expand Down Expand Up @@ -62,8 +67,18 @@ export const SensorRoot = ({repoAddress}: {repoAddress: RepoAddress}) => {
variables: {sensorSelector},
notifyOnNetworkStatusChange: true,
});
const selectionQueryResult = useQuery<
SensorAssetSelectionQuery,
SensorAssetSelectionQueryVariables
>(SENSOR_ASSET_SELECTIONS_QUERY, {
variables: {sensorSelector},
notifyOnNetworkStatusChange: true,
});

const refreshState1 = useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS);
const refreshState2 = useQueryRefreshAtInterval(selectionQueryResult, FIFTEEN_SECONDS);
const refreshState = useMergedRefresh(refreshState1, refreshState2);

const refreshState = useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS);
const {data, loading} = queryResult;

const tabs = (
Expand Down Expand Up @@ -122,6 +137,7 @@ export const SensorRoot = ({repoAddress}: {repoAddress: RepoAddress}) => {
sensor={sensorOrError}
daemonHealth={assetDaemonStatus.healthy}
refreshState={refreshState}
selectionQueryResult={selectionQueryResult}
/>
<SensorPageAutomaterialize
repoAddress={repoAddress}
Expand All @@ -142,6 +158,7 @@ export const SensorRoot = ({repoAddress}: {repoAddress: RepoAddress}) => {
sensor={sensorOrError}
daemonHealth={sensorDaemonStatus.healthy}
refreshState={refreshState}
selectionQueryResult={selectionQueryResult}
/>
<SensorInfo
sensorDaemonStatus={sensorDaemonStatus}
Expand Down Expand Up @@ -197,3 +214,34 @@ const SENSOR_ROOT_QUERY = gql`
${PYTHON_ERROR_FRAGMENT}
${INSTANCE_HEALTH_FRAGMENT}
`;

export const SENSOR_ASSET_SELECTIONS_QUERY = gql`
query SensorAssetSelectionQuery($sensorSelector: SensorSelector!) {
sensorOrError(sensorSelector: $sensorSelector) {
... on Sensor {
id
assetSelection {
...SensorAssetSelectionFragment
}
}
...PythonErrorFragment
}
}
fragment SensorAssetSelectionFragment on AssetSelection {
assetSelectionString
assets {
id
key {
path
}
definition {
id
autoMaterializePolicy {
policyType
}
}
}
}
${PYTHON_ERROR_FRAGMENT}
`;
Original file line number Diff line number Diff line change
@@ -1,28 +1,64 @@
import {Box} from '@dagster-io/ui-components';
import {QueryResult} from '@apollo/client';
import {
Box,
Button,
ButtonLink,
Caption,
Colors,
Dialog,
DialogFooter,
DisclosureTriangleButton,
MiddleTruncate,
Subtitle2,
} from '@dagster-io/ui-components';
import React from 'react';
import {Link} from 'react-router-dom';

import {isHiddenAssetGroupJob} from '../asset-graph/Utils';
import {
SensorAssetSelectionFragment,
SensorAssetSelectionQuery,
SensorAssetSelectionQueryVariables,
} from './types/SensorRoot.types';
import {COMMON_COLLATOR} from '../app/Util';
import {displayNameForAssetKey, isHiddenAssetGroupJob} from '../asset-graph/Utils';
import {assetDetailsPathForKey} from '../assets/assetDetailsPathForKey';
import {SensorType} from '../graphql/types';
import {PipelineReference} from '../pipelines/PipelineReference';
import {VirtualizedItemListForDialog} from '../ui/VirtualizedItemListForDialog';
import {numberFormatter} from '../ui/formatters';
import {isThisThingAJob, useRepository} from '../workspace/WorkspaceContext';
import {RepoAddress} from '../workspace/types';

export const SensorTargetList = ({
sensorType,
targets,
selectionQueryResult,
repoAddress,
}: {
sensorType: SensorType;
targets: {pipelineName: string}[] | null | undefined;
repoAddress: RepoAddress;
selectionQueryResult: QueryResult<SensorAssetSelectionQuery, SensorAssetSelectionQueryVariables>;
}) => {
const repo = useRepository(repoAddress);
if (!targets) {
const assetSelectionResult = selectionQueryResult.data?.sensorOrError;
const assetSelectionData =
assetSelectionResult?.__typename === 'Sensor' ? assetSelectionResult : null;

if (!targets && !assetSelectionData) {
return <span />;
}

const visibleTargets = targets.filter((target) => !isHiddenAssetGroupJob(target.pipelineName));
const selectedAssets = assetSelectionData?.assetSelection;

const visibleTargets = targets?.filter((target) => !isHiddenAssetGroupJob(target.pipelineName));

return (
<Box flex={{direction: 'column', gap: 2}}>
{visibleTargets.length < targets.length && <span>A selection of assets</span>}
{visibleTargets.map((target) =>
{selectedAssets && (
<AssetSelectionLink assetSelection={selectedAssets} sensorType={sensorType} />
)}
{visibleTargets?.map((target) =>
target.pipelineName ? (
<PipelineReference
key={target.pipelineName}
Expand All @@ -35,3 +71,147 @@ export const SensorTargetList = ({
</Box>
);
};

const AssetSelectionLink = ({
assetSelection,
sensorType,
}: {
assetSelection: SensorAssetSelectionFragment;
sensorType: SensorType;
}) => {
const [showAssetSelection, setShowAssetSelection] = React.useState(false);

const sortedAssets = React.useMemo(() => {
return assetSelection.assets
.slice()
.sort((a, b) =>
COMMON_COLLATOR.compare(displayNameForAssetKey(a.key), displayNameForAssetKey(b.key)),
);
}, [assetSelection.assets]);

const assetsWithAMP = React.useMemo(
() => sortedAssets.filter((asset) => !!asset.definition?.autoMaterializePolicy),
[sortedAssets],
);
const assetsWithoutAMP = React.useMemo(
() => sortedAssets.filter((asset) => !asset.definition?.autoMaterializePolicy),
[sortedAssets],
);

return (
<>
<Dialog
isOpen={showAssetSelection}
title="Targeted assets"
onClose={() => setShowAssetSelection(false)}
style={{width: '750px', maxWidth: '80vw', minWidth: '500px', transform: 'scale(1)'}}
canOutsideClickClose
canEscapeKeyClose
>
<Box flex={{direction: 'column'}}>
{sensorType === SensorType.AUTO_MATERIALIZE ? (
<>
<Section
title="Assets with a materialization policy"
titleBorder="bottom"
assets={assetsWithAMP}
/>
<Section
title="Assets without a materialization policy"
titleBorder="top-and-bottom"
assets={assetsWithoutAMP}
/>
</>
) : (
<Section assets={sortedAssets} />
)}
</Box>
<DialogFooter topBorder>
<Button
intent="primary"
onClick={() => {
setShowAssetSelection(false);
}}
>
Close
</Button>
</DialogFooter>
</Dialog>
<ButtonLink
onClick={() => {
setShowAssetSelection(true);
}}
>
{assetSelection.assetSelectionString}
</ButtonLink>
</>
);
};

const Section = ({
assets,
title,
titleBorder = 'top-and-bottom',
}: {
assets: SensorAssetSelectionFragment['assets'];
title?: string;
titleBorder?: React.ComponentProps<typeof Box>['border'];
}) => {
const [isOpen, setIsOpen] = React.useState(true);
return (
<>
{title ? (
<Box border={titleBorder} padding={{right: 24, vertical: 12}}>
<Box
flex={{direction: 'row', gap: 4}}
style={{cursor: 'pointer'}}
onClick={() => {
setIsOpen(!isOpen);
}}
>
<DisclosureTriangleButton onToggle={() => {}} isOpen={isOpen} />
<Subtitle2>
{title} ({numberFormatter.format(assets.length)})
</Subtitle2>
</Box>
</Box>
) : null}
{isOpen ? (
assets.length ? (
<div style={{maxHeight: '300px', overflowY: 'scroll'}}>
<VirtualizedItemListForDialog
padding={0}
items={assets}
renderItem={(asset) => <VirtualizedSelectedAssetRow asset={asset} key={asset.id} />}
itemBorders
/>
</div>
) : (
<Box padding={{horizontal: 24, vertical: 12}}>
<Caption color={Colors.textLight()}>0 assets</Caption>
</Box>
)
) : null}
</>
);
};

const VirtualizedSelectedAssetRow = ({
asset,
}: {
asset: SensorAssetSelectionFragment['assets'][0];
}) => {
return (
<Box
flex={{alignItems: 'center', gap: 4}}
style={{cursor: 'pointer'}}
padding={{horizontal: 24}}
>
<Link to={assetDetailsPathForKey(asset.key)} target="_blank">
<Box style={{overflow: 'hidden'}}>
<MiddleTruncate text={displayNameForAssetKey(asset.key)} />
</Box>
</Link>
</Box>
);
};
Loading

2 comments on commit 45423fe

@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-storybook ready!

✅ Preview
https://dagit-storybook-86kam76lp-elementl.vercel.app

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

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

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

Please sign in to comment.