diff --git a/client/src/components/_common/Button/Button.jsx b/client/src/components/_common/Button/Button.jsx index 1922cc97b..da102f18a 100644 --- a/client/src/components/_common/Button/Button.jsx +++ b/client/src/components/_common/Button/Button.jsx @@ -4,6 +4,7 @@ import Icon from '../Icon'; import styles from './Button.module.css'; import LoadingSpinner from '_common/LoadingSpinner'; +import emptyStringValidator from '_common/CommonUtils'; export const TYPE_MAP = { primary: 'primary', @@ -25,15 +26,6 @@ export const SIZES = [''].concat(Object.keys(SIZE_MAP)); export const ATTRIBUTES = ['button', 'submit', 'reset']; -function isNotEmptyString(props, propName, componentName) { - if (!props[propName] || props[propName].replace(/ /g, '') === '') { - return new Error( - `No text passed to <${componentName}> prop "${propName}". Validation failed.` - ); - } - return null; -} - const Button = ({ children, className, @@ -125,7 +117,7 @@ const Button = ({ ); }; Button.propTypes = { - children: isNotEmptyString, + children: emptyStringValidator, className: PropTypes.string, iconNameBefore: PropTypes.string, iconNameAfter: PropTypes.string, diff --git a/client/src/components/_common/CommonUtils.jsx b/client/src/components/_common/CommonUtils.jsx new file mode 100644 index 000000000..7c91a8096 --- /dev/null +++ b/client/src/components/_common/CommonUtils.jsx @@ -0,0 +1,19 @@ +/** + * Checks that the field is a non-empty string + * @param {Object} props - + * @param {String} propName - name of the property + * @param {String} componentName - name of the component + * @returns {String} Message if error, otherwise null + */ + +export default function emptyStringValidator(props, propName, componentName) { + if ( + !props[propName] || + typeof props[propName] !== 'string' || + props[propName].replace(/ /g, '') === '' + ) { + return new Error( + `No text passed to <${componentName}> prop "${propName}". Validation failed.` + ); + } +} diff --git a/client/src/components/_common/Sidebar/Sidebar.jsx b/client/src/components/_common/Sidebar/Sidebar.jsx index 9568f672a..06e133034 100644 --- a/client/src/components/_common/Sidebar/Sidebar.jsx +++ b/client/src/components/_common/Sidebar/Sidebar.jsx @@ -4,13 +4,7 @@ import { NavLink as RRNavLink } from 'react-router-dom'; import { Nav, NavItem, NavLink } from 'reactstrap'; import Icon from '_common/Icon'; import styles from './Sidebar.module.css'; - -function isNotEmptyString(props, propName, componentName) { - if (!props[propName] || props[propName].replace(/ /g, '') === '') { - return new Error(`No text passed to ${componentName}. Validation failed.`); - } - return null; -} +import emptyStringValidator from '_common/CommonUtils'; const SidebarItem = ({ to, iconName, label, children, disabled, hidden }) => { return ( @@ -38,7 +32,7 @@ const SidebarItem = ({ to, iconName, label, children, disabled, hidden }) => { SidebarItem.propTypes = { to: PropTypes.string.isRequired, iconName: PropTypes.string.isRequired, - label: isNotEmptyString, + label: emptyStringValidator, children: PropTypes.node, disabled: PropTypes.bool, hidden: PropTypes.bool, diff --git a/server/poetry.lock b/server/poetry.lock index 057d4570f..5f746b1be 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aioredis" @@ -291,13 +291,13 @@ zstd = ["zstandard"] [[package]] name = "certifi" -version = "2022.12.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] @@ -987,6 +987,7 @@ files = [ {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, @@ -995,6 +996,7 @@ files = [ {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, @@ -1024,6 +1026,7 @@ files = [ {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, @@ -1032,6 +1035,7 @@ files = [ {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, @@ -2650,12 +2654,12 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uwsgi" -version = "2.0.21" +version = "2.0.22" description = "The uWSGI server" optional = false python-versions = "*" files = [ - {file = "uwsgi-2.0.21.tar.gz", hash = "sha256:35a30d83791329429bc04fe44183ce4ab512fcf6968070a7bfba42fc5a0552a9"}, + {file = "uwsgi-2.0.22.tar.gz", hash = "sha256:4cc4727258671ac5fa17ab422155e9aaef8a2008ebb86e4404b66deaae965db2"}, ] [[package]] @@ -2781,4 +2785,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "a234d7a6b05dbb19cbae45d1ca2f01436e3ac3806d64ab2471e97468d00e2627" +content-hash = "c40d55ad3a597c47d889feb6d959fa7f75622025eeb7670fffe10dfd01e9b559" diff --git a/server/portal/apps/webhooks/views.py b/server/portal/apps/webhooks/views.py index 702045b27..54544b23d 100644 --- a/server/portal/apps/webhooks/views.py +++ b/server/portal/apps/webhooks/views.py @@ -20,6 +20,7 @@ validate_webhook, execute_callback ) +from portal.apps.workspace.api.utils import check_job_for_timeout from django.conf import settings @@ -54,6 +55,8 @@ def validate_tapis_job(job_uuid, job_owner, disallowed_states=[]): if job_data.status in disallowed_states: return None + job_data = check_job_for_timeout(job_data) + return job_data @@ -122,6 +125,7 @@ def post(self, request, *args, **kwargs): job_details = validate_tapis_job(job_uuid, username, disallowed_states=non_terminal_states) if job_details: event_data[Notification.EXTRA]['remoteOutcome'] = job_details.remoteOutcome + event_data[Notification.EXTRA]['status'] = job_details.status try: logger.info('Indexing job output for job={}'.format(job_uuid)) diff --git a/server/portal/apps/workspace/api/utils.py b/server/portal/apps/workspace/api/utils.py new file mode 100644 index 000000000..9d9923dfa --- /dev/null +++ b/server/portal/apps/workspace/api/utils.py @@ -0,0 +1,28 @@ +import json + + +def get_tapis_timeout_error_messages(job_id): + return [ + 'JOBS_EARLY_TERMINATION Job terminated by Tapis because: TIME_EXPIRED', + f'JOBS_USER_APP_FAILURE The user application ({job_id}) ended with remote status "TIMEOUT" and returned exit code: 0:0.' + ] + + +def check_job_for_timeout(job): + """ + Check an interactive job for timeout status and mark it as finished + since Tapis does not have native support for interactive jobs yet + """ + + if (hasattr(job, 'notes')): + notes = json.loads(job.notes) + + is_failed = job.status == 'FAILED' + is_interactive = notes.get('isInteractive', False) + has_timeout_message = job.lastMessage in get_tapis_timeout_error_messages(job.remoteJobId) + + if is_failed and is_interactive and has_timeout_message: + job.status = 'FINISHED' + job.remoteOutcome = 'FINISHED' + + return job diff --git a/server/portal/apps/workspace/api/views.py b/server/portal/apps/workspace/api/views.py index b79716428..9a94db829 100644 --- a/server/portal/apps/workspace/api/views.py +++ b/server/portal/apps/workspace/api/views.py @@ -23,6 +23,7 @@ from portal.apps.onboarding.steps.system_access_v3 import create_system_credentials from portal.apps.users.utils import get_user_data from .handlers.tapis_handlers import tapis_get_handler +from portal.apps.workspace.api.utils import check_job_for_timeout logger = logging.getLogger(__name__) METRICS = logging.getLogger('metrics.{}'.format(__name__)) @@ -138,6 +139,7 @@ def get(self, request, *args, **kwargs): @method_decorator(login_required, name='dispatch') class JobsView(BaseApiView): + def get(self, request, operation=None): allowed_actions = ['listing', 'search', 'select'] @@ -150,6 +152,12 @@ def get(self, request, operation=None): op = getattr(self, operation) data = op(tapis, request) + if (isinstance(data, list)): + for index, job in enumerate(data): + data[index] = check_job_for_timeout(job) + else: + data = check_job_for_timeout(data) + return JsonResponse( { 'status': 200, diff --git a/server/pyproject.toml b/server/pyproject.toml index 2f816f6a1..798b9a066 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -23,7 +23,7 @@ cached-property = "^1.5.1" ipython = "^8.13.2" pycryptodome = "^3.9.7" elasticsearch = "^7.7.1" -uwsgi = "^2.0.18" +uwsgi = "^2.0.22" requests = "^2.31.0" django-impersonate = "^1.5" channels = "^2.4.0"