diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/Icon.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/Icon.tsx index 892968b136034..bebb9da166781 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/components/Icon.tsx +++ b/js_modules/dagster-ui/packages/ui-components/src/components/Icon.tsx @@ -79,6 +79,7 @@ import graph_neighbors from '../icon-svgs/graph_neighbors.svg'; import graph_upstream from '../icon-svgs/graph_upstream.svg'; import history from '../icon-svgs/history.svg'; import history_toggle_off from '../icon-svgs/history_toggle_off.svg'; +import hourglass from '../icon-svgs/hourglass.svg'; import hourglass_bottom from '../icon-svgs/hourglass_bottom.svg'; import id from '../icon-svgs/id.svg'; import infinity from '../icon-svgs/infinity.svg'; @@ -96,6 +97,7 @@ import materialization from '../icon-svgs/materialization.svg'; import menu from '../icon-svgs/menu.svg'; import menu_book from '../icon-svgs/menu_book.svg'; import more_horiz from '../icon-svgs/more_horiz.svg'; +import multi_asset from '../icon-svgs/multi_asset.svg'; import nightlight from '../icon-svgs/nightlight.svg'; import no_access from '../icon-svgs/no_access.svg'; import observation from '../icon-svgs/observation.svg'; @@ -176,6 +178,7 @@ export const Icons = { materialization, observation, job, + multi_asset, op, op_selector, op_dynamic: bolt, @@ -276,6 +279,7 @@ export const Icons = { info, history, history_toggle_off, + hourglass, hourglass_bottom, layers, line_style, diff --git a/js_modules/dagster-ui/packages/ui-components/src/icon-svgs/hourglass.svg b/js_modules/dagster-ui/packages/ui-components/src/icon-svgs/hourglass.svg new file mode 100644 index 0000000000000..a98c7cecec084 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/icon-svgs/hourglass.svg @@ -0,0 +1,3 @@ + + + diff --git a/js_modules/dagster-ui/packages/ui-components/src/icon-svgs/multi_asset.svg b/js_modules/dagster-ui/packages/ui-components/src/icon-svgs/multi_asset.svg new file mode 100644 index 0000000000000..71ad3093ee9d4 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/icon-svgs/multi_asset.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSensorsRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSensorsRoot.tsx index 1869507151330..ba9df517d0c7f 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSensorsRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSensorsRoot.tsx @@ -9,7 +9,7 @@ import { TextInput, Tooltip, } from '@dagster-io/ui-components'; -import {useContext, useMemo} from 'react'; +import {useContext, useMemo, useState} from 'react'; import {BASIC_INSTIGATION_STATE_FRAGMENT} from './BasicInstigationStateFragment'; import {OverviewSensorTable} from './OverviewSensorsTable'; @@ -24,6 +24,7 @@ import {visibleRepoKeys} from './visibleRepoKeys'; import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorFragment'; import {FIFTEEN_SECONDS, useQueryRefreshAtInterval} from '../app/QueryRefresh'; import {useTrackPageView} from '../app/analytics'; +import {SensorType} from '../graphql/types'; import {useDocumentTitle} from '../hooks/useDocumentTitle'; import {useQueryPersistedState} from '../hooks/useQueryPersistedState'; import {useSelectionReducer} from '../hooks/useSelectionReducer'; @@ -36,12 +37,33 @@ import {CheckAllBox} from '../ui/CheckAllBox'; import {useFilters} from '../ui/Filters'; import {useCodeLocationFilter} from '../ui/Filters/useCodeLocationFilter'; import {useInstigationStatusFilter} from '../ui/Filters/useInstigationStatusFilter'; +import {useStaticSetFilter} from '../ui/Filters/useStaticSetFilter'; import {SearchInputSpinner} from '../ui/SearchInputSpinner'; +import {SENSOR_TYPE_META} from '../workspace/VirtualizedSensorRow'; import {WorkspaceContext} from '../workspace/WorkspaceContext'; import {buildRepoAddress} from '../workspace/buildRepoAddress'; import {repoAddressAsHumanString} from '../workspace/repoAddressAsString'; import {RepoAddress} from '../workspace/types'; +function toSetFilterValue(type: SensorType) { + const label = SENSOR_TYPE_META[type].name; + return { + label, + value: {type, label}, + match: [label], + }; +} + +const SENSOR_TYPE_TO_FILTER: Partial>> = { + [SensorType.ASSET]: toSetFilterValue(SensorType.ASSET), + [SensorType.AUTOMATION_POLICY]: toSetFilterValue(SensorType.AUTOMATION_POLICY), + [SensorType.FRESHNESS_POLICY]: toSetFilterValue(SensorType.FRESHNESS_POLICY), + [SensorType.MULTI_ASSET]: toSetFilterValue(SensorType.MULTI_ASSET), + [SensorType.RUN_STATUS]: toSetFilterValue(SensorType.RUN_STATUS), + [SensorType.STANDARD]: toSetFilterValue(SensorType.STANDARD), +}; +const ALL_SENSOR_TYPE_FILTERS = Object.values(SENSOR_TYPE_TO_FILTER); + export const OverviewSensorsRoot = () => { useTrackPageView(); useDocumentTitle('Overview | Sensors'); @@ -56,9 +78,26 @@ export const OverviewSensorsRoot = () => { const codeLocationFilter = useCodeLocationFilter(); const runningStateFilter = useInstigationStatusFilter(); + const [sensorTypes, setSensorTypes] = useState>(() => new Set()); + + const sensorTypeFilter = useStaticSetFilter({ + name: 'Sensor type', + allValues: ALL_SENSOR_TYPE_FILTERS, + icon: 'sensors', + getStringValue: (value) => value.label, + initialState: useMemo(() => { + return new Set(Array.from(sensorTypes).map((type) => SENSOR_TYPE_TO_FILTER[type]!.value)); + }, [sensorTypes]), + + renderLabel: ({value}) => {value.label}, + onStateChanged: (state) => { + setSensorTypes(new Set(Array.from(state).map((value) => value.type))); + }, + }); + const filters = useMemo( - () => [codeLocationFilter, runningStateFilter], - [codeLocationFilter, runningStateFilter], + () => [codeLocationFilter, runningStateFilter, sensorTypeFilter], + [codeLocationFilter, runningStateFilter, sensorTypeFilter], ); const {button: filterButton, activeFiltersJsx} = useFilters({filters}); @@ -81,16 +120,23 @@ export const OverviewSensorsRoot = () => { }, [data, visibleRepos]); const {state: runningState} = runningStateFilter; + const filteredBuckets = useMemo(() => { return repoBuckets.map(({sensors, ...rest}) => { return { ...rest, - sensors: runningState.size - ? sensors.filter(({sensorState}) => runningState.has(sensorState.status)) - : sensors, + sensors: sensors.filter(({sensorState, sensorType}) => { + if (runningState.size && !runningState.has(sensorState.status)) { + return false; + } + if (sensorTypes.size && !sensorTypes.has(sensorType)) { + return false; + } + return true; + }), }; }); - }, [repoBuckets, runningState]); + }, [repoBuckets, runningState, sensorTypes]); const sanitizedSearch = searchValue.trim().toLocaleLowerCase(); const anySearch = sanitizedSearch.length > 0; @@ -306,7 +352,7 @@ export const OverviewSensorsRoot = () => { type RepoBucket = { repoAddress: RepoAddress; - sensors: {name: string; sensorState: BasicInstigationStateFragment}[]; + sensors: {name: string; sensorType: SensorType; sensorState: BasicInstigationStateFragment}[]; }; const buildBuckets = (data?: OverviewSensorsQuery): RepoBucket[] => { diff --git a/js_modules/dagster-ui/packages/ui-core/src/workspace/VirtualizedSensorRow.tsx b/js_modules/dagster-ui/packages/ui-core/src/workspace/VirtualizedSensorRow.tsx index a930718647017..33014cb4673f9 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/workspace/VirtualizedSensorRow.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/workspace/VirtualizedSensorRow.tsx @@ -1,5 +1,14 @@ import {gql, useLazyQuery} from '@apollo/client'; -import {Box, Caption, Checkbox, Colors, MiddleTruncate, Tooltip} from '@dagster-io/ui-components'; +import { + Box, + Caption, + Checkbox, + Colors, + IconName, + MiddleTruncate, + Tag, + Tooltip, +} from '@dagster-io/ui-components'; import * as React from 'react'; import {Link} from 'react-router-dom'; import styled from 'styled-components'; @@ -9,7 +18,7 @@ import {RepoAddress} from './types'; import {SingleSensorQuery, SingleSensorQueryVariables} from './types/VirtualizedSensorRow.types'; import {workspacePathFromAddress} from './workspacePath'; import {FIFTEEN_SECONDS, useQueryRefreshAtInterval} from '../app/QueryRefresh'; -import {InstigationStatus} from '../graphql/types'; +import {InstigationStatus, SensorType} from '../graphql/types'; import {LastRunSummary} from '../instance/LastRunSummary'; import {TICK_TAG_FRAGMENT} from '../instigation/InstigationTick'; import {BasicInstigationStateFragment} from '../overview/types/BasicInstigationStateFragment.types'; @@ -20,8 +29,8 @@ import {SensorTargetList} from '../sensors/SensorTargetList'; import {TickStatusTag} from '../ticks/TickStatusTag'; import {HeaderCell, Row, RowCell} from '../ui/VirtualizedTable'; -const TEMPLATE_COLUMNS_WITH_CHECKBOX = '60px 1.5fr 1fr 76px 120px 148px 180px'; -const TEMPLATE_COLUMNS = '1.5fr 1fr 76px 120px 148px 180px'; +const TEMPLATE_COLUMNS_WITH_CHECKBOX = '60px 1.5fr 120px 1fr 76px 120px 148px 180px'; +const TEMPLATE_COLUMNS = '1.5fr 120px 1fr 76px 120px 148px 180px'; interface SensorRowProps { name: string; @@ -94,6 +103,9 @@ export const VirtualizedSensorRow = (props: SensorRowProps) => { const tick = sensorData?.sensorState.ticks[0]; + const sensorType = sensorData?.sensorType; + const sensorInfo = sensorType ? SENSOR_TYPE_META[sensorType] : null; + return ( @@ -133,6 +145,17 @@ export const VirtualizedSensorRow = (props: SensorRowProps) => { + + {sensorInfo ? ( + sensorInfo.description ? ( + + {sensorInfo.name} + + ) : ( + {sensorInfo.name} + ) + ) : null} + @@ -201,6 +224,7 @@ export const VirtualizedSensorHeader = (props: {checkbox: React.ReactNode}) => { ) : null} Name + Type Target Running Frequency @@ -217,6 +241,49 @@ const RowGrid = styled(Box)<{$showCheckboxColumn: boolean}>` height: 100%; `; +export const SENSOR_TYPE_META: Record< + SensorType, + {name: string; icon: IconName; description: string | null} +> = { + [SensorType.ASSET]: { + name: 'Asset', + icon: 'asset', + description: 'Asset sensors instigate runs when a materialization occurs', + }, + [SensorType.AUTOMATION_POLICY]: { + name: 'Automation', + icon: 'hourglass', + description: 'Automation policy sensors react to defined automation policy conditions', + }, + [SensorType.FRESHNESS_POLICY]: { + name: 'Freshness policy', + icon: 'hourglass', + description: + 'Freshness sensors check the freshness of assets on each tick, then perform an action in response to that status', + }, + [SensorType.MULTI_ASSET]: { + name: 'Multi-asset', + icon: 'multi_asset', + description: + 'Multi asset sensors trigger job executions based on multiple asset materialization event streams', + }, + [SensorType.RUN_STATUS]: { + name: 'Run status', + icon: 'alternate_email', + description: 'Run status sensors react to run status', + }, + [SensorType.STANDARD]: { + name: 'Standard', + icon: 'sensors', + description: null, + }, + [SensorType.UNKNOWN]: { + name: 'Standard', + icon: 'sensors', + description: null, + }, +}; + const SINGLE_SENSOR_QUERY = gql` query SingleSensorQuery($selector: SensorSelector!) { sensorOrError(sensorSelector: $selector) {