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 b7cabc2b6dea2a5f8c2604f88bca60d86c0b8972 Author: Marcus Christie <[email protected]> AuthorDate: Thu Aug 30 11:34:20 2018 -0400 AIRAVATA-2876 Initial interface editor, loading interface for module --- .../applications/ApplicationEditorContainer.vue | 208 ++++++++++------ .../applications/ApplicationInterfaceEditor.vue | 69 ++++- .../applications/ApplicationModuleEditor.vue | 114 ++++----- .../src/store/applications/app_interfaces.js | 46 ++++ .../src/store/applications/app_modules.js | 106 ++++---- .../src/store/applications/index.js | 10 +- .../django_airavata_admin/static/css/admin.css | 4 +- django_airavata/apps/api/exceptions.py | 9 +- django_airavata/apps/api/serializers.py | 28 +-- .../api/static/django_airavata_api/js/index.js | 2 +- .../js/models/ApplicationInterfaceDefinition.js | 62 +++-- .../django_airavata_api/js/service_config.js | 135 +++++----- .../django_airavata_api/js/utils/FetchUtils.js | 277 +++++++++++---------- django_airavata/apps/api/views.py | 14 +- django_airavata/static/common/js/index.js | 24 +- .../common/js/notifications/NotificationList.js | 35 ++- 16 files changed, 666 insertions(+), 477 deletions(-) diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue index 642cb59..b6213c6 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue @@ -1,101 +1,143 @@ <template> - <div> - <div class="row"> - <div class="col"> - <h1 class="h4 mb-4"> - {{ title }} - </h1> - </div> - </div> - <div class="row"> - <div class="col"> - <b-nav tabs> - <b-nav-item exact-active-class="active" exact :to="{name: id ? 'application_module' : 'new_application_module', params: {id: id}}">Details</b-nav-item> - <b-nav-item exact-active-class="active" exact :to="{name: 'application_interface', params: {id: id}}" :disabled="!id">Interface</b-nav-item> - <b-nav-item active-class="active" :to="{name: 'application_deployments', params: {id: id}}" :disabled="!id">Deployments</b-nav-item> - </b-nav> - <router-view name="module" v-if="module" v-model="module" @save="saveModule" @cancel="cancelModule"/> - <router-view name="interface"/> - <router-view name="deployments"/> - <router-view name="deployment"/> - </div> - </div> + <div> + <div class="row"> + <div class="col"> + <h1 class="h4 mb-4"> + {{ title }} + </h1> + </div> </div> + <div class="row"> + <div class="col"> + <b-nav tabs> + <b-nav-item exact-active-class="active" exact :to="{name: id ? 'application_module' : 'new_application_module', params: {id: id}}">Details</b-nav-item> + <b-nav-item exact-active-class="active" exact :to="{name: 'application_interface', params: {id: id}}" :disabled="!id">Interface</b-nav-item> + <b-nav-item active-class="active" :to="{name: 'application_deployments', params: {id: id}}" :disabled="!id">Deployments</b-nav-item> + </b-nav> + <router-view name="module" v-if="module" v-model="module" @save="saveModule" @cancel="cancelModule" /> + <router-view name="interface" v-if="appInterface" v-model="appInterface" @save="saveInterface" @cancel="cancelInterface" /> + <router-view name="deployments" /> + <router-view name="deployment" /> + </div> + </div> + </div> </template> <script> -import {mapActions, mapState} from 'vuex' -import {models} from 'django-airavata-api' +import { mapActions, mapState } from "vuex"; +import { models } from "django-airavata-api"; +import { notifications } from "django-airavata-common-ui"; export default { - name: 'application-editor-container', - props: { - id: String + name: "application-editor-container", + props: { + id: String + }, + data: function() { + return { + module: null, + appInterface: null, + deployments: null, + deployment: null + }; + }, + computed: { + title: function() { + if (this.id) { + return this.module && this.module.appModuleName + ? this.module.appModuleName + : ""; + } else { + return "Create a New Application"; + } }, - data: function() { - return { - module: null, - interface: null, - deployments: null, - deployment: null, - } + ...mapState("applications/modules", ["currentModule"]), + ...mapState("applications/interfaces", ["currentInterface"]) + }, + created() { + this.initialize(); + }, + methods: { + ...mapActions("applications/modules", [ + "loadApplicationModule", + "createApplicationModule", + "updateApplicationModule" + ]), + ...mapActions("applications/interfaces", [ + "loadApplicationInterface", + "createApplicationInterface", + "updateApplicationInterface" + ]), + initialize() { + // TODO: move this to applications store? + if (this.currentModule && this.currentModule.appModuleId === this.id) { + this.module = this.currentModule.clone(); + this.loadApplicationInterface(this.id).catch(error => { + notifications.NotificationList.addError(error); + }); + } else if (this.id) { + this.loadApplicationModule(this.id); + this.loadApplicationInterface(this.id).catch(error => { + notifications.NotificationList.addError(error); + }); + } else { + this.module = new models.ApplicationModule(); + } }, - computed: { - title: function() { - if (this.id) { + saveModule() { + if (this.id) { + this.updateApplicationModule(this.module).then(() => { + this.$router.push({ path: "/applications" }); + }); + } else { + this.createApplicationModule(this.module).then(appModule => { + this.$router.push({ + name: "application_module", + params: { id: appModule.appModuleId } + }); + }); + } + }, + saveInterface() { + // Copy name and description from module + this.appInterface.applicationName = this.module.appModuleName; + this.appInterface.applicationDescription = this.module.appModuleDescription; - return this.module && this.module.appModuleName ? this.module.appModuleName : ""; - } else { - return "Create a New Application"; - } - }, - ...mapState('applications/modules', [ - 'currentModule', - ]), + this.updateApplicationModule(this.module).then(appModule => { + if (this.appInterface.applicationInterfaceId) { + return this.updateApplicationInterface(this.appInterface).then(() => { + this.$router.push({ path: "/applications" }); + }); + } else { + this.appInterface.applicationModules = [this.id]; + return this.createApplicationInterface(this.appInterface).then(() => { + this.$router.push({ path: "/applications" }); + }); + } + }); }, - created () { + cancelModule() { + this.$router.push({ path: "/applications" }); + }, + cancelInterface() { + this.$router.push({ path: "/applications" }); + } + }, + watch: { + $route: function(to, from) { + if (to.params.id !== from.params.id) { this.initialize(); + } }, - methods: { - ...mapActions('applications/modules', [ - 'loadApplicationModule', - 'createApplicationModule', - 'updateApplicationModule', - ]), - initialize() { - if (this.currentModule && this.currentModule.appModuleId === this.id) { - this.module = this.currentModule.clone(); - } else if (this.id) { - this.loadApplicationModule(this.id); - } else { - this.module = new models.ApplicationModule(); - } - }, - saveModule() { - if (this.id) { - this.updateApplicationModule(this.module) - .then(() => { - this.$router.push({path: '/applications'}); - }); - } else { - this.createApplicationModule(this.module) - .then(appModule => { - this.$router.push({name: 'application_module', params: {id: appModule.appModuleId}}); - }); - } - }, - cancelModule() { - this.$router.push({path: '/applications'}); - }, + currentModule: function(newModule) { + // Clone the module from the store so we can modify it locally + this.module = newModule.clone(); }, - watch: { - '$route': 'initialize', - 'currentModule': function(newModule) { - // Clone the module from the store so we can modify it locally - this.module = newModule.clone(); - } + currentInterface: function(newInterface) { + this.appInterface = newInterface.clone(); } -} + } +}; </script> <style> diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue index 1137fad..1618c0c 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInterfaceEditor.vue @@ -1,10 +1,73 @@ <template> - <div>Application Interface Editor</div> + <div> + <div class="row"> + <div class="col"> + <h1 class="h4 mb-4"> + Application Interface + </h1> + </div> + </div> + <div class="row"> + <div class="col"> + <b-form-group label="Enable Archiving Working Directory" label-for="archive-directory"> + <b-form-radio-group id="archive-directory" + v-model="appInterface.archiveWorkingDirectory" + :options="archiveWorkingDirectoryOptions" + @input="emitChanged"> + </b-form-radio-group> + </b-form-group> + </div> + </div> + <div class="row"> + <div class="col"> + <b-button variant="primary" @click="save"> + Save + </b-button> + <b-button variant="secondary" @click="cancel"> + Cancel + </b-button> + </div> + </div> + </div> </template> <script> +import { models } from "django-airavata-api"; + export default { - name: 'application-interface-editor' -} + name: "application-interface-editor", + props: { + value: { + type: models.ApplicationInterfaceDefinition, + required: true + } + }, + data: function() { + return { + appInterface: this.value.clone() + }; + }, + computed: { + archiveWorkingDirectoryOptions() { + return [{ text: "True", value: true }, { text: "False", value: false }]; + } + }, + methods: { + emitChanged() { + this.$emit("input", this.appInterface); + }, + save() { + this.$emit("save"); + }, + cancel() { + this.$emit("cancel"); + } + }, + watch: { + value: function(newValue) { + this.appInterface = newValue.clone(); + } + } +}; </script> diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue index 0ce2952..092fdc9 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationModuleEditor.vue @@ -1,72 +1,66 @@ <template> - <div> - <div class="row"> - <div class="col"> - <h1 class="h4 mb-4"> - Application Details - </h1> - <b-form-group label="Application Name" label-for="application-name"> - <b-form-input id="application-name" - type="text" v-model="appModule.appModuleName" required - @input="emitChanged"></b-form-input> - </b-form-group> - <b-form-group label="Application Version" label-for="application-version"> - <b-form-input id="application-version" - type="text" v-model="appModule.appModuleVersion" - @input="emitChanged"></b-form-input> - </b-form-group> - <b-form-group label="Application Description" label-for="application-description"> - <b-form-textarea id="application-description" - v-model="appModule.appModuleDescription" - :rows="3"></b-form-textarea> - </b-form-group> - </div> - </div> - <div class="row"> - <div class="col"> - <b-button variant="primary" @click="save"> - Save - </b-button> - <b-button variant="secondary" @click="cancel"> - Cancel - </b-button> - </div> - </div> + <div> + <div class="row"> + <div class="col"> + <h1 class="h4 mb-4"> + Application Details + </h1> + <b-form-group label="Application Name" label-for="application-name"> + <b-form-input id="application-name" type="text" v-model="appModule.appModuleName" required @input="emitChanged"></b-form-input> + </b-form-group> + <b-form-group label="Application Version" label-for="application-version"> + <b-form-input id="application-version" type="text" v-model="appModule.appModuleVersion" @input="emitChanged"></b-form-input> + </b-form-group> + <b-form-group label="Application Description" label-for="application-description"> + <b-form-textarea id="application-description" v-model="appModule.appModuleDescription" :rows="3" @input="emitChanged"></b-form-textarea> + </b-form-group> + </div> </div> + <div class="row"> + <div class="col"> + <b-button variant="primary" @click="save"> + Save + </b-button> + <b-button variant="secondary" @click="cancel"> + Cancel + </b-button> + </div> + </div> + </div> </template> <script> -import {models} from 'django-airavata-api' +import { models } from "django-airavata-api"; export default { - name: 'application-module-editor', - props: { - value: { - type: models.ApplicationModule, - required: true, - } - }, - data: function() { - return { - appModule: this.value.clone(), - } + name: "application-module-editor", + props: { + value: { + type: models.ApplicationModule, + required: true + } + }, + data: function() { + return { + appModule: this.value.clone() + }; + }, + methods: { + emitChanged() { + this.$emit("input", this.appModule); }, - methods: { - emitChanged() { - this.$emit('input', this.appModule); - }, - save() { - this.$emit('save'); - }, - cancel() { - this.$emit('cancel'); - } + save() { + this.$emit("save"); }, - watch: { - value: function(newValue) { - this.appModule = newValue.clone(); - } + cancel() { + this.$emit("cancel"); + } + }, + watch: { + value: function(newValue) { + this.appModule = newValue.clone(); } -} + } +}; </script> diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_interfaces.js b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_interfaces.js new file mode 100644 index 0000000..e1c9f9e --- /dev/null +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_interfaces.js @@ -0,0 +1,46 @@ +import { services, models } from 'django-airavata-api' + +export default { + namespaced: true, + state: { + currentInterface: null, + }, + mutations: { + setCurrentInterface(state, currentInterface) { + state.currentInterface = currentInterface; + } + }, + actions: { + loadApplicationInterface({ commit }, appModuleId) { + // Otherwise fetch from the backend + return services.ApplicationModuleService.getApplicationInterface({ lookup: appModuleId }, { ignoreErrors: true }) + .then(appInterface => { + commit('setCurrentInterface', appInterface); + return appInterface; + }) + .catch(error => { + if (error.details.status === 404) { + // If there is no interface, just create a new instance + commit('setCurrentInterface', new models.ApplicationInterfaceDefinition()); + return Promise.resolve(null); + } else { + throw error; + } + }); + }, + createApplicationInterface({ commit }, appInterface) { + return services.ApplicationInterfaceService.create({ data: appInterface }) + .then(appInterface => { + commit('setCurrentInterface', appInterface); + return appInterface; + }); + }, + updateApplicationInterface({ commit }, appInterface) { + return services.ApplicationInterfaceService.update({ lookup: appInterface.applicationInterfaceId, data: appInterface }) + .then(appInterface => { + commit('setCurrentInterface', appInterface); + return appInterface; + }) + } + } +} diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_modules.js b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_modules.js index 3b92e94..2ad6131 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_modules.js +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/app_modules.js @@ -1,61 +1,61 @@ import { services } from 'django-airavata-api' export default { - namespaced: true, - state: { - modules: null, - currentModule: null, + namespaced: true, + state: { + modules: null, + currentModule: null, + }, + mutations: { + setModules(state, modules) { + state.modules = modules; }, - mutations: { - setModules(state, modules) { - state.modules = modules; - }, - setModule(state, module) { - state.currentModule = module; - } + setModule(state, module) { + state.currentModule = module; + } + }, + actions: { + loadApplicationModules({ commit }) { + return services.ApplicationModuleService.list() + .then(appModules => { + commit('setModules', appModules); + return appModules; + }); }, - actions: { - loadApplicationModules({ commit }) { - return services.ApplicationModuleService.list() - .then(appModules => { - commit('setModules', appModules); - return appModules; - }); - }, - loadApplicationModule({ commit, state }, appModuleId) { - if (state.modules) { - const appModule = state.modules.find(appModule => appModule.appModuleId === appModuleId); - if (appModule) { - commit('setModule', appModule); - return Promise.resolve(appModule); - } - } - // Otherwise fetch from the backend - return services.ApplicationModuleService.retrieve({lookup: appModuleId}) - .then(appModule => { - commit('setModule', appModule); - return appModule; - }); - }, - createApplicationModule({ commit, state }, appModule) { - return services.ApplicationModuleService.create({data: appModule}) - .then(appModule => { - commit('setModule', appModule); - const appModulesCopy = state.modules.slice(); - appModulesCopy.push(appModule); - commit('setModules', appModulesCopy); - return appModule; - }); - }, - updateApplicationModule({ commit, state }, appModule) { - return services.ApplicationModuleService.update({lookup: appModule.appModuleId, data: appModule}) - .then(appModule => { - commit('setModule', appModule); - const appModules = state.modules.filter(mod => mod.appModuleId !== appModule.appModuleId) - appModules.push(appModule); - commit('setModules', appModules); - return appModule; - }) + loadApplicationModule({ commit, state }, appModuleId) { + if (state.modules) { + const appModule = state.modules.find(appModule => appModule.appModuleId === appModuleId); + if (appModule) { + commit('setModule', appModule); + return Promise.resolve(appModule); } + } + // Otherwise fetch from the backend + return services.ApplicationModuleService.retrieve({ lookup: appModuleId }) + .then(appModule => { + commit('setModule', appModule); + return appModule; + }); + }, + createApplicationModule({ commit, state }, appModule) { + return services.ApplicationModuleService.create({ data: appModule }) + .then(appModule => { + commit('setModule', appModule); + const appModulesCopy = state.modules.slice(); + appModulesCopy.push(appModule); + commit('setModules', appModulesCopy); + return appModule; + }); + }, + updateApplicationModule({ commit, state }, appModule) { + return services.ApplicationModuleService.update({ lookup: appModule.appModuleId, data: appModule }) + .then(appModule => { + commit('setModule', appModule); + const appModules = state.modules.filter(mod => mod.appModuleId !== appModule.appModuleId) + appModules.push(appModule); + commit('setModules', appModules); + return appModule; + }) } + } } diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/index.js b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/index.js index 2a83e50..b83d783 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/index.js +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/store/applications/index.js @@ -1,8 +1,10 @@ +import interfaces from './app_interfaces' import modules from './app_modules' export default { - namespaced: true, - modules: { - modules - } + namespaced: true, + modules: { + interfaces, + modules, + } } diff --git a/django_airavata/apps/admin/static/django_airavata_admin/static/css/admin.css b/django_airavata/apps/admin/static/django_airavata_admin/static/css/admin.css index 3b633b7..8ad58ac 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/static/css/admin.css +++ b/django_airavata/apps/admin/static/django_airavata_admin/static/css/admin.css @@ -9,7 +9,7 @@ margin-bottom: 10px; } -.new_app input[type="text"], input[type="number"], textarea, select { +/* .new_app input[type="text"], input[type="number"], textarea, select { width: 100%; border-radius: 2px; border: solid #dddddd 1px; @@ -19,7 +19,7 @@ button, input[type="button"] { outline: none; -} +} */ .main_section { width: 100%; diff --git a/django_airavata/apps/api/exceptions.py b/django_airavata/apps/api/exceptions.py index 632d97c..e643f1c 100644 --- a/django_airavata/apps/api/exceptions.py +++ b/django_airavata/apps/api/exceptions.py @@ -12,6 +12,7 @@ log = logging.getLogger(__name__) def custom_exception_handler(exc, context): + log.debug("API exception", exc_info=exc) # Call REST framework's default exception handler first, # to get the standard error response. response = exception_handler(exc, context) @@ -24,10 +25,16 @@ def custom_exception_handler(exc, context): {'detail': str(exc)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + # Generic handler + if response is None: + return Response( + {'detail': str(exc)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + if isinstance(exc, serializers.ValidationError): # Create a default error message for the validation error response.data['detail'] = "ValidationError: {}".format( json.dumps(response.data)) - log.debug("API exception", exc_info=exc) return response diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py index 41182e5..21b5cf7 100644 --- a/django_airavata/apps/api/serializers.py +++ b/django_airavata/apps/api/serializers.py @@ -235,12 +235,6 @@ class ApplicationModuleSerializer( class Meta: required = ('appModuleName',) - def create(self, validated_data): - return ApplicationModule(**validated_data) - - def update(self, instance, validated_data): - return self.create(validated_data) - class InputDataObjectTypeSerializer(serializers.Serializer): name = serializers.CharField(required=False) @@ -293,23 +287,13 @@ class CustomSerializer(serializers.Serializer): return params -class ApplicationInterfaceDescriptionSerializer(CustomSerializer): - url = FullyEncodedHyperlinkedIdentityField(view_name='django_airavata_api:application-interface-detail', lookup_field='applicationInterfaceId', lookup_url_kwarg='app_interface_id') - applicationInterfaceId = serializers.CharField(read_only=True) - applicationName = serializers.CharField(required=False) - applicationDescription = serializers.CharField(required=False) - archiveWorkingDirectory = serializers.BooleanField(required=False) - hasOptionalFileInputs = serializers.BooleanField(required=False) - applicationOutputs = serializers.ListSerializer(child=OutputDataObjectTypeSerializer()) - applicationInputs = serializers.ListSerializer(child=InputDataObjectTypeSerializer()) - applicationModules = serializers.ListSerializer(child=serializers.CharField()) +class ApplicationInterfaceDescriptionSerializer( + thrift_utils.create_serializer_class(ApplicationInterfaceDescription)): - def create(self, validated_data): - params=self.process_list_fields(validated_data) - return ApplicationInterfaceDescription(**params) - - def update(self, instance, validated_data): - raise Exception("Not implemented") + url = FullyEncodedHyperlinkedIdentityField( + view_name='django_airavata_api:application-interface-detail', + lookup_field='applicationInterfaceId', + lookup_url_kwarg='app_interface_id') class CommandObjectSerializer(CustomSerializer): 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 d82fe04..324cdd9 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 @@ -76,7 +76,7 @@ exports.models = { exports.services = { ApplicationDeploymentService, - ApplicationInterfaceService, + ApplicationInterfaceService: ServiceFactory.service("ApplicationInterfaces"), ApplicationModuleService: ServiceFactory.service("ApplicationModules"), CredentialSummaryService: ServiceFactory.service("CredentialSummaries"), ExperimentService, diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js index 0383219..0d98c9c 100644 --- a/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js +++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ApplicationInterfaceDefinition.js @@ -4,36 +4,44 @@ import OutputDataObjectType from './OutputDataObjectType' const FIELDS = [ - 'applicationInterfaceId', - 'applicationName', - 'applicationDescription', - { - name: 'applicationModules', - type: 'string', - list: true, - }, - { - name: 'applicationInputs', - type: InputDataObjectType, - list: true, - }, - { - name: 'applicationOutputs', - type: OutputDataObjectType, - list: true, - }, - 'archiveWorkingDirectory', - 'hasOptionalFileInputs', + 'applicationInterfaceId', + 'applicationName', + 'applicationDescription', + { + name: 'applicationModules', + type: 'string', + list: true, + }, + { + name: 'applicationInputs', + type: InputDataObjectType, + list: true, + }, + { + name: 'applicationOutputs', + type: OutputDataObjectType, + list: true, + }, + { + name: 'archiveWorkingDirectory', + type: 'boolean', + default: false, + }, + { + name: 'hasOptionalFileInputs', + type: 'boolean', + default: false, + }, ]; export default class ApplicationInterfaceDefinition extends BaseModel { - constructor(data = {}) { - super(FIELDS, data); - } + constructor(data = {}) { + super(FIELDS, data); + } - getOrderedApplicationInputs() { - // Use slice() to make a copy and sort that copy - return this.applicationInputs ? this.applicationInputs.slice().sort((a, b) => a.inputOrder - b.inputOrder) : []; - } + getOrderedApplicationInputs() { + // Use slice() to make a copy and sort that copy + return this.applicationInputs ? this.applicationInputs.slice().sort((a, b) => a.inputOrder - b.inputOrder) : []; + } } diff --git a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js index 363fa6c..a3536a3 100644 --- a/django_airavata/apps/api/static/django_airavata_api/js/service_config.js +++ b/django_airavata/apps/api/static/django_airavata_api/js/service_config.js @@ -5,6 +5,7 @@ import Group from './models/Group' import GroupResourceProfile from './models/GroupResourceProfile' import SharedEntity from './models/SharedEntity' import UserProfile from './models/UserProfile' +import ApplicationInterfaceDefinition from './models/ApplicationInterfaceDefinition'; const post = "post"; const get = "get"; @@ -39,61 +40,81 @@ pagination:true # whether to treat the response as a paginated response */ export default { - "ApplicationDeployments": { - url: "/api/application-deployments", - viewSet: true, - queryParams: ['appModuleId', 'groupResourceProfileId'], - modelClass: ApplicationDeploymentDescription, - }, - "ApplicationModules": { - url: "/api/applications", - viewSet: true, - modelClass: ApplicationModule, - }, - "ComputeResources": { - url: "/api/compute-resources", - viewSet: [{ - name: "list" - }, { - name: "names", - url: "/api/compute-resources/all_names/", - requestType: 'get', - }, { - name: "namesList", - url: "/api/compute-resources/all_names_list/", - requestType: 'get', - }], - modelClass: ApplicationDeploymentDescription, - }, - "CredentialSummaries": { - url: "/api/credential-summaries/", - viewSet: [{ - name: "list" - }], - modelClass: CredentialSummary, - }, - "GroupResourceProfiles": { - url: "/api/group-resource-profiles/", - viewSet: true, - modelClass: GroupResourceProfile, - }, - "Groups": { - url: "/api/groups", - viewSet: true, - pagination: true, - queryParams: ['limit', 'offset'], - modelClass: Group, - }, - "SharedEntities": { - url: "/api/shared-entities", - viewSet: true, - modelClass: SharedEntity, - }, - "UserProfiles": { - url: "/api/user-profiles", - viewSet: [{ - name: "list" - }], - modelClass: UserProfile, - }, + "ApplicationDeployments": { + url: "/api/application-deployments", + viewSet: true, + queryParams: ['appModuleId', 'groupResourceProfileId'], + modelClass: ApplicationDeploymentDescription, + }, + "ApplicationInterfaces": { + url: "/api/application-interfaces", + viewSet: true, + modelClass: ApplicationInterfaceDefinition, + }, + "ApplicationModules": { + url: "/api/applications", + viewSet: [{ + name: "list" + }, { + name: "create" + }, { + name: "retrieve" + }, { + name: "update" + }, { + name: "delete" + }, { + name: "getApplicationInterface", + url: "/api/applications/<lookup>/application_interface/", + requestType: 'get', + modelClass: ApplicationInterfaceDefinition + }], + modelClass: ApplicationModule, + }, + "ComputeResources": { + url: "/api/compute-resources", + viewSet: [{ + name: "list" + }, { + name: "names", + url: "/api/compute-resources/all_names/", + requestType: 'get', + }, { + name: "namesList", + url: "/api/compute-resources/all_names_list/", + requestType: 'get', + }], + modelClass: ApplicationDeploymentDescription, + }, + "CredentialSummaries": { + url: "/api/credential-summaries/", + viewSet: [{ + name: "list" + }], + modelClass: CredentialSummary, + }, + "GroupResourceProfiles": { + url: "/api/group-resource-profiles/", + viewSet: true, + modelClass: GroupResourceProfile, + }, + "Groups": { + url: "/api/groups", + viewSet: true, + pagination: true, + queryParams: ['limit', 'offset'], + modelClass: Group, + }, + "SharedEntities": { + url: "/api/shared-entities", + viewSet: true, + modelClass: SharedEntity, + }, + "UserProfiles": { + url: "/api/user-profiles", + viewSet: [{ + name: "list" + }], + modelClass: UserProfile, + }, } diff --git a/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js b/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js index f16d382..c6a796c 100644 --- a/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js +++ b/django_airavata/apps/api/static/django_airavata_api/js/utils/FetchUtils.js @@ -2,161 +2,164 @@ import UnhandledErrorDispatcher from "../errors/UnhandledErrorDispatcher"; var count = 0; const parseQueryParams = function (url, queryParams = "") { - if (queryParams && typeof(queryParams) != "string") { - queryParams = Object.keys(queryParams).map(key => encodeURIComponent(key) + "=" + encodeURIComponent(queryParams[key])).join("&"); - } - if (queryParams && queryParams !== "") { - return url + "?" + queryParams; - } else { - return url; - } + if (queryParams && typeof (queryParams) != "string") { + queryParams = Object.keys(queryParams).map(key => encodeURIComponent(key) + "=" + encodeURIComponent(queryParams[key])).join("&"); + } + if (queryParams && queryParams !== "") { + return url + "?" + queryParams; + } else { + return url; + } } const setSpinnerDisplay = function (display) { - let spinner = document.getElementById("airavata-spinner"); - spinner.style.display = display; + let spinner = document.getElementById("airavata-spinner"); + spinner.style.display = display; } const incrementCount = function () { - count++; - if (count == 1) { - setSpinnerDisplay("block"); - } + count++; + if (count == 1) { + setSpinnerDisplay("block"); + } } const decrementCount = function () { - if (count > 0) { - count--; - if (count == 0) { - setSpinnerDisplay("none"); - } + if (count > 0) { + count--; + if (count == 0) { + setSpinnerDisplay("none"); } + } } export default { - enableSpinner: function () { + enableSpinner: function () { + + }, + disableSpinner: function () { - }, - disableSpinner: function () { + }, + getCSRFToken: function () { + var csrfToken = document.cookie.split(';').map(val => val.trim()).filter(val => val.startsWith("csrftoken" + '=')).map(val => val.split("=")[1]); + if (csrfToken) { + return csrfToken[0]; + } else { + return null; + } + }, + createHeaders: function (contentType = "application/json", accept = "application/json") { + var csrfToken = this.getCSRFToken(); + var headers = new Headers({ + "Content-Type": contentType, + "Accept": accept, + }); + if (csrfToken != null) { + headers.set("X-CSRFToken", csrfToken) + } + return headers; + }, + post: function (url, body, queryParams = "", { mediaType = "application/json", ignoreErrors = false } = {}) { + var headers = this.createHeaders(mediaType) + // Browsers automatically handle content type for FormData request bodies + if (body instanceof FormData) { + headers.delete("Content-Type"); + } + url = parseQueryParams(url, queryParams); + return this.processFetch(url, { + method: 'post', + body: (body instanceof FormData || typeof body === 'string') ? body : JSON.stringify(body), + headers: headers, + credentials: "same-origin", + ignoreErrors + }); + }, + put: function (url, body, { mediaType = "application/json", ignoreErrors = false } = {}) { + var headers = this.createHeaders(mediaType); + return this.processFetch(url, { + method: 'put', + body: (body instanceof FormData || typeof body === 'string') ? body : JSON.stringify(body), + headers: headers, + credentials: "same-origin", + ignoreErrors + }); + }, + get: function (url, queryParams = "", { mediaType = "application/json", ignoreErrors = false } = {}) { + if (queryParams && typeof (queryParams) != "string") { + queryParams = Object.keys(queryParams).map(key => encodeURIComponent(key) + "=" + encodeURIComponent(queryParams[key])).join("&") + } + if (queryParams) { + url = url + "?" + queryParams + } + var headers = this.createHeaders(mediaType); + return this.processFetch(url, { + method: 'get', + headers: headers, + credentials: "same-origin", + ignoreErrors + }); + }, + delete: function (url, { ignoreErrors = false } = {}) { + var headers = this.createHeaders(); + return this.processFetch(url, { + method: 'delete', + headers: headers, + credentials: "same-origin", + ignoreErrors + }); + }, + processFetch: function (url, { method = 'get', headers, credentials = 'same-origin', body, ignoreErrors = false }) { - }, - getCSRFToken: function () { - var csrfToken = document.cookie.split(';').map(val => val.trim()).filter(val => val.startsWith("csrftoken" + '=')).map(val => val.split("=")[1]); - if (csrfToken) { - return csrfToken[0]; + const fetchConfig = { + method, + headers, + credentials, + }; + if (body) { + fetchConfig.body = body; + } + incrementCount(); + return fetch(url, fetchConfig).then((response) => { + decrementCount(); + if (response.ok) { + // No response body + if (response.status === 204) { + return Promise.resolve(); } else { - return null; - } - }, - createHeaders: function (contentType = "application/json", accept = "application/json") { - var csrfToken = this.getCSRFToken(); - var headers = new Headers({ - "Content-Type": contentType, - "Accept": accept, - }); - if (csrfToken != null) { - headers.set("X-CSRFToken", csrfToken) - } - return headers; - }, - post: function (url, body, queryParams = "", {mediaType = "application/json", ignoreErrors = false } = {}) { - var headers = this.createHeaders(mediaType) - // Browsers automatically handle content type for FormData request bodies - if (body instanceof FormData) { - headers.delete("Content-Type"); - } - url = parseQueryParams(url, queryParams); - return this.processFetch(url, { - method: 'post', - body: (body instanceof FormData || typeof body === 'string') ? body : JSON.stringify(body), - headers: headers, - credentials: "same-origin", - ignoreErrors - }); - }, - put: function (url, body, { mediaType = "application/json", ignoreErrors = false } = {}) { - var headers = this.createHeaders(mediaType); - return this.processFetch(url, { - method: 'put', - body: (body instanceof FormData || typeof body === 'string') ? body : JSON.stringify(body), - headers: headers, - credentials: "same-origin" - }); - }, - get: function (url, queryParams = "", { mediaType = "application/json", ignoreErrors = false } = {}) { - if (queryParams && typeof(queryParams) != "string") { - queryParams = Object.keys(queryParams).map(key => encodeURIComponent(key) + "=" + encodeURIComponent(queryParams[key])).join("&") - } - if (queryParams) { - url = url + "?" + queryParams + return Promise.resolve(response.json()) } - var headers = this.createHeaders(mediaType); - return this.processFetch(url, { - method: 'get', - headers: headers, - credentials: "same-origin" - }); - }, - delete: function (url, { ignoreErrors = false } = {}) { - var headers = this.createHeaders(); - return this.processFetch(url, { - method: 'delete', - headers: headers, - credentials: "same-origin" + } else { + return response.json().then(json => { + const error = new Error(json.detail ? json.detail : response.statusText); + error.details = this.createErrorDetails({ url, body, status: response.status, responseBody: json }) + throw error; + }, e => { // In case JSON parsing fails + const error = new Error(response.statusText); + error.details = this.createErrorDetails({ url, body, status: response.status }) + throw error; }); - }, - processFetch: function(url, {method = 'get', headers, credentials = 'same-origin', body, ignoreErrors = false}) { + } + }, (error) => { + decrementCount(); + error.details = this.createErrorDetails({ url, body }); + throw error; + }).catch(error => { - const fetchConfig = { - method, - headers, - credentials, - }; - if (body) { - fetchConfig.body = body; - } - incrementCount(); - return fetch(url, fetchConfig).then((response) => { - decrementCount(); - if (response.ok) { - // No response body - if (response.status === 204) { - return Promise.resolve(); - } else { - return Promise.resolve(response.json()) - } - } else { - return response.json().then(json => { - const error = new Error(json.detail ? json.detail : response.statusText); - error.details = this.createErrorDetails({url, body, status: response.status, responseBody: json}) - throw error; - }, e => { // In case JSON parsing fails - const error = new Error(response.statusText); - error.details = this.createErrorDetails({url, body, status: response.status}) - throw error; - }); - } - }, (error) => { - decrementCount(); - error.details = this.createErrorDetails({url, body}); - throw error; - }).catch(error => { - - if (!ignoreErrors) { - UnhandledErrorDispatcher.reportError({ - message: error.message, - error: error, - details: error.details, - }) - } - throw error; + if (!ignoreErrors) { + UnhandledErrorDispatcher.reportError({ + message: error.message, + error: error, + details: error.details, }) - }, - createErrorDetails: function({url, body, status = null, responseBody = null}={}) { - return { - url, - body, - status, - response: responseBody - } + } + throw error; + }) + }, + createErrorDetails: function ({ url, body, status = null, responseBody = null } = {}) { + return { + url, + body, + status, + response: responseBody } + } } diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py index 2edcae0..63966bc 100644 --- a/django_airavata/apps/api/views.py +++ b/django_airavata/apps/api/views.py @@ -289,13 +289,15 @@ class ApplicationModuleViewSet(APIBackedViewSet): return Response(serializer.data) elif len(app_interfaces) > 1: log.error( - "More than one application interface found for module {}: {}".format(app_module_id, app_interfaces)) - return Response( - {'error': 'More than one application interface found for module id {}'.format(app_module_id)}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) + "More than one application interface found for module {}: {}" + .format(app_module_id, app_interfaces)) + raise Exception( + 'More than one application interface found for module {}' + .format(app_module_id) + ) else: - return Response({'error': 'No application interface found for module id {}'.format(app_module_id)}, - status=status.HTTP_404_NOT_FOUND) + raise Http404("No application interface found for module id {}" + .format(app_module_id)) @detail_route() def application_deployments(self, request, app_module_id): diff --git a/django_airavata/static/common/js/index.js b/django_airavata/static/common/js/index.js index 30b0672..fca2e7e 100644 --- a/django_airavata/static/common/js/index.js +++ b/django_airavata/static/common/js/index.js @@ -9,23 +9,31 @@ import GlobalErrorHandler from './errors/GlobalErrorHandler' import ListLayout from './layouts/ListLayout.vue' +import Notification from './notifications/Notification' +import NotificationList from './notifications/NotificationList' + import * as utils from './utils' exports.components = { - Pager, - ApplicationCard, - Autocomplete, - AutocompleteTextInput, - NotificationsDisplay, - ShareButton, + Pager, + ApplicationCard, + Autocomplete, + AutocompleteTextInput, + NotificationsDisplay, + ShareButton, } exports.errors = { - GlobalErrorHandler, + GlobalErrorHandler, } exports.layouts = { - ListLayout, + ListLayout, +} + +exports.notifications = { + Notification, + NotificationList, } exports.utils = utils; diff --git a/django_airavata/static/common/js/notifications/NotificationList.js b/django_airavata/static/common/js/notifications/NotificationList.js index 86c7b41..de8b6f8 100644 --- a/django_airavata/static/common/js/notifications/NotificationList.js +++ b/django_airavata/static/common/js/notifications/NotificationList.js @@ -1,22 +1,31 @@ +import Notification from './Notification' class NotificationList { - constructor() { - this.notifications = []; - } + constructor() { + this.notifications = []; + } - add(notification) { - this.notifications.push(notification); - } + add(notification) { + this.notifications.push(notification); + } - remove(notification) { - const i = this.notifications.indexOf(notification); - this.notifications.splice(i, 1); - } + // Convenience method for adding an error + addError(error) { + this.notifications.push(new Notification({ + type: "ERROR", + message: error.message + })) + } - get list() { - return this.notifications; - } + remove(notification) { + const i = this.notifications.indexOf(notification); + this.notifications.splice(i, 1); + } + + get list() { + return this.notifications; + } } export default new NotificationList();
