diff --git a/js_modules/dagster-ui/packages/ui-core/client.json b/js_modules/dagster-ui/packages/ui-core/client.json index b9b76909965ac..abacf64582612 100644 --- a/js_modules/dagster-ui/packages/ui-core/client.json +++ b/js_modules/dagster-ui/packages/ui-core/client.json @@ -75,7 +75,6 @@ "LaunchedRunListQuery": "c4a50f5d4e56aff9965413816c16db2489bf487d5dacd786bfadfad508f8f32b", "SelectedTickQuery": "4a6a1911d0769b8b5bb17ed1415d3691da3d029d6760ab42dc56de6431fc1fb6", "TickHistoryQuery": "4dff0791129120937abefb56bf6b21102cd3b67f81f7285763f01d6467f850e8", - "OverviewJobsQuery": "7de05cca36088c46f8dbd3f995d643fc7e79f240eae0bc614bcf68a0729fe7d9", "ConfigPartitionsQuery": "b2982b87aa317ad4df2f4227ac4285280de352fa571e952f56f9f85e2a0096fc", "ConfigPartitionsAssetsQuery": "02438f2590d14870b0df3107680c2d33da2c7a492a3f8a507c591f7ad4555409", "ConfigPartitionForAssetJobQuery": "367eaeeb62b9e2339ab6c07a1e315310fd1a095b7ba7c8fa7a1e51282ca84796", diff --git a/js_modules/dagster-ui/packages/ui-core/src/jobs/JobsPageContent.tsx b/js_modules/dagster-ui/packages/ui-core/src/jobs/JobsPageContent.tsx index c9c6f90cc38e8..4e10016108835 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/jobs/JobsPageContent.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/jobs/JobsPageContent.tsx @@ -1,93 +1,132 @@ import { Box, Colors, + Icon, NonIdealState, Spinner, SpinnerWithText, - TextInput, } from '@dagster-io/ui-components'; -import {useContext, useMemo} from 'react'; +import {useCallback, useContext, useMemo} from 'react'; -import {gql, useQuery} from '../apollo-client'; -import {OverviewJobsQuery, OverviewJobsQueryVariables} from './types/JobsPageContent.types'; -import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorFragment'; -import { - FIFTEEN_SECONDS, - QueryRefreshCountdown, - useQueryRefreshAtInterval, -} from '../app/QueryRefresh'; import {isHiddenAssetGroupJob} from '../asset-graph/Utils'; -import {useQueryPersistedState} from '../hooks/useQueryPersistedState'; -import {RepoFilterButton} from '../instance/RepoFilterButton'; +import {useQueryPersistedFilterState} from '../hooks/useQueryPersistedFilterState'; +import {TruncatedTextWithFullTextOnHover} from '../nav/getLeftNavItemsForOption'; import {OverviewJobsTable} from '../overview/OverviewJobsTable'; import {sortRepoBuckets} from '../overview/sortRepoBuckets'; import {visibleRepoKeys} from '../overview/visibleRepoKeys'; -import {useBlockTraceUntilTrue} from '../performance/TraceContext'; -import {SearchInputSpinner} from '../ui/SearchInputSpinner'; +import {useFilters} from '../ui/BaseFilters/useFilters'; +import {useStaticSetFilter} from '../ui/BaseFilters/useStaticSetFilter'; +import {useCodeLocationFilter} from '../ui/Filters/useCodeLocationFilter'; +import { + Tag, + doesFilterArrayMatchValueArray, + useDefinitionTagFilter, + useTagsForObjects, +} from '../ui/Filters/useDefinitionTagFilter'; import {WorkspaceContext} from '../workspace/WorkspaceContext/WorkspaceContext'; -import {WorkspaceLocationNodeFragment} from '../workspace/WorkspaceContext/types/WorkspaceQueries.types'; +import { + WorkspaceLocationNodeFragment, + WorkspacePipelineFragment, +} from '../workspace/WorkspaceContext/types/WorkspaceQueries.types'; import {buildRepoAddress} from '../workspace/buildRepoAddress'; import {repoAddressAsHumanString} from '../workspace/repoAddressAsString'; import {RepoAddress} from '../workspace/types'; -export const JobsPageContent = () => { - const { - allRepos, - visibleRepos, - loading: workspaceLoading, - data: cachedData, - } = useContext(WorkspaceContext); - const [searchValue, setSearchValue] = useQueryPersistedState({ - queryKey: 'search', - defaults: {search: ''}, - }); +const FILTER_FIELDS = ['jobs', 'tags', 'codeLocations'] as const; +export const JobsPageContent = () => { + const {allRepos, visibleRepos, loading, data: cachedData} = useContext(WorkspaceContext); const repoCount = allRepos.length; - - const queryResultOverview = useQuery( - OVERVIEW_JOBS_QUERY, - { - fetchPolicy: 'network-only', - notifyOnNetworkStatusChange: true, - }, - ); - const {data, loading: queryLoading} = queryResultOverview; - - const refreshState = useQueryRefreshAtInterval(queryResultOverview, FIFTEEN_SECONDS); - // Batch up the data and bucket by repo. const repoBuckets = useMemo(() => { const cachedEntries = Object.values(cachedData).filter( (location): location is Extract => location.__typename === 'WorkspaceLocationEntry', ); - const workspaceOrError = data?.workspaceOrError; - const entries = - workspaceOrError?.__typename === 'Workspace' - ? workspaceOrError.locationEntries - : cachedEntries; const visibleKeys = visibleRepoKeys(visibleRepos); - return buildBuckets(entries).filter(({repoAddress}) => + return buildBuckets(cachedEntries).filter(({repoAddress}) => visibleKeys.has(repoAddressAsHumanString(repoAddress)), ); - }, [cachedData, data, visibleRepos]); - - const loading = !data && workspaceLoading; + }, [cachedData, visibleRepos]); + + const allJobs = useMemo(() => repoBuckets.flatMap((bucket) => bucket.jobs), [repoBuckets]); + const allTags = useTagsForObjects(allJobs, (job) => job.tags); + + const {state: _state, setters} = useQueryPersistedFilterState<{ + jobs: string[]; + tags: Tag[]; + codeLocations: RepoAddress[]; + }>(FILTER_FIELDS); + + const state = useMemo(() => { + return { + ..._state, + codeLocations: _state.codeLocations.map(({name, location}) => + buildRepoAddress(name, location), + ), + }; + }, [_state]); + + const codeLocationFilter = useCodeLocationFilter({ + codeLocations: state.codeLocations, + setCodeLocations: setters.setCodeLocations, + }); - useBlockTraceUntilTrue('OverviewJobs', !loading); + const tagsFilter = useDefinitionTagFilter({allTags, tags: state.tags, setTags: setters.setTags}); + + const jobFilter = useStaticSetFilter({ + name: 'Job', + icon: 'job', + allValues: useMemo( + () => + allJobs.map((job) => ({ + key: job.name, + value: job.name, + match: [job.name], + })), + [allJobs], + ), + renderLabel: ({value}) => ( + + + + + ), + getStringValue: (x) => x, + state: state.jobs, + onStateChanged: useCallback( + (values) => { + setters.setJobs(Array.from(values)); + }, + [setters], + ), + }); - const sanitizedSearch = searchValue.trim().toLocaleLowerCase(); - const anySearch = sanitizedSearch.length > 0; + const filters = useMemo( + () => [codeLocationFilter, jobFilter, tagsFilter], + [codeLocationFilter, jobFilter, tagsFilter], + ); + const {button: filterButton, activeFiltersJsx} = useFilters({filters}); - const filteredBySearch = useMemo(() => { - const searchToLower = sanitizedSearch.toLocaleLowerCase(); + const filteredRepoBuckets = useMemo(() => { return repoBuckets - .map(({repoAddress, jobs}) => ({ - repoAddress, - jobs: jobs.filter(({name}) => name.toLocaleLowerCase().includes(searchToLower)), + .filter((bucket) => { + return !state.codeLocations.length || state.codeLocations.includes(bucket.repoAddress); + }) + .map((bucket) => ({ + ...bucket, + jobs: bucket.jobs.filter((job) => { + if (state.jobs.length && !state.jobs.includes(job.name)) { + return false; + } + if (state.tags.length && !doesFilterArrayMatchValueArray(state.tags, job.tags)) { + return false; + } + return true; + }), })) - .filter(({jobs}) => jobs.length > 0); - }, [repoBuckets, sanitizedSearch]); + .filter((bucket) => !!bucket.jobs.length); + }, [repoBuckets, state]); const content = () => { if (loading) { @@ -101,40 +140,15 @@ export const JobsPageContent = () => { ); } - const anyReposHidden = allRepos.length > visibleRepos.length; - - if (!filteredBySearch.length) { - if (anySearch) { - return ( - - - No jobs matching {searchValue} were found in the selected code - locations - - ) : ( -
- No jobs matching {searchValue} were found in your definitions -
- ) - } - /> -
- ); - } - + if (!filteredRepoBuckets.length) { return ( @@ -142,32 +156,27 @@ export const JobsPageContent = () => { ); } - return ; + return ; }; - const showSearchSpinner = queryLoading && !data; - return ( <> - - {repoCount > 1 ? : null} - : undefined - } - onChange={(e) => setSearchValue(e.target.value)} - placeholder="Filter by job name…" - style={{width: '340px'}} - /> - - + {filterButton} + {activeFiltersJsx.length ? ( + + {activeFiltersJsx} + + ) : null} {loading && !repoCount ? ( @@ -181,16 +190,11 @@ export const JobsPageContent = () => { type RepoBucket = { repoAddress: RepoAddress; - jobs: { - isJob: boolean; - name: string; - }[]; + jobs: WorkspacePipelineFragment[]; }; const buildBuckets = ( - locationEntries: - | Extract['locationEntries'] - | Extract[], + locationEntries: Extract[], ): RepoBucket[] => { const entries = locationEntries.map((entry) => entry.locationOrLoadError); const buckets = []; @@ -203,14 +207,7 @@ const buildBuckets = ( for (const repo of entry.repositories) { const {name, pipelines} = repo; const repoAddress = buildRepoAddress(name, entry.name); - const jobs = pipelines - .filter(({name}) => !isHiddenAssetGroupJob(name)) - .map((pipeline) => { - return { - isJob: pipeline.isJob, - name: pipeline.name, - }; - }); + const jobs = pipelines.filter(({name}) => !isHiddenAssetGroupJob(name)); if (jobs.length > 0) { buckets.push({ @@ -223,39 +220,3 @@ const buildBuckets = ( return sortRepoBuckets(buckets); }; - -const OVERVIEW_JOBS_QUERY = gql` - query OverviewJobsQuery { - workspaceOrError { - ... on Workspace { - id - locationEntries { - id - locationOrLoadError { - ... on RepositoryLocation { - id - name - repositories { - id - name - pipelines { - tags { - key - value - } - id - name - isJob - } - } - } - ...PythonErrorFragment - } - } - } - ...PythonErrorFragment - } - } - - ${PYTHON_ERROR_FRAGMENT} -`; diff --git a/js_modules/dagster-ui/packages/ui-core/src/jobs/types/JobsPageContent.types.ts b/js_modules/dagster-ui/packages/ui-core/src/jobs/types/JobsPageContent.types.ts deleted file mode 100644 index 739d9a889eec6..0000000000000 --- a/js_modules/dagster-ui/packages/ui-core/src/jobs/types/JobsPageContent.types.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Generated GraphQL types, do not edit manually. - -import * as Types from '../../graphql/types'; - -export type OverviewJobsQueryVariables = Types.Exact<{[key: string]: never}>; - -export type OverviewJobsQuery = { - __typename: 'Query'; - workspaceOrError: - | { - __typename: 'PythonError'; - message: string; - stack: Array; - errorChain: Array<{ - __typename: 'ErrorChainLink'; - isExplicitLink: boolean; - error: {__typename: 'PythonError'; message: string; stack: Array}; - }>; - } - | { - __typename: 'Workspace'; - id: string; - locationEntries: Array<{ - __typename: 'WorkspaceLocationEntry'; - id: string; - locationOrLoadError: - | { - __typename: 'PythonError'; - message: string; - stack: Array; - errorChain: Array<{ - __typename: 'ErrorChainLink'; - isExplicitLink: boolean; - error: {__typename: 'PythonError'; message: string; stack: Array}; - }>; - } - | { - __typename: 'RepositoryLocation'; - id: string; - name: string; - repositories: Array<{ - __typename: 'Repository'; - id: string; - name: string; - pipelines: Array<{ - __typename: 'Pipeline'; - id: string; - name: string; - isJob: boolean; - tags: Array<{__typename: 'PipelineTag'; key: string; value: string}>; - }>; - }>; - } - | null; - }>; - }; -}; - -export const OverviewJobsQueryVersion = '7de05cca36088c46f8dbd3f995d643fc7e79f240eae0bc614bcf68a0729fe7d9'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/ui/Filters/useDefinitionTagFilter.tsx b/js_modules/dagster-ui/packages/ui-core/src/ui/Filters/useDefinitionTagFilter.tsx index f59cd2b306b0a..32b88bc55815f 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/ui/Filters/useDefinitionTagFilter.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/ui/Filters/useDefinitionTagFilter.tsx @@ -11,7 +11,7 @@ import {buildTagString} from '../tagAsString'; const emptyArray: any[] = []; -type Tag = Omit; +export type Tag = Omit; export const useDefinitionTagFilter = ({ allTags, @@ -36,17 +36,16 @@ export const useDefinitionTagFilter = ({ menuWidth: '300px', state: memoizedState ?? emptyArray, onStateChanged: (values) => { - setTags?.(Array.from(values)); + const nextTags = Array.from(values); + setTags?.(nextTags); }, canSelectAll: false, }); }; export const useDefinitionTagFilterWithManagedState = ({allTags}: {allTags: Tag[]}) => { - const [tags, setTags] = useQueryPersistedState({ - encode: (tags) => ({tags}), - decode: (qs) => (qs.tags as Tag[]) || [], - }); + const [tags, setTags] = useQueryPersistedState({queryKey: 'tags'}); + return useDefinitionTagFilter({ allTags, tags,