From 7fd63b2440ca6ebb6b63d0f5b1e0fd260e2fa656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Fri, 14 Aug 2026 21:46:13 -0300 Subject: [PATCH 1/4] fix: migrate email log list page into MUI/uicore components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/mui/async-select-input.js | 61 ++++ src/components/mui/chip-multi-select.js | 56 ++++ src/i18n/en.json | 5 +- src/pages/emails/email-log-list-page.js | 355 +++++++++++------------ src/styles/email-logs-page.less | 7 - 5 files changed, 298 insertions(+), 186 deletions(-) create mode 100644 src/components/mui/async-select-input.js create mode 100644 src/components/mui/chip-multi-select.js delete mode 100644 src/styles/email-logs-page.less diff --git a/src/components/mui/async-select-input.js b/src/components/mui/async-select-input.js new file mode 100644 index 000000000..eb6ce8b03 --- /dev/null +++ b/src/components/mui/async-select-input.js @@ -0,0 +1,61 @@ +import React, { useEffect, useState } from "react"; +import { Autocomplete, CircularProgress, TextField } from "@mui/material"; + +const AsyncSelectInput = ({ + id, + label, + value, + onChange, + queryFunction, + formatOption = (item) => ({ value: item.id, label: item.name }) +}) => { + const [options, setOptions] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchOptions = (input) => { + setLoading(true); + queryFunction(input, (results) => { + setOptions(results.map(formatOption)); + setLoading(false); + }); + }; + + useEffect(() => { + fetchOptions(""); + }, []); + + return ( + option.label || ""} + isOptionEqualToValue={(option, val) => option.value === val.value} + onInputChange={(ev, newInput) => fetchOptions(newInput)} + onChange={(ev, selected) => + onChange({ target: { id, value: selected?.value ?? "" } }) + } + renderInput={(params) => ( + + {loading && } + {params.InputProps?.endAdornment} + + ) + } + }} + /> + )} + /> + ); +}; + +export default AsyncSelectInput; diff --git a/src/components/mui/chip-multi-select.js b/src/components/mui/chip-multi-select.js new file mode 100644 index 000000000..2a0e59efa --- /dev/null +++ b/src/components/mui/chip-multi-select.js @@ -0,0 +1,56 @@ +import React from "react"; +import { + Box, + Chip, + FormControl, + InputLabel, + MenuItem, + OutlinedInput, + Select +} from "@mui/material"; +import CancelIcon from "@mui/icons-material/Cancel"; + +const ChipMultiSelect = ({ id, label, value, onChange, options, sx }) => { + const handleDelete = (val) => + onChange({ target: { value: value.filter((v) => v !== val) } }); + + return ( + + {label} + + + ); +}; + +export default ChipMultiSelect; diff --git a/src/i18n/en.json b/src/i18n/en.json index 58eb3a2c0..9406755e9 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -3265,6 +3265,7 @@ } }, "emails": { + "emails": "Emails", "email_templates": "Email Templates", "sent": "Sent", "templates": "Templates", @@ -3319,6 +3320,9 @@ "email_logs": "Email Logs", "email_list": "Email List", "apply_filters": "Apply Filters", + "enabled_filters": "Enabled Filters", + "all": "All", + "not_sent": "Not Sent", "email_templates": "Email Templates", "subject": "Subject", "from_email": "From Email", @@ -3328,7 +3332,6 @@ "payload": "Payload", "select_fields": "Show Columns", "placeholders": { - "select_fields": "Select data to display", "template": "Filter by Template", "sent_date_from": "Filter Sent Date from", "sent_date_to": "Filter Sent Date to" diff --git a/src/pages/emails/email-log-list-page.js b/src/pages/emails/email-log-list-page.js index 417c16571..1796a482c 100644 --- a/src/pages/emails/email-log-list-page.js +++ b/src/pages/emails/email-log-list-page.js @@ -14,37 +14,47 @@ import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; -import { Pagination } from "react-bootstrap"; -import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search" -import Table from "openstack-uicore-foundation/lib/components/table" -import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown" -import DateTimePicker from "openstack-uicore-foundation/lib/components/inputs/datetimepicker"; +import { + Box, + Button, + Grid2, + ToggleButton, + ToggleButtonGroup +} from "@mui/material"; +import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; +import MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; +import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input"; import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods"; -import { SegmentedControl } from "segmented-control"; -import { getSentEmails } from "../../actions/email-actions"; -import "../../styles/email-logs-page.less"; -import EmailTemplateInput from "../../components/inputs/email-template-input"; +import { getSentEmails, queryTemplates } from "../../actions/email-actions"; +import ChipMultiSelect from "../../components/mui/chip-multi-select"; +import AsyncSelectInput from "../../components/mui/async-select-input"; import { DATE_FILTER_ARRAY_SIZE, DEFAULT_CURRENT_PAGE } from "../../utils/constants"; -const SentEmailListPage = function ({ +const SentEmailListPage = ({ emails, - lastPage, currentPage, term, order, orderDir, totalEmails, - match, perPage, filters, - getSentEmails, - ...props -}) { + getSentEmails +}) => { useEffect(() => { - getSentEmails(term, currentPage, perPage, order, orderDir, filters); + getSentEmails( + term, + DEFAULT_CURRENT_PAGE, + perPage, + order, + orderDir, + filters + ); }, []); const defaultFilters = { @@ -70,10 +80,21 @@ const SentEmailListPage = function ({ getSentEmails(term, newPage, perPage, order, orderDir, emailFilters); }; - const handleSort = (index, key, dir, func) => { + const handleSort = (index, key, dir) => { getSentEmails(term, currentPage, perPage, key, dir, emailFilters); }; + const handlePerPageChange = (newPerPage) => { + getSentEmails( + term, + DEFAULT_CURRENT_PAGE, + newPerPage, + order, + orderDir, + emailFilters + ); + }; + const handleSearch = (newTerm) => { getSentEmails( newTerm, @@ -113,37 +134,19 @@ const SentEmailListPage = function ({ } }; - const handleChangeDateFilter = (ev, lastDate) => { - const { value, id } = ev.target; - const newDateFilter = emailFilters[id]; + const handleChangeDateFilter = (value, lastDate) => { + const newDateFilter = emailFilters.sent_date_filter; setEmailFilters({ ...emailFilters, - [id]: lastDate - ? [newDateFilter[0], value.unix()] - : [value.unix(), newDateFilter[1]] + sent_date_filter: lastDate + ? [newDateFilter[0], value ? value.unix() : null] + : [value ? value.unix() : null, newDateFilter[1]] }); }; const handleEmailFilterChange = (ev) => { - const { type, id } = ev.target; - let { value } = ev.target; - if (type === "operatorinput") { - value = Array.isArray(value) - ? value - : `${ev.target.operator}${ev.target.value}`; - if (id === "duration_filter") { - value = Array.isArray(value) - ? value - : `${ev.target.operator}${ev.target.value}`; - } - } - if (type === "mediatypeinput") { - value = { - operator: ev.target.operator, - value: ev.target.value - }; - } + const { id, value } = ev.target; setEmailFilters({ ...emailFilters, [id]: value }); }; @@ -170,11 +173,11 @@ const SentEmailListPage = function ({ }; const fieldNames = [ - { columnKey: "last_error", value: "last_error" }, + { columnKey: "last_error", header: "last_error" }, { columnKey: "payload", - value: "payload", - render: (row, data) =>
{data}
+ header: "payload", + render: (row, data) => {data} } ]; @@ -183,7 +186,7 @@ const SentEmailListPage = function ({ .map((f2) => { let c = { columnKey: f2.columnKey, - value: T.translate(`email_logs.${f2.value}`), + header: T.translate(`email_logs.${f2.header}`), sortable: f2.sortable }; // optional fields @@ -198,20 +201,20 @@ const SentEmailListPage = function ({ { columnKey: "id", value: T.translate("general.id"), sortable: true }, { columnKey: "template", - value: T.translate("email_logs.email_templates"), + header: T.translate("email_logs.email_templates"), styles: { wordBreak: "break-all" }, sortable: true }, - { columnKey: "subject", value: T.translate("email_logs.subject") }, - { columnKey: "from_email", value: T.translate("email_logs.from_email") }, + { columnKey: "subject", header: T.translate("email_logs.subject") }, + { columnKey: "from_email", header: T.translate("email_logs.from_email") }, { columnKey: "to_email", - value: T.translate("email_logs.to_email"), + header: T.translate("email_logs.to_email"), styles: { wordBreak: "break-word" } }, { columnKey: "sent_date", - value: T.translate("email_logs.sent_date"), + header: T.translate("email_logs.sent_date"), sortable: true } ]; @@ -236,172 +239,168 @@ const SentEmailListPage = function ({ return (
-

- {" "} - {T.translate("email_logs.email_list")} ({totalEmails}) -

-
-
- -
-
-
-
-
- {T.translate("email_logs.email_list")} + + + + {totalEmails} {T.translate("emails.emails")} + + + + + + + + + + + -
-
- -
-
-
+ + + + {enabledFilters.includes("is_sent_filter") && ( -
- handleSetSentFilter(newValue)} - style={{ + + handleSetSentFilter(newValue)} + sx={(theme) => ({ width: "100%", height: 40, - color: "#337ab7", - fontSize: "10px" - }} - /> -
+ "& .MuiToggleButtonGroup-grouped": { + flex: 1, + "&.Mui-selected": { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + "&:hover": { + backgroundColor: theme.palette.primary.dark + } + } + } + })} + > + + {T.translate("email_logs.all")} + + + {T.translate("emails.sent")} + + + {T.translate("email_logs.not_sent")} + + +
)} {enabledFilters.includes("sent_date_filter") && ( - <> -
+ + handleChangeDateFilter(ev, false)} + label={T.translate("email_logs.placeholders.sent_date_from")} + format="YYYY-MM-DD HH:mm" + onChange={(value) => handleChangeDateFilter(value, false)} timezone="UTC" value={epochToMomentTimeZone( emailFilters.sent_date_filter[0], "UTC" )} - className="event-list-date-picker" + slotProps={{ + textField: { fullWidth: true, size: "small" } + }} /> -
-
+ + handleChangeDateFilter(ev, true)} + label={T.translate("email_logs.placeholders.sent_date_to")} + format="YYYY-MM-DD HH:mm" + onChange={(value) => handleChangeDateFilter(value, true)} timezone="UTC" value={epochToMomentTimeZone( emailFilters.sent_date_filter[1], "UTC" )} - className="event-list-date-picker" + slotProps={{ + textField: { fullWidth: true, size: "small" } + }} /> -
- + + )} {enabledFilters.includes("template_filter") && ( -
- + ({ + value: t.identifier, + label: t.identifier + })} /> -
+ )} -
-
-
- - -
-
+ + + + {emails.length === 0 &&
{T.translate("emails.no_emails")}
} {emails.length > 0 && ( - <> -
- - - - + )} ); diff --git a/src/styles/email-logs-page.less b/src/styles/email-logs-page.less deleted file mode 100644 index 3e3ad68c4..000000000 --- a/src/styles/email-logs-page.less +++ /dev/null @@ -1,7 +0,0 @@ -.email-logs-table-wrapper { - overflow: auto; -} - -.email-table-payload { - max-width: 300px; -} From b250a432e92b7ad4c539d76c588e6aaf7f1530ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Tue, 18 Aug 2026 12:53:24 -0300 Subject: [PATCH 2/4] fix: clean localization provider, adjust async input, add onError for query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/actions/email-actions.js | 7 +- src/components/mui/async-select-input.js | 61 ---------------- src/pages/emails/email-log-list-page.js | 89 +++++++++++++++++++----- 3 files changed, 77 insertions(+), 80 deletions(-) delete mode 100644 src/components/mui/async-select-input.js diff --git a/src/actions/email-actions.js b/src/actions/email-actions.js index 2e0799df0..63f6ff57c 100644 --- a/src/actions/email-actions.js +++ b/src/actions/email-actions.js @@ -251,7 +251,7 @@ const normalizeEntity = (entity) => { return normalizedEntity; }; -export const queryTemplates = debounce(async (input, callback) => { +export const queryTemplates = debounce(async (input, callback, onError) => { const accessToken = await getAccessTokenSafely(); const endpoint = URI(`${window.EMAIL_API_BASE_URL}/api/v1/mail-templates`); @@ -274,7 +274,10 @@ export const queryTemplates = debounce(async (input, callback) => { callback(options); }) - .catch(fetchErrorHandler); + .catch((err) => { + fetchErrorHandler(err); + if (onError) onError(err); + }); }, DEBOUNCE_WAIT); /** ********************************************************************************************************* */ diff --git a/src/components/mui/async-select-input.js b/src/components/mui/async-select-input.js deleted file mode 100644 index eb6ce8b03..000000000 --- a/src/components/mui/async-select-input.js +++ /dev/null @@ -1,61 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { Autocomplete, CircularProgress, TextField } from "@mui/material"; - -const AsyncSelectInput = ({ - id, - label, - value, - onChange, - queryFunction, - formatOption = (item) => ({ value: item.id, label: item.name }) -}) => { - const [options, setOptions] = useState([]); - const [loading, setLoading] = useState(false); - - const fetchOptions = (input) => { - setLoading(true); - queryFunction(input, (results) => { - setOptions(results.map(formatOption)); - setLoading(false); - }); - }; - - useEffect(() => { - fetchOptions(""); - }, []); - - return ( - option.label || ""} - isOptionEqualToValue={(option, val) => option.value === val.value} - onInputChange={(ev, newInput) => fetchOptions(newInput)} - onChange={(ev, selected) => - onChange({ target: { id, value: selected?.value ?? "" } }) - } - renderInput={(params) => ( - - {loading && } - {params.InputProps?.endAdornment} - - ) - } - }} - /> - )} - /> - ); -}; - -export default AsyncSelectInput; diff --git a/src/pages/emails/email-log-list-page.js b/src/pages/emails/email-log-list-page.js index 1796a482c..5be5b7af9 100644 --- a/src/pages/emails/email-log-list-page.js +++ b/src/pages/emails/email-log-list-page.js @@ -15,21 +15,21 @@ import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; import { + Autocomplete, Box, Button, + CircularProgress, Grid2, + TextField, ToggleButton, ToggleButtonGroup } from "@mui/material"; import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; -import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; -import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input"; import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods"; import { getSentEmails, queryTemplates } from "../../actions/email-actions"; import ChipMultiSelect from "../../components/mui/chip-multi-select"; -import AsyncSelectInput from "../../components/mui/async-select-input"; import { DATE_FILTER_ARRAY_SIZE, DEFAULT_CURRENT_PAGE @@ -75,6 +75,26 @@ const SentEmailListPage = ({ ...filters }); const [selectedColumns, setSelectedColumns] = useState([]); + const [templateOptions, setTemplateOptions] = useState([]); + const [templateLoading, setTemplateLoading] = useState(false); + + const fetchTemplateOptions = (input) => { + setTemplateLoading(true); + queryTemplates( + input, + (results) => { + setTemplateOptions( + results.map((t) => ({ value: t.identifier, label: t.identifier })) + ); + setTemplateLoading(false); + }, + () => setTemplateLoading(false) + ); + }; + + useEffect(() => { + fetchTemplateOptions(""); + }, []); const handlePageChange = (newPage) => { getSentEmails(term, newPage, perPage, order, orderDir, emailFilters); @@ -328,11 +348,12 @@ const SentEmailListPage = ({ )} {enabledFilters.includes("sent_date_filter") && ( - + <> handleChangeDateFilter(value, false)} timezone="UTC" value={epochToMomentTimeZone( @@ -347,7 +368,8 @@ const SentEmailListPage = ({ handleChangeDateFilter(value, true)} timezone="UTC" value={epochToMomentTimeZone( @@ -359,20 +381,53 @@ const SentEmailListPage = ({ }} /> - + )} {enabledFilters.includes("template_filter") && ( - ({ - value: t.identifier, - label: t.identifier - })} + option.label || ""} + isOptionEqualToValue={(option, val) => option.value === val.value} + onInputChange={(ev, newInput) => fetchTemplateOptions(newInput)} + onChange={(ev, selected) => + handleEmailFilterChange({ + target: { + id: "template_filter", + value: selected?.value ?? "" + } + }) + } + renderInput={(params) => ( + + {templateLoading && ( + + )} + {params.InputProps?.endAdornment} + + ) + } + }} + /> + )} /> )} From 5e42e9ba070a2b1dfa81932e2bf55fc3e0162a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Tue, 18 Aug 2026 13:19:38 -0300 Subject: [PATCH 3/4] fix: remove chip multi select, add ref on tempalte request from coderabbit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/mui/chip-multi-select.js | 56 ---------------- src/pages/emails/email-log-list-page.js | 86 ++++++++++++++++++++----- 2 files changed, 69 insertions(+), 73 deletions(-) delete mode 100644 src/components/mui/chip-multi-select.js diff --git a/src/components/mui/chip-multi-select.js b/src/components/mui/chip-multi-select.js deleted file mode 100644 index 2a0e59efa..000000000 --- a/src/components/mui/chip-multi-select.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from "react"; -import { - Box, - Chip, - FormControl, - InputLabel, - MenuItem, - OutlinedInput, - Select -} from "@mui/material"; -import CancelIcon from "@mui/icons-material/Cancel"; - -const ChipMultiSelect = ({ id, label, value, onChange, options, sx }) => { - const handleDelete = (val) => - onChange({ target: { value: value.filter((v) => v !== val) } }); - - return ( - - {label} - - - ); -}; - -export default ChipMultiSelect; diff --git a/src/pages/emails/email-log-list-page.js b/src/pages/emails/email-log-list-page.js index 5be5b7af9..de904672d 100644 --- a/src/pages/emails/email-log-list-page.js +++ b/src/pages/emails/email-log-list-page.js @@ -11,7 +11,7 @@ * limitations under the License. * */ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; import { @@ -19,7 +19,12 @@ import { Box, Button, CircularProgress, + FormControl, Grid2, + InputLabel, + MenuItem, + OutlinedInput, + Select, TextField, ToggleButton, ToggleButtonGroup @@ -29,7 +34,6 @@ import MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input"; import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods"; import { getSentEmails, queryTemplates } from "../../actions/email-actions"; -import ChipMultiSelect from "../../components/mui/chip-multi-select"; import { DATE_FILTER_ARRAY_SIZE, DEFAULT_CURRENT_PAGE @@ -77,18 +81,24 @@ const SentEmailListPage = ({ const [selectedColumns, setSelectedColumns] = useState([]); const [templateOptions, setTemplateOptions] = useState([]); const [templateLoading, setTemplateLoading] = useState(false); + const templateRequestSeqRef = useRef(0); const fetchTemplateOptions = (input) => { + const seq = ++templateRequestSeqRef.current; setTemplateLoading(true); queryTemplates( input, (results) => { + if (seq !== templateRequestSeqRef.current) return; setTemplateOptions( results.map((t) => ({ value: t.identifier, label: t.identifier })) ); setTemplateLoading(false); }, - () => setTemplateLoading(false) + () => { + if (seq !== templateRequestSeqRef.current) return; + setTemplateLoading(false); + } ); }; @@ -295,13 +305,35 @@ const SentEmailListPage = ({ - + + + {T.translate("email_logs.enabled_filters")} + + +