diff --git a/src/components/apps/AppName.js b/src/components/apps/AppName.js index aaccf4ca3..6c51cb692 100644 --- a/src/components/apps/AppName.js +++ b/src/components/apps/AppName.js @@ -1,9 +1,9 @@ /** * A component that displays app's name. * - * @author sriram + * @author sriram, psarando */ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useTranslation } from "i18n"; import PropTypes from "prop-types"; @@ -11,49 +11,15 @@ import Link from "next/link"; import DELink from "components/utils/DELink"; import { ERROR_CODES } from "components/error/errorCode"; import AccessRequestDialog from "components/vice/AccessRequestDialog"; +import RunErrorDialog from "components/vice/RunErrorDialog"; import VicePendingRequestDlg from "components/vice/VicePendingRequestDlg"; import { useUserProfile } from "contexts/userProfile"; -import DEDialog from "components/utils/DEDialog"; -import RunError from "./RunError"; import { useAppLaunchLink } from "./utils"; import ids from "./ids"; import Highlighter from "components/highlighter/Highlighter"; -import { Button, Link as MuiLink, Typography } from "@material-ui/core"; - -function RunErrorDialog(props) { - const { baseId, code, open, viceQuota, runningJobs, onClose } = props; - const { t } = useTranslation("launch"); - const { t: i18Common } = useTranslation("common"); - let title; - if (code === ERROR_CODES.ERR_LIMIT_REACHED) { - title = t("jobLimitReached"); - } else if (code === ERROR_CODES.ERR_FORBIDDEN) { - title = t("accessDenied"); - } - return ( - - {i18Common("ok")} - - } - onClose={onClose} - > - - - - - ); -} +import { Link as MuiLink } from "@material-ui/core"; function AppName(props) { const { @@ -65,10 +31,7 @@ function AppName(props) { searchTerm, limitChecks, } = props; - const [runErrorCodes, setRunErrorCodes] = useState(null); const [userProfile] = useUserProfile(); - const [viceQuota, setViceQuota] = useState(); - const [runningJobs, setRunningJobs] = useState(); const [accessRequestDialogOpen, setAccessRequestDialogOpen] = useState(false); const [pendingRequestDlgOpen, setPendingRequestDlgOpen] = useState(false); @@ -76,13 +39,15 @@ function AppName(props) { const [href, as] = useAppLaunchLink(systemId, appId); const { t } = useTranslation("apps"); - useEffect(() => { - if (limitChecks && !limitChecks.canRun) { - setRunErrorCodes(limitChecks.results[0]?.reasonCodes); - setViceQuota(limitChecks.results[0]?.additionalInfo?.maxJobs); - setRunningJobs(limitChecks.results[0]?.additionalInfo?.runningJobs); - } - }, [limitChecks]); + let runErrorCodes; + let viceQuota; + let runningJobs; + + if (limitChecks?.results) { + runErrorCodes = limitChecks.results[0]?.reasonCodes; + viceQuota = limitChecks.results[0]?.additionalInfo?.maxJobs; + runningJobs = limitChecks.results[0]?.additionalInfo?.runningJobs; + } let title = ""; if (isDisabled) { diff --git a/src/components/apps/launch/index.js b/src/components/apps/launch/index.js index 2ed41d154..31921d3e9 100644 --- a/src/components/apps/launch/index.js +++ b/src/components/apps/launch/index.js @@ -22,11 +22,11 @@ import { addSavedLaunch } from "serviceFacades/savedLaunches"; import { trackIntercomEvent, IntercomEvents } from "common/intercom"; -import RunError from "components/apps/RunError"; import AppLaunchWizard from "./AppLaunchWizard"; import WrappedErrorHandler from "components/error/WrappedErrorHandler"; import { ERROR_CODES } from "components/error/errorCode"; import AccessRequestDialog from "components/vice/AccessRequestDialog"; +import RunError from "components/vice/RunError"; import ids from "components/apps/ids"; import { Button, Typography } from "@material-ui/core"; diff --git a/src/components/forms/FormMultilineTextField.js b/src/components/forms/FormMultilineTextField.js index 46423eba5..60599f723 100644 --- a/src/components/forms/FormMultilineTextField.js +++ b/src/components/forms/FormMultilineTextField.js @@ -5,7 +5,7 @@ import React from "react"; import FormTextField from "./FormTextField"; const FormMultilineTextField = (props) => ( - + ); export default FormMultilineTextField; diff --git a/src/components/instantlaunches/InstantLaunchButtonWrapper.js b/src/components/instantlaunches/InstantLaunchButtonWrapper.js index b0cf4d1eb..e2a95ba39 100644 --- a/src/components/instantlaunches/InstantLaunchButtonWrapper.js +++ b/src/components/instantlaunches/InstantLaunchButtonWrapper.js @@ -16,11 +16,17 @@ import { useDefaultOutputDir } from "components/data/utils"; import withErrorAnnouncer from "components/error/withErrorAnnouncer"; import { getHost } from "components/utils/getHost"; import SignInDialog from "components/utils/SignInDialog"; +import AccessRequestDialog from "components/vice/AccessRequestDialog"; +import RunErrorDialog from "components/vice/RunErrorDialog"; +import VicePendingRequestDlg from "components/vice/VicePendingRequestDlg"; + import constants from "constants.js"; import { useUserProfile } from "contexts/userProfile"; +import ids from "./ids"; import { InstantLaunchSubmissionDialog } from "./index"; import { instantlyLaunch } from "serviceFacades/instantlaunches"; import { useTranslation } from "i18n"; +import { ERROR_CODES } from "components/error/errorCode"; function InstantLaunchButtonWrapper(props) { const { @@ -35,6 +41,11 @@ function InstantLaunchButtonWrapper(props) { const [open, setOpen] = React.useState(false); const [signInDlgOpen, setSignInDlgOpen] = React.useState(false); + const [accessRequestDialogOpen, setAccessRequestDialogOpen] = + React.useState(false); + const [pendingRequestDlgOpen, setPendingRequestDlgOpen] = + React.useState(false); + const [runErrorDetails, setRunErrorDetails] = React.useState(null); const [ilUrl, setIlUrl] = React.useState(); const { t } = useTranslation("launch"); @@ -66,7 +77,25 @@ function InstantLaunchButtonWrapper(props) { }, onError: (err) => { setOpen(false); - showErrorAnnouncer(err.message, err); + + const respData = err.response?.data; + const runErrorCode = respData?.error_code; + const details = respData?.details; + + if (runErrorCode === ERROR_CODES.ERR_PERMISSION_NEEDED) { + if (details?.pendingRequest) { + setPendingRequestDlgOpen(true); + } else { + setAccessRequestDialogOpen(true); + } + } else if ( + runErrorCode === ERROR_CODES.ERR_LIMIT_REACHED || + runErrorCode === ERROR_CODES.ERR_FORBIDDEN + ) { + setRunErrorDetails({ runErrorCode, ...details }); + } else { + showErrorAnnouncer(err.message, err); + } }, }); @@ -91,6 +120,23 @@ function InstantLaunchButtonWrapper(props) { open={signInDlgOpen} handleClose={() => setSignInDlgOpen(false)} /> + setAccessRequestDialogOpen(false)} + /> + setPendingRequestDlgOpen(false)} + /> + setRunErrorDetails(null)} + code={runErrorDetails?.runErrorCode} + runningJobs={runErrorDetails?.runningJobs} + viceQuota={runErrorDetails?.maxJobs} + /> ); } diff --git a/src/components/instantlaunches/ids.js b/src/components/instantlaunches/ids.js index dd838fc9f..465478651 100644 --- a/src/components/instantlaunches/ids.js +++ b/src/components/instantlaunches/ids.js @@ -36,4 +36,6 @@ export default { ADD_LISTING: "addToListing", RM_LISTING: "removeFromListing", ADD_NAV_DRAWER: "addToDrawer", + ACCESS_REQUEST_DLG: "accessRequestDlg", + RUN_ERROR_DLG: "runErrorDlg", }; diff --git a/src/components/apps/RunError.js b/src/components/vice/RunError.js similarity index 54% rename from src/components/apps/RunError.js rename to src/components/vice/RunError.js index 30af21ff9..66b606356 100644 --- a/src/components/apps/RunError.js +++ b/src/components/vice/RunError.js @@ -1,8 +1,7 @@ /** - * @author sriram - * * Return a Translation component based on the App launch run error code. * + * @author sriram, psarando */ import React from "react"; import { Trans, useTranslation } from "i18n"; @@ -10,6 +9,15 @@ import { intercomShow } from "common/intercom"; import { ERROR_CODES } from "components/error/errorCode"; import { Link } from "@material-ui/core"; +const SupportLink = (props) => ( + +); + export default function RunError(props) { const { code, runningJobs, viceQuota } = props; const { t } = useTranslation("launch"); @@ -22,17 +30,7 @@ export default function RunError(props) { components={{ b: , br:
, - support: ( - { - // prevent form submission - event.preventDefault(); - intercomShow(); - }} - /> - ), + support: , }} /> ); @@ -42,17 +40,7 @@ export default function RunError(props) { t={t} i18nKey="launchForbiddenPrompt" components={{ - support: ( - { - // prevent form submission - event.preventDefault(); - intercomShow(); - }} - /> - ), + support: , }} /> ); diff --git a/src/components/vice/RunErrorDialog.js b/src/components/vice/RunErrorDialog.js new file mode 100644 index 000000000..cd9fd7b4d --- /dev/null +++ b/src/components/vice/RunErrorDialog.js @@ -0,0 +1,50 @@ +/** + * A dialog component to display the App launch run error code. + * + * @author sriram, psarando + */ +import React from "react"; + +import { useTranslation } from "i18n"; +import { ERROR_CODES } from "components/error/errorCode"; +import DEDialog from "components/utils/DEDialog"; +import RunError from "./RunError"; + +import { Button, Typography } from "@material-ui/core"; + +const RunErrorDialog = (props) => { + const { baseId, code, open, viceQuota, runningJobs, onClose } = props; + + const { t } = useTranslation(["launch", "common"]); + + let title; + if (code === ERROR_CODES.ERR_LIMIT_REACHED) { + title = t("jobLimitReached"); + } else if (code === ERROR_CODES.ERR_FORBIDDEN) { + title = t("accessDenied"); + } + + return ( + + {t("common:ok")} + + } + onClose={onClose} + > + + + + + ); +}; + +export default RunErrorDialog; diff --git a/src/i18n.js b/src/i18n.js index ad6d850e7..43e1006ba 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -15,17 +15,20 @@ import { } from "next-i18next"; // The always-needed namespaces, mainly from the components in the global -// toolbars, such as bags, global search, and the notifications menu. +// toolbars, such as instant launch drawer items, bags, global search, and the +// notifications menu. const RequiredNamespaces = [ "analyses", "apps", "bags", "common", "instantlaunches", + "launch", "notifications", "search", "sharing", "util", + "vice", ]; // reexport everything diff --git a/src/pages/apps/[systemId]/[appId]/launch.js b/src/pages/apps/[systemId]/[appId]/launch.js index 4849e9cc7..7f824a103 100644 --- a/src/pages/apps/[systemId]/[appId]/launch.js +++ b/src/pages/apps/[systemId]/[appId]/launch.js @@ -124,10 +124,9 @@ export async function getServerSideProps({ locale }) { props: { ...(await serverSideTranslations(locale, [ "data", - "launch", "upload", "urlImport", - // "apps" already included by RequiredNamespaces + // "apps" and "launch" already included by RequiredNamespaces ...RequiredNamespaces, ])), }, diff --git a/src/pages/apps/[systemId]/[appId]/versions/[versionId]/launch.js b/src/pages/apps/[systemId]/[appId]/versions/[versionId]/launch.js index 298a9fa4e..a6e0925a2 100644 --- a/src/pages/apps/[systemId]/[appId]/versions/[versionId]/launch.js +++ b/src/pages/apps/[systemId]/[appId]/versions/[versionId]/launch.js @@ -80,10 +80,9 @@ export async function getServerSideProps({ locale }) { props: { ...(await serverSideTranslations(locale, [ "data", - "launch", "upload", "urlImport", - // "apps" already included by RequiredNamespaces + // "apps" and "launch" already included by RequiredNamespaces ...RequiredNamespaces, ])), }, diff --git a/src/pages/data/ds/[...pathItems].js b/src/pages/data/ds/[...pathItems].js index 031d7e30e..6acfc75b9 100644 --- a/src/pages/data/ds/[...pathItems].js +++ b/src/pages/data/ds/[...pathItems].js @@ -193,7 +193,6 @@ export async function getServerSideProps({ locale }) { props: { ...(await serverSideTranslations(locale, [ "data", - "launch", "metadata", "upload", "urlImport", diff --git a/src/serviceFacades/instantlaunches.js b/src/serviceFacades/instantlaunches.js index 0a564b84b..2667ab586 100644 --- a/src/serviceFacades/instantlaunches.js +++ b/src/serviceFacades/instantlaunches.js @@ -340,9 +340,7 @@ export const instantlyLaunch = ({ instantLaunch, resource, output_dir }) => { // We'll need to get the saved launch info from the API since it contains the // submission, which isn't provided from the data window. - savedLaunchPromise = getSavedLaunch(savedLaunchId).catch((e) => - console.log(e) - ); + savedLaunchPromise = getSavedLaunch(savedLaunchId); } else { // The dashboard logic. // The saved launch ID is a top-level property of the object passed in. @@ -358,7 +356,7 @@ export const instantlyLaunch = ({ instantLaunch, resource, output_dir }) => { // Contains the Promises that resolve to the data needed to perform a job submission. const promiseList = [ savedLaunchPromise, - getAppInfo({ launchId: savedLaunchId }).catch((e) => console.log(e)), + getAppInfo({ launchId: savedLaunchId }), ]; return Promise.all(promiseList) @@ -366,6 +364,21 @@ export const instantlyLaunch = ({ instantLaunch, resource, output_dir }) => { const [ql, app] = values; const { app_version_id, submission } = ql; + if (app.limitChecks && !app.limitChecks.canRun) { + const checkResults = app.limitChecks.results[0]; + const details = checkResults.additionalInfo; + return Promise.reject({ + status: 400, + message: checkResults.reasonCodes[0], + response: { + data: { + error_code: checkResults.reasonCodes[0], + details, + }, + }, + }); + } + // Ensure submission for the correct app version, // since older QL submissions do not have version IDs saved. if (!submission.app_version_id) { diff --git a/stories/dashboard/Dashboard.stories.js b/stories/dashboard/Dashboard.stories.js index bca573d1c..a8a7cc793 100644 --- a/stories/dashboard/Dashboard.stories.js +++ b/stories/dashboard/Dashboard.stories.js @@ -6,6 +6,8 @@ import fetchMock from "fetch-mock"; import { mockAxios } from "../axiosMock"; import { appDetails, listingById } from "./appDetails"; + +import { instantLaunchAppInfo } from "../data/DataMocksInstantLaunch"; import { usageSummaryResponse, usageSummaryComputeLimitExceededResponse, @@ -17,7 +19,10 @@ export default { title: "Dashboard / Display", }; -const DashboardTestTemplate = ({ usageSummaryResponseBody }) => { +const DashboardTestTemplate = ({ + instantLaunchAppInfoResponse, + usageSummaryResponseBody, +}) => { const favoriteUriRegexp = /\/api\/apps\/[^/]+\/[^/]+\/favorite/; mockAxios .onGet(/\/api\/apps\/[^/]+\/[^/]+\/details/) @@ -28,6 +33,9 @@ const DashboardTestTemplate = ({ usageSummaryResponseBody }) => { mockAxios.onGet("/api/dashboard?limit=8").reply(200, testData); mockAxios.onPut(favoriteUriRegexp).reply(200); mockAxios.onDelete(favoriteUriRegexp).reply(200); + mockAxios + .onGet(new RegExp("/api/quicklaunches/.*app-info")) + .reply(200, instantLaunchAppInfoResponse); mockAxios .onGet(/\/api\/resource-usage\/summary.*/) .reply(200, usageSummaryResponseBody); @@ -258,10 +266,104 @@ const DashboardTestTemplate = ({ usageSummaryResponseBody }) => { export const NoLimitsExceeded = DashboardTestTemplate.bind({}); NoLimitsExceeded.args = { + instantLaunchAppInfoResponse: instantLaunchAppInfo, usageSummaryResponseBody: usageSummaryResponse, }; export const ComputeLimitExceeded = DashboardTestTemplate.bind({}); ComputeLimitExceeded.args = { + instantLaunchAppInfoResponse: instantLaunchAppInfo, usageSummaryResponseBody: usageSummaryComputeLimitExceededResponse, }; + +export const InstantLaunchLimitReached = DashboardTestTemplate.bind({}); +InstantLaunchLimitReached.args = { + instantLaunchAppInfoResponse: { + ...instantLaunchAppInfo, + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_LIMIT_REACHED"], + additionalInfo: { + runningJobs: 2, + maxJobs: 2, + usingDefaultSetting: false, + pendingRequest: false, + }, + }, + ], + }, + }, + usageSummaryResponse: usageSummaryResponse, +}; + +export const InstantLaunchVICEForbidden = DashboardTestTemplate.bind({}); +InstantLaunchVICEForbidden.args = { + instantLaunchAppInfoResponse: { + ...instantLaunchAppInfo, + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_FORBIDDEN"], + additionalInfo: { + runningJobs: 0, + maxJobs: 0, + usingDefaultSetting: false, + pendingRequest: false, + }, + }, + ], + }, + }, + usageSummaryResponse: usageSummaryResponse, +}; + +export const InstantLaunchPermissionNeeded = DashboardTestTemplate.bind({}); +InstantLaunchPermissionNeeded.args = { + instantLaunchAppInfoResponse: { + ...instantLaunchAppInfo, + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_PERMISSION_NEEDED"], + additionalInfo: { + runningJobs: 0, + maxJobs: 0, + usingDefaultSetting: false, + pendingRequest: false, + }, + }, + ], + }, + }, + usageSummaryResponse: usageSummaryResponse, +}; + +export const InstantLaunchPermissionPending = DashboardTestTemplate.bind({}); +InstantLaunchPermissionPending.args = { + instantLaunchAppInfoResponse: { + ...instantLaunchAppInfo, + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_PERMISSION_NEEDED"], + additionalInfo: { + runningJobs: 0, + maxJobs: 0, + usingDefaultSetting: false, + pendingRequest: true, + }, + }, + ], + }, + }, + usageSummaryResponse: usageSummaryResponse, +}; diff --git a/stories/data/DataMocksInstantLaunch.js b/stories/data/DataMocksInstantLaunch.js index eccc64fd8..6e75ff828 100644 --- a/stories/data/DataMocksInstantLaunch.js +++ b/stories/data/DataMocksInstantLaunch.js @@ -135,168 +135,3 @@ export const instantLaunchSubmissionResponse = { state: "Submitted", "start-date": "now", }; - -export const usageSummaryResponse = { - cpu_usage: { - id: "daafc764-42bf-425d-83c6-dbdfed410e26", - user_id: "2ec78d4e-0dc3-11e8-a42f-008cfa5ae621", - username: "ipcdev@iplantcollaborative.org", - total: "1919.5484124900004782", - effective_start: "2022-03-01T08:38:11.717336Z", - effective_end: "2023-03-01T08:38:11.717336Z", - last_modified: "2022-08-16T15:29:50.757566Z", - }, - data_usage: { - id: "580cbce3-c94f-4f7e-90de-f073d9088b89", - user_id: "2ec78d4e-0dc3-11e8-a42f-008cfa5ae621", - username: "ipcdev@iplantcollaborative.org", - total: 12612182209, - time: "2022-08-16T15:33:38.352429-07:00", - last_modified: "2022-08-16T15:33:38.348854-07:00", - }, - subscription: { - id: "3ae507e4-07d5-11ed-b0ae-008cfa5ae621", - effective_start_date: "2022-07-19T19:39:51.58586-07:00", - effective_end_date: "2023-07-19T19:39:51.58586-07:00", - plan: { - id: "cdf7ac7a-98dc-11ec-bbe3-406c8f3e9cbb", - name: "Pro", - description: "Professional plan", - }, - quotas: [ - { - id: "3ae561c6-07d5-11ed-b0ae-008cfa5ae621", - quota: 2000, - resource_type: { - id: "99e3bc7e-950a-11ec-84a4-406c8f3e9cbb", - name: "cpu.hours", - description: "", - }, - }, - { - id: "3ae56e78-07d5-11ed-b0ae-008cfa5ae621", - quota: 3298534883328, - resource_type: { - id: "99e3f91e-950a-11ec-84a4-406c8f3e9cbb", - name: "data.size", - description: "", - }, - }, - ], - users: { - id: "", - username: "", - }, - }, - errors: [], -}; - -export const usageSummaryStorageLimitExceededResponse = { - cpu_usage: { - id: "daafc764-42bf-425d-83c6-dbdfed410e26", - user_id: "2ec78d4e-0dc3-11e8-a42f-008cfa5ae621", - username: "ipcdev@iplantcollaborative.org", - total: "1919.5484124900004782", - effective_start: "2022-03-01T08:38:11.717336Z", - effective_end: "2023-03-01T08:38:11.717336Z", - last_modified: "2022-08-16T15:29:50.757566Z", - }, - data_usage: { - id: "580cbce3-c94f-4f7e-90de-f073d9088b89", - user_id: "2ec78d4e-0dc3-11e8-a42f-008cfa5ae621", - username: "ipcdev@iplantcollaborative.org", - total: 1261218220900000, - time: "2022-08-16T15:33:38.352429-07:00", - last_modified: "2022-08-16T15:33:38.348854-07:00", - }, - subscription: { - id: "3ae507e4-07d5-11ed-b0ae-008cfa5ae621", - effective_start_date: "2022-07-19T19:39:51.58586-07:00", - effective_end_date: "2023-07-19T19:39:51.58586-07:00", - plan: { - id: "cdf7ac7a-98dc-11ec-bbe3-406c8f3e9cbb", - name: "Pro", - description: "Professional plan", - }, - quotas: [ - { - id: "3ae561c6-07d5-11ed-b0ae-008cfa5ae621", - quota: 2000, - resource_type: { - id: "99e3bc7e-950a-11ec-84a4-406c8f3e9cbb", - name: "cpu.hours", - description: "", - }, - }, - { - id: "3ae56e78-07d5-11ed-b0ae-008cfa5ae621", - quota: 3298534883328, - resource_type: { - id: "99e3f91e-950a-11ec-84a4-406c8f3e9cbb", - name: "data.size", - description: "", - }, - }, - ], - users: { - id: "", - username: "", - }, - }, - errors: [], -}; - -export const usageSummaryComputeLimitExceededResponse = { - cpu_usage: { - id: "daafc764-42bf-425d-83c6-dbdfed410e26", - user_id: "2ec78d4e-0dc3-11e8-a42f-008cfa5ae621", - username: "ipcdev@iplantcollaborative.org", - total: "2919.5484124900004782", - effective_start: "2022-03-01T08:38:11.717336Z", - effective_end: "2023-03-01T08:38:11.717336Z", - last_modified: "2022-08-16T15:29:50.757566Z", - }, - data_usage: { - id: "580cbce3-c94f-4f7e-90de-f073d9088b89", - user_id: "2ec78d4e-0dc3-11e8-a42f-008cfa5ae621", - username: "ipcdev@iplantcollaborative.org", - total: 12612182209, - time: "2022-08-16T15:33:38.352429-07:00", - last_modified: "2022-08-16T15:33:38.348854-07:00", - }, - subscription: { - id: "3ae507e4-07d5-11ed-b0ae-008cfa5ae621", - effective_start_date: "2022-07-19T19:39:51.58586-07:00", - effective_end_date: "2023-07-19T19:39:51.58586-07:00", - plan: { - id: "cdf7ac7a-98dc-11ec-bbe3-406c8f3e9cbb", - name: "Pro", - description: "Professional plan", - }, - quotas: [ - { - id: "3ae561c6-07d5-11ed-b0ae-008cfa5ae621", - quota: 2000, - resource_type: { - id: "99e3bc7e-950a-11ec-84a4-406c8f3e9cbb", - name: "cpu.hours", - description: "", - }, - }, - { - id: "3ae56e78-07d5-11ed-b0ae-008cfa5ae621", - quota: 3298534883328, - resource_type: { - id: "99e3f91e-950a-11ec-84a4-406c8f3e9cbb", - name: "data.size", - description: "", - }, - }, - ], - users: { - id: "", - username: "", - }, - }, - errors: [], -}; diff --git a/stories/data/Listing.stories.js b/stories/data/Listing.stories.js index 33308f229..b1a92ab87 100644 --- a/stories/data/Listing.stories.js +++ b/stories/data/Listing.stories.js @@ -1,7 +1,7 @@ import React from "react"; -import Listing from "../../src/components/data/listing/Listing"; -import { UploadTrackingProvider } from "../../src/contexts/uploadTracking"; +import Listing from "components/data/listing/Listing"; +import { UploadTrackingProvider } from "contexts/uploadTracking"; import { fileTypesResp, @@ -13,15 +13,16 @@ import { instantLaunchMapping, instantLaunchAppInfo, instantLaunchSavedLaunch, - instantLaunchGlobalSavedLaunches, instantLaunchSubmissionResponse, +} from "./DataMocksInstantLaunch"; +import { usageSummaryResponse, usageSummaryStorageLimitExceededResponse, usageSummaryComputeLimitExceededResponse, -} from "./DataMocksInstantLaunch"; +} from "../usageSummaryMock"; import { mockAxios } from "../axiosMock"; -import constants from "../../src/constants"; +import constants from "constants"; export default { title: "Data / Listing", @@ -49,42 +50,29 @@ function ListingTest(props) { const DataListingTestTemplate = (args) => { const { - pagedDirectoryResponse, - dataRootsResponse, - fileTypesResponse, - deletionResponse, - instantLaunchMappingResponse, instantLaunchAppInfoResponse, - instantLaunchGlobalSavedLaunchesResponse, - instantLaunchSavedLaunchResponse, - instantLaunchSubmissionResponse, usageSummaryResponse, usageSummaryError, } = args; mockAxios .onGet(/\/api\/filesystem\/paged-directory.*/) - .reply(200, pagedDirectoryResponse); - mockAxios.onGet(/\/api\/filesystem\/root.*/).reply(200, dataRootsResponse); - mockAxios - .onGet(/\/api\/filetypes\/type-list/) - .reply(200, fileTypesResponse); + .reply(200, successResp); + mockAxios.onGet(/\/api\/filesystem\/root.*/).reply(200, dataRootsResp); + mockAxios.onGet(/\/api\/filetypes\/type-list/).reply(200, fileTypesResp); - mockAxios.onPost(/\/api\/filesystem\/delete/).reply(200, deletionResponse); + mockAxios.onPost(/\/api\/filesystem\/delete/).reply(200, {}); mockAxios .onGet(/\/api\/instantlaunches\/mappings\/defaults\/latest.*/) - .reply(200, instantLaunchMappingResponse); + .reply(200, instantLaunchMapping); mockAxios .onGet( /\/api\/quicklaunches\/a4b1f851-80c0-415d-ba3c-6663432e4f7e\/app-info.*/ ) .reply(200, instantLaunchAppInfoResponse); - mockAxios - .onGet(/\/api\/quicklaunches\/defaults\/global.*/) - .reply(200, instantLaunchGlobalSavedLaunchesResponse); mockAxios .onGet(/\/api\/quicklaunches\/a4b1f851-80c0-415d-ba3c-6663432e4f7e.*/) - .reply(200, instantLaunchSavedLaunchResponse); + .reply(200, instantLaunchSavedLaunch); mockAxios .onPost(/\/api\/analyses.*/) .reply(200, instantLaunchSubmissionResponse); @@ -97,60 +85,124 @@ const DataListingTestTemplate = (args) => { export const NormalListing = DataListingTestTemplate.bind({}); NormalListing.args = { - pagedDirectoryResponse: successResp, - dataRootsResponse: dataRootsResp, - fileTypesResponse: fileTypesResp, - deletionResponse: {}, - instantLaunchMappingResponse: instantLaunchMapping, instantLaunchAppInfoResponse: instantLaunchAppInfo, - instantLaunchGlobalSavedLaunchesResponse: instantLaunchGlobalSavedLaunches, - instantLaunchSavedLaunchResponse: instantLaunchSavedLaunch, - instantLaunchSubmissionResponse: instantLaunchSubmissionResponse, usageSummaryResponse: usageSummaryResponse, usageSummaryError: false, }; export const StorageLimitExceeded = DataListingTestTemplate.bind({}); StorageLimitExceeded.args = { - pagedDirectoryResponse: successResp, - dataRootsResponse: dataRootsResp, - fileTypesResponse: fileTypesResp, - deletionResponse: {}, - instantLaunchMappingResponse: instantLaunchMapping, instantLaunchAppInfoResponse: instantLaunchAppInfo, - instantLaunchGlobalSavedLaunchesResponse: instantLaunchGlobalSavedLaunches, - instantLaunchSavedLaunchResponse: instantLaunchSavedLaunch, - instantLaunchSubmissionResponse: instantLaunchSubmissionResponse, usageSummaryResponse: usageSummaryStorageLimitExceededResponse, usageSummaryError: false, }; export const ComputeLimitExceeded = DataListingTestTemplate.bind({}); ComputeLimitExceeded.args = { - pagedDirectoryResponse: successResp, - dataRootsResponse: dataRootsResp, - fileTypesResponse: fileTypesResp, - deletionResponse: {}, - instantLaunchMappingResponse: instantLaunchMapping, instantLaunchAppInfoResponse: instantLaunchAppInfo, - instantLaunchGlobalSavedLaunchesResponse: instantLaunchGlobalSavedLaunches, - instantLaunchSavedLaunchResponse: instantLaunchSavedLaunch, - instantLaunchSubmissionResponse: instantLaunchSubmissionResponse, usageSummaryResponse: usageSummaryComputeLimitExceededResponse, usageSummaryError: false, }; export const UsageSummaryError = DataListingTestTemplate.bind({}); UsageSummaryError.args = { - pagedDirectoryResponse: successResp, - dataRootsResponse: dataRootsResp, - fileTypesResponse: fileTypesResp, - deletionResponse: {}, - instantLaunchMappingResponse: instantLaunchMapping, instantLaunchAppInfoResponse: instantLaunchAppInfo, - instantLaunchGlobalSavedLaunchesResponse: instantLaunchGlobalSavedLaunches, - instantLaunchSavedLaunchResponse: instantLaunchSavedLaunch, - instantLaunchSubmissionResponse: instantLaunchSubmissionResponse, usageSummaryResponse: usageSummaryResponse, usageSummaryError: true, }; + +export const InstantLaunchLimitReached = DataListingTestTemplate.bind({}); +InstantLaunchLimitReached.args = { + instantLaunchAppInfoResponse: { + ...instantLaunchAppInfo, + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_LIMIT_REACHED"], + additionalInfo: { + runningJobs: 2, + maxJobs: 2, + usingDefaultSetting: false, + pendingRequest: false, + }, + }, + ], + }, + }, + usageSummaryResponse: usageSummaryResponse, + usageSummaryError: false, +}; + +export const InstantLaunchVICEForbidden = DataListingTestTemplate.bind({}); +InstantLaunchVICEForbidden.args = { + instantLaunchAppInfoResponse: { + ...instantLaunchAppInfo, + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_FORBIDDEN"], + additionalInfo: { + runningJobs: 0, + maxJobs: 0, + usingDefaultSetting: false, + pendingRequest: false, + }, + }, + ], + }, + }, + usageSummaryResponse: usageSummaryResponse, + usageSummaryError: false, +}; + +export const InstantLaunchPermissionNeeded = DataListingTestTemplate.bind({}); +InstantLaunchPermissionNeeded.args = { + instantLaunchAppInfoResponse: { + ...instantLaunchAppInfo, + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_PERMISSION_NEEDED"], + additionalInfo: { + runningJobs: 0, + maxJobs: 0, + usingDefaultSetting: false, + pendingRequest: false, + }, + }, + ], + }, + }, + usageSummaryResponse: usageSummaryResponse, + usageSummaryError: false, +}; + +export const InstantLaunchPermissionPending = DataListingTestTemplate.bind({}); +InstantLaunchPermissionPending.args = { + instantLaunchAppInfoResponse: { + ...instantLaunchAppInfo, + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_PERMISSION_NEEDED"], + additionalInfo: { + runningJobs: 0, + maxJobs: 0, + usingDefaultSetting: false, + pendingRequest: true, + }, + }, + ], + }, + }, + usageSummaryResponse: usageSummaryResponse, + usageSummaryError: false, +}; diff --git a/stories/instantlaunches/InstantLaunchListing.stories.js b/stories/instantlaunches/InstantLaunchListing.stories.js index 50ed1b642..e0d92762e 100644 --- a/stories/instantlaunches/InstantLaunchListing.stories.js +++ b/stories/instantlaunches/InstantLaunchListing.stories.js @@ -1,49 +1,71 @@ import React from "react"; import { mockAxios } from "../axiosMock"; -import { testFullInstantLaunchList } from "./admin/SavedLaunchListData"; -import InstantLaunchListing from "../../src/components/instantlaunches/listing/index"; +import { + qlAppInfoVICELimitReached, + qlAppInfoVICEForbidden, + qlAppInfoPermissionNeeded, + qlAppInfoPermissionPending, + testFullInstantLaunchList, +} from "./admin/SavedLaunchListData"; +import InstantLaunchListing from "components/instantlaunches/listing"; import { usageSummaryResponse, usageSummaryComputeLimitExceededResponse, -} from "../data/DataMocksInstantLaunch"; +} from "../usageSummaryMock"; export default { title: "Instant Launches / Instant Launch Listing", }; const InstantLaunchListTestTemplate = (args) => { - const { instantLaunchListing, usageSummaryResponse, usageSummaryError } = - args; + const { usageSummaryResponse, usageSummaryError } = args; mockAxios .onGet("/api/instantlaunches/metadata/full", { params: { attribute: "ui_location", value: "listing", unit: "" }, }) - .reply(200, instantLaunchListing); + .reply(200, testFullInstantLaunchList); mockAxios .onGet(/\/api\/resource-usage\/summary.*/) .reply(usageSummaryError ? 400 : 200, usageSummaryResponse); + mockAxios + .onGet( + "/api/quicklaunches/2421c19e-ad85-4217-a21f-e0832f824171/app-info" + ) + .reply(200, qlAppInfoVICELimitReached); + mockAxios + .onGet( + "/api/quicklaunches/f0d5c20a-d960-44e3-b9ee-dd1c85e75b69/app-info" + ) + .reply(200, qlAppInfoVICEForbidden); + mockAxios + .onGet( + "/api/quicklaunches/d5f53129-f0af-490a-8221-e76440203cdc/app-info" + ) + .reply(200, qlAppInfoPermissionNeeded); + mockAxios + .onGet( + "/api/quicklaunches/8ebc6c80-2058-4953-b4c9-ecf893deae03/app-info" + ) + .reply(200, qlAppInfoPermissionPending); return ; }; export const NormalListing = InstantLaunchListTestTemplate.bind({}); NormalListing.args = { - instantLaunchListing: testFullInstantLaunchList, usageSummaryResponse: usageSummaryResponse, usageSummaryError: false, }; export const ComputeLimitExceeded = InstantLaunchListTestTemplate.bind({}); ComputeLimitExceeded.args = { - instantLaunchListing: testFullInstantLaunchList, usageSummaryResponse: usageSummaryComputeLimitExceededResponse, usageSummaryError: false, }; export const UsageSummaryError = InstantLaunchListTestTemplate.bind({}); UsageSummaryError.args = { - instantLaunchListing: testFullInstantLaunchList, usageSummaryResponse: usageSummaryResponse, usageSummaryError: true, }; diff --git a/stories/instantlaunches/admin/SavedLaunchListData.js b/stories/instantlaunches/admin/SavedLaunchListData.js index 630c8657c..692f2c8ca 100644 --- a/stories/instantlaunches/admin/SavedLaunchListData.js +++ b/stories/instantlaunches/admin/SavedLaunchListData.js @@ -140,13 +140,13 @@ export const testFullInstantLaunchList = { id: "2A05CA56-CC62-406A-AB34-0870F3644E0C", quick_launch_id: "2421c19e-ad85-4217-a21f-e0832f824171", name: "sample-savedlaunch", - quick_launch_name: "sample-savedlaunch", + quick_launch_name: "instant-launch: Limit Reached", quick_launch_description: "", quick_launch_creator: "ipcdev@iplantcollaborative.org", added_by: "ipcdev@siplantcollaborative.org", added_on: new Date(2021, 3, 27, 9, 22, 55), - app_id: "34f2c392-9a8a-11e8-9c8e-008cfa5ae621", - app_version_id: "34f2c392-9a8a-11e8-9c8e-008cfa5ae621", + app_id: "bc93504c-d584-11e9-8413-008cfa5ae621", + app_version_id: "8c9654d0-127a-11ed-9c8c-008cfa5ae621", app_version: "v0.0.3", app_name: "JupyterLab", is_public: true, @@ -160,7 +160,7 @@ export const testFullInstantLaunchList = { ], }, name: "JupyterLab-0.0.3_analysis1", - app_id: "34f2c392-9a8a-11e8-9c8e-008cfa5ae621", + app_id: "bc93504c-d584-11e9-8413-008cfa5ae621", system_id: "de", debug: false, output_dir: "/iplant/home/ipcdev/analyses", @@ -198,13 +198,13 @@ export const testFullInstantLaunchList = { id: "732F73F6-4656-42C1-8EB9-37B0883019F3", quick_launch_id: "f0d5c20a-d960-44e3-b9ee-dd1c85e75b69", name: "instant-launch-test", - quick_launch_name: "instant-launch-test", + quick_launch_name: "instant-launch: VICE forbidden", quick_launch_description: "", quick_launch_creator: "wregglej@iplantcollaborative.org", added_by: "wregglej@iplantcollaborative.org", added_on: new Date(2021, 3, 27, 9, 22, 55), - app_id: "d61d9a26-e921-11e9-8fe0-008cfa5ae621", - app_version_id: "d61d9a26-e921-11e9-8fe0-008cfa5ae621", + app_id: "7f504738-e06c-11e9-a622-008cfa5ae621", + app_version_id: "8c96822a-127a-11ed-9c8c-008cfa5ae621", app_version: "Unversioned", app_name: "jupyter-lab", is_public: true, @@ -214,7 +214,7 @@ export const testFullInstantLaunchList = { [], }, name: "jupyter-lab-instant-launch", - app_id: "d61d9a26-e921-11e9-8fe0-008cfa5ae621", + app_id: "7f504738-e06c-11e9-a622-008cfa5ae621", system_id: "de", debug: false, output_dir: "/iplant/home/wregglej/analyses_qa", @@ -225,13 +225,13 @@ export const testFullInstantLaunchList = { id: "6B6CBA6F-4468-4E54-AD99-E3AC5E4E08A4", quick_launch_id: "d5f53129-f0af-490a-8221-e76440203cdc", name: "instant-launch-test-1", - quick_launch_name: "instant-launch-test-1", + quick_launch_name: "instant-launch: Permission Needed", quick_launch_description: "", quick_launch_creator: "wregglej@iplantcollaborative.org", added_by: "wregglej@iplantcollaborative.org", added_on: new Date(2021, 3, 27, 9, 22, 55), - app_id: "d61d9a26-e921-11e9-8fe0-008cfa5ae621", - app_version_id: "d61d9a26-e921-11e9-8fe0-008cfa5ae621", + app_id: "8ec235d8-f173-11e9-a56f-008cfa5ae621", + app_version_id: "8c83ce46-127a-11ed-9c8c-008cfa5ae621", app_version: "Unversioned", app_name: "jupyter-lab", is_public: true, @@ -241,7 +241,7 @@ export const testFullInstantLaunchList = { [], }, name: "jupyter-lab-instant-launch-1", - app_id: "d61d9a26-e921-11e9-8fe0-008cfa5ae621", + app_id: "8ec235d8-f173-11e9-a56f-008cfa5ae621", system_id: "de", debug: false, output_dir: "/iplant/home/wregglej/analyses_qa", @@ -252,13 +252,13 @@ export const testFullInstantLaunchList = { id: "0509A78A-0E0A-41F4-A29C-E8E694FA028A", quick_launch_id: "8ebc6c80-2058-4953-b4c9-ecf893deae03", name: "instant-launch-test-2", - quick_launch_name: "instant-launch-test-2", + quick_launch_name: "instant-launch: Permission Pending", quick_launch_description: "", quick_launch_creator: "wregglej@iplantcollaborative.org", added_by: "wregglej@iplantcollaborative.org", added_on: new Date(2021, 3, 27, 9, 22, 55), - app_id: "d61d9a26-e921-11e9-8fe0-008cfa5ae621", - app_version_id: "d61d9a26-e921-11e9-8fe0-008cfa5ae621", + app_id: "8ec235d8-f173-11e9-a56f-008cfa5ae621", + app_version_id: "8c83ce46-127a-11ed-9c8c-008cfa5ae621", app_version: "Unversioned", app_name: "jupyter-lab", is_public: true, @@ -268,7 +268,7 @@ export const testFullInstantLaunchList = { [], }, name: "jupyter-lab-instant-launch-2", - app_id: "d61d9a26-e921-11e9-8fe0-008cfa5ae621", + app_id: "8ec235d8-f173-11e9-a56f-008cfa5ae621", system_id: "de", debug: false, output_dir: "/iplant/home/wregglej/analyses_qa", @@ -368,3 +368,191 @@ export const testInstantLaunchesDashboard = { }, ], }; + +export const qlAppInfoVICELimitReached = { + version_id: "8c9654d0-127a-11ed-9c8c-008cfa5ae621", + version: "v0.0.3", + integration_date: "2019-09-12T17:44:37.000Z", + description: + "Launches a Jupyter Lab with ScyPy \n\nMaintained here: https://github.com/cyverse-vice/jupyterlab-scipy ", + deleted: false, + pipeline_eligibility: { + is_valid: false, + reason: "Job type, interactive, can't currently be included in a pipeline.", + }, + is_favorite: true, + integrator_name: "Tyson Swetnam", + beta: true, + permission: "read", + isBlessed: false, + can_favor: true, + disabled: false, + can_rate: true, + name: "Jupyter Lab SciPy Notebook Latest", + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_LIMIT_REACHED"], + additionalInfo: { + runningJobs: 1, + maxJobs: 1, + usingDefaultSetting: false, + pendingRequest: false, + }, + }, + ], + }, + system_id: "de", + overall_job_type: "interactive", + is_public: true, + id: "bc93504c-d584-11e9-8413-008cfa5ae621", + edited_date: "2019-09-12T17:44:37.000Z", + step_count: 1, + can_run: true, + integrator_email: "tswetnam@arizona.edu", + app_type: "DE", + rating: { average: 4, total: 5 }, +}; + +export const qlAppInfoVICEForbidden = { + version_id: "8c96822a-127a-11ed-9c8c-008cfa5ae621", + version: "Unversioned", + integration_date: "2019-09-28T04:20:25.000Z", + description: + "PlantCV is an open-source image analysis software package targeted for plant phenotyping. Homepage: https://plantcv.danforthcenter.org/", + deleted: false, + pipeline_eligibility: { + is_valid: false, + reason: "Job type, interactive, can't currently be included in a pipeline.", + }, + is_favorite: true, + integrator_name: "Noah Fahlgren", + beta: true, + permission: "read", + isBlessed: false, + can_favor: true, + disabled: false, + can_rate: true, + name: "PlantCV v3.6.0", + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_FORBIDDEN"], + additionalInfo: { + runningJobs: 0, + maxJobs: 0, + usingDefaultSetting: false, + pendingRequest: false, + }, + }, + ], + }, + system_id: "de", + overall_job_type: "interactive", + is_public: true, + id: "7f504738-e06c-11e9-a622-008cfa5ae621", + edited_date: "2019-09-28T04:20:25.000Z", + step_count: 1, + can_run: true, + integrator_email: "nfahlgren@danforthcenter.org", + app_type: "DE", + rating: { average: 3.3333333333333335, total: 3 }, +}; + +export const qlAppInfoPermissionNeeded = { + version_id: "8c83ce46-127a-11ed-9c8c-008cfa5ae621", + version: "Unversioned", + integration_date: "2019-10-18T21:10:53.000Z", + 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", + deleted: false, + pipeline_eligibility: { + is_valid: false, + reason: "Job type, interactive, can't currently be included in a pipeline.", + }, + is_favorite: true, + integrator_name: "Peter Rose", + beta: true, + permission: "read", + isBlessed: false, + can_favor: true, + disabled: false, + can_rate: true, + name: "ten-rules-jupyter", + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_PERMISSION_NEEDED"], + additionalInfo: { + runningJobs: 0, + maxJobs: 0, + usingDefaultSetting: false, + pendingRequest: false, + }, + }, + ], + }, + system_id: "de", + overall_job_type: "interactive", + is_public: true, + id: "8ec235d8-f173-11e9-a56f-008cfa5ae621", + edited_date: "2019-10-18T21:10:53.000Z", + step_count: 1, + can_run: true, + integrator_email: "pwrose@ucsd.edu", + app_type: "DE", + rating: { average: 5, total: 1 }, +}; + +export const qlAppInfoPermissionPending = { + version_id: "8c83ce46-127a-11ed-9c8c-008cfa5ae621", + version: "Unversioned", + integration_date: "2019-10-18T21:10:53.000Z", + 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", + deleted: false, + pipeline_eligibility: { + is_valid: false, + reason: "Job type, interactive, can't currently be included in a pipeline.", + }, + is_favorite: true, + integrator_name: "Peter Rose", + beta: true, + permission: "read", + isBlessed: false, + can_favor: true, + disabled: false, + can_rate: true, + name: "ten-rules-jupyter", + limitChecks: { + canRun: false, + results: [ + { + limitCheckID: "CONCURRENT_VICE_ANALYSES", + reasonCodes: ["ERR_PERMISSION_NEEDED"], + additionalInfo: { + runningJobs: 0, + maxJobs: 0, + usingDefaultSetting: false, + pendingRequest: true, + }, + }, + ], + }, + system_id: "de", + overall_job_type: "interactive", + is_public: true, + id: "8ec235d8-f173-11e9-a56f-008cfa5ae621", + edited_date: "2019-10-18T21:10:53.000Z", + step_count: 1, + can_run: true, + integrator_email: "pwrose@ucsd.edu", + app_type: "DE", + rating: { average: 5, total: 1 }, +}; diff --git a/stories/usageSummaryMock.js b/stories/usageSummaryMock.js index a5d163d16..7be955c93 100644 --- a/stories/usageSummaryMock.js +++ b/stories/usageSummaryMock.js @@ -61,6 +61,14 @@ export const usageSummaryComputeLimitExceededResponse = { }, }; +export const usageSummaryStorageLimitExceededResponse = { + ...usageSummaryResponse, + data_usage: { + ...usageSummaryResponse.data_usage, + total: 1261218220900000, + }, +}; + export const usageSummaryDiskUsage50percentResponse = { ...usageSummaryResponse, data_usage: { ...usageSummaryResponse.data_usage, total: 1649267441664 },