From e13b9f821e282b636091a3f0d729ad664da7317c Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Fri, 9 Aug 2024 16:46:55 -0700 Subject: [PATCH] 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;