diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/DisclosureTriangleButton.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/DisclosureTriangleButton.tsx new file mode 100644 index 0000000000000..84113c2c056f1 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/components/DisclosureTriangleButton.tsx @@ -0,0 +1,22 @@ +import {Icon} from './Icon'; +import {UnstyledButton} from './UnstyledButton'; + +type Props = { + isOpen: boolean; + onToggle: (e: React.MouseEvent) => void; +}; +export const DisclosureTriangleButton = ({isOpen, onToggle}: Props) => { + return ( + { + onToggle(e); + }} + style={{cursor: 'pointer', width: 18}} + > + + + ); +}; diff --git a/js_modules/dagster-ui/packages/ui-components/src/index.ts b/js_modules/dagster-ui/packages/ui-components/src/index.ts index 250adda3a3d2a..03a4b26a3d10f 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/index.ts +++ b/js_modules/dagster-ui/packages/ui-components/src/index.ts @@ -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'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetExecutionButton.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetExecutionButton.tsx index b1478344ab159..f5fff846b7291 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetExecutionButton.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/LaunchAssetExecutionButton.tsx @@ -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) diff --git a/js_modules/dagster-ui/packages/ui-core/src/sensors/SensorDetails.tsx b/js_modules/dagster-ui/packages/ui-core/src/sensors/SensorDetails.tsx index 6ec9d55186a53..edde9fb657927 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/sensors/SensorDetails.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/sensors/SensorDetails.tsx @@ -1,3 +1,4 @@ +import {QueryResult} from '@apollo/client'; import { Box, Button, @@ -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'; @@ -53,11 +58,13 @@ export const SensorDetails = ({ repoAddress, daemonHealth, refreshState, + selectionQueryResult, }: { sensor: SensorFragment; repoAddress: RepoAddress; daemonHealth: boolean | null; refreshState: QueryRefreshState; + selectionQueryResult: QueryResult; }) => { const { name, @@ -154,7 +161,12 @@ export const SensorDetails = ({ Target - + ) : null} diff --git a/js_modules/dagster-ui/packages/ui-core/src/sensors/SensorRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/sensors/SensorRoot.tsx index b342d7d42da93..4572ba1f05965 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/sensors/SensorRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/sensors/SensorRoot.tsx @@ -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'; @@ -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 = ( @@ -122,6 +137,7 @@ export const SensorRoot = ({repoAddress}: {repoAddress: RepoAddress}) => { sensor={sensorOrError} daemonHealth={assetDaemonStatus.healthy} refreshState={refreshState} + selectionQueryResult={selectionQueryResult} /> { sensor={sensorOrError} daemonHealth={sensorDaemonStatus.healthy} refreshState={refreshState} + selectionQueryResult={selectionQueryResult} /> ; }) => { const repo = useRepository(repoAddress); - if (!targets) { + const assetSelectionResult = selectionQueryResult.data?.sensorOrError; + const assetSelectionData = + assetSelectionResult?.__typename === 'Sensor' ? assetSelectionResult : null; + + if (!targets && !assetSelectionData) { return ; } - const visibleTargets = targets.filter((target) => !isHiddenAssetGroupJob(target.pipelineName)); + const selectedAssets = assetSelectionData?.assetSelection; + + const visibleTargets = targets?.filter((target) => !isHiddenAssetGroupJob(target.pipelineName)); return ( - {visibleTargets.length < targets.length && A selection of assets} - {visibleTargets.map((target) => + {selectedAssets && ( + + )} + {visibleTargets?.map((target) => target.pipelineName ? ( ); }; + +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 ( + <> + setShowAssetSelection(false)} + style={{width: '750px', maxWidth: '80vw', minWidth: '500px', transform: 'scale(1)'}} + canOutsideClickClose + canEscapeKeyClose + > + + {sensorType === SensorType.AUTO_MATERIALIZE ? ( + <> +
+
+ + ) : ( +
+ )} + + + + +
+ { + setShowAssetSelection(true); + }} + > + {assetSelection.assetSelectionString} + + + ); +}; + +const Section = ({ + assets, + title, + titleBorder = 'top-and-bottom', +}: { + assets: SensorAssetSelectionFragment['assets']; + title?: string; + titleBorder?: React.ComponentProps['border']; +}) => { + const [isOpen, setIsOpen] = React.useState(true); + return ( + <> + {title ? ( + + { + setIsOpen(!isOpen); + }} + > + {}} isOpen={isOpen} /> + + {title} ({numberFormatter.format(assets.length)}) + + + + ) : null} + {isOpen ? ( + assets.length ? ( +
+ } + itemBorders + /> +
+ ) : ( + + 0 assets + + ) + ) : null} + + ); +}; + +const VirtualizedSelectedAssetRow = ({ + asset, +}: { + asset: SensorAssetSelectionFragment['assets'][0]; +}) => { + return ( + + + + + + + + ); +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/sensors/types/SensorRoot.types.ts b/js_modules/dagster-ui/packages/ui-core/src/sensors/types/SensorRoot.types.ts index c9b9422a3c500..aba4632239d33 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/sensors/types/SensorRoot.types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/sensors/types/SensorRoot.types.ts @@ -128,3 +128,63 @@ export type SensorRootQuery = { }; }; }; + +export type SensorAssetSelectionQueryVariables = Types.Exact<{ + sensorSelector: Types.SensorSelector; +}>; + +export type SensorAssetSelectionQuery = { + __typename: 'Query'; + sensorOrError: + | { + __typename: 'PythonError'; + message: string; + stack: Array; + errorChain: Array<{ + __typename: 'ErrorChainLink'; + isExplicitLink: boolean; + error: {__typename: 'PythonError'; message: string; stack: Array}; + }>; + } + | { + __typename: 'Sensor'; + id: string; + assetSelection: { + __typename: 'AssetSelection'; + assetSelectionString: string | null; + assets: Array<{ + __typename: 'Asset'; + id: string; + key: {__typename: 'AssetKey'; path: Array}; + definition: { + __typename: 'AssetNode'; + id: string; + autoMaterializePolicy: { + __typename: 'AutoMaterializePolicy'; + policyType: Types.AutoMaterializePolicyType; + } | null; + } | null; + }>; + } | null; + } + | {__typename: 'SensorNotFoundError'} + | {__typename: 'UnauthorizedError'}; +}; + +export type SensorAssetSelectionFragment = { + __typename: 'AssetSelection'; + assetSelectionString: string | null; + assets: Array<{ + __typename: 'Asset'; + id: string; + key: {__typename: 'AssetKey'; path: Array}; + definition: { + __typename: 'AssetNode'; + id: string; + autoMaterializePolicy: { + __typename: 'AutoMaterializePolicy'; + policyType: Types.AutoMaterializePolicyType; + } | null; + } | null; + }>; +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/ui/VirtualizedItemListForDialog.tsx b/js_modules/dagster-ui/packages/ui-core/src/ui/VirtualizedItemListForDialog.tsx index 442cbc560dfd9..c815a9c972431 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/ui/VirtualizedItemListForDialog.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/ui/VirtualizedItemListForDialog.tsx @@ -8,9 +8,15 @@ interface Props { items: T[]; renderItem: (item: T) => React.ReactNode; itemBorders?: boolean; + padding?: React.CSSProperties['padding']; } -export function VirtualizedItemListForDialog({items, renderItem, itemBorders = true}: Props) { +export function VirtualizedItemListForDialog({ + items, + renderItem, + itemBorders = true, + padding = '8px 24px', +}: Props) { const container = React.useRef(null); const rowVirtualizer = useVirtualizer({ @@ -24,12 +30,18 @@ export function VirtualizedItemListForDialog({items, renderItem, itemBorders const virtualItems = rowVirtualizer.getVirtualItems(); return ( - + {virtualItems.map(({index, key, size, start}) => { const assetKey = items[index]!; return ( - + { height, } = props; - const [querySensor, queryResult] = useLazyQuery( - SINGLE_SENSOR_QUERY, - { - variables: { - selector: { - repositoryName: repoAddress.name, - repositoryLocationName: repoAddress.location, - sensorName: name, - }, + const [querySensor, sensorQueryResult] = useLazyQuery< + SingleSensorQuery, + SingleSensorQueryVariables + >(SINGLE_SENSOR_QUERY, { + variables: { + selector: { + repositoryName: repoAddress.name, + repositoryLocationName: repoAddress.location, + sensorName: name, + }, + }, + }); + + const [querySensorAssetSelection, sensorAssetSelectionQueryResult] = useLazyQuery< + SensorAssetSelectionQuery, + SensorAssetSelectionQueryVariables + >(SENSOR_ASSET_SELECTIONS_QUERY, { + variables: { + sensorSelector: { + repositoryName: repoAddress.name, + repositoryLocationName: repoAddress.location, + sensorName: name, }, }, + }); + + useDelayedRowQuery( + React.useCallback(() => { + querySensor(); + querySensorAssetSelection(); + }, [querySensor, querySensorAssetSelection]), ); - useDelayedRowQuery(querySensor); - useQueryRefreshAtInterval(queryResult, FIFTEEN_SECONDS); + useQueryRefreshAtInterval(sensorQueryResult, FIFTEEN_SECONDS); + useQueryRefreshAtInterval(sensorAssetSelectionQueryResult, FIFTEEN_SECONDS); - const {data} = queryResult; + const {data} = sensorQueryResult; const sensorData = React.useMemo(() => { if (data?.sensorOrError.__typename !== 'Sensor') { @@ -146,19 +171,28 @@ export const VirtualizedSensorRow = (props: SensorRowProps) => { - {sensorInfo ? ( - sensorInfo.description ? ( - +
+ {sensorInfo ? ( + sensorInfo.description ? ( + + {sensorInfo.name} + + ) : ( {sensorInfo.name} - - ) : ( - {sensorInfo.name} - ) - ) : null} + ) + ) : null} +
- + {sensorData ? ( + + ) : null} @@ -175,7 +209,7 @@ export const VirtualizedSensorRow = (props: SensorRowProps) => { {humanizeSensorInterval(sensorData.minIntervalSeconds)} ) : ( - + )} @@ -184,7 +218,7 @@ export const VirtualizedSensorRow = (props: SensorRowProps) => { ) : ( - + )} @@ -197,7 +231,7 @@ export const VirtualizedSensorRow = (props: SensorRowProps) => { showSummary={false} /> ) : ( - + )} diff --git a/js_modules/dagster-ui/packages/ui-core/src/workspace/VirtualizedWorkspaceTable.tsx b/js_modules/dagster-ui/packages/ui-core/src/workspace/VirtualizedWorkspaceTable.tsx index c32c581bc1226..b9a29c1cf5f74 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/workspace/VirtualizedWorkspaceTable.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/workspace/VirtualizedWorkspaceTable.tsx @@ -1,4 +1,4 @@ -import {LazyQueryExecFunction, QueryResult} from '@apollo/client'; +import {QueryResult} from '@apollo/client'; import {Caption, Colors} from '@dagster-io/ui-components'; import * as React from 'react'; import styled from 'styled-components'; @@ -78,12 +78,14 @@ const CaptionTextContainer = styled.div` const JOB_QUERY_DELAY = 100; -export const useDelayedRowQuery = (lazyQueryFn: LazyQueryExecFunction) => { +export const useDelayedRowQuery = (lazyQueryFn: () => void) => { React.useEffect(() => { const timer = setTimeout(() => { lazyQueryFn(); }, JOB_QUERY_DELAY); - return () => clearTimeout(timer); + return () => { + clearTimeout(timer); + }; }, [lazyQueryFn]); };