diff --git a/querybook/server/logic/schedule.py b/querybook/server/logic/schedule.py index 4e78d4925..4b30f6e47 100644 --- a/querybook/server/logic/schedule.py +++ b/querybook/server/logic/schedule.py @@ -11,6 +11,7 @@ TaskRunRecord, ) from models.datadoc import DataDoc +from models.board import BoardItem DATADOC_SCHEDULE_PREFIX = "run_data_doc_" @@ -194,6 +195,14 @@ def get_scheduled_data_docs_by_user( if "name" in filters: query = query.filter(DataDoc.title.contains(filters.get("name"))) + if filters.get("status") is not None: + query = query.filter(TaskSchedule.enabled == filters.get("status")) + + if filters.get("board_ids"): + query = query.join(BoardItem, BoardItem.data_doc_id == DataDoc.id).filter( + BoardItem.parent_board_id.in_(filters.get("board_ids")) + ) + count = query.count() docs_with_schedules = query.offset(offset).limit(limit).all() docs_with_schedules_and_records = get_task_run_record_run_with_schedule( diff --git a/querybook/webapp/components/AppAdmin/components/EnvironmentSelection/EnvironmentSelection.tsx b/querybook/webapp/components/AppAdmin/components/EnvironmentSelection/EnvironmentSelection.tsx index 2bc796612..c21667ffe 100644 --- a/querybook/webapp/components/AppAdmin/components/EnvironmentSelection/EnvironmentSelection.tsx +++ b/querybook/webapp/components/AppAdmin/components/EnvironmentSelection/EnvironmentSelection.tsx @@ -2,12 +2,7 @@ import { useField, useFormikContext } from 'formik'; import React, { useMemo } from 'react'; import { SimpleField } from 'ui/FormikField/SimpleField'; - -interface OptionsType { - value: string; - key: string; - hidden?: boolean; -} +import { OptionsType } from 'const/options'; export const EnvironmentSelection = ({ options = [], diff --git a/querybook/webapp/components/DataDocScheduleList/DataDocBoardsSelect.tsx b/querybook/webapp/components/DataDocScheduleList/DataDocBoardsSelect.tsx new file mode 100644 index 000000000..6eddb5f19 --- /dev/null +++ b/querybook/webapp/components/DataDocScheduleList/DataDocBoardsSelect.tsx @@ -0,0 +1,56 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { IStoreState } from 'redux/store/types'; +import { SimpleField } from 'ui/FormikField/SimpleField'; +import { IBoard } from 'const/board'; +import { IOption } from 'lib/utils/react-select'; +import { OptionTypeBase } from 'react-select'; + +export interface IDataDocBoardsSelectProps { + onChange: (params: OptionTypeBase[]) => void; + value: IOption[]; + label?: string; + name: string; +} + +export const DataDocBoardsSelect: React.FC = ({ + onChange, + value, + label, + name, +}) => { + const boardById: Record = useSelector( + (state: IStoreState) => state.board.boardById + ); + + const boardOptions: IOption[] = useMemo(() => { + return Object.values(boardById).map((board) => ({ + value: board.id, + label: board.name, + })); + }, [boardById]); + + const selectedBoards: IOption[] = useMemo( + () => + boardOptions.filter((board) => + value.map((v) => v.value).includes(board.value) + ), + [] + ); + + return ( + ) => v} + closeMenuOnSelect={false} + hideSelectedOptions={false} + isMulti + type="react-select" + /> + ); +}; diff --git a/querybook/webapp/components/DataDocScheduleList/DataDocSchedsFilters.tsx b/querybook/webapp/components/DataDocScheduleList/DataDocSchedsFilters.tsx new file mode 100644 index 000000000..4fdfdbfcb --- /dev/null +++ b/querybook/webapp/components/DataDocScheduleList/DataDocSchedsFilters.tsx @@ -0,0 +1,87 @@ +import React, { useEffect } from 'react'; +import { Formik } from 'formik'; +import { debounce } from 'lodash'; +import { useDispatch } from 'react-redux'; +import { OptionTypeBase } from 'react-select'; +import { Popover } from 'ui/Popover/Popover'; +import { SimpleField } from 'ui/FormikField/SimpleField'; +import { DataDocBoardsSelect } from './DataDocBoardsSelect'; +import { IScheduledDocFilters } from 'redux/scheduledDataDoc/types'; +import { fetchBoards } from 'redux/board/action'; +import { UpdateFiltersType, StatusType } from 'const/schedFiltersType'; + +const enabledOptions = [ + { key: 'all', value: 'All' }, + { key: 'enabled', value: 'Enabled' }, + { key: 'disabled', value: 'Disabled' }, +]; + +export const DataDocSchedsFilters: React.FC<{ + setShowSearchFilter: (arg: boolean) => void; + updateFilters: (arg: UpdateFiltersType) => void; + filterButton: HTMLAnchorElement | null; + filters: IScheduledDocFilters; +}> = ({ setShowSearchFilter, filters, updateFilters, filterButton }) => { + const dispatch = useDispatch(); + useEffect(() => { + dispatch(fetchBoards()); + }, []); + const handleUpdateStatus = React.useCallback((value: StatusType) => { + updateFilters({ + key: 'status', + value, + }); + }, []); + + const handleUpdateList = React.useCallback( + debounce((params: OptionTypeBase[]) => { + updateFilters({ + key: 'board_ids', + value: params, + }); + }, 500), + [] + ); + return ( + { + setShowSearchFilter(false); + }} + anchor={filterButton} + > +
+
+ undefined} // Just for fixing ts + > + {({}) => { + return ( + <> + + + + ); + }} + +
+
+
+ ); +}; diff --git a/querybook/webapp/components/DataDocScheduleList/DataDocScheduleList.tsx b/querybook/webapp/components/DataDocScheduleList/DataDocScheduleList.tsx index ee4129215..2cc6d9b54 100644 --- a/querybook/webapp/components/DataDocScheduleList/DataDocScheduleList.tsx +++ b/querybook/webapp/components/DataDocScheduleList/DataDocScheduleList.tsx @@ -1,17 +1,25 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - +import React, { + useEffect, + useMemo, + useState, + useRef, + useCallback, +} from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { Dispatch, IStoreState } from 'redux/store/types'; import { getScheduledDocs } from 'redux/scheduledDataDoc/action'; import { IScheduledDocFilters } from 'redux/scheduledDataDoc/types'; -import { Dispatch, IStoreState } from 'redux/store/types'; import { Checkbox } from 'ui/Checkbox/Checkbox'; import { Container } from 'ui/Container/Container'; import { DebouncedInput } from 'ui/DebouncedInput/DebouncedInput'; import { Pagination } from 'ui/Pagination/Pagination'; import { PrettyNumber } from 'ui/PrettyNumber/PrettyNumber'; import { AccentText, EmptyText } from 'ui/StyledText/StyledText'; +import { IconButton } from 'ui/Button/IconButton'; import { DataDocScheduleItem } from './DataDocScheduleItem'; +import { DataDocSchedsFilters } from './DataDocSchedsFilters'; +import { UpdateFiltersType } from 'const/schedFiltersType'; import './DataDocScheduleList.scss'; @@ -24,20 +32,40 @@ function useDataDocScheduleFiltersAndPagination() { } = useSelector((state: IStoreState) => state.scheduledDocs); const [docName, setDocName] = useState(initFilters.name ?? ''); - const [scheduledOnly, setScheduledOnly] = useState( - initFilters.scheduled_only ?? false - ); + + const [extraFilters, setExtraFilters] = useState({ + status: initFilters.status ?? null, + board_ids: initFilters.board_ids ?? [], + scheduled_only: initFilters.scheduled_only ?? false, + }); + + const updateFilters = useCallback(({ key, value }: UpdateFiltersType) => { + setExtraFilters((state) => ({ + ...state, + [key]: value, + })); + }, []); const filters: IScheduledDocFilters = useMemo(() => { const _filters: IScheduledDocFilters = {}; if (docName) { _filters.name = docName; } - if (scheduledOnly) { + + if (extraFilters.scheduled_only) { _filters.scheduled_only = true; } + + if (extraFilters.status !== null) { + _filters.status = extraFilters.status; + } + + if (extraFilters.board_ids) { + _filters.board_ids = extraFilters.board_ids; + } + return _filters; - }, [docName, scheduledOnly]); + }, [docName, extraFilters]); const [page, setPage] = useState(initPage); const [pageSize, setPageSize] = useState(initPageSize); @@ -45,13 +73,13 @@ function useDataDocScheduleFiltersAndPagination() { return { filters, setDocName, - setScheduledOnly, numberOfResults, page, setPage, pageSize, setPageSize, + updateFilters, }; } @@ -92,7 +120,7 @@ const DataDocScheduleList: React.FC = () => { filters, setDocName, - setScheduledOnly, + updateFilters, } = useDataDocScheduleFiltersAndPagination(); const dataDocsWithSchedule = useDataDocWithSchedules( @@ -102,6 +130,24 @@ const DataDocScheduleList: React.FC = () => { ); const totalPages = Math.ceil(numberOfResults / pageSize); + const [showSearchFilter, setShowSearchFilter] = useState(false); + const filterButtonRef = useRef(); + + const handleUpdateScheduledOnly = React.useCallback((value: boolean) => { + updateFilters({ + key: 'scheduled_only', + value, + }); + }, []); + + const searchFiltersPickerDOM = showSearchFilter && ( + + ); return ( @@ -116,11 +162,23 @@ const DataDocScheduleList: React.FC = () => { }} /> + + { + setShowSearchFilter(true); + }} + icon="Sliders" + /> + {searchFiltersPickerDOM}
diff --git a/querybook/webapp/const/options.tsx b/querybook/webapp/const/options.tsx new file mode 100644 index 000000000..8447074e7 --- /dev/null +++ b/querybook/webapp/const/options.tsx @@ -0,0 +1,5 @@ +export interface OptionsType { + value: string; + key: string; + hidden?: boolean; +} diff --git a/querybook/webapp/const/schedFiltersType.ts b/querybook/webapp/const/schedFiltersType.ts new file mode 100644 index 000000000..a18b24d0e --- /dev/null +++ b/querybook/webapp/const/schedFiltersType.ts @@ -0,0 +1,8 @@ +import { OptionTypeBase } from 'react-select'; + +export type StatusType = 'all' | 'enabled' | 'disabled'; + +export type UpdateFiltersType = + | { key: 'status'; value: StatusType } + | { key: 'scheduled_only'; value: boolean } + | { key: 'board_ids'; value: OptionTypeBase[] }; diff --git a/querybook/webapp/redux/scheduledDataDoc/action.ts b/querybook/webapp/redux/scheduledDataDoc/action.ts index d25d2cacd..f2e285c4e 100644 --- a/querybook/webapp/redux/scheduledDataDoc/action.ts +++ b/querybook/webapp/redux/scheduledDataDoc/action.ts @@ -1,6 +1,24 @@ import { DataDocScheduleResource } from 'resource/dataDoc'; +import { IOption } from 'lib/utils/react-select'; import { IScheduledDoc, IScheduledDocFilters, ThunkResult } from './types'; +import { StatusType } from 'const/schedFiltersType'; + +function reformatBoardIds(boardIds: IOption[]): number[] | null { + if (boardIds.length) { + return boardIds.map((board) => board.value); + } + + return null; +} + +function reformatStatus(status: StatusType): boolean | null { + if (status === 'all') { + return null; + } + + return status === 'enabled'; +} export function getScheduledDocs({ paginationPage, @@ -25,7 +43,11 @@ export function getScheduledDocs({ envId, limit: pageSize, offset: page * pageSize, - filters, + filters: { + ...filters, + status: reformatStatus(filters.status), + board_ids: reformatBoardIds(filters.board_ids), + }, }); dispatch({ diff --git a/querybook/webapp/redux/scheduledDataDoc/reducer.ts b/querybook/webapp/redux/scheduledDataDoc/reducer.ts index 69847fb32..a29d13d6c 100644 --- a/querybook/webapp/redux/scheduledDataDoc/reducer.ts +++ b/querybook/webapp/redux/scheduledDataDoc/reducer.ts @@ -8,6 +8,7 @@ const initialState: Readonly = { pageSize: 20, numberOfResults: 0, filters: { + status: 'all', scheduled_only: true, }, }; diff --git a/querybook/webapp/redux/scheduledDataDoc/types.ts b/querybook/webapp/redux/scheduledDataDoc/types.ts index 2c861f9ac..9adfcd327 100644 --- a/querybook/webapp/redux/scheduledDataDoc/types.ts +++ b/querybook/webapp/redux/scheduledDataDoc/types.ts @@ -8,11 +8,25 @@ import { IDataDoc } from 'const/datadoc'; import { ITaskSchedule, ITaskStatusRecord } from 'const/schedule'; import { IStoreState } from '../store/types'; +import { IOption } from 'lib/utils/react-select'; +import { StatusType } from 'const/schedFiltersType'; -export interface IScheduledDocFilters { +interface IBasicScheduledDocFilters { name?: string; scheduled_only?: boolean; } + +export interface IScheduledDocFilters extends IBasicScheduledDocFilters { + status?: StatusType; + board_ids?: IOption[]; +} + +export interface ITransformedScheduledDocFilters + extends IBasicScheduledDocFilters { + board_ids?: number[]; + status?: boolean; +} + export interface IScheduledDoc { doc: IDataDoc; last_record?: ITaskStatusRecord; diff --git a/querybook/webapp/resource/dataDoc.ts b/querybook/webapp/resource/dataDoc.ts index e035e41a6..824190a31 100644 --- a/querybook/webapp/resource/dataDoc.ts +++ b/querybook/webapp/resource/dataDoc.ts @@ -18,7 +18,7 @@ import dataDocSocket from 'lib/data-doc/datadoc-socketio'; import ds from 'lib/datasource'; import { IScheduledDoc, - IScheduledDocFilters, + ITransformedScheduledDocFilters, } from 'redux/scheduledDataDoc/types'; export const DataDocResource = { @@ -188,7 +188,7 @@ export const DataDocScheduleResource = { envId: number; limit: number; offset: number; - filters: IScheduledDocFilters; + filters: ITransformedScheduledDocFilters; }) => ds.fetch<{ docs: IScheduledDoc[]; count: number }>( '/datadoc/scheduled/', diff --git a/querybook/webapp/ui/SimpleReactSelect/SimpleReactSelect.tsx b/querybook/webapp/ui/SimpleReactSelect/SimpleReactSelect.tsx index 37f03ed47..a489b901e 100644 --- a/querybook/webapp/ui/SimpleReactSelect/SimpleReactSelect.tsx +++ b/querybook/webapp/ui/SimpleReactSelect/SimpleReactSelect.tsx @@ -5,6 +5,7 @@ import Creatable from 'react-select/creatable'; import { makeReactSelectStyle } from 'lib/utils/react-select'; import { overlayRoot } from 'ui/Overlay/Overlay'; import { AccentText } from 'ui/StyledText/StyledText'; +import { IOption } from 'lib/utils/react-select'; export interface ISelectOption { value: T; @@ -19,6 +20,11 @@ export interface ISimpleReactSelectProps { isDisabled?: boolean; creatable?: boolean; selectProps?: Partial>; + closeMenuOnSelect?: boolean; + hideSelectedOptions?: boolean; + isMulti?: boolean; + optionSelector?: (o: ISelectOption) => any; + defaultValue?: any; // Clear selection user picks value clearAfterSelect?: boolean; @@ -36,6 +42,8 @@ export function SimpleReactSelect({ selectProps = {}, withDeselect = false, clearAfterSelect = false, + optionSelector = (val) => val?.value, + ...otherParams }: ISimpleReactSelectProps) { const overrideSelectProps = useMemo(() => { const override: Partial> = {}; @@ -65,7 +73,7 @@ export function SimpleReactSelect({ ); const onSelectChange = useCallback( - (val: ISelectOption) => onChange(val?.value), + (val: ISelectOption) => onChange(optionSelector(val)), [onChange] ); @@ -88,7 +96,7 @@ export function SimpleReactSelect({ {creatable ? ( ) : ( - )} );