This is an automated email from the ASF dual-hosted git repository.
machristie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git
The following commit(s) were added to refs/heads/master by this push:
new 59c861a AIRAVATA-3179 switch to cilent side validation for group comp
prefs
59c861a is described below
commit 59c861a0ba9752a0c2de97fc630bc6fa3253982d
Author: Marcus Christie <[email protected]>
AuthorDate: Fri Nov 1 17:06:18 2019 -0400
AIRAVATA-3179 switch to cilent side validation for group comp prefs
---
.../BatchQueueResourcePolicy.vue | 88 +++++++-
.../ComputePreference.vue | 235 ++++++++++++++++-----
django_airavata/apps/api/serializers.py | 4 +-
.../js/models/BatchQueueResourcePolicy.js | 46 +++-
.../js/models/GroupComputeResourcePreference.js | 62 +++---
5 files changed, 331 insertions(+), 104 deletions(-)
diff --git
a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/BatchQueueResourcePolicy.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/BatchQueueResourcePolicy.vue
index 9cf4164..1a3dc11 100644
---
a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/BatchQueueResourcePolicy.vue
+++
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/BatchQueueResourcePolicy.vue
@@ -1,23 +1,65 @@
<template>
<div class="row">
<div class="col">
- <b-form-group label="Maximum Allowed Nodes"
label-for="max-allowed-nodes">
- <b-form-input id="max-allowed-nodes" type="number"
v-model="data.maxAllowedNodes" @input="policyUpdated" min="1"
:max="batchQueue.maxNodes"
- :formatter="numberFormatter" :placeholder="'Max Nodes: ' +
batchQueue.maxNodes">
+ <b-form-group
+ label="Maximum Allowed Nodes"
+ label-for="max-allowed-nodes"
+ :invalid-feedback="validationFeedback.maxAllowedNodes.invalidFeedback"
+ :state="validationFeedback.maxAllowedNodes.state"
+ >
+ <b-form-input
+ id="max-allowed-nodes"
+ type="number"
+ v-model="data.maxAllowedNodes"
+ @input="policyUpdated"
+ min="1"
+ :max="batchQueue.maxNodes"
+ :formatter="numberFormatter"
+ :placeholder="'Max Nodes: ' + batchQueue.maxNodes"
+ :state="validationFeedback.maxAllowedNodes.state"
+ >
</b-form-input>
</b-form-group>
</div>
<div class="col">
- <b-form-group label="Maximum Allowed Cores"
label-for="max-allowed-cores">
- <b-form-input id="max-allowed-cores" type="number"
v-model="data.maxAllowedCores" @input="policyUpdated" min="1"
:max="batchQueue.maxProcessors"
- :formatter="numberFormatter" :placeholder="'Max Cores: ' +
batchQueue.maxProcessors">
+ <b-form-group
+ label="Maximum Allowed Cores"
+ label-for="max-allowed-cores"
+ :invalid-feedback="validationFeedback.maxAllowedCores.invalidFeedback"
+ :state="validationFeedback.maxAllowedCores.state"
+ >
+ <b-form-input
+ id="max-allowed-cores"
+ type="number"
+ v-model="data.maxAllowedCores"
+ @input="policyUpdated"
+ min="1"
+ :max="batchQueue.maxProcessors"
+ :formatter="numberFormatter"
+ :placeholder="'Max Cores: ' + batchQueue.maxProcessors"
+ :state="validationFeedback.maxAllowedCores.state"
+ >
</b-form-input>
</b-form-group>
</div>
<div class="col">
- <b-form-group label="Maximum Allowed Wall Time"
label-for="max-allowed-walltime">
- <b-form-input id="max-allowed-walltime" type="number"
v-model="data.maxAllowedWalltime" @input="policyUpdated" min="1"
:max="batchQueue.maxRunTime"
- :formatter="numberFormatter" :placeholder="'Max Wall Time: ' +
batchQueue.maxRunTime">
+ <b-form-group
+ label="Maximum Allowed Wall Time"
+ label-for="max-allowed-walltime"
+
:invalid-feedback="validationFeedback.maxAllowedWalltime.invalidFeedback"
+ :state="validationFeedback.maxAllowedWalltime.state"
+ >
+ <b-form-input
+ id="max-allowed-walltime"
+ type="number"
+ v-model="data.maxAllowedWalltime"
+ @input="policyUpdated"
+ min="1"
+ :max="batchQueue.maxRunTime"
+ :formatter="numberFormatter"
+ :placeholder="'Max Wall Time: ' + batchQueue.maxRunTime"
+ :state="validationFeedback.maxAllowedWalltime.state"
+ >
</b-form-input>
</b-form-group>
</div>
@@ -26,6 +68,7 @@
<script>
import { models } from "django-airavata-api";
+import { errors as uiErrors } from "django-airavata-common-ui";
export default {
name: "batch-queue-resource-policy",
@@ -39,6 +82,10 @@ export default {
type: models.BatchQueue
}
},
+ created() {
+ this.$on("input", this.validate);
+ this.validate();
+ },
data: function() {
const localValue = this.value
? this.value.clone()
@@ -62,7 +109,28 @@ export default {
},
numberFormatter: function(value) {
const num = parseInt(value);
- return !isNaN(num) ? num : null;
+ return !isNaN(num) ? "" + num : value;
+ },
+ validate() {
+ if (this.valid) {
+ this.$emit('valid');
+ } else {
+ this.$emit('invalid');
+ }
+ }
+ },
+ computed: {
+ valid() {
+ return Object.keys(this.validation).length === 0;
+ },
+ validation() {
+ return this.data.validate(this.batchQueue);
+ },
+ validationFeedback() {
+ return uiErrors.ValidationErrors.createValidationFeedback(
+ this.data,
+ this.validation
+ );
}
}
};
diff --git
a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputePreference.vue
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputePreference.vue
index cf95963..794a552 100644
---
a/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputePreference.vue
+++
b/django_airavata/apps/admin/static/django_airavata_admin/src/components/admin/group_resource_preferences/ComputePreference.vue
@@ -3,8 +3,14 @@
<div class="row">
<div class="col">
<h1 class="h4 mb-4">
- <div v-if="localGroupResourceProfile"
class="group-resource-profile-name text-muted text-uppercase">
- <i class="fa fa-server" aria-hidden="true"></i> {{
localGroupResourceProfile.groupResourceProfileName }}</div>
+ <div
+ v-if="localGroupResourceProfile"
+ class="group-resource-profile-name text-muted text-uppercase"
+ >
+ <i
+ class="fa fa-server"
+ aria-hidden="true"
+ ></i> {{ localGroupResourceProfile.groupResourceProfileName
}}</div>
{{ computeResource.hostName }}
</h1>
</div>
@@ -13,16 +19,36 @@
<div class="col">
<div class="card">
<div class="card-body">
- <b-form-group label="Login Username" label-for="login-username">
- <b-form-input id="login-username" type="text"
v-model="data.loginUserName">
+ <b-form-group
+ label="Login Username"
+ label-for="login-username"
+
:invalid-feedback="validationFeedback.loginUserName.invalidFeedback"
+ :state="validationFeedback.loginUserName.state"
+ >
+ <b-form-input
+ id="login-username"
+ type="text"
+ required
+ v-model="data.loginUserName"
+ :state="validationFeedback.loginUserName.state"
+ @input="validate"
+ >
</b-form-input>
</b-form-group>
- <b-form-group label="SSH Credential"
label-for="credential-store-token">
- <ssh-credential-selector
v-model="data.resourceSpecificCredentialStoreToken"
+ <b-form-group
+ label="SSH Credential"
+ label-for="credential-store-token"
+ >
+ <ssh-credential-selector
+ v-model="data.resourceSpecificCredentialStoreToken"
v-if="localGroupResourceProfile"
:null-option-default-credential-token="localGroupResourceProfile.defaultCredentialStoreToken"
-
:null-option-disabled="!localGroupResourceProfile.defaultCredentialStoreToken">
- <template slot="null-option-label"
slot-scope="nullOptionLabelScope">
+
:null-option-disabled="!localGroupResourceProfile.defaultCredentialStoreToken"
+ >
+ <template
+ slot="null-option-label"
+ slot-scope="nullOptionLabelScope"
+ >
<span v-if="nullOptionLabelScope.defaultCredentialSummary">
Use the default SSH credential for {{
localGroupResourceProfile.groupResourceProfileName }} ({{
nullOptionLabelScope.defaultCredentialSummary.description
}})
@@ -33,12 +59,31 @@
</template>
</ssh-credential-selector>
</b-form-group>
- <b-form-group label="Allocation Project Number"
label-for="allocation-number">
- <b-form-input id="allocation-number" type="text"
v-model="data.allocationProjectNumber">
+ <b-form-group
+ label="Allocation Project Number"
+ label-for="allocation-number"
+ >
+ <b-form-input
+ id="allocation-number"
+ type="text"
+ v-model="data.allocationProjectNumber"
+ >
</b-form-input>
</b-form-group>
- <b-form-group label="Scratch Location"
label-for="scratch-location"
:invalid-feedback="validationFeedback.scratchLocation.invalidFeedback"
:state="validationFeedback.scratchLocation.state">
- <b-form-input id="scratch-location" type="text" required
v-model="data.scratchLocation"
:state="validationFeedback.scratchLocation.state">
+ <b-form-group
+ label="Scratch Location"
+ label-for="scratch-location"
+
:invalid-feedback="validationFeedback.scratchLocation.invalidFeedback"
+ :state="validationFeedback.scratchLocation.state"
+ >
+ <b-form-input
+ id="scratch-location"
+ type="text"
+ required
+ v-model="data.scratchLocation"
+ :state="validationFeedback.scratchLocation.state"
+ @input="validate"
+ >
</b-form-input>
</b-form-group>
</div>
@@ -50,15 +95,28 @@
<div class="card">
<div class="card-body">
<h5 class="card-title">Policy</h5>
- <b-form-group label="Allowed Queues"
v-if="localComputeResourcePolicy">
- <div v-for="batchQueue in computeResource.batchQueues"
:key="batchQueue.queueName">
- <b-form-checkbox
:checked="localComputeResourcePolicy.allowedBatchQueues.includes(batchQueue.queueName)"
- @input="batchQueueChecked(batchQueue, $event)">
+ <b-form-group
+ label="Allowed Queues"
+ v-if="localComputeResourcePolicy"
+ >
+ <div
+ v-for="batchQueue in computeResource.batchQueues"
+ :key="batchQueue.queueName"
+ >
+ <b-form-checkbox
+
:checked="localComputeResourcePolicy.allowedBatchQueues.includes(batchQueue.queueName)"
+ @input="batchQueueChecked(batchQueue, $event)"
+ >
{{ batchQueue.queueName }}
</b-form-checkbox>
- <batch-queue-resource-policy
v-if="localComputeResourcePolicy.allowedBatchQueues.includes(batchQueue.queueName)"
- :batch-queue="batchQueue"
:value="localBatchQueueResourcePolicies.find(pol => pol.queuename ===
batchQueue.queueName)"
- @input="updatedBatchQueueResourcePolicy(batchQueue, $event)"
/>
+ <batch-queue-resource-policy
+
v-if="localComputeResourcePolicy.allowedBatchQueues.includes(batchQueue.queueName)"
+ :batch-queue="batchQueue"
+ :value="localBatchQueueResourcePolicies.find(pol =>
pol.queuename === batchQueue.queueName)"
+ @input="updatedBatchQueueResourcePolicy(batchQueue, $event)"
+ @valid="recordValidBatchQueueResourcePolicy(batchQueue)"
+ @invalid="recordInvalidBatchQueueResourcePolicy(batchQueue)"
+ />
</div>
</b-form-group>
</div>
@@ -67,9 +125,21 @@
</div>
<div class="row">
<div class="col d-flex justify-content-end">
- <b-button variant="primary" @click="save">Save</b-button>
- <b-button class="ml-2" variant="danger"
@click="remove">Delete</b-button>
- <b-button class="ml-2" variant="secondary"
@click="cancel">Cancel</b-button>
+ <b-button
+ variant="primary"
+ @click="save"
+ :disabled="!valid"
+ >Save</b-button>
+ <b-button
+ class="ml-2"
+ variant="danger"
+ @click="remove"
+ >Delete</b-button>
+ <b-button
+ class="ml-2"
+ variant="secondary"
+ @click="cancel"
+ >Cancel</b-button>
</div>
</div>
</div>
@@ -81,7 +151,11 @@ import BatchQueueResourcePolicy from
"./BatchQueueResourcePolicy.vue";
import SSHCredentialSelector from
"../../credentials/SSHCredentialSelector.vue";
import { models, services, errors } from "django-airavata-api";
-import { mixins, notifications, errors as uiErrors } from
"django-airavata-common-ui";
+import {
+ mixins,
+ notifications,
+ errors as uiErrors
+} from "django-airavata-common-ui";
export default {
name: "compute-preference",
@@ -135,6 +209,7 @@ export default {
} else if (!this.computeResourcePolicy) {
this.createDefaultComputeResourcePolicy(computeResourcePromise);
}
+ this.$on("input", this.validate);
},
data: function() {
return {
@@ -156,15 +231,29 @@ export default {
batchQueues: [],
jobSubmissionInterfaces: []
},
- validationErrors: null
+ validationErrors: null,
+ invalidBatchQueueResourcePolicies: []
};
},
computed: {
-
+ groupComputeResourceValidation() {
+ return this.data.validate();
+ },
validationFeedback() {
return uiErrors.ValidationErrors.createValidationFeedback(
this.data,
- this.validationErrors
+ this.groupComputeResourceValidation
+ );
+ },
+ valid() {
+ return (
+ this.allowedInvalidBatchQueueResourcePolicies.length === 0 &&
+ Object.keys(this.groupComputeResourceValidation).length === 0
+ );
+ },
+ allowedInvalidBatchQueueResourcePolicies() {
+ return this.invalidBatchQueueResourcePolicies.filter(queueName =>
+ this.localComputeResourcePolicy.allowedBatchQueues.includes(queueName)
);
}
},
@@ -234,33 +323,44 @@ export default {
);
return this.saveOrUpdate(groupResourceProfile)
.then(groupResourceProfile => {
- // Navigate back to GroupResourceProfile with success message
- this.$router.push({
- name: "group_resource_preference",
- params: {
- value: groupResourceProfile,
- id: groupResourceProfile.groupResourceProfileId
- }
- });
+ // Navigate back to GroupResourceProfile with success message
+ this.$router.push({
+ name: "group_resource_preference",
+ params: {
+ value: groupResourceProfile,
+ id: groupResourceProfile.groupResourceProfileId
+ }
+ });
})
.catch(error => {
-
- if (errors.ErrorUtils.isValidationError(error) &&
'computePreferences' in error.details.response) {
- const computePreferencesIndex =
groupResourceProfile.computePreferences.findIndex(cp => cp.computeResourceId
=== this.host_id);
- this.validationErrors =
error.details.response.computePreferences[computePreferencesIndex];
+ if (
+ errors.ErrorUtils.isValidationError(error) &&
+ "computePreferences" in error.details.response
+ ) {
+ const computePreferencesIndex =
groupResourceProfile.computePreferences.findIndex(
+ cp => cp.computeResourceId === this.host_id
+ );
+ this.validationErrors =
+ error.details.response.computePreferences[
+ computePreferencesIndex
+ ];
} else {
this.validationErrors = null;
notifications.NotificationList.addError(error);
}
- })
+ });
},
saveOrUpdate(groupResourceProfile) {
if (this.id) {
- return DjangoAiravataAPI.services.GroupResourceProfileService
- .update({ data: groupResourceProfile, lookup: this.id }, {
ignoreErrors: true })
+ return DjangoAiravataAPI.services.GroupResourceProfileService.update(
+ { data: groupResourceProfile, lookup: this.id },
+ { ignoreErrors: true }
+ );
} else {
- return DjangoAiravataAPI.services.GroupResourceProfileService
- .create({ data: groupResourceProfile }, { ignoreErrors: true })
+ return DjangoAiravataAPI.services.GroupResourceProfileService.create(
+ { data: groupResourceProfile },
+ { ignoreErrors: true }
+ );
}
},
remove: function() {
@@ -269,18 +369,19 @@ export default {
this.host_id
);
if (removedChildren) {
- DjangoAiravataAPI.services.GroupResourceProfileService
- .update({ data: groupResourceProfile, lookup: this.id })
- .then(groupResourceProfile => {
- // Navigate back to GroupResourceProfile with success message
- this.$router.push({
- name: "group_resource_preference",
- params: {
- value: groupResourceProfile,
- id: this.id
- }
- });
+ DjangoAiravataAPI.services.GroupResourceProfileService.update({
+ data: groupResourceProfile,
+ lookup: this.id
+ }).then(groupResourceProfile => {
+ // Navigate back to GroupResourceProfile with success message
+ this.$router.push({
+ name: "group_resource_preference",
+ params: {
+ value: groupResourceProfile,
+ id: this.id
+ }
});
+ });
} else {
// Since nothing was removed, just handle this like a cancel
this.cancel();
@@ -309,6 +410,32 @@ export default {
);
this.localComputeResourcePolicy = defaultComputeResourcePolicy;
});
+ },
+ recordValidBatchQueueResourcePolicy(batchQueue) {
+ if (
+ this.invalidBatchQueueResourcePolicies.includes(batchQueue.queueName)
+ ) {
+ const index = this.invalidBatchQueueResourcePolicies.indexOf(
+ batchQueue.queueName
+ );
+ this.invalidBatchQueueResourcePolicies.splice(index, 1);
+ }
+ this.validate(); // propagate validation
+ },
+ recordInvalidBatchQueueResourcePolicy(batchQueue) {
+ if (
+ !this.invalidBatchQueueResourcePolicies.includes(batchQueue.queueName)
+ ) {
+ this.invalidBatchQueueResourcePolicies.push(batchQueue.queueName);
+ }
+ this.validate(); // propagate validation
+ },
+ validate() {
+ if (this.valid) {
+ this.$emit("valid");
+ } else {
+ this.$emit("invalid");
+ }
}
},
beforeRouteEnter: function(to, from, next) {
diff --git a/django_airavata/apps/api/serializers.py
b/django_airavata/apps/api/serializers.py
index 5929bb0..58b1470 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -569,9 +569,7 @@ class UserProfileSerializer(
class GroupComputeResourcePreferenceSerializer(
thrift_utils.create_serializer_class(GroupComputeResourcePreference)):
-
- class Meta:
- required = ('scratchLocation',)
+ pass
class GroupResourceProfileSerializer(
diff --git
a/django_airavata/apps/api/static/django_airavata_api/js/models/BatchQueueResourcePolicy.js
b/django_airavata/apps/api/static/django_airavata_api/js/models/BatchQueueResourcePolicy.js
index 9f4b0d2..5f95665 100644
---
a/django_airavata/apps/api/static/django_airavata_api/js/models/BatchQueueResourcePolicy.js
+++
b/django_airavata/apps/api/static/django_airavata_api/js/models/BatchQueueResourcePolicy.js
@@ -1,19 +1,43 @@
-import BaseModel from './BaseModel'
-
+import BaseModel from "./BaseModel";
const FIELDS = [
- 'resourcePolicyId',
- 'computeResourceId',
- 'groupResourceProfileId',
- 'queuename',
- 'maxAllowedNodes',
- 'maxAllowedCores',
- 'maxAllowedWalltime',
+ "resourcePolicyId",
+ "computeResourceId",
+ "groupResourceProfileId",
+ "queuename",
+ "maxAllowedNodes",
+ "maxAllowedCores",
+ "maxAllowedWalltime"
];
export default class BatchQueueResourcePolicy extends BaseModel {
+ constructor(data = {}) {
+ super(FIELDS, data);
+ }
- constructor(data = {}) {
- super(FIELDS, data);
+ validate(batchQueue) {
+ let validationResults = {};
+ if (this.maxAllowedNodes && this.maxAllowedNodes < 1) {
+ validationResults["maxAllowedNodes"] = "Must be at least 1.";
+ } else if (this.maxAllowedNodes > batchQueue.maxNodes) {
+ validationResults[
+ "maxAllowedNodes"
+ ] = `Must be at most ${batchQueue.maxNodes}.`;
+ }
+ if (this.maxAllowedCores && this.maxAllowedCores < 1) {
+ validationResults["maxAllowedCores"] = "Must be at least 1.";
+ } else if (this.maxAllowedCores > batchQueue.maxProcessors) {
+ validationResults[
+ "maxAllowedCores"
+ ] = `Must be at most ${batchQueue.maxProcessors}.`;
+ }
+ if (this.maxAllowedWalltime && this.maxAllowedWalltime < 1) {
+ validationResults["maxAllowedWalltime"] = "Must be at least 1.";
+ } else if (this.maxAllowedWalltime > batchQueue.maxRunTime) {
+ validationResults[
+ "maxAllowedWalltime"
+ ] = `Must be at most ${batchQueue.maxRunTime}.`;
}
+ return validationResults;
+ }
}
diff --git
a/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
b/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
index 1dacbc5..facaa85 100644
---
a/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
+++
b/django_airavata/apps/api/static/django_airavata_api/js/models/GroupComputeResourcePreference.js
@@ -1,34 +1,44 @@
-import BaseModel from './BaseModel'
-
+import BaseModel from "./BaseModel";
const FIELDS = [
- 'computeResourceId',
- 'groupResourceProfileId',
- {
- name: 'overridebyAiravata',
- type: 'boolean',
- default: true,
- },
- 'loginUserName',
- 'preferredJobSubmissionProtocol',
- 'preferredDataMovementProtocol',
- 'preferredBatchQueue',
- 'scratchLocation',
- 'allocationProjectNumber',
- 'resourceSpecificCredentialStoreToken',
- 'usageReportingGatewayId',
- 'qualityOfService',
- 'reservation',
- 'reservationStartTime',
- 'reservationEndTime',
- 'sshAccountProvisioner',
- 'groupSSHAccountProvisionerConfigs',
- 'sshAccountProvisionerAdditionalInfo',
+ "computeResourceId",
+ "groupResourceProfileId",
+ {
+ name: "overridebyAiravata",
+ type: "boolean",
+ default: true
+ },
+ "loginUserName",
+ "preferredJobSubmissionProtocol",
+ "preferredDataMovementProtocol",
+ "preferredBatchQueue",
+ "scratchLocation",
+ "allocationProjectNumber",
+ "resourceSpecificCredentialStoreToken",
+ "usageReportingGatewayId",
+ "qualityOfService",
+ "reservation",
+ "reservationStartTime",
+ "reservationEndTime",
+ "sshAccountProvisioner",
+ "groupSSHAccountProvisionerConfigs",
+ "sshAccountProvisionerAdditionalInfo"
];
export default class GroupComputeResourcePreference extends BaseModel {
+ constructor(data = {}) {
+ super(FIELDS, data);
+ }
- constructor(data = {}) {
- super(FIELDS, data);
+ validate() {
+ let validationResults = {};
+ if (this.isEmpty(this.loginUserName)) {
+ validationResults["loginUserName"] =
+ "Please provide a login username.";
+ }
+ if (this.isEmpty(this.scratchLocation)) {
+ validationResults["scratchLocation"] = "Please provide a scratch
location.";
}
+ return validationResults;
+ }
}