diff --git a/src/components/common/Alerts/AlertsData.ts b/src/components/common/Alerts/AlertsData.ts index 859172cf22126aca3cf983289ba5b37b0f277019..b94eeb01154c49b50da2e7d42d418416fcf56cb0 100644 --- a/src/components/common/Alerts/AlertsData.ts +++ b/src/components/common/Alerts/AlertsData.ts @@ -7,7 +7,14 @@ interface BrowserError { status: string; } -const isBrowserError = (error: unknown | BrowserError) => { +const isBrowserError = ( + error: + | BrowserError + | FetchBaseQueryError + | SerializedError + | BrowserError + | unknown +) => { return ( error != null && typeof error === "object" && @@ -17,7 +24,7 @@ const isBrowserError = (error: unknown | BrowserError) => { }; export const isFetchBaseQueryError = ( - error: unknown + error: FetchBaseQueryError | SerializedError | BrowserError | unknown ): error is FetchBaseQueryError => { return ( error != null && @@ -27,9 +34,9 @@ export const isFetchBaseQueryError = ( ); }; -export const isErrorWithMessage = ( - error: unknown -): error is { message: string } => { +export const isSerializedError = ( + error: FetchBaseQueryError | SerializedError | BrowserError | unknown +): error is SerializedError => { return ( error != null && typeof error === "object" && @@ -38,24 +45,37 @@ export const isErrorWithMessage = ( ); }; -export const getErrorMessage = ( +export const getErrorState = ( error: FetchBaseQueryError | SerializedError | BrowserError | unknown ) => { if (error) { if (isFetchBaseQueryError(error)) { const customError = error.data as ErrorMessage; if (customError) { - return customError.description - ? customError.description - : customError.error; + return { + message: customError.description || customError.error, + status: error.status + }; } - } else if (isErrorWithMessage(error)) { - return error.message; + } else if (isSerializedError(error)) { + return { + message: error.message, + status: error.code + }; } else if (isBrowserError(error)) { const browserError = error as BrowserError; - return browserError.error; + return { + message: browserError.error, + status: browserError.status + }; } - return "Unknown error"; + return { + message: "Unknown error", + status: 404 + }; } - return "Unknown error"; + return { + message: "Unknown error", + status: 404 + }; }; diff --git a/src/components/common/Alerts/ApiAlertError.tsx b/src/components/common/Alerts/ApiAlertError.tsx index a59fdc8e63cbe0df57d66d73a5277ac0d9bc5ab8..ed48ac3167462b3d20c7a99982e03ffec5194708 100644 --- a/src/components/common/Alerts/ApiAlertError.tsx +++ b/src/components/common/Alerts/ApiAlertError.tsx @@ -1,5 +1,5 @@ import { Alert } from "@mui/material"; -import { getErrorMessage } from "./AlertsData"; +import { getErrorState } from "./AlertsData"; import { ApiError } from "../../../types/common"; interface ApiAlertErrorProps { @@ -7,5 +7,5 @@ interface ApiAlertErrorProps { } export const ApiAlertError = ({ error }: ApiAlertErrorProps) => ( - <Alert severity="error">{getErrorMessage(error)}</Alert> + <Alert severity="error">{getErrorState(error).message}</Alert> ); diff --git a/src/components/common/Git/GitRefLink.tsx b/src/components/common/Git/GitRefLink.tsx index 35551659cd65accdd0881dd39a2b8a8296f03908..2209edfb32e055c39924f421865ef38df96f8687 100644 --- a/src/components/common/Git/GitRefLink.tsx +++ b/src/components/common/Git/GitRefLink.tsx @@ -1,10 +1,10 @@ import { ExternalLink } from "@ess-ics/ce-ui-common"; interface GitRefLinkProps { - url: string | undefined; - revision: string; - displayReference: string; - disableExternalLinkIcon: boolean; + url?: string; + revision?: string; + displayReference?: string; + disableExternalLinkIcon?: boolean; } export const GitRefLink = ({ diff --git a/src/components/common/Helper.tsx b/src/components/common/Helper.tsx index fcc90eed4339c385fc87f500fe1b5bc21165d211..ab415c671e63b6779c21820042844385cc358f59 100644 --- a/src/components/common/Helper.tsx +++ b/src/components/common/Helper.tsx @@ -1,61 +1,6 @@ -import { useState, useEffect } from "react"; +import { type Dispatch, type SetStateAction } from "react"; import env from "../../config/env"; - -export function formatToList(items) { - if (!items) { - return null; - } - - return ( - <ul style={{ padding: 0 }}> - {items.map((item) => ( - <li - style={{ listStylePosition: "inside" }} - key={item} - > - {item} - </li> - ))} - </ul> - ); -} - -export function searchInArray(array, searchText) { - if (array) { - const found = array.find((element) => - element.toLowerCase().includes(searchText) - ); - - return found; - } - - return false; -} - -function getWindowDimensions() { - const { innerWidth: width, innerHeight: height } = window; - return { - width, - height - }; -} - -export function useWindowDimensions() { - const [windowDimensions, setWindowDimensions] = useState( - getWindowDimensions() - ); - - useEffect(() => { - function handleResize() { - setWindowDimensions(getWindowDimensions()); - } - - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); - - return windowDimensions; -} +import { Pagination } from "../../types/common"; function applicationSubTitle() { const title = `${env.ENVIRONMENT_TITLE}`; @@ -67,65 +12,44 @@ function applicationSubTitle() { return ""; } -export function applicationTitle(...breadcrumbs) { - return [`CE deploy & monitor ${applicationSubTitle()}`, ...breadcrumbs].join( - " / " - ); +export function applicationTitle(title: string = "") { + return [`CE deploy & monitor ${applicationSubTitle()}`, title].join(" / "); +} +interface initRequestParamsProps { + pagination: Pagination; + filter?: string; } -export function initRequestParams(lazyParams, filter, columnSort) { +export function initRequestParams({ + pagination, + filter +}: initRequestParamsProps) { const requestParams = { - page: lazyParams.page, - limit: lazyParams.rows + page: pagination.page, + limit: pagination.rows, + query: "" }; if (filter != null && filter) { requestParams.query = filter; } - if (columnSort) { - if (columnSort.sortOrder === 1) { - requestParams.orderAsc = true; - } else { - requestParams.orderAsc = false; - } - } - return requestParams; } -export const getErrorMessage = (error) => { - const { response, status: requestStatus, message: requestMessage } = error; - if (response && response?.body) { - const { - body: { description }, - status: httpStatus - } = response; - - return `${httpStatus ?? requestStatus ?? "unknown"}: ${ - description ?? requestMessage ?? "An unknown error has occurred" - }`; - } - return `${requestStatus}: ${requestMessage}`; -}; - -export const isAbortError = (error) => error?.name === "AbortError"; - -export const compareArrays = (a1, a2) => - a1.length === a2.length && - a1.every((element, index) => element === a2[index]); +interface onFetchEntityErrorProps { + message?: string; + status?: string | number; + setNotFoundError: Dispatch<SetStateAction<string | null>>; +} -export function onFetchEntityError(message, status, setNotFoundError) { - const notFound = status === 404; +export function onFetchEntityError({ + message, + status, + setNotFoundError +}: onFetchEntityErrorProps) { + const notFound = status === 404 || status === "404"; if (notFound) { - setNotFoundError(message); + setNotFoundError(message ?? ""); } return !notFound; } - -export function pick(obj, keys) { - const ret = {}; - for (const key of keys) { - ret[key] = obj[key]; - } - return ret; -} diff --git a/src/components/common/LogStream/LogStreamConsole.tsx b/src/components/common/LogStream/LogStreamConsole.tsx index ec2cf2cd6444088dee30314794907f87b371c34c..fa0e2a926ae69ef12042f8961d05c653ec334825 100644 --- a/src/components/common/LogStream/LogStreamConsole.tsx +++ b/src/components/common/LogStream/LogStreamConsole.tsx @@ -6,7 +6,7 @@ import { LinearProgress } from "@mui/material"; around content that should be displayed*/ interface LogStreamConsoleProps { - log: string; + log?: string; dataReady: boolean; height: string; loading?: boolean; @@ -79,7 +79,7 @@ export const LogStreamConsole = ({ "& pre": { height: height } }} // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: log }} + dangerouslySetInnerHTML={{ __html: log || "" }} /> ) : ( <div style={{ width: "100%" }}> diff --git a/src/components/common/LogStream/LogStreamConsoleDialog.tsx b/src/components/common/LogStream/LogStreamConsoleDialog.tsx index caac80851a6e773eaea3153d046e80eb0f58d674..4e70afad260ade8cf39a3607df6057e41c3fcd28 100644 --- a/src/components/common/LogStream/LogStreamConsoleDialog.tsx +++ b/src/components/common/LogStream/LogStreamConsoleDialog.tsx @@ -6,9 +6,9 @@ interface LogStreamConsoleDialogProps { title: string; loading: boolean; open: boolean; - log: string; + log?: string; dataReady: boolean; - children: JSX.Element; + children?: JSX.Element; onDialogClose: () => void; } @@ -22,39 +22,37 @@ export const LogStreamConsoleDialog = ({ children }: LogStreamConsoleDialogProps) => { return ( - <> - <Dialog - title={ - <Typography - variant="h2" - marginY={1} - > - {title} - </Typography> - } - content={ - loading ? ( - <div style={{ width: "100%", height: "600px" }}> - <LinearProgress color="primary" /> - </div> - ) : ( - <Container maxWidth="xl"> - {children} - <LogStreamConsole - log={log} - dataReady={dataReady} - height="600px" - /> - </Container> - ) - } - open={open} - onClose={onDialogClose} - DialogProps={{ - fullWidth: true, - maxWidth: "xl" - }} - /> - </> + <Dialog + title={ + <Typography + variant="h2" + marginY={1} + > + {title} + </Typography> + } + content={ + loading ? ( + <div style={{ width: "100%", height: "600px" }}> + <LinearProgress color="primary" /> + </div> + ) : ( + <Container maxWidth="xl"> + {children} + <LogStreamConsole + log={log} + dataReady={dataReady} + height="600px" + /> + </Container> + ) + } + open={open} + onClose={onDialogClose} + DialogProps={{ + fullWidth: true, + maxWidth: "xl" + }} + /> ); }; diff --git a/src/components/common/Loki/LokiPanel.tsx b/src/components/common/Loki/LokiPanel.tsx index 8cb6b5167d27611c29613b9100a1ba9daca0a8d0..2718bb19b7ca2662dbd439525a0137e574181e13 100644 --- a/src/components/common/Loki/LokiPanel.tsx +++ b/src/components/common/Loki/LokiPanel.tsx @@ -1,11 +1,25 @@ -import { useState, useEffect, useCallback, useMemo } from "react"; -import { Stack, LinearProgress, Box } from "@mui/material"; +import { + useState, + useEffect, + useCallback, + useMemo, + type Dispatch, + type SetStateAction +} from "react"; +import { + Stack, + LinearProgress, + Box, + type SelectChangeEvent +} from "@mui/material"; import { closeSnackbar } from "notistack"; import { formatDateAndTime } from "@ess-ics/ce-ui-common"; import Convert from "ansi-to-html"; import { useLazyFetchSyslogLinesQuery, - useLazyFetchProcServLogLinesQuery + useLazyFetchProcServLogLinesQuery, + LokiResponse, + LokiMessage } from "../../../store/deployApi"; import { ApiAlertError } from "../Alerts/ApiAlertError"; import { LogStreamConsole } from "../LogStream/LogStreamConsole"; @@ -13,7 +27,7 @@ import { LogStreamConsoleDialog } from "../LogStream/LogStreamConsoleDialog"; import { TimeRange } from "../Inputs/TimeRange"; import { PopoutButton } from "../Buttons/PopoutButton"; import { useCustomSnackbar } from "../snackbar/Snackbar"; -import { isAbortError } from "../Helper"; +import type { SnackbarKey } from "notistack"; const TIME_RANGE_VALUES = [ { @@ -33,27 +47,79 @@ const TIME_RANGE_VALUES = [ */ const LOG_POLL_INTERVAL = 5000; -export function LokiPanel({ hostName, iocName, isSyslog, isExpanded }) { - const showWarning = useCustomSnackbar(); + +interface LokiPanelProps { + hostName?: string; + iocName?: string; + isSyslog?: boolean; + isExpanded: boolean; +} + +interface PreprocessLogProps { + logData: LokiResponse | undefined; + showWarning: (message: string) => SnackbarKey; + timeRange: number; + alertIds: SnackbarKey[]; + setAlertIds: Dispatch<SetStateAction<SnackbarKey[]>>; +} + +export function LokiPanel({ + hostName, + iocName, + isSyslog, + isExpanded +}: LokiPanelProps) { + const { showWarning } = useCustomSnackbar(); const [timeRange, setTimeRange] = useState(720); const [logDialogOpen, setLogDialogOpen] = useState(false); const [periodChange, setPeriodChange] = useState(false); - const [alertIds, setAlertIds] = useState([]); + const [alertIds, setAlertIds] = useState<SnackbarKey[]>([]); const [html, setHtml] = useState(""); + const params = useMemo( + () => ({ + hostName: hostName || "", + iocName: iocName ?? "", + timeRange: timeRange + }), + [hostName, iocName, timeRange] + ); + + const [ + getSysLogData, + { data: sysLogData, error: sysLogError, isLoading: sysDataIsLoading } + ] = useLazyFetchSyslogLinesQuery({ + pollingInterval: isExpanded ? LOG_POLL_INTERVAL : 0 + }); + + const [ + getProcServLog, + { data: procServLog, error: procServLogError, isLoading: procDataIsLoading } + ] = useLazyFetchProcServLogLinesQuery({ + pollingInterval: isExpanded ? LOG_POLL_INTERVAL : 0 + }); + const preprocessLog = useCallback( - (logData, showWarning, timeRange, alertIds, setAlertIds) => { + ({ + logData, + showWarning, + timeRange, + alertIds, + setAlertIds + }: PreprocessLogProps) => { if (!logData) { return ""; } + const logLines = logData.lines; + if (logData?.warning && alertIds?.length === 0) { - const warningId = showWarning(logData.warning, "warning"); - setAlertIds((alertIds) => [...alertIds, warningId]); + const warningId = showWarning(logData.warning); + setAlertIds((prev) => [...prev, warningId]); } - if (logData && logData.lines?.length > 0) { - const logHtml = logData.lines.map((line) => formatLogLine(line)); + if (logLines && logLines.length > 0) { + const logHtml = logLines.map((line) => formatLogLine(line)); return `<html> <head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <style type="text/css">.logdate { color: #5cb85c; } body.pre{font-family: Monaco, Menlo, Consolas, "Courier New", monospace;} @@ -65,72 +131,44 @@ export function LokiPanel({ hostName, iocName, isSyslog, isExpanded }) { </html>`; } - return `<html><body><pre> - No messages found for ${TIME_RANGE_VALUES.find((range) => range.value === timeRange).text} period - </pre></body></html>`; + return `<html><body><pre> - No messages found for ${TIME_RANGE_VALUES.find(({ value }: { value: number }) => value === timeRange)?.text} period - </pre></body></html>`; }, [] ); - const logsToPreprocess = useCallback( - ( - isSysLog, - logData, - procServLog, - showWarning, - timeRange, - alertIds, - setAlertIds - ) => { - if (isSysLog === true) { - return preprocessLog( - logData, - showWarning, - timeRange, - alertIds, - setAlertIds - ); - } - - return preprocessLog( - procServLog, + const logsToPreprocess = useCallback(() => { + if (isSyslog === true) { + return preprocessLog({ + logData: sysLogData, showWarning, timeRange, alertIds, setAlertIds - ); - }, - [preprocessLog] - ); - - const params = useMemo( - () => ({ - hostName: hostName, - iocName: iocName, - timeRange: timeRange - }), - [hostName, iocName, timeRange] - ); - - const [ - getSysLogData, - { data: sysLogData, error: sysLogError, isLoading: sysDataIsLoading } - ] = useLazyFetchSyslogLinesQuery({ - pollingInterval: isExpanded ? LOG_POLL_INTERVAL : 0 - }); + }); + } - const [ - getProcServLog, - { data: procServLog, error: procServLogError, isLoading: procDataIsLoading } - ] = useLazyFetchProcServLogLinesQuery({ - pollingInterval: isExpanded ? LOG_POLL_INTERVAL : 0 - }); + return preprocessLog({ + logData: procServLog, + showWarning, + timeRange, + alertIds, + setAlertIds + }); + }, [ + alertIds, + isSyslog, + preprocessLog, + procServLog, + showWarning, + sysLogData, + timeRange + ]); const hasLogError = !!sysLogError || !!procServLogError; - const hasAbortError = - isAbortError(sysLogError) || isAbortError(procServLogError); - const handleTimeRangeChange = (event) => { + const handleTimeRangeChange = (event: SelectChangeEvent<number>) => { setPeriodChange(true); - setTimeRange(event.target?.value); + setTimeRange(Number(event?.target?.value)); }; // remove progressBar if intervall has been changed, and data received @@ -158,31 +196,10 @@ export function LokiPanel({ hostName, iocName, isSyslog, isExpanded }) { }, [getSysLogData, getProcServLog, isSyslog, isExpanded, params]); useEffect(() => { - setHtml( - logsToPreprocess( - isSyslog, - sysLogData, - procServLog, - showWarning, - timeRange, - alertIds, - setAlertIds - ) - ); - }, [ - setHtml, - alertIds, - isSyslog, - logsToPreprocess, - procServLog, - showWarning, - sysLogData, - timeRange - ]); + setHtml(logsToPreprocess()); + }, [logsToPreprocess]); - const dataReady = () => { - return sysLogData || procServLog; - }; + const dataReady = !!sysLogData || !!procServLog; if (sysDataIsLoading || procDataIsLoading) { return ( @@ -231,7 +248,7 @@ export function LokiPanel({ hostName, iocName, isSyslog, isExpanded }) { position: "relative" }} > - {hasLogError && !hasAbortError && ( + {hasLogError && ( <Box sx={{ position: "absolute", @@ -255,14 +272,14 @@ export function LokiPanel({ hostName, iocName, isSyslog, isExpanded }) { ); } -function formatLogLine(logLine) { +function formatLogLine(logLine: LokiMessage) { const convert = new Convert(); return ( "<span><span class=logdate>" + formatDateAndTime(logLine.logDate) + "</span> " + - convert.toHtml(logLine.logMessage) + + convert.toHtml(logLine.logMessage ?? "") + "<br/></span>" ); } diff --git a/src/components/common/Popover/Popover.tsx b/src/components/common/Popover/Popover.tsx index 680cdea228ed1deb77b94673a873d78a9f5de3bc..1b5a8d1f38af10b3c4240cf52bb27b2c3b8474e7 100644 --- a/src/components/common/Popover/Popover.tsx +++ b/src/components/common/Popover/Popover.tsx @@ -29,15 +29,36 @@ const StyledMuiPopover = styled(MuiPopover)(({ theme }) => ({ * @param {...Object} popoverProps - Mui Popover props for e.g. overriding behavior such as anchor and transform origin * @returns JSX component */ + +interface PopoverProps { + renderOwningComponent: ({ + ariaOwns, + ariaHasPopup, + handlePopoverOpen, + handlePopoverClose + }: { + ariaOwns: string; + ariaHasPopup: boolean; + handlePopoverOpen: (event: React.MouseEvent<HTMLElement>) => void; + handlePopoverClose: () => void; + }) => JSX.Element; + popoverContents: JSX.Element; + id: string; + anchorOrigin: { + vertical: "top" | "center" | "bottom"; + horizontal: "left" | "center" | "right"; + }; +} + export const Popover = ({ renderOwningComponent, popoverContents, id, - ...popoverProps -}) => { - const [anchorEl, setAnchorEl] = useState(null); + anchorOrigin +}: PopoverProps) => { + const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null); - const handlePopoverOpen = (event) => { + const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => { setAnchorEl(event.currentTarget); }; @@ -47,7 +68,7 @@ export const Popover = ({ const open = Boolean(anchorEl); const elemId = id ? id : "popover-id"; - const ariaOwns = open ? elemId : undefined; + const ariaOwns = open ? elemId : ""; return ( <> @@ -63,7 +84,7 @@ export const Popover = ({ anchorEl={anchorEl} onClose={handlePopoverClose} disableRestoreFocus - {...popoverProps} + anchorOrigin={anchorOrigin} > {popoverContents} </StyledMuiPopover> diff --git a/src/components/common/Status/Status.tsx b/src/components/common/Status/Status.tsx index 0b1e669f1270c730fb0e79ff2147f6612f9ef498..e59543303cc0d0abd6b2e9d4c89c8fad85b83a43 100644 --- a/src/components/common/Status/Status.tsx +++ b/src/components/common/Status/Status.tsx @@ -1,13 +1,19 @@ import { useState, useEffect } from "react"; -import { LabeledIcon } from "@ess-ics/ce-ui-common"; import { object, bool, array, func } from "prop-types"; import { useTheme, Skeleton } from "@mui/material"; -import { PlayCircleFilled, PauseCircleFilled } from "@mui/icons-material"; import { StatusPopoverContent } from "./StatusPopoverContent"; import { StatusBadge } from "./StatusBadge"; import { StatusIcon } from "./StatusIcon"; import { statusesWithAlerts, statusConfig } from "./StatusData"; import { Popover } from "../../common/Popover"; +import { + HostStatusResponse, + HostAlertResponse, + IocStatusResponse, + IocAlertResponse +} from "../../../store/deployApi"; +import { getHostStatus } from "../../host/HostStatus"; +import { getIOCStatus } from "../../IOC/IOCStatus/IOCStatusData"; const propsTypes = { state: object, @@ -16,9 +22,23 @@ const propsTypes = { getStatusFcn: func }; -export function Status({ id, state, alert, hideAlerts = false, getStatusFcn }) { - const [status, setStatus] = useState(null); +interface StatusProps { + id?: string | number | undefined; + state: HostStatusResponse | IocStatusResponse | undefined; + alert: HostAlertResponse | IocAlertResponse | undefined; + hideAlerts?: boolean; + getStatusFcn: typeof getHostStatus | typeof getIOCStatus; +} + +export function Status({ + id, + state, + alert, + hideAlerts = false, + getStatusFcn +}: StatusProps) { const theme = useTheme(); + const [status, setStatus] = useState<string | null>(null); useEffect(() => { if (state) { @@ -45,10 +65,7 @@ export function Status({ id, state, alert, hideAlerts = false, getStatusFcn }) { style={{ color: theme.palette.status.icons }} > {!hideAlerts && statusesWithAlerts.includes(status) ? ( - <StatusBadge - status={status} - theme={theme} - > + <StatusBadge status={status}> <StatusIcon alerts={alert?.alerts ?? []} status={status} @@ -85,51 +102,3 @@ export function Status({ id, state, alert, hideAlerts = false, getStatusFcn }) { } Status.propsTypes = propsTypes; - -export function CommandTypeIcon({ - type, - colorStyle = "colors", - labelPosition = "tooltip" -}) { - const theme = useTheme(); - - const commandTypeColors = { - start: theme.palette.status.ok.main, - stop: theme.palette.status.fail.main - }; - - const commandTypeBlack = { - start: theme.palette.status.icons, - stop: theme.palette.status.icons - }; - - const colorConfig = { - black: commandTypeBlack, - colors: commandTypeColors - }; - - const commandTypeIcons = { - start: { - title: "Set to active", - icon: PlayCircleFilled - }, - stop: { - title: "Set to inactive", - icon: PauseCircleFilled - } - }; - - const iconStyle = { fill: colorConfig[colorStyle][type.toLowerCase()] }; - const iconTitle = commandTypeIcons[type.toLowerCase()].title; - const statusIcon = commandTypeIcons[type.toLowerCase()].icon; - - return ( - <LabeledIcon - label={iconTitle} - LabelProps={{ noWrap: true }} - labelPosition={labelPosition} - Icon={statusIcon} - IconProps={{ style: { iconStyle } }} - /> - ); -} diff --git a/src/components/common/Status/StatusBadge.tsx b/src/components/common/Status/StatusBadge.tsx index c3dfa9120d4e630572f87341f384cd3542818f09..3cedb480361a4cae56d7b85a2db5431757ec689e 100644 --- a/src/components/common/Status/StatusBadge.tsx +++ b/src/components/common/Status/StatusBadge.tsx @@ -1,5 +1,6 @@ +import { type ReactNode } from "react"; import { string, object, arrayOf, oneOfType, node } from "prop-types"; -import { Stack } from "@mui/material"; +import { Stack, useTheme } from "@mui/material"; import WarningAmberIcon from "@mui/icons-material/WarningAmber"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import { STATUS } from "./StatusData"; @@ -17,7 +18,13 @@ const commonStyles = { fontSize: "20px" }; -export const StatusBadge = ({ status, theme, children }) => { +interface StatusBadgeProps { + status: string; + children: ReactNode; +} + +export const StatusBadge = ({ status, children }: StatusBadgeProps) => { + const theme = useTheme(); const showWarning = status === STATUS.warning || status === STATUS.inactiveWarning || diff --git a/src/components/common/Status/StatusData.ts b/src/components/common/Status/StatusData.ts index 57719e4f31801e210fef10092f10ae12843f7ae3..8e784a3a06c04708d0cb911addfba8a3f831ffdc 100644 --- a/src/components/common/Status/StatusData.ts +++ b/src/components/common/Status/StatusData.ts @@ -74,54 +74,3 @@ export const statusConfig = { icon: HelpOutline } }; - -export const getStatus = (state, alert) => { - let { isActive } = state; - const isDeployed = !state.alertSeverity; - const alertSeverity = alert?.alertSeverity?.toLowerCase(); - - if ( - isActive && - (alertSeverity === undefined || alertSeverity === SEVERITY.info) - ) { - // Active status / running - return STATUS.active; - } else if ( - !isActive && - isDeployed && - (!alertSeverity || alertSeverity === SEVERITY.info) - ) { - // stopped/paused - return STATUS.disabled; - } else if (!isActive && isDeployed && alertSeverity === SEVERITY.alert) { - return STATUS.disabledAlert; - } else if (!isActive && isDeployed && alertSeverity === SEVERITY.warning) { - return STATUS.disabledWarning; - } else if ( - isActive && - alertSeverity !== undefined && - alertSeverity === SEVERITY.alert - ) { - // error - return STATUS.alert; - } else if ( - isActive && - alertSeverity !== undefined && - alertSeverity === SEVERITY.warning - ) { - // warning - return STATUS.warning; - } else if ( - isActive === false && - (!alertSeverity || alertSeverity === SEVERITY.info) - ) { - return STATUS.inactive; - } else if (isActive === false && alertSeverity === SEVERITY.alert) { - return STATUS.inactiveAlert; - } else if (isActive === false && alertSeverity === SEVERITY.warning) { - return STATUS.inactiveWarning; - } else { - // unknown fallback state - return null; - } -}; diff --git a/src/components/common/Status/StatusIcon.tsx b/src/components/common/Status/StatusIcon.tsx index 2515bd8c8057e9252bf491c6486d79342cb2bc14..876c948d852f54834be66285467db344f008d009 100644 --- a/src/components/common/Status/StatusIcon.tsx +++ b/src/components/common/Status/StatusIcon.tsx @@ -1,12 +1,18 @@ import { array, string } from "prop-types"; import { statusConfig, statusesWithAlerts } from "./StatusData"; +import { Alert } from "../../../store/deployApi"; const propTypes = { alerts: array, status: string }; -const getLabel = (alerts, status) => { +interface StatusIconProps { + alerts: Alert[]; + status: string; +} + +const getLabel = ({ alerts, status }: StatusIconProps) => { if (statusesWithAlerts.includes(status)) { const warnings = alerts.filter((alert) => alert.type === "WARNING"); const errors = alerts.filter((alert) => alert.type === "ERROR"); @@ -19,12 +25,10 @@ const getLabel = (alerts, status) => { return statusConfig[status].title; }; -const getIcon = (status) => statusConfig[status].icon; - -export const StatusIcon = ({ alerts, status }) => { - const Icon = getIcon(status); +export const StatusIcon = ({ alerts, status }: StatusIconProps) => { + const Icon = statusConfig[status].icon; - return <Icon aria-label={getLabel(alerts, status)} />; + return <Icon aria-label={getLabel({ alerts, status })} />; }; StatusIcon.propTypes = propTypes; diff --git a/src/components/common/Status/StatusPopoverContent.tsx b/src/components/common/Status/StatusPopoverContent.tsx index 5f139799697bb17ae4ee07143766610fafb81a81..187d3889b884e5efe674e37448eb3e76abb12612 100644 --- a/src/components/common/Status/StatusPopoverContent.tsx +++ b/src/components/common/Status/StatusPopoverContent.tsx @@ -2,19 +2,29 @@ import { string, arrayOf, object } from "prop-types"; import { Typography, Stack } from "@mui/material"; import { AlertBanner, useUniqueKeys } from "@ess-ics/ce-ui-common"; import { SEVERITY } from "./StatusData"; +import { Alert } from "../../../store/deployApi"; const propsTypes = { title: string, alerts: arrayOf(object), object }; -export const StatusPopoverContent = ({ title, alerts }) => { + +interface StatusPopoverContentProps { + title: string; + alerts: Alert[]; +} +export const StatusPopoverContent = ({ + title, + alerts +}: StatusPopoverContentProps) => { // Filter out INFO level alerts // And for now filter out links on alerts due to issues with // focus behavior of popovers // Also normalize the type to lowercase since AlertBanner expects lowercase const sanitizedAlerts = ( - alerts?.filter((alert) => alert?.type.toLowerCase() !== SEVERITY.info) || [] + alerts?.filter((alert) => alert?.type?.toLowerCase() !== SEVERITY.info) || + [] ).map((alert) => ({ ...alert, type: alert?.type?.toLowerCase(), diff --git a/src/components/common/Status/index.ts b/src/components/common/Status/index.ts index 7d5d12af51a63a548e7abced9b1e94543de05b8a..2721feda423d8c97c407e2513cef9321c50feeef 100644 --- a/src/components/common/Status/index.ts +++ b/src/components/common/Status/index.ts @@ -1,17 +1,15 @@ -import { Status, CommandTypeIcon } from "./Status"; +import { Status } from "./Status"; import { StatusBadge } from "./StatusBadge"; import { StatusIcon } from "./StatusIcon"; import { StatusPopoverContent } from "./StatusPopoverContent"; -import { statusConfig, STATUS, SEVERITY, getStatus } from "./StatusData"; +import { statusConfig, STATUS, SEVERITY } from "./StatusData"; export { Status, - CommandTypeIcon, StatusPopoverContent, StatusBadge, StatusIcon, STATUS, SEVERITY, - statusConfig, - getStatus + statusConfig }; diff --git a/src/components/common/User/UserAvatar.tsx b/src/components/common/User/UserAvatar.tsx index c9e7d3f8ae0b5aaedce0b569cf77701bdd3f9b0f..50e14c8fb1335f5a61242f1e3a0668afd0e90419 100644 --- a/src/components/common/User/UserAvatar.tsx +++ b/src/components/common/User/UserAvatar.tsx @@ -2,7 +2,12 @@ import { Avatar, Tooltip, styled } from "@mui/material"; import { Link } from "react-router-dom"; import { useInfoFromUserNameQuery } from "../../../store/deployApi"; -export const UserAvatar = styled(({ username, size = 48 }) => { +interface UserAvatarProps { + username?: string; + size?: number; +} + +export const UserAvatar = styled(({ username, size = 48 }: UserAvatarProps) => { const { data: user, isLoading, @@ -20,16 +25,16 @@ export const UserAvatar = styled(({ username, size = 48 }) => { <Avatar sx={{ width: size, height: size }} alt={username} - variant="circle" + variant="circular" > - {username.toUpperCase().slice(0, 2)} + {username?.toUpperCase().slice(0, 2)} </Avatar> ) : ( <Avatar sx={{ width: size, height: size }} src={user?.avatar} alt={username} - variant="circle" + variant="circular" /> )} </Link> diff --git a/src/components/common/User/UserOperationList.tsx b/src/components/common/User/UserOperationList.tsx index 6afdcdcd3aba98eb23c436b966166b97cd9227ef..b96c01c2f340c050c7bb1474c237da2e533119b8 100644 --- a/src/components/common/User/UserOperationList.tsx +++ b/src/components/common/User/UserOperationList.tsx @@ -8,8 +8,9 @@ import { ROWS_PER_PAGE } from "../../../constants"; import { useLazyListJobsQuery } from "../../../store/deployApi"; +import { Pagination } from "../../../types/common"; -export function UserOperationList({ userName }) { +export function UserOperationList({ userName }: { userName?: string }) { const [getJobs, { data: jobs, isFetching }] = useLazyListJobsQuery({ pollingInterval: DEFAULT_POLLING_INTERVAL_MILLIS }); @@ -28,9 +29,7 @@ export function UserOperationList({ userName }) { const callGetjobs = useCallback(() => { const requestParams = initRequestParams(pagination); - requestParams.user = userName; - - getJobs(requestParams); + getJobs({ ...requestParams, user: userName }); }, [getJobs, pagination, userName]); useEffect(() => { @@ -38,12 +37,12 @@ export function UserOperationList({ userName }) { }, [callGetjobs]); // Invoked by Table on change to pagination - const onPage = (params) => { + const onPage = (params: Pagination) => { setPagination(params); }; return ( - <Card variant="plain"> + <Card> <CardHeader title={"Jobs"} titleTypographyProps={{ @@ -57,7 +56,6 @@ export function UserOperationList({ userName }) { loading={isFetching || !jobs} pagination={pagination} onPage={onPage} - rowType="userPageJobLog" /> </Card> ); diff --git a/src/components/common/snackbar/Snackbar.tsx b/src/components/common/snackbar/Snackbar.tsx index 512f866d1a888a6366c355fc6570be6183852d1f..3c33e5713520f27fef80d3346874f38700740b1b 100644 --- a/src/components/common/snackbar/Snackbar.tsx +++ b/src/components/common/snackbar/Snackbar.tsx @@ -1,38 +1,62 @@ -import { useSnackbar } from "notistack"; -import { Button } from "@mui/material"; +import { useCallback } from "react"; +import { useSnackbar, SnackbarKey } from "notistack"; +import { Button, styled } from "@mui/material"; + +// eslint-disable-next-line react-refresh/only-export-components +const SnackbarButton = styled(Button)(() => ({ + color: "#f0f0f0", + borderColor: "#f0f0f0", + "&:hover": { + backgroundColor: "#8d8d8d", + borderColor: "#8d8d8d", + boxShadow: "none" + } +})); export function useCustomSnackbar() { const { enqueueSnackbar, closeSnackbar } = useSnackbar(); - function showError(errorMessage, severity = "error") { - console.error("Snackbar: " + errorMessage); - const action = (key) => ( - <Button - variant="text" - onClick={() => { - closeSnackbar(key); - }} - sx={{ - // Note all colors must be absolute or theme defaults because - // Snackbar loads before the custom ThemeProvider theme loads - color: "#f0f0f0", - borderColor: "#f0f0f0", - "&:hover": { - backgroundColor: "#8d8d8d", - borderColor: "#8d8d8d", - boxShadow: "none" - } - }} - > - Dismiss - </Button> - ); - enqueueSnackbar(errorMessage, { - variant: severity, - autoHideDuration: null, - action - }); - } + const showError = useCallback( + (message: string) => { + const action = (key: SnackbarKey) => ( + <SnackbarButton + variant="text" + onClick={() => { + closeSnackbar(key); + }} + > + Dismiss + </SnackbarButton> + ); + + return enqueueSnackbar(message, { + variant: "error", + autoHideDuration: null, + action + }); + }, + [enqueueSnackbar, closeSnackbar] + ); + + const showSuccess = useCallback( + (message: string) => { + return enqueueSnackbar(message, { + variant: "success", + autoHideDuration: 3000 + }); + }, + [enqueueSnackbar] + ); + + const showWarning = useCallback( + (message: string) => { + return enqueueSnackbar(message, { + variant: "warning", + autoHideDuration: 3000 + }); + }, + [enqueueSnackbar] + ); - return showError; + return { showSuccess, showError, showWarning }; } diff --git a/src/stories/components/common/IOC/IOCTable.stories.tsx b/src/stories/components/common/IOC/IOCTable.stories.tsx index e64c16abd8677aff7484eef3f4de48f87738d197..dca0a174629ccbc7b8326716bbeeb8a8446751b7 100644 --- a/src/stories/components/common/IOC/IOCTable.stories.tsx +++ b/src/stories/components/common/IOC/IOCTable.stories.tsx @@ -1,5 +1,6 @@ +import { Meta, StoryFn } from "@storybook/react"; +import { http, delay } from "msw"; import { Box } from "@mui/material"; -import { http } from "msw"; import { IOCTable } from "../../../../components/IOC/IOCTable"; import iocs from "../../../../mocks/fixtures/PagedIOCResponse.json"; import { RouterHarness } from "../../../../mocks/AppHarness"; @@ -8,6 +9,8 @@ import { paginationNoResults } from "../../../utils/common-args"; +type IOCTableStory = StoryFn<typeof IOCTable>; + export default { title: "IOC/IOCTable", argTypes: { @@ -19,9 +22,9 @@ export default { pagination: hideStorybookControls, onPage: hideStorybookControls } -}; +} as Meta<typeof IOCTable>; -const Template = (args) => { +const Template: IOCTableStory = (args) => { return ( <RouterHarness> <Box height="90vh"> @@ -31,7 +34,7 @@ const Template = (args) => { ); }; -export const Empty = (args) => <Template {...args} />; +export const Empty: IOCTableStory = (args) => <Template {...args} />; Empty.args = { rowType: "explore", @@ -41,14 +44,14 @@ Empty.args = { onPage: () => {} }; -export const EmptyLoading = (args) => <Template {...args} />; +export const EmptyLoading: IOCTableStory = (args) => <Template {...args} />; EmptyLoading.args = { ...Empty.args, loading: true }; -export const BeforeAsync = (args) => <Template {...args} />; +export const BeforeAsync: IOCTableStory = (args) => <Template {...args} />; BeforeAsync.args = { ...Empty.args, @@ -58,17 +61,16 @@ BeforeAsync.args = { BeforeAsync.argTypes = { loading: hideStorybookControls }; + BeforeAsync.parameters = { msw: { handlers: [ - http.get("*/iocs/*", (req, res, ctx) => res(ctx.delay("infinite"))), - http.get("*/iocs/:ioc_id/status", (req, res, ctx) => { - res(ctx.delay("infinite")); - }) + http.get("*/iocs/*", async () => await delay("infinite")), + http.get("*/iocs/:ioc_id/status", async () => await delay("infinite")) ] } }; -export const AfterAsync = (args) => <Template {...args} />; +export const AfterAsync: IOCTableStory = (args) => <Template {...args} />; AfterAsync.args = { ...BeforeAsync.args }; AfterAsync.argTypes = { ...BeforeAsync.argTypes };