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) {