From 98c24f584e8bac07622eefa70e3553b654112b65 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 24 Mar 2025 10:51:50 +0000 Subject: [PATCH] feat(executions): add execution filters --- packages/web/package.json | 3 +- .../src/components/DatePickerInput/index.jsx | 42 +++++++ .../src/components/ExecutionFilters/index.jsx | 112 ++++++++++++++++++ .../web/src/components/IntlProvider/index.jsx | 2 +- .../web/src/components/SearchInput/index.jsx | 5 +- packages/web/src/hooks/useExecutionFilters.js | 99 ++++++++++++++++ packages/web/src/hooks/useExecutions.js | 22 +++- packages/web/src/locales/en.json | 10 +- packages/web/src/pages/Executions/index.jsx | 79 +++++++++++- packages/web/yarn.lock | 68 ++++++++++- 10 files changed, 423 insertions(+), 19 deletions(-) create mode 100644 packages/web/src/components/DatePickerInput/index.jsx create mode 100644 packages/web/src/components/ExecutionFilters/index.jsx create mode 100644 packages/web/src/hooks/useExecutionFilters.js diff --git a/packages/web/package.json b/packages/web/package.json index ab2349bf..db77a792 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -14,6 +14,7 @@ "@mui/icons-material": "^5.11.9", "@mui/lab": "^5.0.0-alpha.120", "@mui/material": "^5.11.10", + "@mui/x-date-pickers": "^7.28.0", "@tanstack/react-query": "^5.24.1", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", @@ -23,7 +24,7 @@ "clipboard-copy": "^4.0.1", "compare-versions": "^4.1.3", "lodash": "^4.17.21", - "luxon": "^2.3.1", + "luxon": "^3.6.0", "mui-color-input": "^2.0.0", "notistack": "^3.0.1", "react": "^18.2.0", diff --git a/packages/web/src/components/DatePickerInput/index.jsx b/packages/web/src/components/DatePickerInput/index.jsx new file mode 100644 index 00000000..bf78b0b2 --- /dev/null +++ b/packages/web/src/components/DatePickerInput/index.jsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { useIntl } from 'react-intl'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; + +import useFormatMessage from 'hooks/useFormatMessage'; + +export default function DatePickerInput({ + onChange, + defaultValue = '', + label, + disableFuture = false, + minDate, + maxDate, +}) { + const intl = useIntl(); + const formatMessage = useFormatMessage(); + + const props = { + label, + views: ['year', 'month', 'day'], + onChange, + disableFuture, + disableHighlightToday: true, + minDate, + maxDate, + }; + + if (defaultValue) { + props.defaultValue = defaultValue; + } + + return ( + + + + ); +} diff --git a/packages/web/src/components/ExecutionFilters/index.jsx b/packages/web/src/components/ExecutionFilters/index.jsx new file mode 100644 index 00000000..89e9dc70 --- /dev/null +++ b/packages/web/src/components/ExecutionFilters/index.jsx @@ -0,0 +1,112 @@ +import FilterAltIcon from '@mui/icons-material/FilterAlt'; +import { + Box, + Button, + Collapse, + FormControl, + InputLabel, + MenuItem, + Select, + Typography, + useMediaQuery, +} from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { DateTime } from 'luxon'; +import { useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +import Can from 'components/Can'; +import DatePickerInput from 'components/DatePickerInput'; +import useExecutionFilters from 'hooks/useExecutionFilters'; +import useFormatMessage from 'hooks/useFormatMessage'; + +export default function ExecutionFilters({ onFilterChange }) { + const theme = useTheme(); + const formatMessage = useFormatMessage(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + + const { + filters, + filterByStartDateTime, + filterByEndDateTime, + filterByStatus, + } = useExecutionFilters(); + + const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); + + return ( + + {/* Mobile: Toggle Button for Filters */} + {isMobile && ( + + )} + + {/* Filters Box (Always Visible on Large Screens) */} + + + {/* Status Filter */} + + + {formatMessage('executionFilters.statusFilterLabel')} + + + + + + {/* Date Filters */} + + + + + + + ); +} diff --git a/packages/web/src/components/IntlProvider/index.jsx b/packages/web/src/components/IntlProvider/index.jsx index bb33a89a..272ee9ec 100644 --- a/packages/web/src/components/IntlProvider/index.jsx +++ b/packages/web/src/components/IntlProvider/index.jsx @@ -6,7 +6,7 @@ import englishMessages from 'locales/en.json'; const IntlProvider = ({ children }) => { return ( diff --git a/packages/web/src/components/SearchInput/index.jsx b/packages/web/src/components/SearchInput/index.jsx index d04dd438..6ac33b0d 100644 --- a/packages/web/src/components/SearchInput/index.jsx +++ b/packages/web/src/components/SearchInput/index.jsx @@ -7,8 +7,9 @@ import FormControl from '@mui/material/FormControl'; import SearchIcon from '@mui/icons-material/Search'; import useFormatMessage from 'hooks/useFormatMessage'; -export default function SearchInput({ onChange, defaultValue = '', value }) { +export default function SearchInput({ onChange, value }) { const formatMessage = useFormatMessage(); + return ( @@ -17,7 +18,6 @@ export default function SearchInput({ onChange, defaultValue = '', value }) { { + setSearchParams((current) => { + const { status: currentStatus, ...rest } = searchParamsObject; + + if (status) { + return { ...rest, status }; + } + + return rest; + }); + }; + + const filterByStartDateTime = (startDateTime) => { + const startDateTimeString = startDateTime?.toMillis(); + + setSearchParams((current) => { + const { startDateTime: currentStartDateTime, ...rest } = + searchParamsObject; + + if (startDateTimeString) { + return { ...rest, startDateTime: startDateTimeString }; + } + + return rest; + }); + }; + + const filterByEndDateTime = (endDateTime) => { + const endDateTimeString = endDateTime?.endOf('day').toMillis(); + + setSearchParams((current) => { + const { endDateTime: currentEndDateTime, ...rest } = searchParamsObject; + + if (endDateTimeString) { + return { ...rest, endDateTime: endDateTimeString }; + } + + return rest; + }); + }; + + const enhanceExistingSearchParams = (key, value) => { + const searchParamsObject = objectifyUrlSearchParams(searchParams); + + if (value === undefined) { + const { [key]: keyToRemove, ...remainingSearchParams } = + searchParamsObject; + + return new URLSearchParams(remainingSearchParams).toString(); + } + + return new URLSearchParams({ + ...searchParamsObject, + [key]: value, + }).toString(); + }; + + return { + filters: { + startDateTime: startDateTime + ? DateTime.fromMillis(startDateTime) + : undefined, + endDateTime: endDateTime ? DateTime.fromMillis(endDateTime) : undefined, + status, + }, + requestFriendlyFilters: { + ...(startDateTime && { startDateTime }), + ...(endDateTime && { endDateTime }), + ...(status && { status }), + ...(folderId && { folderId }), + }, + filterByStartDateTime, + filterByEndDateTime, + filterByStatus, + enhanceExistingSearchParams, + }; +} diff --git a/packages/web/src/hooks/useExecutions.js b/packages/web/src/hooks/useExecutions.js index c0f1fc28..17cfcea9 100644 --- a/packages/web/src/hooks/useExecutions.js +++ b/packages/web/src/hooks/useExecutions.js @@ -2,13 +2,29 @@ import { useQuery } from '@tanstack/react-query'; import api from 'helpers/api'; -export default function useExecutions({ page }, { refetchInterval } = {}) { +export default function useExecutions( + { endDateTime, startDateTime, name, page, status }, + { refetchInterval } = {}, +) { const query = useQuery({ - queryKey: ['executions', { page }], + queryKey: [ + 'executions', + { + endDateTime, + startDateTime, + name, + page, + status, + }, + ], queryFn: async ({ signal }) => { - const { data } = await api.get(`/v1/executions`, { + const { data } = await api.get('/v1/executions', { params: { + endDateTime, + startDateTime, + name, page, + status, }, signal, }); diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index be164512..90504e13 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -395,5 +395,13 @@ "flowFilters.statusFilterLabel": "Status", "flowFilters.statusFilterAnyOption": "All", "flowFilters.statusFilterPublishedOption": "Published", - "flowFilters.statusFilterDraftOption": "Draft" + "flowFilters.statusFilterDraftOption": "Draft", + "executionFilters.hideFilters": "Hide filters", + "executionFilters.showFilters": "Show Filters", + "executionFilters.statusFilterLabel": "Status", + "executionFilters.statusFilterAnyOption": "All", + "executionFilters.statusFilterSuccessfulOption": "Successful", + "executionFilters.statusFilterFailedOption": "Failed", + "executionFilters.startDateLabel": "Start Date", + "executionFilters.endDateLabel": "End Date" } diff --git a/packages/web/src/pages/Executions/index.jsx b/packages/web/src/pages/Executions/index.jsx index 5ca6ea12..3b060517 100644 --- a/packages/web/src/pages/Executions/index.jsx +++ b/packages/web/src/pages/Executions/index.jsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Link, useSearchParams } from 'react-router-dom'; +import { Link, useSearchParams, useNavigate } from 'react-router-dom'; import Box from '@mui/material/Box'; import Grid from '@mui/material/Grid'; import CircularProgress from '@mui/material/CircularProgress'; @@ -7,26 +7,85 @@ import Divider from '@mui/material/Divider'; import Pagination from '@mui/material/Pagination'; import PaginationItem from '@mui/material/PaginationItem'; +import SearchInput from 'components/SearchInput'; +import ExecutionFilters from 'components/ExecutionFilters'; import NoResultFound from 'components/NoResultFound'; import ExecutionRow from 'components/ExecutionRow'; import Container from 'components/Container'; import PageTitle from 'components/PageTitle'; import useFormatMessage from 'hooks/useFormatMessage'; import useExecutions from 'hooks/useExecutions'; +import useExecutionFilters from 'hooks/useExecutionFilters'; +import objectifyUrlSearchParams from 'helpers/objectifyUrlSearchParams'; export default function Executions() { const formatMessage = useFormatMessage(); const [searchParams, setSearchParams] = useSearchParams(); + const { requestFriendlyFilters } = useExecutionFilters(); + const navigate = useNavigate(); const page = parseInt(searchParams.get('page') || '', 10) || 1; + const name = searchParams.get('name') || ''; + const status = searchParams.get('status'); + const startDateTime = searchParams.get('startDateTime'); + const endDateTime = searchParams.get('endDateTime'); + const [searchValue, setSearchValue] = React.useState(name); - const { data, isLoading: isExecutionsLoading } = useExecutions( - { page: page }, + const { + data, + isSuccess, + isLoading: isExecutionsLoading, + } = useExecutions( + { page, name, ...requestFriendlyFilters }, { refetchInterval: 5000 }, ); - const { data: executions, meta: pageInfo } = data || {}; - + const executions = data?.data || []; + const pageInfo = data?.meta; const hasExecutions = executions?.length; + const navigateToLastPage = isSuccess && !hasExecutions && page > 1; + + const onSearchChange = React.useCallback( + (event) => { + const value = event.target.value; + + setSearchValue(value); + + setSearchParams({ + name: value, + ...requestFriendlyFilters, + }); + }, + [requestFriendlyFilters, setSearchParams], + ); + + const getPathWithSearchParams = (page) => { + const searchParamsObject = objectifyUrlSearchParams(searchParams); + + const newSearchParams = new URLSearchParams({ + ...searchParamsObject, + page, + }); + + return { search: newSearchParams.toString() }; + }; + + React.useEffect( + function resetSearchValue() { + if (!searchParams.has('name')) { + setSearchValue(''); + } + }, + [searchParams], + ); + + React.useEffect( + function redirectToLastPage() { + if (navigateToLastPage) { + navigate(getPathWithSearchParams(pageInfo.totalPages)); + } + }, + [navigateToLastPage], + ); return ( @@ -42,10 +101,18 @@ export default function Executions() { > {formatMessage('executions.title')} + + + + + + + + {isExecutionsLoading && ( ( )} diff --git a/packages/web/yarn.lock b/packages/web/yarn.lock index 8ca090a0..a4c777ea 100644 --- a/packages/web/yarn.lock +++ b/packages/web/yarn.lock @@ -1077,6 +1077,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" + integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.25.9", "@babel/template@^7.3.3": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" @@ -1945,6 +1952,11 @@ resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.19.tgz#c941954dd24393fdce5f07830d44440cf4ab6c80" integrity sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA== +"@mui/types@~7.2.24": + version "7.2.24" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.24.tgz#5eff63129d9c29d80bbf2d2e561bd0690314dec2" + integrity sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw== + "@mui/utils@^5.15.14", "@mui/utils@^5.16.5", "@mui/utils@^5.16.6": version "5.16.6" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.16.6.tgz#905875bbc58d3dcc24531c3314a6807aba22a711" @@ -1957,6 +1969,39 @@ prop-types "^15.8.1" react-is "^18.3.1" +"@mui/utils@^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta": + version "6.4.8" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.8.tgz#f80ee0c0ac47f1cd47c2031a5fb87243322b6bf3" + integrity sha512-C86gfiZ5BfZ51KqzqoHi1WuuM2QdSKoFhbkZeAfQRB+jCc4YNhhj11UXFVMMsqBgZ+Zy8IHNJW3M9Wj/LOwRXQ== + dependencies: + "@babel/runtime" "^7.26.0" + "@mui/types" "~7.2.24" + "@types/prop-types" "^15.7.14" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^19.0.0" + +"@mui/x-date-pickers@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.28.0.tgz#1daa089722b7b3b7458ad9af1ef39ae5ec9a9918" + integrity sha512-m1bfkZLOw3cMogeh6q92SjykVmLzfptnz3ZTgAlFKV7UBnVFuGUITvmwbgTZ1Mz3FmLVnGUQYUpZWw0ZnoghNA== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta" + "@mui/x-internals" "7.28.0" + "@types/react-transition-group" "^4.4.11" + clsx "^2.1.1" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + +"@mui/x-internals@7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.28.0.tgz#b0a04f4c0f53f2f91d13a46f357f731b77c832c5" + integrity sha512-p4GEp/09bLDumktdIMiw+OF4p+pJOOjTG0VUvzNxjbHB9GxbBKoMcHrmyrURqoBnQpWIeFnN/QAoLMFSpfwQbw== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta" + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -2581,6 +2626,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== +"@types/prop-types@^15.7.14": + version "15.7.14" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" + integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== + "@types/q@^1.5.1": version "1.5.8" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.8.tgz#95f6c6a08f2ad868ba230ead1d2d7f7be3db3837" @@ -2603,6 +2653,11 @@ dependencies: "@types/react" "*" +"@types/react-transition-group@^4.4.11": + version "4.4.12" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" + integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== + "@types/react@*", "@types/react@16 || 17 || 18": version "18.3.12" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60" @@ -7236,10 +7291,10 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -luxon@^2.3.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.5.2.tgz#17ed497f0277e72d58a4756d6a9abee4681457b6" - integrity sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA== +luxon@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.6.0.tgz#e84453dbdbd716b5eac95bee702b379863059f83" + integrity sha512-WE7p0p7W1xji9qxkLYsvcIxZyfP48GuFrWIBQZIsbjCyf65dG1rv4n83HcOyEyhvzxJCrUoObCRNFgRNIQ5KNA== lz-string@^1.4.4: version "1.5.0" @@ -8725,6 +8780,11 @@ react-is@^18.0.0, react-is@^18.3.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +react-is@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.0.0.tgz#d6669fd389ff022a9684f708cf6fa4962d1fea7a" + integrity sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g== + react-json-tree@^0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/react-json-tree/-/react-json-tree-0.16.2.tgz#697bd9413407d2448ddff3c8891cd4395342539e"