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 && (
+ }
+ onClick={() => setMobileFiltersOpen((prev) => !prev)}
+ fullWidth
+ sx={{ mb: 2 }}
+ >
+ {mobileFiltersOpen
+ ? formatMessage('executionFilters.hideFilters')
+ : formatMessage('executionFilters.showFilters')}
+
+ )}
+
+ {/* 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"