You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2021/06/18 19:38:28 UTC
[airavata-django-portal] 11/20: AIRAVATA-3453 Initial version of
resource selection components
This is an automated email from the ASF dual-hosted git repository.
machristie pushed a commit to branch airavata-3453
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git
commit 0c18ce115ad233a4afe4e84e1d2316cc4384a8fc
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue May 11 16:36:16 2021 -0400
AIRAVATA-3453 Initial version of resource selection components
---
.../api/static/django_airavata_api/js/index.js | 6 +-
.../js/web-components/ComputeResourceSelector.vue | 84 +++++++++++
.../js/web-components/ExperimentEditor.vue | 18 +--
.../js/web-components/QueueSettingsEditor.vue | 13 ++
.../js/web-components/ResourceSelectionEditor.vue | 160 +++++++++++++++++++++
.../js/web-components/store.js | 35 ++++-
6 files changed, 306 insertions(+), 10 deletions(-)
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 f0c74e8..c0804f1 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
@@ -35,6 +35,7 @@ import SetEnvPaths from "./models/SetEnvPaths";
import SharedEntity from "./models/SharedEntity";
import StoragePreference from "./models/StoragePreference";
import SummaryType from "./models/SummaryType";
+import UserConfigurationData from "./models/UserConfigurationData";
import UserPermission from "./models/UserPermission";
import CloudJobSubmissionService from "./services/CloudJobSubmissionService";
@@ -93,6 +94,7 @@ const models = {
SharedEntity,
StoragePreference,
SummaryType,
+ UserConfigurationData,
UserPermission,
};
@@ -110,7 +112,9 @@ const services = {
ExperimentSearchService: ServiceFactory.service("ExperimentSearch"),
ExperimentService: ServiceFactory.service("Experiments"),
ExperimentStatisticsService: ServiceFactory.service("ExperimentStatistics"),
- ExperimentStoragePathService: ServiceFactory.service("ExperimentStoragePaths"),
+ ExperimentStoragePathService: ServiceFactory.service(
+ "ExperimentStoragePaths"
+ ),
FullExperimentService: ServiceFactory.service("FullExperiments"),
GatewayResourceProfileService: ServiceFactory.service(
"GatewayResourceProfile"
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ComputeResourceSelector.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ComputeResourceSelector.vue
new file mode 100644
index 0000000..3b28d83
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ComputeResourceSelector.vue
@@ -0,0 +1,84 @@
+<template>
+ <b-form-group label="Compute Resource" label-for="compute-resource">
+ <b-form-select
+ id="compute-resource"
+ v-model="resourceHostId"
+ :options="computeResourceOptions"
+ required
+ @input="computeResourceChanged"
+ >
+ <template slot="first">
+ <option :value="null" disabled>Select a Compute Resource</option>
+ </template>
+ </b-form-select>
+ </b-form-group>
+</template>
+
+<script>
+import { getComputeResourceNames } from "./store";
+export default {
+ name: "compute-resource-selector",
+ props: {
+ value: {
+ // compute resource host id
+ type: String,
+ default: null,
+ },
+ computeResources: {
+ type: Array, // of compute resource host ids
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ resourceHostId: this.value,
+ computeResourceNames: {},
+ };
+ },
+ created() {
+ this.loadComputeResourceNames();
+ },
+ computed: {
+ computeResourceOptions: function () {
+ const computeResourceOptions = this.computeResources.map(
+ (computeHostId) => {
+ return {
+ value: computeHostId,
+ text:
+ computeHostId in this.computeResourceNames
+ ? this.computeResourceNames[computeHostId]
+ : "",
+ };
+ }
+ );
+ computeResourceOptions.sort((a, b) => a.text.localeCompare(b.text));
+ return computeResourceOptions;
+ },
+ },
+ methods: {
+ async loadComputeResourceNames() {
+ this.computeResourceNames = await getComputeResourceNames();
+ },
+ computeResourceChanged() {
+ this.emitValueChanged();
+ },
+ emitValueChanged: function () {
+ const inputEvent = new CustomEvent("input", {
+ detail: [this.resourceHostId],
+ composed: true,
+ bubbles: true,
+ });
+ this.$el.dispatchEvent(inputEvent);
+ },
+ },
+ watch: {
+ value() {
+ this.resourceHostId = this.value;
+ },
+ },
+};
+</script>
+
+<style>
+@import "./styles.css";
+</style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ExperimentEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ExperimentEditor.vue
index 25d77d8..e7acb23 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ExperimentEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ExperimentEditor.vue
@@ -24,9 +24,9 @@
<!-- programmatically define slots as native slots (not Vue slots), see #mounted() -->
</div>
</template>
- <div @input="updateGroupResourceProfileId">
- <slot name="experiment-group-resource-profile">
- <adpf-group-resource-profile-selector :value="experiment.userConfigurationData.groupResourceProfileId"/>
+ <div @input="updateUserConfigurationData">
+ <slot name="experiment-resource-selection">
+ <adpf-resource-selection-editor ref="resourceSelectionEditor" />
</slot>
</div>
<slot name="save-button">
@@ -45,6 +45,7 @@ import {
export default {
props: {
+ // TODO: rename to applicationModuleId?
applicationId: {
type: String,
required: true,
@@ -79,6 +80,9 @@ export default {
// TODO: add support for other input types
this.$refs[input.name][0].append(slot);
}
+ // Can't set objects via attributes, must set as prop
+ this.$refs.resourceSelectionEditor.value = this.experiment.userConfigurationData;
+ this.$refs.resourceSelectionEditor.applicationModuleId = this.applicationId;
});
},
data() {
@@ -100,9 +104,9 @@ export default {
const [projectId] = event.detail;
this.experiment.projectId = projectId;
},
- updateGroupResourceProfileId(event) {
- const [groupResourceProfileId] = event.detail;
- this.experiment.userConfigurationData.groupResourceProfileId = groupResourceProfileId;
+ updateUserConfigurationData(event) {
+ const [userConfigurationData] = event.detail;
+ this.experiment.userConfigurationData = userConfigurationData;
},
onSubmit(event) {
// console.log(event);
@@ -137,8 +141,6 @@ export default {
this.applicationModule.appModuleName +
" on " +
new Date().toLocaleString();
- experiment.userConfigurationData.computationalResourceScheduling.resourceHostId =
- "js-169-51.jetstream-cloud.org_6672e8fe-8d63-4bbe-8bf8-4ea04092e72f";
this.$emit("loaded", experiment);
return experiment;
}
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/QueueSettingsEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/QueueSettingsEditor.vue
new file mode 100644
index 0000000..a02d2bb
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/QueueSettingsEditor.vue
@@ -0,0 +1,13 @@
+<template>
+ <div>QueueSettingsEditor</div>
+</template>
+
+<script>
+export default {
+
+}
+</script>
+
+<style>
+
+</style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ResourceSelectionEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ResourceSelectionEditor.vue
new file mode 100644
index 0000000..d6824ec
--- /dev/null
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/ResourceSelectionEditor.vue
@@ -0,0 +1,160 @@
+<template>
+ <div v-if="userConfigurationData">
+ <div @input.stop="updateGroupResourceProfileId">
+ <adpf-group-resource-profile-selector
+ slot="resource-selection-grp"
+ :value="userConfigurationData.groupResourceProfileId"
+ />
+ </div>
+ <div @input.stop="updateComputeResourceHostId">
+ <adpf-compute-resource-selector
+ ref="computeResourceSelector"
+ slot="resource-selection-compute-resource"
+ :value="
+ userConfigurationData.computationalResourceScheduling.resourceHostId
+ "
+ />
+ </div>
+ <div @input.stop="updateComputationalResourceScheduling">
+ <adpf-queue-settings-editor
+ slot="resource-selection-queue-settings"
+ :value="userConfigurationData.computationalResourceScheduling"
+ :queues="queues"
+ :max-allowed-nodes="maxAllowedNodes"
+ :max-allowed-cores="maxAllowedCores"
+ :max-allowed-walltime="maxAllowedWalltime"
+ :max-memory="maxMemory"
+ />
+ </div>
+ </div>
+</template>
+
+<script>
+import { models } from "django-airavata-api";
+import {
+ getApplicationDeployments,
+ getDefaultComputeResourceId,
+} from "./store";
+export default {
+ // TODO: better name? UserConfigurationDataEditor?
+ name: "resource-selection-editor",
+ props: {
+ value: {
+ type: models.UserConfigurationData,
+ },
+ applicationModuleId: {
+ type: String,
+ },
+ },
+ data() {
+ return {
+ userConfigurationData: this.cloneValue(),
+ applicationDeployments: [],
+ queues: [],
+ maxAllowedNodes: 0,
+ maxAllowedCores: 0,
+ maxAllowedWalltime: 0,
+ maxMemory: 0,
+ defaultComputeResourceId: null,
+ };
+ },
+ computed: {
+ computeResources() {
+ return this.applicationDeployments.map((dep) => dep.computeHostId);
+ },
+ groupResourceProfileId() {
+ return this.userConfigurationData
+ ? this.userConfigurationData.groupResourceProfileId
+ : null;
+ },
+ resourceHostId() {
+ return this.userConfigurationData &&
+ this.userConfigurationData.computationalResourceScheduling
+ ? this.userConfigurationData.computationalResourceScheduling
+ .resourceHostId
+ : null;
+ },
+ },
+ methods: {
+ emitValueChanged: function () {
+ const inputEvent = new CustomEvent("input", {
+ detail: [this.userConfigurationData],
+ composed: true,
+ bubbles: true,
+ });
+ this.$el.dispatchEvent(inputEvent);
+ },
+ updateGroupResourceProfileId(event) {
+ const [groupResourceProfileId] = event.detail;
+ this.userConfigurationData.groupResourceProfileId = groupResourceProfileId;
+ this.emitValueChanged();
+ this.loadApplicationDeployments();
+ },
+ updateComputeResourceHostId(event) {
+ const [computeResourceHostId] = event.detail;
+ this.userConfigurationData.computationalResourceScheduling.resourceHostId = computeResourceHostId;
+ this.emitValueChanged();
+ // TODO: recalculate queues for the selected host
+ },
+ updateComputationalResourceScheduling(event) {
+ const [computationalResourceScheduling] = event.detail;
+ this.userConfigurationData.computationalResourceScheduling = computationalResourceScheduling;
+ this.emitValueChanged();
+ // TODO: recalculate maxes for the selected queue, etc.
+ },
+ async loadApplicationDeployments() {
+ this.applicationDeployments = await getApplicationDeployments(
+ this.applicationModuleId,
+ this.groupResourceProfileId
+ );
+ if (
+ !this.userConfigurationData.computationalResourceScheduling
+ .computeHostId
+ ) {
+ this.userConfigurationData.computationalResourceScheduling.resourceHostId = this.getDefaultResourceHostId();
+ }
+ },
+ cloneValue() {
+ return this.value ? new models.UserConfigurationData(this.value) : null;
+ },
+ async loadData() {
+ if (this.groupResourceProfileId) {
+ this.loadApplicationDeployments();
+ }
+ this.loadDefaultComputeResourceId();
+ },
+ async loadDefaultComputeResourceId() {
+ this.defaultComputeResourceId = await getDefaultComputeResourceId();
+ },
+ getDefaultResourceHostId() {
+ if (
+ this.defaultComputeResourceId &&
+ this.computeResources.find(
+ (crid) => crid === this.defaultComputeResourceId
+ )
+ ) {
+ return this.defaultComputeResourceId;
+ } else if (this.computeResources.length > 0) {
+ // Just pick the first one
+ return this.computeResources[0];
+ }
+ },
+ bindWebComponentProps() {
+ this.$nextTick(() => {
+ this.$refs.computeResourceSelector.computeResources = this.computeResources;
+ this.$refs.computeResourceSelector.value = this.resourceHostId;
+ });
+ },
+ },
+ watch: {
+ value() {
+ this.userConfigurationData = this.cloneValue();
+ this.loadData();
+ },
+ computeResources: "bindWebComponentProps",
+ resourceHostId: "bindWebComponentProps",
+ },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js
index 387ca5a..866fcea 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/web-components/store.js
@@ -1,4 +1,4 @@
-import { services } from "django-airavata-api";
+import { errors, services, utils } from "django-airavata-api";
const CACHE = {
APPLICATION_MODULES: {},
APPLICATION_INTERFACES: {},
@@ -16,6 +16,8 @@ export async function getApplicationModule(applicationId) {
}
export async function getApplicationInterfaceForModule(applicationId) {
+ // TODO: I'm not sure this is the right pattern. Perhaps the promise should be
+ // put in the cache and the cache entry should be 'await'-ed.
if (applicationId in CACHE.APPLICATION_INTERFACES) {
return CACHE.APPLICATION_INTERFACES[applicationId];
}
@@ -54,6 +56,11 @@ export async function getDefaultGroupResourceProfileId() {
return prefs.most_recent_group_resource_profile_id;
}
+export async function getDefaultComputeResourceId() {
+ const prefs = await getWorkspacePreferences();
+ return prefs.most_recent_compute_resource_id;
+}
+
export async function getExperiment(experimentId) {
return await services.ExperimentService.retrieve({ lookup: experimentId });
}
@@ -65,3 +72,29 @@ export async function getProjects() {
export async function getGroupResourceProfiles() {
return await services.GroupResourceProfileService.list();
}
+
+export async function getApplicationDeployments(
+ applicationId,
+ groupResourceProfileId
+) {
+ return await services.ApplicationDeploymentService.list(
+ {
+ appModuleId: applicationId,
+ groupResourceProfileId: groupResourceProfileId,
+ },
+ { ignoreErrors: true }
+ )
+ .catch((error) => {
+ // Ignore unauthorized errors, force user to pick another GroupResourceProfile
+ if (!errors.ErrorUtils.isUnauthorizedError(error)) {
+ return Promise.reject(error);
+ }
+ })
+ // Report all other error types
+ .catch(utils.FetchUtils.reportError);
+}
+
+export async function getComputeResourceNames() {
+ // TODO: cache these
+ return await services.ComputeResourceService.names();
+}