From ef060578925462288bc546f82d3a2e8445f48ac4 Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Thu, 8 Aug 2024 17:53:32 -0700 Subject: [PATCH 1/7] CORE-2002 Add running VICE analyses countdown timers Adds countdown timers for running VICE analyses in the listing, submission landing, and dashboard. Also refactors fetching the time limit and enabling the extend action, hopefully simplifying some of the logic. --- public/static/locales/en/analyses.json | 1 + .../landing/AnalysisSubmissionLanding.js | 53 ++-- src/components/analyses/listing/Actions.js | 11 +- src/components/analyses/listing/Listing.js | 52 +-- src/components/analyses/listing/TableView.js | 295 ++++++++++-------- .../analyses/toolbar/AnalysesDotMenu.js | 51 ++- .../analyses/useAnalysisTimeLimitCountdown.js | 88 ++++++ src/components/analyses/utils.js | 23 +- .../dashboard/dashboardItem/ItemBase.js | 21 +- 9 files changed, 364 insertions(+), 231 deletions(-) create mode 100644 src/components/analyses/useAnalysisTimeLimitCountdown.js diff --git a/public/static/locales/en/analyses.json b/public/static/locales/en/analyses.json index a2799b936..925911bb8 100644 --- a/public/static/locales/en/analyses.json +++ b/public/static/locales/en/analyses.json @@ -119,6 +119,7 @@ "terminateAnalysisTitle": "Terminate Analysis", "terminateAnalysisTitle_plural": "Terminate Analyses", "terminateBtn": "Terminate", + "timeLimitCountdown": "{{timeLimitCountdown}} remaining", "timeLimitError": "Unable to get the current time limit. Please try again.", "timeLimitExtended": "Time limit for {{analysisName}} has been extended. This analysis will end on {{newTimeLimit}}", "theirs": "Shared with me", diff --git a/src/components/analyses/landing/AnalysisSubmissionLanding.js b/src/components/analyses/landing/AnalysisSubmissionLanding.js index 0e06af191..8a7aa8910 100644 --- a/src/components/analyses/landing/AnalysisSubmissionLanding.js +++ b/src/components/analyses/landing/AnalysisSubmissionLanding.js @@ -7,12 +7,10 @@ import { cancelAnalysis, extendVICEAnalysisTimeLimit, getAnalysis, - getTimeLimitForVICEAnalysis, renameAnalysis, updateAnalysisComment, useAnalysisInfo, useAnalysisParameters, - VICE_TIME_LIMIT_QUERY_KEY, } from "serviceFacades/analyses"; import { getAnalysisShareWithSupportRequest } from "serviceFacades/sharing"; import { @@ -29,6 +27,7 @@ import DetailsPanel from "../details/DetailsPanel"; import InfoPanel from "../details/InfoPanel"; import ParamsPanel from "../details/ParamsPanel"; import AnalysisStatusIcon from "../AnalysisStatusIcon"; +import useAnalysisTimeLimitCountdown from "../useAnalysisTimeLimitCountdown"; import ShareWithSupportDialog from "components/analyses/ShareWithSupportDialog"; import TerminateAnalysisDialog from "components/analyses/TerminateAnalysisDialog"; @@ -104,16 +103,19 @@ export default function AnalysisSubmissionLanding(props) { const [confirmExtendTimeLimitDlgOpen, setConfirmExtendTimeLimitDlgOpen] = React.useState(false); - const [timeLimitQueryEnabled, setTimeLimitQueryEnabled] = - React.useState(false); - const [timeLimit, setTimeLimit] = React.useState(); const [pendingTerminationDlgOpen, setPendingTerminationDlgOpen] = React.useState(false); + const { timeLimit, timeLimitCountdown } = useAnalysisTimeLimitCountdown( + analysis, + showErrorAnnouncer + ); + const username = getAnalysisUser(analysis, config); const isBatch = isBatchAnalysis(analysis); const isVICE = isInteractive(analysis); - const allowTimeExtn = allowAnalysisTimeExtn(analysis, username, config); + const allowTimeExtn = + timeLimit && allowAnalysisTimeExtn(analysis, username, config); const allowCancel = allowAnalysesCancel([analysis], username, config); const allowRelaunch = allowAnalysesRelaunch([analysis]); const allowEdit = allowAnalysisEdit(analysis, username, config); @@ -187,22 +189,6 @@ export default function AnalysisSubmissionLanding(props) { onSuccess: setParameters, }); - const { isFetching: isFetchingTimeLimit } = useQuery({ - queryKey: [VICE_TIME_LIMIT_QUERY_KEY, id], - queryFn: () => getTimeLimitForVICEAnalysis(id), - enabled: timeLimitQueryEnabled, - onSuccess: (resp) => { - //convert the response from seconds to milliseconds - setTimeLimit({ - [id]: formatDate(resp?.time_limit * 1000), - }); - setConfirmExtendTimeLimitDlgOpen(true); - }, - onError: (error) => { - showErrorAnnouncer(t("timeLimitError"), error); - }, - }); - const { mutate: shareAnalysesMutation, isLoading: shareLoading } = useMutation(submitAnalysisSupportRequest, { onSuccess: (responses) => { @@ -277,7 +263,6 @@ export default function AnalysisSubmissionLanding(props) { useMutation(extendVICEAnalysisTimeLimit, { onSuccess: (resp) => { setConfirmExtendTimeLimitDlgOpen(false); - setTimeLimit(null); //convert the response from seconds to milliseconds announce({ text: t("timeLimitExtended", { @@ -304,11 +289,7 @@ export default function AnalysisSubmissionLanding(props) { }); }; - const busy = - isFetching || - analysisLoading || - isFetchingTimeLimit || - extensionLoading; + const busy = isFetching || analysisLoading || extensionLoading; if (busy) { return ; @@ -354,6 +335,16 @@ export default function AnalysisSubmissionLanding(props) { analysis={analysis} date={formatDate(analysis?.startdate)} /> + {timeLimitCountdown && ( + + {t("timeLimitCountdown", { + timeLimitCountdown, + })} + + )} @@ -461,7 +452,9 @@ export default function AnalysisSubmissionLanding(props) { } handleRefresh={refreshAnalysis} handleTimeLimitExtnClick={() => { - setTimeLimitQueryEnabled(true); + setConfirmExtendTimeLimitDlgOpen( + true + ); }} handleShareWithSupport={() => setHelpOpen(true) @@ -593,7 +586,7 @@ export default function AnalysisSubmissionLanding(props) { confirmButtonText={t("extend")} title={t("extendTime")} contentText={t("extendTimeLimitMessage", { - timeLimit: timeLimit ? timeLimit[id] : "", + timeLimit: formatDate(timeLimit), })} /> diff --git a/src/components/analyses/listing/Actions.js b/src/components/analyses/listing/Actions.js index ba274ddb8..9b184f0cd 100644 --- a/src/components/analyses/listing/Actions.js +++ b/src/components/analyses/listing/Actions.js @@ -13,8 +13,6 @@ import ids from "../ids"; import buildID from "components/utils/DebugIDUtil"; import { - allowAnalysesCancel, - allowAnalysisTimeExtn, isBatchAnalysis, isInteractive, isTerminated, @@ -22,8 +20,6 @@ import { useRelaunchLink, } from "../utils"; -import { useConfig } from "contexts/config"; - import { Grid, IconButton } from "@mui/material"; import { Cancel as CancelIcon, @@ -92,26 +88,23 @@ export default function Actions(props) { const { analysis, allowBatchDrillDown = true, + allowCancel, + allowTimeExtn, handleDetailsClick, handleInteractiveUrlClick, handleTerminateSelected, handleBatchIconClick, setPendingTerminationDlgOpen, baseId, - username, handleTimeLimitExtnClick, setVICELogsDlgOpen, } = props; - const [config] = useConfig(); - const interactiveUrls = analysis.interactive_urls; const isDisabled = analysis.app_disabled; const isBatch = isBatchAnalysis(analysis); const isVICE = isInteractive(analysis); - const allowCancel = allowAnalysesCancel([analysis], username, config); - const allowTimeExtn = allowAnalysisTimeExtn(analysis, username, config); const [relaunchHref, relaunchAs] = useRelaunchLink(analysis); const [outputFolderHref, outputFolderAs] = useGotoOutputFolderLink( analysis?.resultfolderid diff --git a/src/components/analyses/listing/Listing.js b/src/components/analyses/listing/Listing.js index b81f4fcfc..9ab08963f 100644 --- a/src/components/analyses/listing/Listing.js +++ b/src/components/analyses/listing/Listing.js @@ -19,7 +19,6 @@ import ViceLogsViewer from "components/analyses/ViceLogsViewer"; import { ANALYSES_LISTING_QUERY_KEY, - VICE_TIME_LIMIT_QUERY_KEY, cancelAnalyses, deleteAnalyses, getAnalyses, @@ -27,7 +26,6 @@ import { renameAnalysis, updateAnalysisComment, extendVICEAnalysisTimeLimit, - getTimeLimitForVICEAnalysis, } from "serviceFacades/analyses"; import { useBagAddItems } from "serviceFacades/bags"; @@ -131,7 +129,6 @@ function Listing(props) { useState(false); const [terminateAnalysisDlgOpen, setTerminateAnalysisDlgOpen] = useState(false); - const [timeLimitQueryEnabled, setTimeLimitQueryEnabled] = useState(false); const [timeLimit, setTimeLimit] = useState(); const handleTerminateSelected = () => setTerminateAnalysisDlgOpen(true); @@ -139,21 +136,6 @@ function Listing(props) { // Get QueryClient from the context const queryClient = useQueryClient(); - /** - * There is a small gap between when the user click on the Extend button / menu item - * and the time loading mask appears because the getTimeLimitForVICEAnalysis query is - * controlled by timeLimitQueryEnabled state. So the user has a chance to click - * on the same button again or choose to extend time limit on another analysis or - * even do something else. To avoid potential race condition, - * I am storing / checking the selected analysis id for which the timestamp is fetched. - * This should also help in debugging if the race condition still happens. - */ - useEffect(() => { - if (timeLimit && selected?.length > 0 && timeLimit[selected[0]]) { - setConfirmExtendTimeLimitDlgOpen(true); - } - }, [timeLimit, selected]); - const { isFetching, error } = useQuery({ queryKey: analysesKey, queryFn: () => getAnalyses(analysesKey[1]), @@ -280,30 +262,16 @@ function Listing(props) { }, }); - const { isFetching: isFetchingTimeLimit } = useQuery({ - queryKey: [VICE_TIME_LIMIT_QUERY_KEY, selected[0]], - queryFn: () => getTimeLimitForVICEAnalysis(selected[0]), - enabled: timeLimitQueryEnabled, - onSuccess: (resp) => { - //convert the response from seconds to milliseconds - setTimeLimit({ - [selected[0]]: formatDate(resp?.time_limit * 1000), - }); - }, - onError: (error) => { - showErrorAnnouncer(t("timeLimitError"), error); - }, - }); - const { mutate: doTimeLimitExtension, isLoading: extensionLoading } = useMutation(extendVICEAnalysisTimeLimit, { onSuccess: (resp) => { setConfirmExtendTimeLimitDlgOpen(false); - setTimeLimit(null); + const newTimeLimit = resp?.time_limit * 1000; + setTimeLimit(newTimeLimit || null); //convert the response from seconds to milliseconds announce({ text: t("timeLimitExtended", { - newTimeLimit: formatDate(resp?.time_limit * 1000), + newTimeLimit: formatDate(newTimeLimit), analysisName: getSelectedAnalyses()[0]?.name, }), variant: SUCCESS, @@ -713,7 +681,6 @@ function Listing(props) { cancelLoading, deleteLoading, relaunchLoading, - isFetchingTimeLimit, extensionLoading, ]); @@ -721,7 +688,6 @@ function Listing(props) { <> setTimeLimitQueryEnabled(true)} + handleTimeLimitExtnClick={(timeLimit) => { + setTimeLimit(timeLimit); + setConfirmExtendTimeLimitDlgOpen(true); + }} onRefreshSelected={onRefreshSelected} setVICELogsDlgOpen={setVICELogsDlgOpen} /> @@ -767,8 +736,9 @@ function Listing(props) { handleDetailsClick={onDetailsSelected} handleStatusClick={handleStatusClick} setPendingTerminationDlgOpen={setPendingTerminationDlgOpen} - handleTimeLimitExtnClick={() => { - setTimeLimitQueryEnabled(true); + handleTimeLimitExtnClick={(timeLimit) => { + setTimeLimit(timeLimit); + setConfirmExtendTimeLimitDlgOpen(true); }} setVICELogsDlgOpen={setVICELogsDlgOpen} /> @@ -865,7 +835,7 @@ function Listing(props) { confirmButtonText={t("extend")} title={t("extendTime")} contentText={t("extendTimeLimitMessage", { - timeLimit: timeLimit ? timeLimit[selected[0]] : "", + timeLimit: formatDate(timeLimit), })} /> ({ name: { @@ -76,11 +84,13 @@ function AnalysisName(props) { ); } -function AnalysisDuration({ analysis }) { +function AnalysisDuration({ analysis, timeLimitCountdown }) { const { elapsedTime, totalRunTime } = useAnalysisRunTime(analysis); return ( - {totalRunTime || elapsedTime} + + {timeLimitCountdown || totalRunTime || elapsedTime} + ); } @@ -112,6 +122,129 @@ function Status(props) { ); } +const AnalysisRow = withErrorAnnouncer((props) => { + const { + baseId, + index, + analysis, + username, + parentId, + selected, + handleClick, + handleInteractiveUrlClick, + handleTerminateSelected, + handleBatchIconClick, + handleDetailsClick, + handleCheckboxClick, + handleStatusClick, + setPendingTerminationDlgOpen, + handleTimeLimitExtnClick, + setVICELogsDlgOpen, + showErrorAnnouncer, + } = props; + + const { timeLimit, timeLimitCountdown } = useAnalysisTimeLimitCountdown( + analysis, + showErrorAnnouncer + ); + + const [config] = useConfig(); + + const theme = useTheme(); + const { classes } = useStyles(); + const { classes: running } = useRunningAnalysesStyles(); + const { t } = useTranslation("analyses"); + + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + const id = analysis.id; + const isSelected = selected.indexOf(id) !== -1; + const rowId = buildID(baseId, id); + const allowCancel = allowAnalysesCancel([analysis], username, config); + const allowTimeExtn = + timeLimit && allowAnalysisTimeExtn(analysis, username, config); + + return ( + handleClick(event, id, index)} + className={ + !isSelected && analysis.status === analysisStatus.RUNNING + ? running.backdrop + : undefined + } + role="checkbox" + aria-checked={isSelected} + tabIndex={-1} + selected={isSelected} + hover + > + + handleCheckboxClick(event, id, index)} + inputProps={{ + "aria-label": t("ariaCheckbox", { + label: analysis.name, + }), + }} + /> + + + + + + + + + + {formatDate(analysis.startdate)} + + + + + + {!isSmall && ( + + + handleTimeLimitExtnClick(timeLimit) + } + setVICELogsDlgOpen={setVICELogsDlgOpen} + /> + + )} + + ); +}); + const columnData = (t) => { const fields = analysisFields(t); return [ @@ -179,8 +312,6 @@ function TableView(props) { } = props; const theme = useTheme(); - const { classes } = useStyles(); - const { classes: running } = useRunningAnalysesStyles(); const { t } = useTranslation("analyses"); const isSmall = useMediaQuery(theme.breakpoints.down("md")); @@ -233,131 +364,35 @@ function TableView(props) { )} {analyses && analyses.length > 0 && - analyses.map((analysis, index) => { - const id = analysis.id; - const isSelected = selected.indexOf(id) !== -1; - const rowId = buildID(baseId, tableId, id); - - return ( - - handleClick(event, id, index) - } - className={ - !isSelected && - analysis.status === - analysisStatus.RUNNING - ? running.backdrop - : undefined - } - role="checkbox" - aria-checked={isSelected} - tabIndex={-1} - selected={isSelected} - hover - key={id} - id={rowId} - > - - - handleCheckboxClick( - event, - id, - index - ) - } - inputProps={{ - "aria-label": t( - "ariaCheckbox", - { - label: analysis.name, - } - ), - }} - /> - - - - - - - - - - {formatDate(analysis.startdate)} - - - - - - {!isSmall && ( - - - - )} - - ); - })} + analyses.map((analysis, index) => ( + + ))} )} diff --git a/src/components/analyses/toolbar/AnalysesDotMenu.js b/src/components/analyses/toolbar/AnalysesDotMenu.js index 38f3e6598..7954f0a75 100644 --- a/src/components/analyses/toolbar/AnalysesDotMenu.js +++ b/src/components/analyses/toolbar/AnalysesDotMenu.js @@ -8,6 +8,7 @@ import React from "react"; import { useTranslation } from "i18n"; import Link from "next/link"; +import { useQuery } from "react-query"; import ids from "../ids"; @@ -19,14 +20,22 @@ import { allowAnalysisTimeExtn, isBatchAnalysis, isInteractive, + isInteractiveRunning, useGotoOutputFolderLink, useRelaunchLink, } from "../utils"; +import withErrorAnnouncer from "components/error/withErrorAnnouncer"; import buildID from "components/utils/DebugIDUtil"; import DotMenu from "components/dotMenu/DotMenu"; import { OutputFolderMenuItem } from "./OutputFolderMenuItem"; import { RelaunchMenuItem } from "./RelaunchMenuItem"; + +import { + VICE_TIME_LIMIT_QUERY_KEY, + getTimeLimitForVICEAnalysis, +} from "serviceFacades/analyses"; + import { ListItemIcon, ListItemText, @@ -83,6 +92,7 @@ function DotMenuItems(props) { onFilterSelected, setPendingTerminationDlgOpen, handleTimeLimitExtnClick, + timeLimit, setVICELogsDlgOpen, } = props; const { t } = useTranslation("analyses"); @@ -240,7 +250,7 @@ function DotMenuItems(props) { id={buildID(baseId, ids.MENUITEM_EXTEND_TIME_LIMIT)} onClick={() => { onClose(); - handleTimeLimitExtnClick(); + handleTimeLimitExtnClick(timeLimit); }} > @@ -317,15 +327,19 @@ function AnalysesDotMenu({ canShare, setSharingDlgOpen, setVICELogsDlgOpen, + showErrorAnnouncer, ...props }) { // These props need to be spread down into DotMenuItems below. const { baseId, isSingleSelection } = props; - const { t } = useTranslation("common"); + const { t } = useTranslation(["analyses", "common"]); const [config] = useConfig(); + const [timeLimit, setTimeLimit] = React.useState(); + const selectedAnalyses = getSelectedAnalyses ? getSelectedAnalyses() : null; + const analysis = isSingleSelection && selectedAnalyses[0]; let isBatch = false, isVICE = false, @@ -337,24 +351,30 @@ function AnalysesDotMenu({ if (selectedAnalyses) { if (isSingleSelection) { - allowEdit = allowAnalysisEdit( - selectedAnalyses[0], - username, - config - ); - isBatch = isBatchAnalysis(selectedAnalyses[0]); - isVICE = isInteractive(selectedAnalyses[0]); - allowTimeExtn = allowAnalysisTimeExtn( - selectedAnalyses[0], - username, - config - ); + allowEdit = allowAnalysisEdit(analysis, username, config); + isBatch = isBatchAnalysis(analysis); + isVICE = isInteractive(analysis); + allowTimeExtn = + timeLimit && allowAnalysisTimeExtn(analysis, username, config); } allowCancel = allowAnalysesCancel(selectedAnalyses, username, config); allowDelete = allowAnalysesDelete(selectedAnalyses, username, config); allowRelaunch = allowAnalysesRelaunch(selectedAnalyses); } + useQuery({ + queryKey: [VICE_TIME_LIMIT_QUERY_KEY, analysis?.id], + queryFn: () => getTimeLimitForVICEAnalysis(analysis?.id), + enabled: !!analysis?.id && isInteractiveRunning(analysis), + onSuccess: (resp) => { + //convert the response from seconds to milliseconds + setTimeLimit(resp?.time_limit * 1000 || null); + }, + onError: (error) => { + showErrorAnnouncer(t("timeLimitError"), error); + }, + }); + return ( )} /> ); } -export default AnalysesDotMenu; +export default withErrorAnnouncer(AnalysesDotMenu); diff --git a/src/components/analyses/useAnalysisTimeLimitCountdown.js b/src/components/analyses/useAnalysisTimeLimitCountdown.js new file mode 100644 index 000000000..fcc2385be --- /dev/null +++ b/src/components/analyses/useAnalysisTimeLimitCountdown.js @@ -0,0 +1,88 @@ +/** + * @author psarando + * + * A hook to return a running VICE analysis' time limit and formatted countdown. + * + * The time limit is based on the value returned by the + * `/analyses/${id}/time-limit` endpoint, and converted into milliseconds. + * + * The countdown timer value is calculated from this time limit value, + * and formatted with `date-fns/formatDuration`. + * + * Updates the countdown timer value every second. + */ + +import { useEffect, useState } from "react"; + +import { formatDuration, intervalToDuration, toDate } from "date-fns"; +import { useQuery } from "react-query"; + +import { useTranslation } from "i18n"; + +import { + VICE_TIME_LIMIT_QUERY_KEY, + getTimeLimitForVICEAnalysis, +} from "serviceFacades/analyses"; + +import { isInteractiveRunning } from "./utils"; + +const timeLimitToCountdown = (timeLimitMS) => { + if (timeLimitMS > 0) { + const start = new Date(); + const end = toDate(parseInt(timeLimitMS, 10)); + + if (end > start) { + return formatDuration(intervalToDuration({ start, end })); + } + } + + return null; +}; + +function useAnalysisTimeLimitCountdown(analysis, showErrorAnnouncer) { + const [timeLimit, setTimeLimit] = useState(); + const [timeLimitCountdown, setTimeLimitCountdown] = useState(); + + const { t } = useTranslation("analyses"); + + const runningVICE = isInteractiveRunning(analysis); + + useQuery({ + queryKey: [VICE_TIME_LIMIT_QUERY_KEY, analysis?.id], + queryFn: () => getTimeLimitForVICEAnalysis(analysis?.id), + enabled: !!analysis?.id && runningVICE, + onSuccess: (resp) => { + //convert the response from seconds to milliseconds + const timeLimitMS = resp?.time_limit * 1000; + setTimeLimit(timeLimitMS || null); + setTimeLimitCountdown(timeLimitToCountdown(timeLimitMS)); + }, + onError: (error) => { + if (showErrorAnnouncer) { + showErrorAnnouncer(t("timeLimitError"), error); + } else { + console.error(t("timeLimitError"), error); + } + }, + }); + + useEffect(() => { + const handleUpdateRunningTime = () => { + setTimeLimitCountdown(timeLimitToCountdown(timeLimit)); + }; + + let interval; + if (runningVICE) { + interval = setInterval(handleUpdateRunningTime, 1000); + handleUpdateRunningTime(); + } else { + setTimeLimitCountdown(null); + } + + return () => clearInterval(interval); + }, [analysis, runningVICE, timeLimit]); + + return { timeLimit, timeLimitCountdown }; +} + +export default useAnalysisTimeLimitCountdown; diff --git a/src/components/analyses/utils.js b/src/components/analyses/utils.js index cbfffa8ea..312538c1e 100644 --- a/src/components/analyses/utils.js +++ b/src/components/analyses/utils.js @@ -34,6 +34,22 @@ const isInteractive = (analysis) => { ); }; +/** + * Check if the analysis of type VICE + * @param {object} analysis + * @returns {boolean} + */ +const isInteractiveRunning = (analysis) => { + if (!analysis) { + return false; + } + + return ( + analysis.interactive_urls?.length > 0 && + analysis.status === analysisStatus.RUNNING + ); +}; + /** * Check if the user can extend the time limit * @param {object} analysis @@ -42,12 +58,8 @@ const isInteractive = (analysis) => { * @returns {boolean} */ const allowAnalysisTimeExtn = (analysis, currentUser, config) => { - if (!analysis) { - return false; - } return ( - analysis?.interactive_urls?.length > 0 && - analysis.status === analysisStatus.RUNNING && + isInteractiveRunning(analysis) && currentUser === getAnalysisUser(analysis, config) ); }; @@ -237,6 +249,7 @@ const isTerminated = (analysis) => { export { getAnalysisUser, isInteractive, + isInteractiveRunning, allowAnalysisTimeExtn, isBatchAnalysis, allowAnalysesCancel, diff --git a/src/components/dashboard/dashboardItem/ItemBase.js b/src/components/dashboard/dashboardItem/ItemBase.js index 9a6c53b6e..d3ab36a10 100644 --- a/src/components/dashboard/dashboardItem/ItemBase.js +++ b/src/components/dashboard/dashboardItem/ItemBase.js @@ -3,6 +3,7 @@ import ReactPlayer from "react-player/youtube"; import { useTranslation } from "i18n"; import AnalysisSubheader from "./AnalysisSubheader"; +import useAnalysisTimeLimitCountdown from "components/analyses/useAnalysisTimeLimitCountdown"; import buildID from "components/utils/DebugIDUtil"; import analysisStatus from "components/models/analysisStatus"; @@ -72,7 +73,7 @@ const DashboardItem = ({ item }) => { }); const { classes: running } = useRunningAnalysesStyles(); - const { t } = useTranslation(["dashboard", "apps"]); + const { t } = useTranslation(["dashboard", "apps", "analyses"]); const isMediumOrLarger = useMediaQuery(theme.breakpoints.up("md")); @@ -85,6 +86,11 @@ const DashboardItem = ({ item }) => { const isRunningAnalysis = item.kind === "analyses" && item.content.status === analysisStatus.RUNNING; + + const { timeLimitCountdown } = useAnalysisTimeLimitCountdown( + isRunningAnalysis && item.content + ); + return ( { classes: { colorPrimary: classes.cardHeaderText }, }} /> + {timeLimitCountdown && ( + + )} Date: Fri, 9 Aug 2024 15:36:52 -0700 Subject: [PATCH 2/7] CORE-2002 Replace dashboard AnalysisSubheader with countdown Adding another CardHeader for the countdown timer can hide the actions at the bottom of the card, so replace the "Running" subheader with the countdown timer instead. --- .../dashboard/dashboardItem/ItemBase.js | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/components/dashboard/dashboardItem/ItemBase.js b/src/components/dashboard/dashboardItem/ItemBase.js index d3ab36a10..4047f96c6 100644 --- a/src/components/dashboard/dashboardItem/ItemBase.js +++ b/src/components/dashboard/dashboardItem/ItemBase.js @@ -83,9 +83,9 @@ const DashboardItem = ({ item }) => { const description = fns.cleanDescription(item.content.description); const [origination, date] = item.getOrigination(t); + const isAnalysis = item.kind === constants.KIND_ANALYSES; const isRunningAnalysis = - item.kind === "analyses" && - item.content.status === analysisStatus.RUNNING; + isAnalysis && item.content.status === analysisStatus.RUNNING; const { timeLimitCountdown } = useAnalysisTimeLimitCountdown( isRunningAnalysis && item.content @@ -117,11 +117,17 @@ const DashboardItem = ({ item }) => { classes: { colorPrimary: classes.cardHeaderText }, }} subheader={ - item.kind === constants.KIND_ANALYSES ? ( - + isAnalysis ? ( + timeLimitCountdown ? ( + t("analyses:timeLimitCountdown", { + timeLimitCountdown, + }) + ) : ( + + ) ) : ( t("origination", { origination, @@ -133,22 +139,14 @@ const DashboardItem = ({ item }) => { subheaderTypographyProps={{ noWrap: true, variant: "caption", - classes: { colorPrimary: classes.cardHeaderText }, + style: + isRunningAnalysis && timeLimitCountdown + ? { + color: theme.palette.primary.main, + } + : null, }} /> - {timeLimitCountdown && ( - - )} Date: Fri, 9 Aug 2024 16:46:55 -0700 Subject: [PATCH 3/7] CORE-2002 Add countdown timers with controls to analyses stories Updated the analyses listing, submission landing, and dashboard stories to display the running VICE countdown timer, but only when a time limit is selected in the story controls. Updated the start and end dates in the `info` analysis mock to help the "Running for" duration labels to make more sense. --- stories/analyses/AnalysesMocks.js | 84 ++++++++++--------- .../AnalysisSubmissionLanding.stories.js | 53 +++++++++--- stories/analyses/ArgTypes.js | 31 +++++++ stories/analyses/Listing.stories.js | 21 ++++- stories/dashboard/Dashboard.stories.js | 32 ++++++- 5 files changed, 166 insertions(+), 55 deletions(-) create mode 100644 stories/analyses/ArgTypes.js diff --git a/stories/analyses/AnalysesMocks.js b/stories/analyses/AnalysesMocks.js index aeeb4a8db..a028c5695 100644 --- a/stories/analyses/AnalysesMocks.js +++ b/stories/analyses/AnalysesMocks.js @@ -38,6 +38,28 @@ export const agaveWordCountAnalysis = { "/iplant/home/ipcdev/analyses/Word_Count_DE-2_0.0.3_analysis1-2020-04-09-00-23-54.1", }; +export const runningVICEAnalysis = { + description: "", + name: "ten-rules-jupyter_analysis1", + can_share: true, + username: "ipcdev@iplantcollaborative.org", + app_id: "8ec235d8-f173-11e9-a56f-008cfa5ae621", + system_id: "de", + app_disabled: false, + batch: false, + enddate: "0", + status: "Running", + id: "8712ea7c-7f0c-11ea-be65-c2a97b34bb42", + startdate: "1586950235151", + app_description: + "Jupyter Notebooks for: Ten simple rules for writing and sharing computational analyses in Jupyter Notebooks. Rule A, Birmingham A, Zuniga C, Altintas I, Huang SC, Knight R, Moshiri N, Nguyen MH, Rosenthal SB, Pérez F, Rose PW. PLoS Comput Biol. 2019 Jul 25;15(7):e1007007. doi: 10.1371/journal.pcbi.1007007", + interactive_urls: ["https://a444587f3.cyverse.run:4343"], + notify: true, + resultfolderid: + "/iplant/home/ipcdev/analyses_qa/ten-rules-jupyter_analysis1-2020-04-15-11-30-35.1", + app_name: "ten-rules-jupyter", +}; + export const listing = { analyses: [ { @@ -100,27 +122,7 @@ export const listing = { "/iplant/home/ipcdev/analyses_qa/DE_Word_Count_analysis1-2020-04-15-11-31-27.4", app_name: "DE Word Count", }, - { - description: "", - name: "ten-rules-jupyter_analysis1", - can_share: true, - username: "ipcdev@iplantcollaborative.org", - app_id: "8ec235d8-f173-11e9-a56f-008cfa5ae621", - system_id: "de", - app_disabled: false, - batch: false, - enddate: "0", - status: "Running", - id: "8712ea7c-7f0c-11ea-be65-c2a97b34bb42", - startdate: "1586950235151", - app_description: - "Jupyter Notebooks for: Ten simple rules for writing and sharing computational analyses in Jupyter Notebooks. Rule A, Birmingham A, Zuniga C, Altintas I, Huang SC, Knight R, Moshiri N, Nguyen MH, Rosenthal SB, Pérez F, Rose PW. PLoS Comput Biol. 2019 Jul 25;15(7):e1007007. doi: 10.1371/journal.pcbi.1007007", - interactive_urls: ["https://a444587f3.cyverse.run:4343"], - notify: true, - resultfolderid: - "/iplant/home/ipcdev/analyses_qa/ten-rules-jupyter_analysis1-2020-04-15-11-30-35.1", - app_name: "ten-rules-jupyter", - }, + runningVICEAnalysis, deWordCountAnalysis, agaveWordCountAnalysis, ], @@ -920,8 +922,8 @@ export const info = { { step_number: 1, external_id: "853900453991617001-242ac116-0001-007", - startdate: "1553818229058", - enddate: "1553818332000", + startdate: "1586398229058", + enddate: "1586398332000", status: "Completed", app_step_number: 1, step_type: "Agave", @@ -929,96 +931,96 @@ export const info = { { status: "PENDING", message: "Job accepted and queued for submission.", - timestamp: "1553818231000", + timestamp: "1586398231000", }, { status: "PROCESSING_INPUTS", message: "Attempt 1 to stage job inputs", - timestamp: "1553818247000", + timestamp: "1586398247000", }, { status: "PROCESSING_INPUTS", message: "Identifying input files for staging", - timestamp: "1553818247000", + timestamp: "1586398247000", }, { status: "STAGING_INPUTS", message: "Copy in progress", - timestamp: "1553818253000", + timestamp: "1586398253000", }, { status: "STAGED", message: "Job inputs staged to execution system", - timestamp: "1553818256000", + timestamp: "1586398256000", }, { status: "SUBMITTING", message: "Preparing job for submission.", - timestamp: "1553818275000", + timestamp: "1586398275000", }, { status: "SUBMITTING", message: "Attempt 1 to submit job", - timestamp: "1553818275000", + timestamp: "1586398275000", }, { status: "STAGING_JOB", message: // eslint-disable-next-line no-template-curly-in-string "Fetching app assets from agave://data.iplantcollaborative.org/${foundation.service.apps.default.public.dir}/cut_columns-0.0.0u1.zip", - timestamp: "1553818278000", + timestamp: "1586398278000", }, { status: "STAGING_JOB", message: "Staging runtime assets to agave://cyverseUK-Batch2/sarahr/job-853900453991617001-242ac116-0001-007-6869ed8f-ab38-4aaf-bb12-d0842e9fcb73_0001", - timestamp: "1553818283000", + timestamp: "1586398283000", }, { status: "QUEUED", message: "CLI job successfully forked as process id 928228", - timestamp: "1553818304000", + timestamp: "1586398304000", }, { status: "RUNNING", message: "CLI job successfully forked as process id 928228", - timestamp: "1553818304000", + timestamp: "1586398304000", }, { status: "RUNNING", message: "Job receieved duplicate RUNNING notification", - timestamp: "1553818307000", + timestamp: "1586398307000", }, { status: "CLEANING_UP", message: "Job completion detected by process monitor.", - timestamp: "1553818332000", + timestamp: "1586398332000", }, { status: "ARCHIVING", message: "Beginning to archive output.", - timestamp: "1553818335000", + timestamp: "1586398335000", }, { status: "ARCHIVING", message: "Attempt 1 to archive job output", - timestamp: "1553818335000", + timestamp: "1586398335000", }, { status: "ARCHIVING", message: "Archiving agave://cyverseUK-Batch2/sarahr/job-853900453991617001-242ac116-0001-007-6869ed8f-ab38-4aaf-bb12-d0842e9fcb73_0001 to agave://qairods.cyverse.org//sarahr/analyses_qa/cut_201903281639-2019-03-29-00-10-15.5", - timestamp: "1553818340000", + timestamp: "1586398340000", }, { status: "ARCHIVING_FINISHED", message: "Job archiving completed successfully.", - timestamp: "1553818415000", + timestamp: "1586398415000", }, { status: "FINISHED", message: "Job complete", - timestamp: "1553818416000", + timestamp: "1586398416000", }, ], }, diff --git a/stories/analyses/AnalysisSubmissionLanding.stories.js b/stories/analyses/AnalysisSubmissionLanding.stories.js index bab473934..62641edb4 100644 --- a/stories/analyses/AnalysisSubmissionLanding.stories.js +++ b/stories/analyses/AnalysisSubmissionLanding.stories.js @@ -1,29 +1,43 @@ import React from "react"; +import { useQueryClient } from "react-query"; import { AXIOS_DELAY, mockAxios } from "../axiosMock"; -import { deWordCountAnalysis, params, info } from "./AnalysesMocks"; +import { + deWordCountAnalysis, + runningVICEAnalysis, + params, + info, +} from "./AnalysesMocks"; import AnalysisSubmissionLanding from "components/analyses/landing/AnalysisSubmissionLanding"; +import { VICE_TIME_LIMIT_QUERY_KEY } from "serviceFacades/analyses"; +import { convertTimeLimitArgType, TimeLimitArgType } from "./ArgTypes"; export default { - title: "Submission Landing", + title: "Analyses / Submission Landing", }; -export const AnalysisSubmissionLandingTest = () => { - mockAxios - .onGet( - `/api/analyses?filter=[{"field":"id","value":"${deWordCountAnalysis.id}"}]` - ) - .reply(200, { analyses: [deWordCountAnalysis] }); +export const AnalysisSubmissionLandingTest = ({ analysis, timeLimit }) => { + const queryClient = useQueryClient(); + + React.useEffect(() => { + queryClient.invalidateQueries(VICE_TIME_LIMIT_QUERY_KEY); + }, [timeLimit, queryClient]); + + mockAxios.reset(); mockAxios - .onGet(`/api/analyses/${deWordCountAnalysis.id}/history`) - .reply(200, info); + .onGet(`/api/analyses?filter=[{"field":"id","value":"${analysis.id}"}]`) + .reply(200, { analyses: [analysis] }); + mockAxios.onGet(`/api/analyses/${analysis.id}/history`).reply(200, info); mockAxios - .onGet(`/api/analyses/${deWordCountAnalysis.id}/parameters`) + .onGet(`/api/analyses/${analysis.id}/parameters`) .reply(200, params); + mockAxios + .onGet(new RegExp("/api/analyses/.*/time-limit")) + .reply(200, { time_limit: convertTimeLimitArgType(timeLimit) }); return ( ); @@ -31,3 +45,18 @@ export const AnalysisSubmissionLandingTest = () => { AnalysisSubmissionLandingTest.parameters = { chromatic: { delay: AXIOS_DELAY * 2 }, }; +AnalysisSubmissionLandingTest.args = { + analysis: "DE", + timeLimit: "null", +}; +AnalysisSubmissionLandingTest.argTypes = { + analysis: { + options: ["DE", "VICE"], + mapping: { + DE: deWordCountAnalysis, + VICE: runningVICEAnalysis, + }, + control: { type: "select" }, + }, + ...TimeLimitArgType, +}; diff --git a/stories/analyses/ArgTypes.js b/stories/analyses/ArgTypes.js new file mode 100644 index 000000000..31485e9cd --- /dev/null +++ b/stories/analyses/ArgTypes.js @@ -0,0 +1,31 @@ +export const TimeLimitArgType = { + timeLimit: { + name: "Time Limit", + // The time-limit endpoint can return the literal string "null". + options: ["null", "3d", "3h", "30m", "30s"], + control: { + type: "select", + labels: { + "3d": "3 days", + "3h": "3 hours", + "30m": "30 minutes", + "30s": "30 seconds", + }, + }, + }, +}; + +export const convertTimeLimitArgType = (timeLimit) => { + switch (timeLimit) { + case "3d": + return new Date().getTime() / 1000 + 3 * 24 * 60 * 60; + case "3h": + return new Date().getTime() / 1000 + 3 * 60 * 60; + case "30m": + return new Date().getTime() / 1000 + 30 * 60; + case "30s": + return new Date().getTime() / 1000 + 30; + default: + return timeLimit; + } +}; diff --git a/stories/analyses/Listing.stories.js b/stories/analyses/Listing.stories.js index 66ce5f609..70d379c5c 100644 --- a/stories/analyses/Listing.stories.js +++ b/stories/analyses/Listing.stories.js @@ -1,5 +1,7 @@ import React from "react"; +import { useQueryClient } from "react-query"; + import { useTranslation } from "i18n"; import constants from "../../src/constants"; @@ -7,11 +9,13 @@ import constants from "../../src/constants"; import { mockAxios } from "../axiosMock"; import { info, listing } from "./AnalysesMocks"; +import { convertTimeLimitArgType, TimeLimitArgType } from "./ArgTypes"; import Listing from "components/analyses/listing/Listing"; import analysisFields from "components/analyses/analysisFields"; import { NotificationsProvider } from "contexts/pushNotifications"; +import { VICE_TIME_LIMIT_QUERY_KEY } from "serviceFacades/analyses"; export default { title: "Analyses / Listing", @@ -70,8 +74,18 @@ const errorResponse = { reason: "This error will only occur once! Please try again...", }; -export const AnalysesListingTest = () => { +export const AnalysesListingTest = ({ timeLimit }) => { + const queryClient = useQueryClient(); + + React.useEffect(() => { + queryClient.invalidateQueries(VICE_TIME_LIMIT_QUERY_KEY); + }, [timeLimit, queryClient]); + mockAxios.onGet(new RegExp("/api/analyses/.*/history")).reply(200, info); + mockAxios.onGet(new RegExp("/api/analyses/.*/time-limit")).reply(200, { + time_limit: convertTimeLimitArgType(timeLimit), + }); + mockAxios.onGet("/api/analyses").reply(200, listing); mockAxios.onPost("/api/analyses/relauncher").replyOnce(500, errorResponse); @@ -147,3 +161,8 @@ export const AnalysesListingTest = () => { return ; }; + +AnalysesListingTest.args = { + timeLimit: "null", +}; +AnalysesListingTest.argTypes = TimeLimitArgType; diff --git a/stories/dashboard/Dashboard.stories.js b/stories/dashboard/Dashboard.stories.js index a8a7cc793..7e3933db4 100644 --- a/stories/dashboard/Dashboard.stories.js +++ b/stories/dashboard/Dashboard.stories.js @@ -1,12 +1,19 @@ import React from "react"; -import Dashboard from "../../src/components/dashboard"; +import { useQueryClient } from "react-query"; + +import Dashboard from "components/dashboard"; +import { VICE_TIME_LIMIT_QUERY_KEY } from "serviceFacades/analyses"; import fetchMock from "fetch-mock"; import { mockAxios } from "../axiosMock"; import { appDetails, listingById } from "./appDetails"; +import { + convertTimeLimitArgType, + TimeLimitArgType, +} from "../analyses/ArgTypes"; import { instantLaunchAppInfo } from "../data/DataMocksInstantLaunch"; import { usageSummaryResponse, @@ -22,7 +29,14 @@ export default { const DashboardTestTemplate = ({ instantLaunchAppInfoResponse, usageSummaryResponseBody, + timeLimit, }) => { + const queryClient = useQueryClient(); + + React.useEffect(() => { + queryClient.invalidateQueries(VICE_TIME_LIMIT_QUERY_KEY); + }, [timeLimit, queryClient]); + const favoriteUriRegexp = /\/api\/apps\/[^/]+\/[^/]+\/favorite/; mockAxios .onGet(/\/api\/apps\/[^/]+\/[^/]+\/details/) @@ -40,6 +54,10 @@ const DashboardTestTemplate = ({ .onGet(/\/api\/resource-usage\/summary.*/) .reply(200, usageSummaryResponseBody); + mockAxios.onGet(new RegExp("/api/analyses/.*/time-limit")).reply(200, { + time_limit: convertTimeLimitArgType(timeLimit), + }); + // mocks for noembed.com image thumbnails fetchMock.restore(); fetchMock.get( @@ -266,18 +284,23 @@ const DashboardTestTemplate = ({ export const NoLimitsExceeded = DashboardTestTemplate.bind({}); NoLimitsExceeded.args = { + timeLimit: "null", instantLaunchAppInfoResponse: instantLaunchAppInfo, usageSummaryResponseBody: usageSummaryResponse, }; +NoLimitsExceeded.argTypes = TimeLimitArgType; export const ComputeLimitExceeded = DashboardTestTemplate.bind({}); ComputeLimitExceeded.args = { + timeLimit: "null", instantLaunchAppInfoResponse: instantLaunchAppInfo, usageSummaryResponseBody: usageSummaryComputeLimitExceededResponse, }; +ComputeLimitExceeded.argTypes = TimeLimitArgType; export const InstantLaunchLimitReached = DashboardTestTemplate.bind({}); InstantLaunchLimitReached.args = { + timeLimit: "null", instantLaunchAppInfoResponse: { ...instantLaunchAppInfo, limitChecks: { @@ -298,9 +321,11 @@ InstantLaunchLimitReached.args = { }, usageSummaryResponse: usageSummaryResponse, }; +InstantLaunchLimitReached.argTypes = TimeLimitArgType; export const InstantLaunchVICEForbidden = DashboardTestTemplate.bind({}); InstantLaunchVICEForbidden.args = { + timeLimit: "null", instantLaunchAppInfoResponse: { ...instantLaunchAppInfo, limitChecks: { @@ -321,9 +346,11 @@ InstantLaunchVICEForbidden.args = { }, usageSummaryResponse: usageSummaryResponse, }; +InstantLaunchVICEForbidden.argTypes = TimeLimitArgType; export const InstantLaunchPermissionNeeded = DashboardTestTemplate.bind({}); InstantLaunchPermissionNeeded.args = { + timeLimit: "null", instantLaunchAppInfoResponse: { ...instantLaunchAppInfo, limitChecks: { @@ -344,9 +371,11 @@ InstantLaunchPermissionNeeded.args = { }, usageSummaryResponse: usageSummaryResponse, }; +InstantLaunchPermissionNeeded.argTypes = TimeLimitArgType; export const InstantLaunchPermissionPending = DashboardTestTemplate.bind({}); InstantLaunchPermissionPending.args = { + timeLimit: "null", instantLaunchAppInfoResponse: { ...instantLaunchAppInfo, limitChecks: { @@ -367,3 +396,4 @@ InstantLaunchPermissionPending.args = { }, usageSummaryResponse: usageSummaryResponse, }; +InstantLaunchPermissionPending.argTypes = TimeLimitArgType; From 49f71002e17f79f40ba45fbd3bc685711229d656 Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Fri, 9 Aug 2024 18:08:45 -0700 Subject: [PATCH 4/7] CORE-2002 Fix AnalysisRunTime display for Tapis jobs This will also fix the duration display for analyses stories, since they all share a job history mock, with a Tapis job in the first step. --- src/components/analyses/useAnalysisRunTime.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/analyses/useAnalysisRunTime.js b/src/components/analyses/useAnalysisRunTime.js index 0f01e3c61..67da91799 100644 --- a/src/components/analyses/useAnalysisRunTime.js +++ b/src/components/analyses/useAnalysisRunTime.js @@ -42,9 +42,11 @@ function useAnalysisRunTime( // Make sure we're looking at the correct step // (e.g. step_type === "Interactive" or step_number === 1) const step = resp?.steps?.find(stepFilterFn); - // Find the first Running update + // Find the first Running update, ignoring case for Tapis jobs. const runningUpdate = step?.updates?.find( - (update) => update.status === analysisStatus.RUNNING + (update) => + update.status.toUpperCase() === + analysisStatus.RUNNING.toUpperCase() ); // Record the timestamp setRunningStart(parseInt(runningUpdate?.timestamp || 0)); From 3bdf1088d5e7577f4af12cc7997a3e2d5fa2edec Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Mon, 12 Aug 2024 15:21:56 -0700 Subject: [PATCH 5/7] CORE-2002 Update VICE countdown timer format Display countdown timer as "Time Remaining: HHh:MMm", and only update every minute. Since this is a shorter string, it can now fit in the AnalysisSubheader in place of the running duration and date string in the dashboard. --- public/static/locales/en/analyses.json | 2 +- public/static/locales/en/dashboard.json | 1 + src/components/analyses/listing/TableView.js | 7 +++++- .../analyses/useAnalysisTimeLimitCountdown.js | 23 +++++++++++-------- .../dashboardItem/AnalysisSubheader.js | 9 +++++--- .../dashboard/dashboardItem/ItemBase.js | 15 ++++-------- stories/analyses/ArgTypes.js | 11 +++++---- 7 files changed, 40 insertions(+), 28 deletions(-) diff --git a/public/static/locales/en/analyses.json b/public/static/locales/en/analyses.json index 925911bb8..72918e932 100644 --- a/public/static/locales/en/analyses.json +++ b/public/static/locales/en/analyses.json @@ -119,7 +119,7 @@ "terminateAnalysisTitle": "Terminate Analysis", "terminateAnalysisTitle_plural": "Terminate Analyses", "terminateBtn": "Terminate", - "timeLimitCountdown": "{{timeLimitCountdown}} remaining", + "timeLimitCountdown": "Time Remaining: {{timeLimitCountdown}}", "timeLimitError": "Unable to get the current time limit. Please try again.", "timeLimitExtended": "Time limit for {{analysisName}} has been extended. This analysis will end on {{newTimeLimit}}", "theirs": "Shared with me", diff --git a/public/static/locales/en/dashboard.json b/public/static/locales/en/dashboard.json index cb5646642..40012a2af 100644 --- a/public/static/locales/en/dashboard.json +++ b/public/static/locales/en/dashboard.json @@ -5,6 +5,7 @@ "analysisCompletedOrigination": "{{status}} in {{totalRunTime}} - ({{date}})", "analysisOrigination": "{{status}} - {{date}}", "analysisRunningOrigination": "{{status}} for {{runningTime}} ({{date}})", + "analysisRunningTimeLimit": "{{status}} - Time Remaining: {{timeLimitCountdown}}", "banner": "Banner", "buy": "Buy", "by": "By", diff --git a/src/components/analyses/listing/TableView.js b/src/components/analyses/listing/TableView.js index 82cbe1fd5..178e05150 100644 --- a/src/components/analyses/listing/TableView.js +++ b/src/components/analyses/listing/TableView.js @@ -85,11 +85,16 @@ function AnalysisName(props) { } function AnalysisDuration({ analysis, timeLimitCountdown }) { + const { t } = useTranslation("analyses"); const { elapsedTime, totalRunTime } = useAnalysisRunTime(analysis); return ( - {timeLimitCountdown || totalRunTime || elapsedTime} + {timeLimitCountdown + ? t("timeLimitCountdown", { + timeLimitCountdown, + }) + : totalRunTime || elapsedTime} ); } diff --git a/src/components/analyses/useAnalysisTimeLimitCountdown.js b/src/components/analyses/useAnalysisTimeLimitCountdown.js index fcc2385be..cfc0b24dc 100644 --- a/src/components/analyses/useAnalysisTimeLimitCountdown.js +++ b/src/components/analyses/useAnalysisTimeLimitCountdown.js @@ -7,14 +7,14 @@ * `/analyses/${id}/time-limit` endpoint, and converted into milliseconds. * * The countdown timer value is calculated from this time limit value, - * and formatted with `date-fns/formatDuration`. + * and formatted as `HHh:MMm`. * - * Updates the countdown timer value every second. + * Updates the countdown timer value every minute. */ import { useEffect, useState } from "react"; -import { formatDuration, intervalToDuration, toDate } from "date-fns"; +import { millisecondsToHours, millisecondsToMinutes, toDate } from "date-fns"; import { useQuery } from "react-query"; import { useTranslation } from "i18n"; @@ -28,11 +28,16 @@ import { isInteractiveRunning } from "./utils"; const timeLimitToCountdown = (timeLimitMS) => { if (timeLimitMS > 0) { - const start = new Date(); - const end = toDate(parseInt(timeLimitMS, 10)); - - if (end > start) { - return formatDuration(intervalToDuration({ start, end })); + const now = new Date(); + const end = toDate(timeLimitMS); + + if (end > now) { + const millisRemaining = end - now; + const hours = millisecondsToHours(millisRemaining); + const mins = millisecondsToMinutes(millisRemaining) - hours * 60; + if (mins > 0 || hours > 0) { + return `${hours}h:${mins}m`; + } } } @@ -73,7 +78,7 @@ function useAnalysisTimeLimitCountdown(analysis, showErrorAnnouncer) { let interval; if (runningVICE) { - interval = setInterval(handleUpdateRunningTime, 1000); + interval = setInterval(handleUpdateRunningTime, 60 * 1000); handleUpdateRunningTime(); } else { setTimeLimitCountdown(null); diff --git a/src/components/dashboard/dashboardItem/AnalysisSubheader.js b/src/components/dashboard/dashboardItem/AnalysisSubheader.js index 04ad4798c..02985e189 100644 --- a/src/components/dashboard/dashboardItem/AnalysisSubheader.js +++ b/src/components/dashboard/dashboardItem/AnalysisSubheader.js @@ -1,6 +1,6 @@ /** * - * @author aramsey, sriram + * @author aramsey, sriram, psarando * * A component that displays analysis run time and status * @@ -13,7 +13,7 @@ import useAnalysisRunTime from "components/analyses/useAnalysisRunTime"; import analysisStatus from "components/models/analysisStatus"; export default function AnalysisSubheader(props) { - const { analysis, date: formattedDate } = props; + const { analysis, date: formattedDate, timeLimitCountdown } = props; const { t } = useTranslation(["dashboard", "apps"]); const { elapsedTime, totalRunTime } = useAnalysisRunTime(analysis); const theme = useTheme(); @@ -32,7 +32,9 @@ export default function AnalysisSubheader(props) { , diff --git a/src/components/dashboard/dashboardItem/ItemBase.js b/src/components/dashboard/dashboardItem/ItemBase.js index 4047f96c6..1f2988227 100644 --- a/src/components/dashboard/dashboardItem/ItemBase.js +++ b/src/components/dashboard/dashboardItem/ItemBase.js @@ -118,16 +118,11 @@ const DashboardItem = ({ item }) => { }} subheader={ isAnalysis ? ( - timeLimitCountdown ? ( - t("analyses:timeLimitCountdown", { - timeLimitCountdown, - }) - ) : ( - - ) + ) : ( t("origination", { origination, diff --git a/stories/analyses/ArgTypes.js b/stories/analyses/ArgTypes.js index 31485e9cd..34a075656 100644 --- a/stories/analyses/ArgTypes.js +++ b/stories/analyses/ArgTypes.js @@ -2,14 +2,15 @@ export const TimeLimitArgType = { timeLimit: { name: "Time Limit", // The time-limit endpoint can return the literal string "null". - options: ["null", "3d", "3h", "30m", "30s"], + options: ["null", "6d", "3d", "3h", "30m", "2m"], control: { type: "select", labels: { + "6d": "6 days", "3d": "3 days", "3h": "3 hours", "30m": "30 minutes", - "30s": "30 seconds", + "2m": "2 minutes", }, }, }, @@ -17,14 +18,16 @@ export const TimeLimitArgType = { export const convertTimeLimitArgType = (timeLimit) => { switch (timeLimit) { + case "6d": + return new Date().getTime() / 1000 + 6 * 24 * 60 * 60; case "3d": return new Date().getTime() / 1000 + 3 * 24 * 60 * 60; case "3h": return new Date().getTime() / 1000 + 3 * 60 * 60; case "30m": return new Date().getTime() / 1000 + 30 * 60; - case "30s": - return new Date().getTime() / 1000 + 30; + case "2m": + return new Date().getTime() / 1000 + 2 * 60; default: return timeLimit; } From 05e192f3fbbffea35fef6b1fa159fbe3f9b8fb91 Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Mon, 12 Aug 2024 15:58:34 -0700 Subject: [PATCH 6/7] CORE-2002 Fix warnings in AnalysisSubmissionLanding --- src/components/analyses/details/InfoPanel.js | 4 ++-- src/components/analyses/details/ParamsPanel.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/analyses/details/InfoPanel.js b/src/components/analyses/details/InfoPanel.js index 30ef868dd..241fe6937 100644 --- a/src/components/analyses/details/InfoPanel.js +++ b/src/components/analyses/details/InfoPanel.js @@ -59,7 +59,7 @@ function Updates(props) { const timestamp = update.timestamp; const message = update.message; return ( - <> + - + ); })} diff --git a/src/components/analyses/details/ParamsPanel.js b/src/components/analyses/details/ParamsPanel.js index 8842cf54d..33e1ab756 100644 --- a/src/components/analyses/details/ParamsPanel.js +++ b/src/components/analyses/details/ParamsPanel.js @@ -80,7 +80,11 @@ function AnalysisParams(props) { let columns = columnData(t); if (isParamsFetching) { - return ; + return ( + + +
+ ); } if (!parameters && !isParamsFetching && !paramsFetchError) { From f1fb17193456ba34d700abe5dc4fc57aec3feb1c Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Wed, 14 Aug 2024 14:09:46 -0700 Subject: [PATCH 7/7] CORE-2002 Update VICE countdown timer format Display countdown timer as "Time Remaining: HH:MM", without units. --- src/components/analyses/useAnalysisTimeLimitCountdown.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/analyses/useAnalysisTimeLimitCountdown.js b/src/components/analyses/useAnalysisTimeLimitCountdown.js index cfc0b24dc..11bb6f970 100644 --- a/src/components/analyses/useAnalysisTimeLimitCountdown.js +++ b/src/components/analyses/useAnalysisTimeLimitCountdown.js @@ -7,7 +7,7 @@ * `/analyses/${id}/time-limit` endpoint, and converted into milliseconds. * * The countdown timer value is calculated from this time limit value, - * and formatted as `HHh:MMm`. + * and formatted as `HH:MM`. * * Updates the countdown timer value every minute. */ @@ -36,7 +36,9 @@ const timeLimitToCountdown = (timeLimitMS) => { const hours = millisecondsToHours(millisRemaining); const mins = millisecondsToMinutes(millisRemaining) - hours * 60; if (mins > 0 || hours > 0) { - return `${hours}h:${mins}m`; + return [hours, mins] + .map((n) => String(n).padStart(2, "0")) + .join(":"); } } }