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
commit 0ae6ff1078a2e8870e2d450ae5d32729064cd88b Author: Marcus Christie <[email protected]> AuthorDate: Tue Sep 11 13:11:36 2018 -0400 AIRAVATA-2876 App deployment editor: commands, env vars, defaults --- .../applications/ApplicationDeploymentEditor.vue | 108 ++++++++++++----- .../applications/CommandObjectsEditor.vue | 57 +++++++++ .../components/applications/SetEnvPathsEditor.vue | 55 +++++++++ .../src/components/commons/vmodel_mixin.js | 14 ++- .../admin/static/django_airavata_admin/src/main.js | 4 +- django_airavata/apps/api/serializers.py | 133 ++++++++------------- .../api/static/django_airavata_api/js/index.js | 4 +- .../js/models/ApplicationDeploymentDescription.js | 8 +- .../js/models/ComputeResourceDescription.js | 2 +- .../js/models/{SetEnvPath.js => SetEnvPaths.js} | 2 +- 10 files changed, 264 insertions(+), 123 deletions(-) diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue index 9bbed4b..015f7f5 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue @@ -14,18 +14,33 @@ <b-form-group label="Application Deployment Description" label-for="deployment-description"> <b-form-textarea id="deployment-description" v-model="data.appDeploymentDescription" :rows="3"></b-form-textarea> </b-form-group> - <b-card title="Module Load Commands"> - <b-input-group v-for="moduleLoadCmd in data.moduleLoadCmds" :key="moduleLoadCmd.key" class="mb-1"> - <b-form-input type="text" v-model="moduleLoadCmd.command" required ref="moduleLoadCmdInputs" /> - <b-input-group-append> - <b-button variant="secondary" @click="deleteModuleLoadCmd(moduleLoadCmd)"> - <i class="fa fa-trash"></i> - <span class="sr-only">Delete</span> - </b-button> - </b-input-group-append> - </b-input-group> - <b-button variant="secondary" @click="addModuleLoadCmd">Add Module Load Command</b-button> - </b-card> + <command-objects-editor title="Module Load Commands" add-button-label="Add Module Load Command" v-model="data.moduleLoadCmds" + /> + <set-env-paths-editor title="Library Prepend Paths" add-button-label="Add a Library Prepend Path" v-model="data.libPrependPaths" + /> + <set-env-paths-editor title="Library Append Paths" add-button-label="Add a Library Append Path" v-model="data.libAppendPaths" + /> + <set-env-paths-editor title="Environment Variables" add-button-label="Add Environment Variable" v-model="data.setEnvironment" + /> + <command-objects-editor title="Pre Job Commands" add-button-label="Add Pre Job Command" v-model="data.preJobCommands" /> + <command-objects-editor title="Post Job Commands" add-button-label="Add Post Job Command" v-model="data.postJobCommands" + /> + <b-form-group label="Default Queue Name" label-for="default-queue-name"> + <b-form-select id="default-queue-name" v-model="data.defaultQueueName" :options="queueNameOptions" @change="defaultQueueChanged"> + <template slot="first"> + <option :value="null">Select a Default Queue</option> + </template> + </b-form-select> + </b-form-group> + <b-form-group label="Default Node Count" label-for="default-node-count"> + <b-form-input id="default-node-count" type="number" v-model="data.defaultNodeCount" min="0" :max="maxNodes" :disabled="defaultQueueAttributesDisabled"></b-form-input> + </b-form-group> + <b-form-group label="Default CPU Count" label-for="default-cpu-count"> + <b-form-input id="default-cpu-count" type="number" v-model="data.defaultCPUCount" min="0" :max="maxCPUCount" :disabled="defaultQueueAttributesDisabled"></b-form-input> + </b-form-group> + <b-form-group label="Default Walltime" label-for="default-walltime"> + <b-form-input id="default-walltime" type="number" v-model="data.defaultWalltime" min="0" :max="maxWalltime" :disabled="defaultQueueAttributesDisabled"></b-form-input> + </b-form-group> </div> </div> <div class="row mb-4"> @@ -44,6 +59,8 @@ <script> import { models, services } from "django-airavata-api"; import vmodel_mixin from "../commons/vmodel_mixin"; +import CommandObjectsEditor from "./CommandObjectsEditor.vue"; +import SetEnvPathsEditor from "./SetEnvPathsEditor.vue"; export default { name: "application-deployment-editor", @@ -57,6 +74,10 @@ export default { required: true } }, + components: { + CommandObjectsEditor, + SetEnvPathsEditor + }, data() { return { computeResource: null @@ -77,12 +98,52 @@ export default { text: parType.name }; }); + }, + queueNameOptions() { + if (!this.computeResource) { + return []; + } + return this.computeResource.batchQueues.map(queue => { + return { + value: queue.queueName, + text: queue.queueName + }; + }); + }, + maxNodes() { + const queue = this.computeResource + ? this.computeResource.batchQueues.find( + q => q.queueName === this.data.defaultQueueName + ) + : null; + return queue ? queue.maxNodes : 0; + }, + maxCPUCount() { + const queue = this.computeResource + ? this.computeResource.batchQueues.find( + q => q.queueName === this.data.defaultQueueName + ) + : null; + return queue ? queue.maxProcessors : 0; + }, + maxWalltime() { + const queue = this.computeResource + ? this.computeResource.batchQueues.find( + q => q.queueName === this.data.defaultQueueName + ) + : null; + return queue ? queue.maxRuntime : 0; + }, + defaultQueueAttributesDisabled() { + return !this.data.defaultQueueName; } }, created() { services.ComputeResourceService.retrieve({ lookup: this.data.computeHostId - }).then(computeResource => (this.computeResource = computeResource)); + }).then(computeResource => { + this.computeResource = computeResource; + }); }, methods: { save() { @@ -91,22 +152,13 @@ export default { cancel() { this.$emit("cancel"); }, - addModuleLoadCmd() { - if (!this.data.moduleLoadCmds) { - this.data.moduleLoadCmds = []; - } - this.data.moduleLoadCmds.push(new models.CommandObject()); - this.$nextTick(() => - this.$refs.moduleLoadCmdInputs[ - this.$refs.moduleLoadCmdInputs.length - 1 - ].focus() - ); - }, - deleteModuleLoadCmd(moduleLoadCmd) { - const index = this.data.moduleLoadCmds.findIndex( - cmd => cmd.key === moduleLoadCmd.key + defaultQueueChanged(queueName) { + const queue = this.computeResource.batchQueues.find( + q => q.queueName === queueName ); - this.data.moduleLoadCmds.splice(index, 1); + this.data.defaultNodeCount = queue.defaultNodeCount; + this.data.defaultCPUCount = queue.defaultCPUCount; + this.data.defaultWalltime = queue.defaultWalltime; } } }; diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/CommandObjectsEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/CommandObjectsEditor.vue new file mode 100644 index 0000000..043ac46 --- /dev/null +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/CommandObjectsEditor.vue @@ -0,0 +1,57 @@ +<template> + <b-card :title="title" title-tag="h5"> + <b-input-group v-for="commandObject in data" :key="commandObject.key" class="mb-1"> + <b-form-input type="text" v-model="commandObject.command" required ref="commandObjectInputs" /> + <b-input-group-append> + <b-button variant="secondary" @click="deleteCommandObject(commandObject)"> + <i class="fa fa-trash"></i> + <span class="sr-only">Delete</span> + </b-button> + </b-input-group-append> + </b-input-group> + <b-button variant="secondary" @click="addCommandObject">{{ addButtonLabel }}</b-button> + </b-card> +</template> + +<script> +import vmodel_mixin from "../commons/vmodel_mixin"; +import { models, services } from "django-airavata-api"; + +export default { + name: "command-objects-editor", + mixins: [vmodel_mixin], + props: { + value: { + type: Array + }, + title: { + type: String, + required: true + }, + addButtonLabel: { + type: String, + required: true + } + }, + methods: { + addCommandObject() { + if (!this.data) { + this.data = []; + } + this.data.push(new models.CommandObject()); + this.$nextTick(() => + this.$refs.commandObjectInputs[ + this.$refs.commandObjectInputs.length - 1 + ].focus() + ); + }, + deleteCommandObject(commandObject) { + const index = this.data.findIndex( + cmd => cmd.key === commandObject.key + ); + this.data.splice(index, 1); + } + } +}; +</script> + diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/SetEnvPathsEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/SetEnvPathsEditor.vue new file mode 100644 index 0000000..4a5b449 --- /dev/null +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/SetEnvPathsEditor.vue @@ -0,0 +1,55 @@ +<template> + <b-card :title="title" title-tag="h5"> + <b-input-group v-for="setEnvPath in data" :key="setEnvPath.key" class="mb-1 align-items-center"> + <b-form-input type="text" v-model="setEnvPath.name" required placeholder="NAME" ref="nameInputs" /> + <font-awesome-icon icon="equals" class="mx-1" /> + <b-form-input type="text" v-model="setEnvPath.value" required placeholder="VALUE" /> + <b-input-group-append> + <b-button variant="secondary" @click="deleteEnvPath(setEnvPath)"> + <i class="fa fa-trash"></i> + <span class="sr-only">Delete</span> + </b-button> + </b-input-group-append> + </b-input-group> + <b-button variant="secondary" @click="addEnvPath">{{ addButtonLabel }}</b-button> + </b-card> +</template> + +<script> +import vmodel_mixin from "../commons/vmodel_mixin"; +import { models, services } from "django-airavata-api"; + +export default { + name: "set-env-paths-editor", + mixins: [vmodel_mixin], + props: { + value: { + type: Array + }, + title: { + type: String, + required: true + }, + addButtonLabel: { + type: String, + required: true + } + }, + methods: { + addEnvPath() { + if (!this.data) { + this.data = []; + } + this.data.push(new models.SetEnvPaths()); + this.$nextTick(() => + this.$refs.nameInputs[this.$refs.nameInputs.length - 1].focus() + ); + }, + deleteEnvPath(setEnvPath) { + const index = this.data.findIndex(env => env.key === setEnvPath.key); + this.data.splice(index, 1); + } + } +}; +</script> + diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/commons/vmodel_mixin.js b/django_airavata/apps/admin/static/django_airavata_admin/src/components/commons/vmodel_mixin.js index 94e6834..21cb486 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/commons/vmodel_mixin.js +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/commons/vmodel_mixin.js @@ -20,11 +20,15 @@ export default { } }, methods: { - copyValue() { - if (this.value instanceof models.BaseModel) { - return this.value.clone(); - } else if (typeof this.value === 'object') { - return JSON.parse(JSON.stringify(this.value)); + copyValue(value) { + if (value instanceof Array) { + return value.map(item => this.copyValue(item)) + } else { + if (value instanceof models.BaseModel) { + return value.clone(); + } else if (typeof value === 'object') { + return JSON.parse(JSON.stringify(value)); + } } } }, diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/main.js b/django_airavata/apps/admin/static/django_airavata_admin/src/main.js index 3c49b77..f327b95 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/src/main.js +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/main.js @@ -12,10 +12,10 @@ import BootstrapVue from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' import { library as faLibrary } from '@fortawesome/fontawesome-svg-core' -import { faGripVertical } from '@fortawesome/free-solid-svg-icons' +import { faGripVertical, faEquals } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' -faLibrary.add(faGripVertical) +faLibrary.add(faGripVertical, faEquals) Vue.component('font-awesome-icon', FontAwesomeIcon) diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py index 9f91207..eb77164 100644 --- a/django_airavata/apps/api/serializers.py +++ b/django_airavata/apps/api/serializers.py @@ -140,6 +140,27 @@ class StoredJSONField(serializers.JSONField): return value +class OrderedListField(serializers.ListField): + + def __init__(self, *args, **kwargs): + self.order_by = kwargs.pop('order_by', None) + super().__init__(*args, **kwargs) + + def to_representation(self, instance): + rep = super().to_representation(instance) + if rep is not None: + rep.sort(key=lambda item: item[self.order_by]) + return rep + + def to_internal_value(self, data): + validated_data = super().to_internal_value(data) + # Update order field based on order in array + items = validated_data if validated_data else [] + for i in range(len(items)): + items[i][self.order_by] = i + return validated_data + + class GroupSerializer(serializers.Serializer): url = FullyEncodedHyperlinkedIdentityField(view_name='django_airavata_api:group-detail', lookup_field='id', lookup_url_kwarg='group_id') id = serializers.CharField(default=GroupModel.thrift_spec[1][4], allow_null=True) @@ -236,55 +257,9 @@ class ApplicationModuleSerializer( required = ('appModuleName',) -class InputDataObjectTypeSerializer(serializers.Serializer): - name = serializers.CharField(required=False) - value = serializers.CharField(required=False) - type = serializers.IntegerField(required=False) - applicationArgument = serializers.CharField(required=False) - standardInput = serializers.BooleanField(required=False) - metaData = StoredJSONField(required=False) - inputOrder = serializers.IntegerField(required=False) - isRequired = serializers.BooleanField(required=False) - requiredToAddedToCommandLine = serializers.BooleanField(required=False) - dataStaged = serializers.BooleanField(required=False) - storageResourceId = serializers.CharField(required=False) - isReadOnly = serializers.BooleanField(required=False) - - def create(self, validated_data): - return InputDataObjectType(**validated_data) - - def update(self, instance, validated_data): - raise Exception("Not implemented") - - -class OutputDataObjectTypeSerializer(serializers.Serializer): - name = serializers.CharField(required=False) - value = serializers.CharField(required=False) - type = serializers.IntegerField(required=False) - applicationArgument = serializers.CharField(required=False) - isRequired = serializers.BooleanField(required=False) - requiredToAddedToCommandLine = serializers.BooleanField(required=False) - dataMovement = serializers.CharField(required=False) - location = serializers.CharField(required=False) - searchQuery = serializers.CharField(required=False) - outputStreaming = serializers.BooleanField(required=False) - storageResourceId = serializers.CharField(required=False) - - def create(self, validated_data): - return OutputDataObjectType(**validated_data) - - def update(self, instance, validated_data): - raise Exception("Not implemented") - - -class CustomSerializer(serializers.Serializer): - def process_list_fields(self, validated_data): - fields = self.fields - params = copy.deepcopy(validated_data) - for field_name, serializer in fields.items(): - if isinstance(serializer, serializers.ListSerializer): - params[field_name] = serializer.create(params[field_name]) - return params +class InputDataObjectTypeSerializer( + thrift_utils.create_serializer_class(InputDataObjectType)): + pass class ApplicationInterfaceDescriptionSerializer( @@ -294,23 +269,19 @@ class ApplicationInterfaceDescriptionSerializer( view_name='django_airavata_api:application-interface-detail', lookup_field='applicationInterfaceId', lookup_url_kwarg='app_interface_id') + applicationInputs = OrderedListField( + order_by='inputOrder', + child=InputDataObjectTypeSerializer()) - def to_representation(self, instance): - rep = super().to_representation(instance) - # Sort applicationInputs by 'inputOrder' - if rep['applicationInputs'] is not None: - rep['applicationInputs'].sort( - key=lambda input: input['inputOrder']) - return rep - def to_internal_value(self, data): - validated_data = super().to_internal_value(data) - # Update application input order based on order in array - app_inputs = validated_data.get('applicationInputs', []) - if app_inputs is not None: - for i in range(len(app_inputs)): - app_inputs[i]['inputOrder'] = i - return validated_data +class CommandObjectSerializer( + thrift_utils.create_serializer_class(CommandObject)): + pass + + +class SetEnvPathsSerializer( + thrift_utils.create_serializer_class(SetEnvPaths)): + pass class ApplicationDeploymentDescriptionSerializer( @@ -327,6 +298,24 @@ class ApplicationDeploymentDescriptionSerializer( lookup_field='appDeploymentId', lookup_url_kwarg='app_deployment_id') userHasWriteAccess = serializers.SerializerMethodField() + moduleLoadCmds = OrderedListField( + order_by='commandOrder', + child=CommandObjectSerializer()) + preJobCommands = OrderedListField( + order_by='commandOrder', + child=CommandObjectSerializer()) + postJobCommands = OrderedListField( + order_by='commandOrder', + child=CommandObjectSerializer()) + libPrependPaths = OrderedListField( + order_by='envPathOrder', + child=SetEnvPathsSerializer()) + libAppendPaths = OrderedListField( + order_by='envPathOrder', + child=SetEnvPathsSerializer()) + setEnvironment = OrderedListField( + order_by='envPathOrder', + child=SetEnvPathsSerializer()) def get_userHasWriteAccess(self, appDeployment): request = self.context['request'] @@ -334,22 +323,6 @@ class ApplicationDeploymentDescriptionSerializer( request.authz_token, appDeployment.appDeploymentId, ResourcePermissionType.WRITE) - def to_representation(self, instance): - rep = super().to_representation(instance) - if rep['moduleLoadCmds'] is not None: - rep['moduleLoadCmds'].sort( - key=lambda cmd: cmd['commandOrder']) - return rep - - def to_internal_value(self, data): - validated_data = super().to_internal_value(data) - # Update application input order based on order in array - module_load_cmds = validated_data.get('moduleLoadCmds', []) - if module_load_cmds is not None: - for i in range(len(module_load_cmds)): - module_load_cmds[i]['commandOrder'] = i - return validated_data - class ComputeResourceDescriptionSerializer(thrift_utils.create_serializer_class(ComputeResourceDescription)): pass diff --git a/django_airavata/apps/api/static/django_airavata_api/js/index.js b/django_airavata/apps/api/static/django_airavata_api/js/index.js index bfb9569..037201d 100644 --- a/django_airavata/apps/api/static/django_airavata_api/js/index.js +++ b/django_airavata/apps/api/static/django_airavata_api/js/index.js @@ -23,7 +23,7 @@ import OutputDataObjectType from './models/OutputDataObjectType' import ParallelismType from './models/ParallelismType' import Project from './models/Project' import ResourcePermissionType from './models/ResourcePermissionType' -import SetEnvPath from './models/SetEnvPath' +import SetEnvPaths from './models/SetEnvPaths' import SharedEntity from './models/SharedEntity' import SummaryType from './models/SummaryType' import UserPermission from './models/UserPermission' @@ -76,7 +76,7 @@ exports.models = { ParallelismType, Project, ResourcePermissionType, - SetEnvPath, + SetEnvPaths, SharedEntity, SummaryType, UserPermission, diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationDeploymentDescription.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationDeploymentDescription.js index 1f35bfb..e660cfe 100644 --- a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationDeploymentDescription.js +++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationDeploymentDescription.js @@ -1,7 +1,7 @@ import BaseModel from './BaseModel' import ParallelismType from './ParallelismType'; import CommandObject from './CommandObject'; -import SetEnvPath from './SetEnvPath'; +import SetEnvPaths from './SetEnvPaths'; const FIELDS = [ @@ -22,17 +22,17 @@ const FIELDS = [ }, { name: 'libPrependPaths', - type: SetEnvPath, + type: SetEnvPaths, list: true, }, { name: 'libAppendPaths', - type: SetEnvPath, + type: SetEnvPaths, list: true, }, { name: 'setEnvironment', - type: SetEnvPath, + type: SetEnvPaths, list: true, }, { diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceDescription.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceDescription.js index f59d770..1ba15bb 100644 --- a/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceDescription.js +++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ComputeResourceDescription.js @@ -35,7 +35,7 @@ const FIELDS = [ 'defaultWalltime', ] -export default class FullExperiment extends BaseModel { +export default class ComputeResourceDescription extends BaseModel { constructor(data = {}) { super(FIELDS, data); } diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPath.js b/django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPaths.js similarity index 83% rename from django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPath.js rename to django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPaths.js index 88165c7..2d3595c 100644 --- a/django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPath.js +++ b/django_airavata/apps/api/static/django_airavata_api/js/models/SetEnvPaths.js @@ -8,7 +8,7 @@ const FIELDS = [ 'envPathOrder', ]; -export default class SetEnvPath extends BaseModel { +export default class SetEnvPaths extends BaseModel { constructor(data = {}) { super(FIELDS, data);
