Skip to content

Commit

Permalink
Merge branch 'task/WP-65-DropdownViewFullPath' of github.com:TACC/Cor…
Browse files Browse the repository at this point in the history
…e-Portal into task/WP-65-DropdownViewFullPath
  • Loading branch information
Taylor Grafft authored and Taylor Grafft committed Oct 3, 2023
2 parents c531d37 + 3c0ca4a commit f9605ac
Show file tree
Hide file tree
Showing 15 changed files with 294 additions and 91 deletions.
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [3.2.0] - 2023-10-02: V3 integration improvements; bug fixes

### Added

- WP-211: App Form updates to allow target path (#857)
- WP-272: Include username in Onboarding Admin user listing (#861)

### Changed

- WP-189 Handle timeout exit code for interactive app jobs (#851)
- WP-163 Compress Archive Path Fix (#846)
- WP-105: create common utils function (#850)
- WP-172: Minimize unit test warnings (#855)
- WP-62: Changed upload function to use TAPIS file insert api (#859)

### Fixed
- WP-249 Shared Workspace Copy Bug Fix (#858)
- WP-262 Workspace file operations bug fixes (#862)
- WP-276: Fixed Data Files Add button dropdown off-centered UI (#863)
- Quick: handle missing default system; enable work as default system locally (#867)
- WP-52 Jobs View Infinite Scroll Fix (#865)
- WP-228: Fixed sorting for system list (#860)
- WP-209: fix deprecated warnings (part II) (#852)


## [3.1.2] - 2023-08-22: Secure user search endpoint

### Fixed
Expand Down Expand Up @@ -929,6 +954,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
v1.0.0 Production release as of Feb 28, 2020.

[unreleased]: https://github.com/TACC/Core-Portal/compare/v3.1.2...HEAD
[3.2.0]: https://github.com/TACC/Core-Portal/releases/tag/v3.2.0
[3.1.2]: https://github.com/TACC/Core-Portal/releases/tag/v3.1.2
[3.1.1]: https://github.com/TACC/Core-Portal/releases/tag/v3.1.1
[3.1.0]: https://github.com/TACC/Core-Portal/releases/tag/v3.1.0
Expand Down
71 changes: 57 additions & 14 deletions client/src/components/Applications/AppForm/AppForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ import { Link } from 'react-router-dom';
import { getSystemName } from 'utils/systems';
import FormSchema from './AppFormSchema';
import {
checkAndSetDefaultTargetPath,
isTargetPathField,
getInputFieldFromTargetPathField,
getQueueMaxMinutes,
getMaxMinutesValidation,
getNodeCountValidation,
getCoresPerNodeValidation,
getTargetPathFieldName,
updateValuesForQueue,
} from './AppFormUtils';
import DataFilesSelectModal from '../../DataFiles/DataFilesModals/DataFilesSelectModal';
Expand Down Expand Up @@ -201,7 +205,7 @@ export const AppSchemaForm = ({ app }) => {
const hasCorral =
configuration.length &&
['corral.tacc.utexas.edu', 'data.tacc.utexas.edu'].some((s) =>
defaultHost.endsWith(s)
defaultHost?.endsWith(s)
);
return {
allocations: matchingExecutionHost
Expand Down Expand Up @@ -459,17 +463,49 @@ export const AppSchemaForm = ({ app }) => {
onSubmit={(values, { setSubmitting, resetForm }) => {
const job = cloneDeep(values);

job.fileInputs = Object.entries(job.fileInputs)
.map(([k, v]) => {
// filter out read only inputs. 'FIXED' inputs are tracked as readOnly
if (
Object.hasOwn(appFields.fileInputs, k) &&
appFields.fileInputs[k].readOnly
)
return;
return { name: k, sourceUrl: v };
})
.filter((fileInput) => fileInput && fileInput.sourceUrl); // filter out any empty values
// Transform input field values into format that jobs service wants.
// File Input structure will have 2 fields if target path is required by the app.
// field 1 - has source url
// field 2 - has target path for the source url.
// tapis wants only 1 field with 2 properties - source url and target path.
// The logic below handles that scenario by merging the related fields into 1 field.
job.fileInputs = Object.values(
Object.entries(job.fileInputs)
.map(([k, v]) => {
// filter out read only inputs. 'FIXED' inputs are tracked as readOnly
if (
Object.hasOwn(appFields.fileInputs, k) &&
appFields.fileInputs[k].readOnly
)
return;
return {
name: k,
sourceUrl: !isTargetPathField(k) ? v : null,
targetDir: isTargetPathField(k) ? v : null,
};
})
.reduce((acc, entry) => {
// merge input field and targetPath fields into one.
const key = getInputFieldFromTargetPathField(entry.name);
if (!acc[key]) {
acc[key] = {};
}
acc[key]['name'] = key;
acc[key]['sourceUrl'] =
acc[key]['sourceUrl'] ?? entry.sourceUrl;
acc[key]['targetPath'] =
acc[key]['targetPath'] ?? entry.targetDir;
return acc;
}, {})
)
.flat()
.filter((fileInput) => fileInput.sourceUrl) // filter out any empty values
.map((fileInput) => {
fileInput.targetPath = checkAndSetDefaultTargetPath(
fileInput.targetPath
);
return fileInput;
});

job.parameterSet = Object.assign(
{},
Expand Down Expand Up @@ -559,8 +595,15 @@ export const AppSchemaForm = ({ app }) => {
</div>
{Object.entries(appFields.fileInputs).map(
([name, field]) => {
// TODOv3 handle fileInputArrays https://jira.tacc.utexas.edu/browse/TV3-81
return (
// TODOv3 handle fileInputArrays https://jira.tacc.utexas.edu/browse/WP-81
return isTargetPathField(name) ? (
<FormField
{...field}
name={`fileInputs.${name}`}
placeholder="Target Path Name"
key={`fileInputs.${name}`}
/>
) : (
<FormField
{...field}
name={`fileInputs.${name}`}
Expand Down
34 changes: 34 additions & 0 deletions client/src/components/Applications/AppForm/AppFormSchema.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import * as Yup from 'yup';
import {
checkAndSetDefaultTargetPath,
getTargetPathFieldName,
} from './AppFormUtils';

const FormSchema = (app) => {
const appFields = {
Expand Down Expand Up @@ -97,6 +101,9 @@ const FormSchema = (app) => {
}
);

// The default is to not show target path for file inputs.
const showTargetPathForFileInputs =
app.definition.notes.showTargetPath ?? false;
(app.definition.jobAttributes.fileInputs || []).forEach((i) => {
const input = i;
/* TODOv3 consider hidden file inputs https://jira.tacc.utexas.edu/browse/WP-102
Expand Down Expand Up @@ -131,6 +138,33 @@ const FormSchema = (app) => {
input.sourceUrl === null || typeof input.sourceUrl === 'undefined'
? ''
: input.sourceUrl;

// Add targetDir for all sourceUrl
if (!showTargetPathForFileInputs) {
return;
}
const targetPathName = getTargetPathFieldName(input.name);
appFields.schema.fileInputs[targetPathName] = Yup.string();
appFields.schema.fileInputs[targetPathName] = appFields.schema.fileInputs[
targetPathName
].matches(
/^tapis:\/\//g,
"Input file Target Directory must be a valid Tapis URI, starting with 'tapis://'"
);

appFields.schema.fileInputs[targetPathName] = false;
appFields.fileInputs[targetPathName] = {
label: 'Target Path for ' + input.name,
description:
'The name of the ' +
input.name +
' after it is copied to the target system, but before the job is run. Leave this value blank to just use the name of the input file.',
required: false,
readOnly: field.readOnly,
type: 'text',
};
appFields.defaults.fileInputs[targetPathName] =
checkAndSetDefaultTargetPath(input.targetPath);
});
return appFields;
};
Expand Down
56 changes: 56 additions & 0 deletions client/src/components/Applications/AppForm/AppFormUtils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as Yup from 'yup';
import { getSystemName } from 'utils/systems';

export const TARGET_PATH_FIELD_PREFIX = '_TargetPath_';

export const getQueueMaxMinutes = (app, queueName) => {
return app.exec_sys.batchLogicalQueues.find((q) => q.name === queueName)
.maxMinutes;
Expand Down Expand Up @@ -165,3 +167,57 @@ export const updateValuesForQueue = (app, values) => {

return updatedValues;
};

/**
* Get the field name used for target path in AppForm
*
* @function
* @param {String} inputFieldName
* @returns {String} field Name prefixed with target path
*/
export const getTargetPathFieldName = (inputFieldName) => {
return TARGET_PATH_FIELD_PREFIX + inputFieldName;
};

/**
* Whether a field name is a system defined field for Target Path
*
* @function
* @param {String} inputFieldName
* @returns {String} field Name suffixed with target path
*/
export const isTargetPathField = (inputFieldName) => {
return inputFieldName && inputFieldName.startsWith(TARGET_PATH_FIELD_PREFIX);
};

/**
* From target path field name, derive the original input field name.
*
* @function
* @param {String} targetPathFieldName
* @returns {String} actual field name
*/
export const getInputFieldFromTargetPathField = (targetPathFieldName) => {
return targetPathFieldName.replace(TARGET_PATH_FIELD_PREFIX, '');
};

/**
* Sets the default value if target path is not set.
*
* @function
* @param {String} targetPathFieldValue
* @returns {String} target path value
*/
export const checkAndSetDefaultTargetPath = (targetPathFieldValue) => {
if (targetPathFieldValue === null || targetPathFieldValue === undefined) {
return '*';
}

targetPathFieldValue = targetPathFieldValue.trim();

if (targetPathFieldValue.trim() === '') {
return '*';
}

return targetPathFieldValue;
};
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@ const DataFilesMoveModal = React.memo(() => {

const onOpened = () => {
fetchListing({
api: 'tapis',
scheme: 'private',
system: selectedSystem.system,
path: `${selectedSystem.homeDir || ''}`,
...params,
});
};

Expand All @@ -68,7 +65,7 @@ const DataFilesMoveModal = React.memo(() => {
setDisabled(true);
move({
destSystem: system,
destPath: path,
destPath: path || '/',
callback: reloadPage,
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
padding-left: var(--horizontal-buffer);
}
#data-files-add {
width: 140px;
width: 174px;
}

.data-files-nav {
Expand All @@ -36,6 +36,7 @@
border-right: 10px solid transparent;
border-bottom: 10px solid var(--global-color-accent--normal);
border-left: 10px solid transparent;
margin-left: 20px;
content: '';
}
.dropdown-menu::after {
Expand All @@ -45,6 +46,7 @@
border-right: 9px solid transparent;
border-bottom: 9px solid #ffffff;
border-left: 9px solid transparent;
margin-left: 20px;
content: '';
}

Expand Down
43 changes: 22 additions & 21 deletions client/src/components/Jobs/Jobs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,14 @@ function JobsView({
const infiniteScrollCallback = useCallback(() => {
// TODOv3: dropV2Jobs
const dispatchType = version === 'v3' ? 'GET_JOBS' : 'GET_V2_JOBS';
dispatch({
type: dispatchType,
params: { offset: jobs.length, queryString: query.query_string || '' },
});
}, [dispatch, jobs, query.query_string]);

if (!isJobLoading) {
dispatch({
type: dispatchType,
params: { offset: jobs.length, queryString: query.query_string || '' },
});
}
}, [dispatch, jobs, query.query_string, isJobLoading]);

const jobDetailLink = useCallback(
({
Expand Down Expand Up @@ -217,22 +220,20 @@ function JobsView({
disabled={isJobLoading || isNotificationLoading}
/>
)}
<div className={includeSearchbar ? 'o-flex-item-table-wrap' : ''}>
<InfiniteScrollTable
tableColumns={filterColumns}
tableData={jobs}
onInfiniteScroll={infiniteScrollCallback}
isLoading={isJobLoading || isNotificationLoading}
className={showDetails ? 'jobs-detailed-view' : 'jobs-view'}
noDataText={
<Section className={'no-results-message'}>
<SectionMessage type="info">{noDataText}</SectionMessage>
</Section>
}
getRowProps={rowProps}
columnMemoProps={[version]} /* TODOv3: dropV2Jobs. */
/>
</div>
<InfiniteScrollTable
tableColumns={filterColumns}
tableData={jobs}
onInfiniteScroll={infiniteScrollCallback}
isLoading={isJobLoading || isNotificationLoading}
className={showDetails ? 'jobs-detailed-view' : 'jobs-view'}
noDataText={
<Section className={'no-results-message'}>
<SectionMessage type="info">{noDataText}</SectionMessage>
</Section>
}
getRowProps={rowProps}
columnMemoProps={[version]} /* TODOv3: dropV2Jobs. */
/>
</>
);
}
Expand Down
Loading

0 comments on commit f9605ac

Please sign in to comment.