Merge branch 'AIRAVATA-3654' into develop
diff --git a/README.md b/README.md
index d0623ed..5f776a2 100644
--- a/README.md
+++ b/README.md
@@ -24,8 +24,9 @@
 [the most recent LTS version of Node.js](https://nodejs.org/en/download/). You
 can also use [nvm](https://github.com/nvm-sh/nvm) to manage the Node.js install.
 If you have nvm installed you can run `nvm install && nvm use` before running
-any yarn commands. See [the Yarn package manager](https://yarnpkg.com) for
-information on how to install yarn.
+any yarn commands. See
+[the Yarn package manager](https://classic.yarnpkg.com/lang/en/) for information
+on how to install Yarn 1 (Classic).
 
 1.  Checkout this project and create a virtual environment.
 
diff --git a/django_airavata/apps/admin/apps.py b/django_airavata/apps/admin/apps.py
index 82876b9..a9a6642 100644
--- a/django_airavata/apps/admin/apps.py
+++ b/django_airavata/apps/admin/apps.py
@@ -25,7 +25,7 @@
             'label': 'Manage Users',
             'icon': 'fa fa-users',
             'url': 'django_airavata_admin:users',
-            'active_prefixes': ['users'],
+            'active_prefixes': ['users', 'extended-user-profile'],
             'enabled': lambda req: (req.is_gateway_admin or
                                     req.is_read_only_gateway_admin),
         },
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInputFieldEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInputFieldEditor.vue
index 417bc44..e87e4f1 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInputFieldEditor.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationInputFieldEditor.vue
@@ -66,15 +66,29 @@
           :disabled="readonly"
         ></b-form-input>
       </b-form-group>
+      <b-form-group
+        class="flex-fill"
+        label="Required on Command Line"
+        :label-for="id + '-required-command-line'"
+        description="Add this input's value to the command line in the generated job script."
+      >
+        <b-form-radio-group
+          :id="id + '-required-command-line'"
+          v-model="data.requiredToAddedToCommandLine"
+          :options="trueFalseOptions"
+          :disabled="readonly"
+        >
+        </b-form-radio-group>
+      </b-form-group>
       <div class="d-flex">
         <b-form-group
           class="flex-fill"
-          label="Standard Input"
-          :label-for="id + '-standard-input'"
+          label="Required"
+          :label-for="id + '-required'"
         >
           <b-form-radio-group
-            :id="id + '-standard-input'"
-            v-model="data.standardInput"
+            :id="id + '-required'"
+            v-model="data.isRequired"
             :options="trueFalseOptions"
             :disabled="readonly"
           >
@@ -105,47 +119,6 @@
           :disabled="readonly"
         />
       </b-form-group>
-      <div class="d-flex">
-        <b-form-group
-          class="flex-fill"
-          label="Data is staged"
-          :label-for="id + '-data-staged'"
-        >
-          <b-form-radio-group
-            :id="id + '-data-staged'"
-            v-model="data.dataStaged"
-            :options="trueFalseOptions"
-            :disabled="readonly"
-          >
-          </b-form-radio-group>
-        </b-form-group>
-        <b-form-group
-          class="flex-fill"
-          label="Required"
-          :label-for="id + '-required'"
-        >
-          <b-form-radio-group
-            :id="id + '-required'"
-            v-model="data.isRequired"
-            :options="trueFalseOptions"
-            :disabled="readonly"
-          >
-          </b-form-radio-group>
-        </b-form-group>
-      </div>
-      <b-form-group
-        class="flex-fill"
-        label="Required on Command Line"
-        :label-for="id + '-required-command-line'"
-      >
-        <b-form-radio-group
-          :id="id + '-required-command-line'"
-          v-model="data.requiredToAddedToCommandLine"
-          :options="trueFalseOptions"
-          :disabled="readonly"
-        >
-        </b-form-radio-group>
-      </b-form-group>
       <b-form-group
         label="Advanced Input Field Modification Metadata"
         :label-for="id + '-metadata'"
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 c33c8c4..3114370 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
@@ -37,6 +37,22 @@
             cores, walltime limit).
           </div>
         </b-form-group>
+        <b-form-group
+          label="Queue Settings Calculator"
+          description="Select function to automatically compute queue settings."
+        >
+          <b-form-select
+            v-model="data.queueSettingsCalculatorId"
+            :options="queueSettingsCalculatorOptions"
+            :disabled="queueSettingsCalculatorOptions.length === 0"
+          >
+            <template slot="first">
+              <option :value="null">
+                If applicable, select a queue settings calculator
+              </option>
+            </template>
+          </b-form-select>
+        </b-form-group>
       </div>
     </div>
     <div class="w-100">
@@ -45,9 +61,13 @@
         label-for="application-description"
       >
         <b-form-textarea
-          id="application-description" :rows="5"
+          id="application-description"
+          :rows="5"
           v-model="data.applicationDescription"
-          :state="!data.applicationDescription || data.applicationDescription.length < 500"
+          :state="
+            !data.applicationDescription ||
+            data.applicationDescription.length < 500
+          "
         >
         </b-form-textarea>
         <b-form-valid-feedback v-if="!!data.applicationDescription">
@@ -120,8 +140,8 @@
 </template>
 
 <script>
-import {models} from "django-airavata-api";
-import {mixins} from "django-airavata-common-ui";
+import { models, services } from "django-airavata-api";
+import { mixins } from "django-airavata-common-ui";
 import ApplicationInputFieldEditor from "./ApplicationInputFieldEditor.vue";
 import ApplicationOutputFieldEditor from "./ApplicationOutputFieldEditor.vue";
 
@@ -144,13 +164,28 @@
     ApplicationOutputFieldEditor,
     draggable,
   },
+  created() {
+    this.loadQueueSettingsCalculators();
+  },
   computed: {
     trueFalseOptions() {
       return [
-        {text: "True", value: true},
-        {text: "False", value: false},
+        { text: "True", value: true },
+        { text: "False", value: false },
       ];
     },
+    queueSettingsCalculatorOptions() {
+      if (this.queueSettingsCalculators) {
+        return this.queueSettingsCalculators.map((qsc) => {
+          return {
+            text: qsc.name,
+            value: qsc.id,
+          };
+        });
+      } else {
+        return [];
+      }
+    },
   },
   data() {
     return {
@@ -160,6 +195,7 @@
         handle: ".drag-handle",
       },
       collapseApplicationInputs: false,
+      queueSettingsCalculators: null,
     };
   },
   methods: {
@@ -209,6 +245,9 @@
     onDragEnd() {
       this.collapseApplicationInputs = false;
     },
+    async loadQueueSettingsCalculators() {
+      this.queueSettingsCalculators = await services.QueueSettingsCalculatorService.list();
+    },
   },
 };
 </script>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfileContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfileContainer.vue
new file mode 100644
index 0000000..8a38d1b
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfileContainer.vue
@@ -0,0 +1,120 @@
+<template>
+  <div class="has-fixed-footer">
+    <div class="row mb-2">
+      <div class="col-auto mr-auto">
+        <h1 class="h4">Extended User Profile Editor</h1>
+        <p class="text-muted small">
+          Add and edit additional user profile fields for gateway users to
+          complete.
+        </p>
+      </div>
+    </div>
+    <transition-group name="fade">
+      <div
+        v-for="field in extendedUserProfileFields"
+        class="row"
+        :key="field.key"
+      >
+        <div class="col">
+          <extended-user-profile-field-editor
+            :extendedUserProfileField="field"
+            @valid="recordValidChildComponent(field)"
+            @invalid="recordInvalidChildComponent(field)"
+          />
+        </div>
+      </div>
+    </transition-group>
+    <div ref="bottom" />
+    <div class="fixed-footer">
+      <b-dropdown text="Add Field">
+        <b-dropdown-item @click="addField('text')">Text</b-dropdown-item>
+        <b-dropdown-item @click="addField('single_choice')"
+          >Single Choice</b-dropdown-item
+        >
+        <b-dropdown-item @click="addField('multi_choice')"
+          >Multi Choice</b-dropdown-item
+        >
+        <b-dropdown-item @click="addField('user_agreement')"
+          >User Agreement</b-dropdown-item
+        >
+      </b-dropdown>
+      <b-button variant="primary" @click="save" :disabled="!valid" class="ml-2"
+        >Save</b-button
+      >
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapActions, mapGetters } from "vuex";
+import ExtendedUserProfileFieldEditor from "./field-editors/ExtendedUserProfileFieldEditor.vue";
+import { mixins } from "django-airavata-common-ui";
+export default {
+  mixins: [mixins.ValidationParent],
+  components: { ExtendedUserProfileFieldEditor },
+  data() {
+    return {};
+  },
+  created() {
+    this.loadExtendedUserProfileFields();
+  },
+  methods: {
+    ...mapActions("extendedUserProfile", [
+      "loadExtendedUserProfileFields",
+      "saveExtendedUserProfileFields",
+      "addExtendedUserProfileField",
+    ]),
+    addField(field_type) {
+      this.addExtendedUserProfileField({ field_type });
+      this.$nextTick(() => {
+        this.$refs.bottom.scrollIntoView();
+      });
+    },
+    addOption(field) {
+      if (!field.options) {
+        field.options = [];
+      }
+      field.options.push({ id: null, name: "" });
+    },
+    deleteOption(field, option) {
+      const i = field.options.indexOf(option);
+      field.options.splice(i, 1);
+    },
+    addLink(field) {
+      if (!field.links) {
+        field.links = [];
+      }
+      field.links.push({
+        id: null,
+        url: "",
+        title: "",
+        display_link: true,
+        display_inline: false,
+      });
+    },
+    addConditional(field) {
+      if (!field.conditional) {
+        field.conditional = {
+          id: null,
+          conditions: [],
+          require_when: true,
+          show_when: true,
+        };
+      }
+    },
+    deleteLink(field, link) {
+      const i = field.links.indexOf(link);
+      field.links.splice(i, 1);
+    },
+    save() {
+      this.saveExtendedUserProfileFields();
+    },
+  },
+  computed: {
+    ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]),
+    valid() {
+      return this.childComponentsAreValid;
+    },
+  },
+};
+</script>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
new file mode 100644
index 0000000..c3e376c
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/ExtendedUserProfilePanel.vue
@@ -0,0 +1,82 @@
+<template>
+  <b-card header="Extended User Profile">
+    <b-table :items="items" :fields="fields" small borderless>
+      <template #cell(value)="{ value, item }">
+        <!-- only show a valid checkmark when there is a user provided value -->
+        <i v-if="value && item.valid" class="fas fa-check text-success"></i>
+        <i v-if="!item.valid" class="fas fa-times text-danger"></i>
+        <template v-if="Array.isArray(value)">
+          <ul>
+            <li v-for="result in value" :key="result">
+              {{ result }}
+            </li>
+          </ul>
+        </template>
+        <template v-else> {{ value }} </template>
+      </template>
+    </b-table>
+  </b-card>
+</template>
+
+<script>
+import { models } from "django-airavata-api";
+import { mapActions, mapGetters } from "vuex";
+export default {
+  props: {
+    iamUserProfile: {
+      type: models.IAMUserProfile,
+      required: true,
+    },
+  },
+  created() {
+    this.loadExtendedUserProfileFields();
+    this.loadExtendedUserProfileValues({
+      username: this.iamUserProfile.userId,
+    });
+  },
+  computed: {
+    ...mapGetters("extendedUserProfile", [
+      "extendedUserProfileFields",
+      "extendedUserProfileValues",
+    ]),
+    fields() {
+      return ["name", "value"];
+    },
+    items() {
+      if (this.extendedUserProfileFields && this.extendedUserProfileValues) {
+        const items = [];
+        for (const field of this.extendedUserProfileFields) {
+          const value = this.getValue(field);
+          items.push({
+            name: field.name,
+            value: value ? value.value_display : null,
+            // if no value, consider it invalid only if it is required
+            valid: value ? value.valid : !field.required,
+          });
+        }
+        return items;
+      } else {
+        return [];
+      }
+    },
+  },
+  methods: {
+    ...mapActions("extendedUserProfile", [
+      "loadExtendedUserProfileFields",
+      "loadExtendedUserProfileValues",
+    ]),
+    getValue(field) {
+      return this.extendedUserProfileValues.find(
+        (v) => v.ext_user_profile_field === field.id
+      );
+    },
+  },
+};
+</script>
+
+<style scoped>
+ul {
+  display: inline-block;
+  padding-left: 20px;
+}
+</style>
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
index bc27702..ed0f762 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserDetailsContainer.vue
@@ -25,6 +25,7 @@
         @save="groupsUpdated"
       />
       <user-profile-panel :iamUserProfile="iamUserProfile" />
+      <extended-user-profile-panel :iamUserProfile="iamUserProfile" />
       <external-idp-user-info-panel
         v-if="hasExternalIDPUserInfo"
         :externalIDPUserInfo="localIAMUserProfile.externalIDPUserInfo"
@@ -78,6 +79,7 @@
 import EditGroupsPanel from "./EditGroupsPanel.vue";
 import ExternalIDPUserInfoPanel from "./ExternalIDPUserInfoPanel.vue";
 import UserProfilePanel from "./UserProfilePanel.vue";
+import ExtendedUserProfilePanel from "./ExtendedUserProfilePanel.vue";
 
 export default {
   name: "user-details-container",
@@ -100,6 +102,7 @@
     EditGroupsPanel,
     "external-idp-user-info-panel": ExternalIDPUserInfoPanel,
     UserProfilePanel,
+    ExtendedUserProfilePanel,
   },
   data() {
     return {
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
index 7eeeff4..c03f7eb 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/UserManagementContainer.vue
@@ -5,6 +5,11 @@
         <h1 class="h4 mb-4">Manage Users</h1>
       </div>
       <div class="col-auto">
+        <b-button :to="{ name: 'extended-user-profile' }"
+          >Extended User Profile</b-button
+        >
+      </div>
+      <div class="col-auto">
         <b-dropdown :text="menuText">
           <b-dropdown-item
             :to="{ name: 'identity-service-users' }"
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/field-editors/ExtendedUserProfileFieldEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/field-editors/ExtendedUserProfileFieldEditor.vue
new file mode 100644
index 0000000..7f1371b
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/users/field-editors/ExtendedUserProfileFieldEditor.vue
@@ -0,0 +1,366 @@
+<template>
+  <b-card :title="title">
+    <b-form-group label="Name" label-cols="3">
+      <b-form-input v-model="name" :state="validateState($v.name)" />
+      <b-form-invalid-feedback :state="validateState($v.name)"
+        >This field is required.</b-form-invalid-feedback
+      >
+    </b-form-group>
+    <b-form-group label="Help text" label-cols="3">
+      <b-form-input v-model="help_text" />
+    </b-form-group>
+    <b-form-group>
+      <b-form-checkbox v-model="required" switch> Required </b-form-checkbox>
+    </b-form-group>
+    <b-card title="Options" v-if="extendedUserProfileField.supportsChoices">
+      <transition-group name="fade">
+        <template
+          v-for="({ $model: choice, display_text: $v_display_text },
+          index) in $v.choices.$each.$iter"
+        >
+          <b-form-group :key="choice.key">
+            <b-input-group>
+              <b-form-input
+                :value="choice.display_text"
+                @input="
+                  handleChoiceDisplayTextChanged(
+                    choice,
+                    $event,
+                    $v_display_text
+                  )
+                "
+                :state="validateState($v_display_text)"
+              />
+              <b-input-group-append>
+                <b-button
+                  @click="handleChoiceMoveUp(choice)"
+                  :disabled="index === 0"
+                  v-b-tooltip.hover.left
+                  title="Move Up"
+                >
+                  <i class="fa fa-arrow-up" aria-hidden="true"></i>
+                </b-button>
+                <b-button
+                  @click="handleChoiceMoveDown(choice)"
+                  :disabled="
+                    index === extendedUserProfileField.choices.length - 1
+                  "
+                  v-b-tooltip.hover.left
+                  title="Move Down"
+                >
+                  <i class="fa fa-arrow-down" aria-hidden="true"></i>
+                </b-button>
+                <b-button
+                  @click="handleChoiceDeleted(choice)"
+                  variant="danger"
+                  v-b-tooltip.hover.left
+                  title="Delete Option"
+                >
+                  <i class="fa fa-trash" aria-hidden="true"></i>
+                </b-button>
+              </b-input-group-append>
+            </b-input-group>
+            <b-form-invalid-feedback :state="validateState($v_display_text)"
+              >This field is required.</b-form-invalid-feedback
+            >
+          </b-form-group>
+        </template>
+        <b-form-group :key="'other'" v-if="extendedUserProfileField.other">
+          <b-input-group>
+            <b-form-input
+              placeholder="User will see: Other (please specify)"
+              disabled
+            />
+            <b-input-group-append>
+              <b-button disabled>
+                <i class="fa fa-arrow-up" aria-hidden="true"></i>
+              </b-button>
+              <b-button disabled>
+                <i class="fa fa-arrow-down" aria-hidden="true"></i>
+              </b-button>
+              <b-button
+                @click="other = false"
+                variant="danger"
+                v-b-tooltip.hover.left
+                title="Remove Other option"
+              >
+                <i class="fa fa-trash" aria-hidden="true"></i>
+              </b-button>
+            </b-input-group-append>
+          </b-input-group>
+        </b-form-group>
+      </transition-group>
+      <b-form-group>
+        <b-button
+          @click="addChoice({ field: extendedUserProfileField })"
+          size="sm"
+          >Add Option</b-button
+        >
+      </b-form-group>
+      <b-form-group>
+        <b-form-checkbox v-model="other" switch>
+          Allow user to type in an "Other" option
+        </b-form-checkbox>
+      </b-form-group>
+    </b-card>
+
+    <template v-if="links && links.length > 0">
+      <transition-group name="fade">
+        <b-card
+          :title="`Link: ${link.label}`"
+          v-for="{ $model: link, label: $v_label, url: $v_url } in $v.links
+            .$each.$iter"
+          :key="link.key"
+        >
+          <b-form-group label="Label" label-cols="3">
+            <b-form-input
+              :value="link.label"
+              @input="handleLinkLabelChanged(link, $event, $v_label)"
+              :state="validateState($v_label)"
+            />
+            <b-form-invalid-feedback :state="validateState($v_label)"
+              >This field is required.</b-form-invalid-feedback
+            >
+          </b-form-group>
+          <b-form-group label="URL" label-cols="3">
+            <b-form-input
+              :value="link.url"
+              @input="handleLinkURLChanged(link, $event, $v_url)"
+              :state="validateState($v_url)"
+            />
+            <b-form-invalid-feedback :state="validateState($v_url)"
+              >This field is required.</b-form-invalid-feedback
+            >
+          </b-form-group>
+          <b-row>
+            <b-col>
+              <b-form-group>
+                <b-form-checkbox
+                  :checked="link.display_link"
+                  @input="handleLinkDisplayLinkChanged(link, $event)"
+                  switch
+                >
+                  Show as link?
+                </b-form-checkbox>
+              </b-form-group>
+            </b-col>
+            <b-col>
+              <b-form-group>
+                <b-form-checkbox
+                  :checked="link.display_inline"
+                  @input="handleLinkDisplayInlineChanged(link, $event)"
+                  switch
+                >
+                  Show inline?
+                </b-form-checkbox>
+              </b-form-group>
+            </b-col>
+          </b-row>
+          <b-button @click="handleLinkDeleted(link)" variant="danger" size="sm">
+            Delete Link
+          </b-button>
+        </b-card>
+      </transition-group>
+    </template>
+    <b-button @click="addLink({ field: extendedUserProfileField })" size="sm"
+      >Add Link</b-button
+    >
+    <b-button
+      @click="handleMoveUp({ field: extendedUserProfileField })"
+      :disabled="
+        extendedUserProfileFields.indexOf(extendedUserProfileField) === 0
+      "
+      size="sm"
+      >Move Up</b-button
+    >
+    <b-button
+      @click="handleMoveDown({ field: extendedUserProfileField })"
+      :disabled="
+        extendedUserProfileFields.indexOf(extendedUserProfileField) ===
+        extendedUserProfileFields.length - 1
+      "
+      size="sm"
+      >Move Down</b-button
+    >
+    <b-button @click="handleDelete" variant="danger" size="sm">Delete</b-button>
+  </b-card>
+</template>
+
+<script>
+import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { required } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
+export default {
+  mixins: [validationMixin],
+  props: ["extendedUserProfileField"],
+  computed: {
+    ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]),
+    name: {
+      get() {
+        return this.extendedUserProfileField.name;
+      },
+      set(value) {
+        this.setName({ value, field: this.extendedUserProfileField });
+        this.$v.name.$touch();
+      },
+    },
+    help_text: {
+      get() {
+        return this.extendedUserProfileField.help_text;
+      },
+      set(value) {
+        this.setHelpText({ value, field: this.extendedUserProfileField });
+      },
+    },
+    required: {
+      get() {
+        return this.extendedUserProfileField.required;
+      },
+      set(value) {
+        this.setRequired({ value, field: this.extendedUserProfileField });
+      },
+    },
+    other: {
+      get() {
+        return this.extendedUserProfileField.other;
+      },
+      set(value) {
+        this.setOther({ value, field: this.extendedUserProfileField });
+      },
+    },
+    title() {
+      const fieldTypes = {
+        text: "Text",
+        single_choice: "Single Choice",
+        multi_choice: "Multi Choice",
+        user_agreement: "User Agreement",
+      };
+      return `${fieldTypes[this.extendedUserProfileField.field_type]}: ${
+        this.name
+      }`;
+    },
+    choices() {
+      return this.extendedUserProfileField.choices;
+    },
+    links() {
+      return this.extendedUserProfileField.links;
+    },
+    valid() {
+      return !this.$v.$invalid;
+    },
+  },
+  validations() {
+    return {
+      name: {
+        required,
+      },
+      choices: {
+        $each: {
+          display_text: {
+            required,
+          },
+        },
+      },
+      links: {
+        $each: {
+          label: {
+            required,
+          },
+          url: {
+            required,
+          },
+        },
+      },
+    };
+  },
+  methods: {
+    ...mapMutations("extendedUserProfile", [
+      "setName",
+      "setHelpText",
+      "setRequired",
+      "setOther",
+      "addChoice",
+      "updateChoiceDisplayText",
+      "deleteChoice",
+      "updateChoiceIndex",
+      "addLink",
+      "updateLinkLabel",
+      "updateLinkURL",
+      "updateLinkDisplayLink",
+      "updateLinkDisplayInline",
+      "deleteLink",
+      "updateFieldIndex",
+      "deleteField",
+    ]),
+    handleChoiceDisplayTextChanged(choice, display_text, $v) {
+      this.updateChoiceDisplayText({ choice, display_text });
+      $v.$touch();
+    },
+    handleChoiceDeleted(choice) {
+      this.deleteChoice({ field: this.extendedUserProfileField, choice });
+    },
+    handleChoiceMoveUp(choice) {
+      let index = this.extendedUserProfileField.choices.indexOf(choice);
+      index--;
+      this.updateChoiceIndex({
+        field: this.extendedUserProfileField,
+        choice,
+        index,
+      });
+    },
+    handleChoiceMoveDown(choice) {
+      let index = this.extendedUserProfileField.choices.indexOf(choice);
+      index++;
+      this.updateChoiceIndex({
+        field: this.extendedUserProfileField,
+        choice,
+        index,
+      });
+    },
+    handleLinkLabelChanged(link, label, $v) {
+      this.updateLinkLabel({ link, label });
+      $v.$touch();
+    },
+    handleLinkURLChanged(link, url, $v) {
+      this.updateLinkURL({ link, url });
+      $v.$touch();
+    },
+    handleLinkDisplayLinkChanged(link, display_link) {
+      this.updateLinkDisplayLink({ link, display_link });
+    },
+    handleLinkDisplayInlineChanged(link, display_inline) {
+      this.updateLinkDisplayInline({ link, display_inline });
+    },
+    handleLinkDeleted(link) {
+      this.deleteLink({ field: this.extendedUserProfileField, link });
+    },
+    handleMoveUp({ field }) {
+      let index = this.extendedUserProfileFields.indexOf(field);
+      index--;
+      this.updateFieldIndex({ field, index });
+    },
+    handleMoveDown({ field }) {
+      let index = this.extendedUserProfileFields.indexOf(field);
+      index++;
+      this.updateFieldIndex({ field, index });
+    },
+    handleDelete() {
+      this.deleteField({
+        field: this.extendedUserProfileField,
+      });
+    },
+    validateState: errors.vuelidateHelpers.validateState,
+  },
+  watch: {
+    valid: {
+      handler(valid) {
+        this.$emit(valid ? "valid" : "invalid");
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+
+<style></style>
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 8feb8d9..8d0fb8f 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
@@ -6,6 +6,7 @@
 import router from "./router";
 
 import "flatpickr/dist/flatpickr.css";
+import createStore from "./store";
 
 entry((Vue) => {
   Vue.config.productionTip = false;
@@ -14,7 +15,10 @@
   Vue.use(VueRouter);
   Vue.use(VueFlatPickr);
 
+  const store = createStore(Vue);
+
   new Vue({
+    store,
     render: (h) => h(components.MainLayout, [h(App)]),
     router,
   }).$mount("#app");
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/router.js b/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
index de0c0d4..b5e722e 100644
--- a/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/router.js
@@ -9,6 +9,7 @@
 import CredentialStoreDashboard from "./components/dashboards/CredentialStoreDashboard";
 import DevelopersContainer from "./components/developers//DevelopersContainer.vue";
 import ExperimentStatisticsContainer from "./components/statistics/ExperimentStatisticsContainer";
+import ExtendedUserProfileContainer from "./components/users/ExtendedUserProfileContainer";
 import GatewayResourceProfileEditorContainer from "./components/gatewayprofile/GatewayResourceProfileEditorContainer.vue";
 import GroupComputeResourcePreference from "./components/admin/group_resource_preferences/GroupComputeResourcePreference";
 import IdentityServiceUserManagementContainer from "./components/users/IdentityServiceUserManagementContainer.vue";
@@ -141,6 +142,11 @@
     ],
   },
   {
+    path: "/extended-user-profile",
+    component: ExtendedUserProfileContainer,
+    name: "extended-user-profile",
+  },
+  {
     path: "/notices",
     component: NoticesManagementContainer,
     name: "notices",
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/store/index.js b/django_airavata/apps/admin/static/django_airavata_admin/src/store/index.js
new file mode 100644
index 0000000..0ee4817
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/store/index.js
@@ -0,0 +1,16 @@
+import Vuex from "vuex";
+import extendedUserProfile from "./modules/extendedUserProfile";
+
+const debug = process.env.NODE_ENV !== "production";
+
+function createStore(Vue) {
+  Vue.use(Vuex);
+  return new Vuex.Store({
+    modules: {
+      extendedUserProfile,
+    },
+    strict: debug,
+  });
+}
+
+export default createStore;
diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/store/modules/extendedUserProfile.js b/django_airavata/apps/admin/static/django_airavata_admin/src/store/modules/extendedUserProfile.js
new file mode 100644
index 0000000..a058bee
--- /dev/null
+++ b/django_airavata/apps/admin/static/django_airavata_admin/src/store/modules/extendedUserProfile.js
@@ -0,0 +1,190 @@
+import { models, services } from "django-airavata-api";
+
+const state = () => ({
+  extendedUserProfileFields: null,
+  extendedUserProfileValues: null,
+  deletedExtendedUserProfileFields: [],
+});
+
+const getters = {
+  extendedUserProfileFields: (state) => state.extendedUserProfileFields,
+  extendedUserProfileValues: (state) => state.extendedUserProfileValues,
+};
+
+const actions = {
+  async loadExtendedUserProfileFields({ commit }) {
+    const extendedUserProfileFields = await services.ExtendedUserProfileFieldService.list();
+    commit("setExtendedUserProfileFields", { extendedUserProfileFields });
+  },
+  async loadExtendedUserProfileValues({ commit }, { username }) {
+    const extendedUserProfileValues = await services.ExtendedUserProfileValueService.list(
+      { username }
+    );
+    commit("setExtendedUserProfileValues", { extendedUserProfileValues });
+  },
+  async saveExtendedUserProfileFields({ commit, dispatch, state }) {
+    let order = 1;
+    for (const field of state.extendedUserProfileFields) {
+      commit("setOrder", { field, order: order++ });
+      if (field.supportsChoices) {
+        for (let index = 0; index < field.choices.length; index++) {
+          const choice = field.choices[index];
+          commit("setChoiceOrder", { choice, order: index });
+        }
+      }
+      for (let index = 0; index < field.links.length; index++) {
+        const link = field.links[index];
+        commit("setLinkOrder", { link, order: index });
+      }
+      // Create or update each field
+      if (field.id) {
+        await services.ExtendedUserProfileFieldService.update({
+          lookup: field.id,
+          data: field,
+        });
+      } else {
+        await services.ExtendedUserProfileFieldService.create({ data: field });
+      }
+    }
+    if (state.deletedExtendedUserProfileFields.length > 0) {
+      for (const field of state.deletedExtendedUserProfileFields) {
+        await services.ExtendedUserProfileFieldService.delete({
+          lookup: field.id,
+        });
+      }
+      commit("resetDeletedExtendedUserProfileFields");
+    }
+    // Reload the fields
+    dispatch("loadExtendedUserProfileFields");
+  },
+  async addExtendedUserProfileField({ state, commit }, { field_type }) {
+    const field = new models.ExtendedUserProfileField({
+      field_type,
+      name: `New Field ${state.extendedUserProfileFields.length + 1}`,
+      description: "",
+      help_text: "",
+      required: true,
+      links: [],
+      other: false,
+      choices: [],
+      checkbox_label: "",
+    });
+    commit("addExtendedUserProfileField", { field });
+  },
+};
+
+function getField(state, field) {
+  const extendedUserProfileField = state.extendedUserProfileFields.find(
+    (f) => f === field
+  );
+  return extendedUserProfileField;
+}
+function setFieldProp(state, field, prop, value) {
+  const extendedUserProfileField = getField(state, field);
+  extendedUserProfileField[prop] = value;
+}
+
+const mutations = {
+  setExtendedUserProfileFields(state, { extendedUserProfileFields }) {
+    state.extendedUserProfileFields = extendedUserProfileFields;
+  },
+  setExtendedUserProfileValues(state, { extendedUserProfileValues }) {
+    state.extendedUserProfileValues = extendedUserProfileValues;
+  },
+  setName(state, { value, field }) {
+    setFieldProp(state, field, "name", value);
+  },
+  setHelpText(state, { value, field }) {
+    setFieldProp(state, field, "help_text", value);
+  },
+  setRequired(state, { value, field }) {
+    setFieldProp(state, field, "required", value);
+  },
+  setOrder(state, { order, field }) {
+    setFieldProp(state, field, "order", order);
+  },
+  setOther(state, { value, field }) {
+    setFieldProp(state, field, "other", value);
+  },
+  addExtendedUserProfileField(state, { field }) {
+    if (!state.extendedUserProfileFields) {
+      state.extendedUserProfileFields = [];
+    }
+    state.extendedUserProfileFields.push(field);
+  },
+  addChoice(state, { field }) {
+    field.choices.push(
+      new models.ExtendedUserProfileFieldChoice({
+        display_text: "",
+      })
+    );
+  },
+  setChoiceOrder(state, { choice, order }) {
+    choice.order = order;
+  },
+  updateChoiceDisplayText(state, { choice, display_text }) {
+    choice.display_text = display_text;
+  },
+  updateChoiceIndex(state, { field, choice, index }) {
+    const currentIndex = field.choices.indexOf(choice);
+    field.choices.splice(currentIndex, 1);
+    field.choices.splice(index, 0, choice);
+  },
+  deleteChoice(state, { field, choice }) {
+    const index = field.choices.indexOf(choice);
+    field.choices.splice(index, 1);
+  },
+  addLink(state, { field }) {
+    field.links.push(
+      new models.ExtendedUserProfileFieldLink({
+        label: "",
+        url: "",
+        display_link: true,
+        display_inline: false,
+      })
+    );
+  },
+  updateLinkLabel(state, { link, label }) {
+    link.label = label;
+  },
+  updateLinkURL(state, { link, url }) {
+    link.url = url;
+  },
+  updateLinkDisplayLink(state, { link, display_link }) {
+    link.display_link = display_link;
+  },
+  updateLinkDisplayInline(state, { link, display_inline }) {
+    link.display_inline = display_inline;
+  },
+  setLinkOrder(state, { link, order }) {
+    link.order = order;
+  },
+  deleteLink(state, { field, link }) {
+    const index = field.links.indexOf(link);
+    field.links.splice(index, 1);
+  },
+  updateFieldIndex(state, { field, index }) {
+    const currentIndex = state.extendedUserProfileFields.indexOf(field);
+    state.extendedUserProfileFields.splice(currentIndex, 1);
+    state.extendedUserProfileFields.splice(index, 0, field);
+  },
+  deleteField(state, { field }) {
+    const index = state.extendedUserProfileFields.indexOf(field);
+    state.extendedUserProfileFields.splice(index, 1);
+    // later when we save we'll need to sync this delete with the server
+    if (field.id) {
+      state.deletedExtendedUserProfileFields.push(field);
+    }
+  },
+  resetDeletedExtendedUserProfileFields(state) {
+    state.deletedExtendedUserProfileFields = [];
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  getters,
+  actions,
+  mutations,
+};
diff --git a/django_airavata/apps/admin/urls.py b/django_airavata/apps/admin/urls.py
index 24a4b90..f526727 100644
--- a/django_airavata/apps/admin/urls.py
+++ b/django_airavata/apps/admin/urls.py
@@ -15,5 +15,6 @@
             name='gateway_resource_profile'),
     re_path(r'^notices/', views.notices, name='notices'),
     re_path(r'^users/', views.users, name='users'),
+    path('extended-user-profile/', views.extended_user_profile, name="extended_user_profile"),
     path('developers/', views.developers, name='developers'),
 ]
diff --git a/django_airavata/apps/admin/views.py b/django_airavata/apps/admin/views.py
index caca63a..e8be104 100644
--- a/django_airavata/apps/admin/views.py
+++ b/django_airavata/apps/admin/views.py
@@ -54,6 +54,12 @@
 
 
 @login_required
+def extended_user_profile(request):
+    request.active_nav_item = 'users'
+    return render(request, 'admin/admin_base.html')
+
+
+@login_required
 def experiment_statistics(request):
     request.active_nav_item = 'experiment-statistics'
     return render(request, 'admin/admin_base.html')
diff --git a/django_airavata/apps/api/migrations/0009_applicationsettings_queue_settings_calculator_id.py b/django_airavata/apps/api/migrations/0009_applicationsettings_queue_settings_calculator_id.py
new file mode 100644
index 0000000..bc7bf8f
--- /dev/null
+++ b/django_airavata/apps/api/migrations/0009_applicationsettings_queue_settings_calculator_id.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.15 on 2022-08-25 13:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_airavata_api', '0008_merge_20220601_1951'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='applicationsettings',
+            name='queue_settings_calculator_id',
+            field=models.CharField(max_length=255, null=True),
+        ),
+    ]
diff --git a/django_airavata/apps/api/models.py b/django_airavata/apps/api/models.py
index 829c88f..45c8816 100644
--- a/django_airavata/apps/api/models.py
+++ b/django_airavata/apps/api/models.py
@@ -72,3 +72,4 @@
 class ApplicationSettings(models.Model):
     application_module_id = models.CharField(max_length=255, unique=True)
     show_queue_settings = models.BooleanField(default=True)
+    queue_settings_calculator_id = models.CharField(max_length=255, null=True)
diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index 84d32b5..d429ae0 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -60,7 +60,11 @@
     NotificationPriority,
     Project
 )
-from airavata_django_portal_sdk import experiment_util, user_storage
+from airavata_django_portal_sdk import (
+    experiment_util,
+    queue_settings_calculators,
+    user_storage
+)
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.urls import reverse
@@ -336,6 +340,7 @@
     applicationOutputs = OutputDataObjectTypeSerializer(many=True)
     userHasWriteAccess = serializers.SerializerMethodField()
     showQueueSettings = serializers.BooleanField(required=False)
+    queueSettingsCalculatorId = serializers.CharField(allow_null=True, required=False)
 
     def to_representation(self, instance):
         representation = super().to_representation(instance)
@@ -343,28 +348,34 @@
         application_settings, created = models.ApplicationSettings.objects.get_or_create(
             application_module_id=application_module_id)
         representation["showQueueSettings"] = application_settings.show_queue_settings
+        # check that queue_settings_calculator_id exists
+        if queue_settings_calculators.exists(application_settings.queue_settings_calculator_id):
+            representation["queueSettingsCalculatorId"] = application_settings.queue_settings_calculator_id
         return representation
 
     def create(self, validated_data):
-        showQueueSettings = validated_data.pop("showQueueSettings", None)
+        showQueueSettings = validated_data.pop("showQueueSettings", True)
+        queueSettingsCalculatorId = validated_data.pop("queueSettingsCalculatorId", None)
         application_interface = super().create(validated_data)
         application_module_id = application_interface.applicationModules[0]
-        if showQueueSettings is not None:
-            models.ApplicationSettings.objects.update_or_create(
-                application_module_id=application_module_id,
-                defaults={"show_queue_settings": showQueueSettings}
-            )
+        models.ApplicationSettings.objects.update_or_create(
+            application_module_id=application_module_id,
+            defaults={"show_queue_settings": showQueueSettings,
+                      "queue_settings_calculator_id": queueSettingsCalculatorId}
+        )
         return application_interface
 
     def update(self, instance, validated_data):
-        showQueueSettings = validated_data.pop("showQueueSettings", None)
+        defaults = {}
+        if "showQueueSettings" in validated_data:
+            defaults["show_queue_settings"] = validated_data.pop("showQueueSettings")
+        if "queueSettingsCalculatorId" in validated_data:
+            defaults["queue_settings_calculator_id"] = validated_data.pop("queueSettingsCalculatorId")
         application_interface = super().update(instance, validated_data)
         application_module_id = application_interface.applicationModules[0]
-        if showQueueSettings is not None:
-            models.ApplicationSettings.objects.update_or_create(
-                application_module_id=application_module_id,
-                defaults={"show_queue_settings": showQueueSettings}
-            )
+        models.ApplicationSettings.objects.update_or_create(
+            application_module_id=application_module_id, defaults=defaults
+        )
         return application_interface
 
     def get_userHasWriteAccess(self, appDeployment):
@@ -531,6 +542,7 @@
 class DataProductSerializer(
         thrift_utils.create_serializer_class(DataProductModel)):
     creationTime = UTCPosixTimestampDateTimeField()
+    modifiedTime = UTCPosixTimestampDateTimeField()
     lastModifiedTime = UTCPosixTimestampDateTimeField()
     replicaLocations = DataReplicaLocationSerializer(many=True)
     downloadURL = serializers.SerializerMethodField()
@@ -932,6 +944,7 @@
     downloadURL = serializers.SerializerMethodField()
     dataProductURI = serializers.CharField(source='data-product-uri')
     createdTime = serializers.DateTimeField(source='created_time')
+    modifiedTime = serializers.DateTimeField(source='modified_time')
     mimeType = serializers.CharField(source='mime_type')
     size = serializers.IntegerField()
     hidden = serializers.BooleanField()
@@ -946,6 +959,7 @@
     name = serializers.CharField()
     path = serializers.CharField()
     createdTime = serializers.DateTimeField(source='created_time')
+    modifiedTime = serializers.DateTimeField(source='modified_time')
     size = serializers.IntegerField()
     hidden = serializers.BooleanField()
     url = FullyEncodedHyperlinkedIdentityField(
@@ -972,6 +986,7 @@
     name = serializers.CharField()
     path = serializers.CharField()
     createdTime = serializers.DateTimeField(source='created_time')
+    modifiedTime = serializers.DateTimeField(source='modified_time')
     size = serializers.IntegerField()
     url = serializers.SerializerMethodField()
 
@@ -1162,3 +1177,8 @@
     fileUploadMaxFileSize = serializers.IntegerField()
     tusEndpoint = serializers.CharField()
     pgaUrl = serializers.CharField()
+
+
+class QueueSettingsCalculatorSerializer(serializers.Serializer):
+    id = serializers.CharField()
+    name = serializers.CharField()
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 ca08f1d..008335c 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
@@ -18,6 +18,9 @@
 import Experiment from "./models/Experiment";
 import ExperimentSearchFields from "./models/ExperimentSearchFields";
 import ExperimentState from "./models/ExperimentState";
+import ExtendedUserProfileField from "./models/ExtendedUserProfileField";
+import ExtendedUserProfileFieldChoice from "./models/ExtendedUserProfileFieldChoice";
+import ExtendedUserProfileFieldLink from "./models/ExtendedUserProfileFieldLink";
 import FullExperiment from "./models/FullExperiment";
 import Group from "./models/Group";
 import GroupComputeResourcePreference from "./models/GroupComputeResourcePreference";
@@ -80,6 +83,9 @@
   Experiment,
   ExperimentSearchFields,
   ExperimentState,
+  ExtendedUserProfileField,
+  ExtendedUserProfileFieldChoice,
+  ExtendedUserProfileFieldLink,
   FullExperiment,
   Group,
   GroupComputeResourcePreference,
@@ -120,6 +126,12 @@
   ExperimentStoragePathService: ServiceFactory.service(
     "ExperimentStoragePaths"
   ),
+  ExtendedUserProfileFieldService: ServiceFactory.service(
+    "ExtendedUserProfileFields"
+  ),
+  ExtendedUserProfileValueService: ServiceFactory.service(
+    "ExtendedUserProfileValues"
+  ),
   FullExperimentService: ServiceFactory.service("FullExperiments"),
   GatewayResourceProfileService: ServiceFactory.service(
     "GatewayResourceProfile"
@@ -135,6 +147,9 @@
 
   ParserService: ServiceFactory.service("Parsers"),
   ProjectService: ServiceFactory.service("Projects"),
+  QueueSettingsCalculatorService: ServiceFactory.service(
+    "QueueSettingsCalculators"
+  ),
   SCPDataMovementService,
   ServiceFactory,
   SettingsService: ServiceFactory.service("Settings"),
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 008f3a0..d75164c 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
@@ -44,6 +44,11 @@
     type: "boolean",
     default: true,
   },
+  {
+    name: "queueSettingsCalculatorId",
+    type: "string",
+    default: null,
+  },
 ];
 
 export default class ApplicationInterfaceDefinition extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileField.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileField.js
new file mode 100644
index 0000000..04938ca
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileField.js
@@ -0,0 +1,76 @@
+import BaseModel from "./BaseModel";
+import ExtendedUserProfileFieldChoice from "./ExtendedUserProfileFieldChoice";
+import ExtendedUserProfileFieldLink from "./ExtendedUserProfileFieldLink";
+import uuidv4 from "uuid/v4";
+
+const FIELDS = [
+  "id",
+  "name",
+  "help_text",
+  "order",
+  {
+    name: "created_date",
+    type: "date",
+  },
+  {
+    name: "updated_date",
+    type: "date",
+  },
+  "field_type",
+  {
+    name: "links",
+    list: true,
+    type: ExtendedUserProfileFieldLink,
+  },
+  // For user_agreement type
+  "checkbox_label",
+  // For single_choice and multi_choice types
+  {
+    name: "choices",
+    list: true,
+    type: ExtendedUserProfileFieldChoice,
+  },
+  "other",
+  "required",
+];
+
+export default class ExtendedUserProfileField extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+    this._key = data.key ? data.key : uuidv4();
+  }
+
+  get key() {
+    return this._key;
+  }
+
+  toJSON() {
+    const copy = Object.assign({}, this);
+    // Remove unnecessary properties
+    switch (this.field_type) {
+      case "text":
+        delete copy["other"];
+        delete copy["choices"];
+        delete copy["checkbox_label"];
+        break;
+      case "single_choice":
+      case "multi_choice":
+        delete copy["checkbox_label"];
+        break;
+      case "user_agreement":
+        delete copy["other"];
+        delete copy["choices"];
+        break;
+      default:
+        // eslint-disable-next-line no-console
+        console.error("Unrecognized field type", this.field_type);
+        break;
+    }
+    return copy;
+  }
+  get supportsChoices() {
+    return (
+      this.field_type === "single_choice" || this.field_type === "multi_choice"
+    );
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldChoice.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldChoice.js
new file mode 100644
index 0000000..3ea1ea5
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldChoice.js
@@ -0,0 +1,24 @@
+import BaseModel from "./BaseModel";
+import uuidv4 from "uuid/v4";
+
+const FIELDS = ["id", "display_text", "order"];
+
+export default class ExtendedUserProfileFieldChoice extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+    this._key = data.key ? data.key : uuidv4();
+  }
+
+  get key() {
+    return this._key;
+  }
+
+  toJSON() {
+    const copy = Object.assign({}, this);
+    // id must either have a value or be missing, it can't be null
+    if (!copy.id) {
+      delete copy.id;
+    }
+    return copy;
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldLink.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldLink.js
new file mode 100644
index 0000000..31e007e
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileFieldLink.js
@@ -0,0 +1,31 @@
+import BaseModel from "./BaseModel";
+import uuidv4 from "uuid/v4";
+
+const FIELDS = [
+  "id",
+  "label",
+  "url",
+  "order",
+  "display_link",
+  "display_inline",
+];
+
+export default class ExtendedUserProfileFieldLink extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+    this._key = data.key ? data.key : uuidv4();
+  }
+
+  get key() {
+    return this._key;
+  }
+
+  toJSON() {
+    const copy = Object.assign({}, this);
+    // id must either have a value or be missing, it can't be null
+    if (!copy.id) {
+      delete copy.id;
+    }
+    return copy;
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js
new file mode 100644
index 0000000..02618cb
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/ExtendedUserProfileValue.js
@@ -0,0 +1,46 @@
+import BaseModel from "./BaseModel";
+
+const FIELDS = [
+  "id",
+  "value_type",
+  "ext_user_profile_field",
+  "text_value",
+  "choices",
+  "other_value",
+  "agreement_value",
+  "valid",
+  "value_display",
+];
+
+export default class ExtendedUserProfileValue extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
+
+  toJSON() {
+    const copy = Object.assign({}, this);
+    // Remove unnecessary properties
+    switch (this.value_type) {
+      case "text":
+        delete copy["other_value"];
+        delete copy["choices"];
+        delete copy["agreement_value"];
+        break;
+      case "single_choice":
+      case "multi_choice":
+        delete copy["text_value"];
+        delete copy["agreement_value"];
+        break;
+      case "user_agreement":
+        delete copy["text_value"];
+        delete copy["other_value"];
+        delete copy["choices"];
+        break;
+      default:
+        // eslint-disable-next-line no-console
+        console.error("Unrecognized value type", this.value_type);
+        break;
+    }
+    return copy;
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/Notification.js b/django_airavata/apps/api/static/django_airavata_api/js/models/Notification.js
index 52e6f11..07fd669 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/Notification.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/Notification.js
@@ -23,7 +23,11 @@
     type: NotificationPriority,
   },
   "userHasWriteAccess",
-  "showInDashboard"
+  {
+    name: "showInDashboard",
+    type: "boolean",
+    default: false,
+  },
 ];
 
 export default class Notification extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/QueueSettingsCalculator.js b/django_airavata/apps/api/static/django_airavata_api/js/models/QueueSettingsCalculator.js
new file mode 100644
index 0000000..4396d74
--- /dev/null
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/QueueSettingsCalculator.js
@@ -0,0 +1,9 @@
+import BaseModel from "./BaseModel";
+
+const FIELDS = ["id", "name"];
+
+export default class QueueSettingsCalculator extends BaseModel {
+  constructor(data = {}) {
+    super(FIELDS, data);
+  }
+}
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/User.js b/django_airavata/apps/api/static/django_airavata_api/js/models/User.js
index e414444..198a927 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/User.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/User.js
@@ -8,7 +8,8 @@
   "email",
   "pending_email_change",
   "complete",
-  "username_valid"
+  "username_valid",
+  "ext_user_profile_valid",
 ];
 
 export default class User extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js
index f1c0c3a..2ff65df 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js
@@ -4,6 +4,7 @@
   "name",
   "path",
   { name: "createdTime", type: "date" },
+  { name: "modifiedTime", type: "date" },
   "size",
   "hidden",
 ];
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js
index 85d070a..ee6ac6a 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js
@@ -5,6 +5,7 @@
   "downloadURL",
   "dataProductURI",
   { name: "createdTime", type: "date" },
+  { name: "modifiedTime", type: "date" },
   "size",
   "mimeType",
 ];
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 95ee7ac..c02058d 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
@@ -10,6 +10,8 @@
 import ExperimentStatistics from "./models/ExperimentStatistics";
 import ExperimentStoragePath from "./models/ExperimentStoragePath";
 import ExperimentSummary from "./models/ExperimentSummary";
+import ExtendedUserProfileField from "./models/ExtendedUserProfileField";
+import ExtendedUserProfileValue from "./models/ExtendedUserProfileValue";
 import FullExperiment from "./models/FullExperiment";
 import GatewayResourceProfile from "./models/GatewayResourceProfile";
 import Group from "./models/Group";
@@ -19,6 +21,7 @@
 import Notification from "./models/Notification";
 import Parser from "./models/Parser";
 import Project from "./models/Project";
+import QueueSettingsCalculator from "./models/QueueSettingsCalculator";
 import Settings from "./models/Settings";
 import SharedEntity from "./models/SharedEntity";
 import StoragePreference from "./models/StoragePreference";
@@ -245,6 +248,27 @@
       },
     },
   },
+  ExtendedUserProfileFields: {
+    url: "/auth/extended-user-profile-fields",
+    viewSet: true,
+    modelClass: ExtendedUserProfileField,
+  },
+  ExtendedUserProfileValues: {
+    url: "/auth/extended-user-profile-values",
+    viewSet: true,
+    modelClass: ExtendedUserProfileValue,
+    queryParams: ["username"],
+    methods: {
+      saveAll: {
+        url: "/auth/extended-user-profile-values/save-all/",
+        requestType: "post",
+        modelClass: ExtendedUserProfileValue,
+        bodyParams: {
+          name: "data",
+        },
+      },
+    },
+  },
   FullExperiments: {
     url: "/api/full-experiments",
     viewSet: [
@@ -341,6 +365,20 @@
     queryParams: ["limit", "offset"],
     modelClass: Project,
   },
+  QueueSettingsCalculators: {
+    url: "/api/queue-settings-calculators",
+    viewSet: ["retrieve", "list"],
+    methods: {
+      calculate: {
+        url: "/api/queue-settings-calculators/<lookup>/calculate/",
+        requestType: "post",
+        bodyParams: {
+          name: "data",
+        },
+      },
+    },
+    modelClass: QueueSettingsCalculator,
+  },
   Settings: {
     url: "/api/settings/",
     methods: {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js b/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js
index 5aa1e61..29bf84a 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/services/ServiceFactory.js
@@ -274,6 +274,9 @@
           }
         };
         let resultHandler = (data) => {
+          if (Array.isArray(data)) {
+            return data.map((item) => resultHandler(item));
+          }
           return config.modelClass ? new config.modelClass(data) : data;
         };
         switch (config.requestType.toLowerCase()) {
diff --git a/django_airavata/apps/api/urls.py b/django_airavata/apps/api/urls.py
index 61b2717..f655d64 100644
--- a/django_airavata/apps/api/urls.py
+++ b/django_airavata/apps/api/urls.py
@@ -45,6 +45,8 @@
                 basename='iam-user-profile')
 router.register(r'unverified-email-users', views.UnverifiedEmailUserViewSet,
                 basename='unverified-email-user-profile')
+router.register(r'queue-settings-calculators', views.QueueSettingsCalculatorViewSet,
+                basename='queue-settings-calculator')
 
 app_name = 'django_airavata_api'
 urlpatterns = [
diff --git a/django_airavata/apps/api/view_utils.py b/django_airavata/apps/api/view_utils.py
index ddef7e3..3f06ad0 100644
--- a/django_airavata/apps/api/view_utils.py
+++ b/django_airavata/apps/api/view_utils.py
@@ -221,3 +221,8 @@
                     request.is_read_only_gateway_admin)
         else:
             return request.is_gateway_admin
+
+
+class ReadOnly(permissions.BasePermission):
+    def has_permission(self, request, view):
+        return request.method in permissions.SAFE_METHODS
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index 79816b3..75fe425 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -23,7 +23,11 @@
 from airavata.model.experiment.ttypes import ExperimentSearchFields
 from airavata.model.group.ttypes import ResourcePermissionType
 from airavata.model.user.ttypes import Status
-from airavata_django_portal_sdk import experiment_util, user_storage
+from airavata_django_portal_sdk import (
+    experiment_util,
+    queue_settings_calculators,
+    user_storage
+)
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
@@ -1862,3 +1866,28 @@
                                       experiment_id,
                                       test_mode=test_mode,
                                       **params.dict())
+
+
+class QueueSettingsCalculatorViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericAPIBackedViewSet):
+    serializer_class = serializers.QueueSettingsCalculatorSerializer
+
+    def get_list(self):
+        return queue_settings_calculators.get_all()
+
+    def get_instance(self, lookup_value):
+        calcs = queue_settings_calculators.get_all()
+        calc = [calc for calc in calcs if calc.id == lookup_value]
+        if len(calc) == 0:
+            return None
+        return calc[0]
+
+    @action(methods=['post'], detail=True, serializer_class=serializers.ExperimentSerializer)
+    def calculate(self, request, pk=None):
+
+        serializer = self.get_serializer(data=request.data)
+        result = {}
+        # Just ignore invalid experiment model since likely caused by late initialization
+        if serializer.is_valid():
+            experiment_model = serializer.save()
+            result = queue_settings_calculators.calculate_queue_settings(pk, request, experiment_model)
+        return Response(result)
diff --git a/django_airavata/apps/auth/middleware.py b/django_airavata/apps/auth/middleware.py
index 0226956..8e37405 100644
--- a/django_airavata/apps/auth/middleware.py
+++ b/django_airavata/apps/auth/middleware.py
@@ -90,8 +90,14 @@
             reverse('django_airavata_auth:user_profile'),
             reverse('django_airavata_auth:logout'),
         ]
-        if (hasattr(request.user, "user_profile") and
-            not request.user.user_profile.is_complete and
+        incomplete_user_profile = (hasattr(request.user, "user_profile") and
+                                   not request.user.user_profile.is_complete)
+        # Exclude admin's from the ext user profile check since they will be
+        # creating/editing the ext user profile fields
+        invalid_ext_user_profile = (not getattr(request, "is_gateway_admin", False) and
+                                    hasattr(request.user, "user_profile") and
+                                    not request.user.user_profile.is_ext_user_profile_valid)
+        if ((incomplete_user_profile or invalid_ext_user_profile) and
             request.path not in allowed_paths and
                 'text/html' in request.META['HTTP_ACCEPT']):
             return redirect('django_airavata_auth:user_profile')
diff --git a/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py b/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py
new file mode 100644
index 0000000..2b5601c
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser.py
@@ -0,0 +1,107 @@
+# Generated by Django 3.2.11 on 2022-03-11 14:25
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_airavata_auth', '0013_auto_20220118_1650'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ExtendedUserProfileField',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=64)),
+                ('help_text', models.TextField(blank=True)),
+                ('order', models.IntegerField()),
+                ('created_date', models.DateTimeField(auto_now_add=True)),
+                ('updated_date', models.DateTimeField(auto_now=True)),
+                ('deleted', models.BooleanField(default=False)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileAgreementField',
+            fields=[
+                ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='user_agreement', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
+                ('checkbox_label', models.TextField(blank=True)),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilefield',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileMultiChoiceField',
+            fields=[
+                ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='multi_choice', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
+                ('other', models.BooleanField(default=False)),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilefield',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileSingleChoiceField',
+            fields=[
+                ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='single_choice', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
+                ('other', models.BooleanField(default=False)),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilefield',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileTextField',
+            fields=[
+                ('field_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='text', serialize=False, to='django_airavata_auth.extendeduserprofilefield')),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilefield',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileInfo',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('id_value', models.BigIntegerField(null=True)),
+                ('text_value', models.CharField(blank=True, max_length=255)),
+                ('created_date', models.DateTimeField(auto_now_add=True)),
+                ('updated_date', models.DateTimeField(auto_now=True)),
+                ('ext_user_profile_field', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_airavata_auth.extendeduserprofilefield')),
+                ('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extended_profile', to='django_airavata_auth.userprofile')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileFieldLink',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('label', models.TextField()),
+                ('url', models.URLField()),
+                ('order', models.IntegerField()),
+                ('display_link', models.BooleanField(default=True)),
+                ('display_inline', models.BooleanField(default=False)),
+                ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='django_airavata_auth.extendeduserprofilefield')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileSingleChoiceFieldChoice',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('display_text', models.CharField(max_length=255)),
+                ('order', models.IntegerField()),
+                ('deleted', models.BooleanField(default=False)),
+                ('single_choice_field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='django_airavata_auth.extendeduserprofilesinglechoicefield')),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileMultiChoiceFieldChoice',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('display_text', models.CharField(max_length=255)),
+                ('order', models.IntegerField()),
+                ('deleted', models.BooleanField(default=False)),
+                ('multi_choice_field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='django_airavata_auth.extendeduserprofilemultichoicefield')),
+            ],
+            options={
+                'abstract': False,
+            },
+        ),
+    ]
diff --git a/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py b/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py
new file mode 100644
index 0000000..49da5c1
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0015_auto_20220329_1708.py
@@ -0,0 +1,72 @@
+# Generated by Django 3.2.11 on 2022-03-29 17:08
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_airavata_auth', '0014_extendeduserprofileagreementfield_extendeduserprofilefield_extendeduserprofilefieldlink_extendeduser'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ExtendedUserProfileMultiChoiceValueChoice',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('value', models.BigIntegerField()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileValue',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('created_date', models.DateTimeField(auto_now_add=True)),
+                ('updated_date', models.DateTimeField(auto_now=True)),
+                ('ext_user_profile_field', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='django_airavata_auth.extendeduserprofilefield')),
+                ('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extended_profile', to='django_airavata_auth.userprofile')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileAgreementValue',
+            fields=[
+                ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='user_agreement', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+                ('agreement_value', models.BooleanField()),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilevalue',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileMultiChoiceValue',
+            fields=[
+                ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='multi_choice', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+                ('other_value', models.TextField(blank=True)),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilevalue',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileSingleChoiceValue',
+            fields=[
+                ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='single_choice', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+                ('choice', models.BigIntegerField(null=True)),
+                ('other_value', models.TextField(blank=True)),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilevalue',),
+        ),
+        migrations.CreateModel(
+            name='ExtendedUserProfileTextValue',
+            fields=[
+                ('value_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='text', serialize=False, to='django_airavata_auth.extendeduserprofilevalue')),
+                ('text_value', models.TextField()),
+            ],
+            bases=('django_airavata_auth.extendeduserprofilevalue',),
+        ),
+        migrations.DeleteModel(
+            name='ExtendedUserProfileInfo',
+        ),
+        migrations.AddField(
+            model_name='extendeduserprofilemultichoicevaluechoice',
+            name='multi_choice_value',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='django_airavata_auth.extendeduserprofilemultichoicevalue'),
+        ),
+    ]
diff --git a/django_airavata/apps/auth/migrations/0016_extendeduserprofilefield_required.py b/django_airavata/apps/auth/migrations/0016_extendeduserprofilefield_required.py
new file mode 100644
index 0000000..4c6dd6f
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0016_extendeduserprofilefield_required.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.11 on 2022-05-10 20:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_airavata_auth', '0015_auto_20220329_1708'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='extendeduserprofilefield',
+            name='required',
+            field=models.BooleanField(default=True),
+        ),
+    ]
diff --git a/django_airavata/apps/auth/migrations/0017_auto_20220616_1831.py b/django_airavata/apps/auth/migrations/0017_auto_20220616_1831.py
new file mode 100644
index 0000000..47ed795
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0017_auto_20220616_1831.py
@@ -0,0 +1,55 @@
+# Generated by Django 3.2.11 on 2022-06-16 18:31
+
+from django.db import migrations
+
+from django_airavata.apps.auth.models import (
+    USER_PROFILE_COMPLETED_TEMPLATE,
+)
+
+
+def default_templates(apps, schema_editor):
+
+    EmailTemplate = apps.get_model("django_airavata_auth", "EmailTemplate")
+    user_profile_completed_template = EmailTemplate(
+        template_type=USER_PROFILE_COMPLETED_TEMPLATE,
+        subject="User {{first_name}} {{last_name}} ({{username}}) has completed their profile",
+        body="""
+            <p>Gateway Portal: {{http_host}}</p>
+            <p>Tenant: {{gateway_id}}</p>
+            <h3>User Profile</h3>
+            <p>Username: {{username}}</p>
+            <p>Name: {{first_name}} {{last_name}}</p>
+            <p>Email: {{email}}</p>
+            {% if extended_profile_values %}
+            <h3>Extended User Profile</h3>
+            <table><tr><th>Name</th><th>Value</th></tr>
+            {% for value in extended_profile_values %}
+                <tr><td>{{ value.ext_user_profile_field.name }}</td>
+                {% if value.value_display_list and value.value_display_list|length > 1 %}
+                <td><ul>
+                    {% for display_item in value.value_display_list %}
+                    <li>{{ display_item }}</li>
+                    {% endfor %}
+                </ul></td>
+                {% elif value.value_display_list and value.value_display_list|length == 1 %}
+                <td>{{ value.value_display_list|first }}</td>
+                {% else %}
+                <td>{{ value.value_display_list }}</td>
+                {% endif %}
+                </tr>
+            {% endfor %}
+            </table>
+            {% endif %}
+        """.strip())
+    user_profile_completed_template.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_airavata_auth', '0016_extendeduserprofilefield_required'),
+    ]
+
+    operations = [
+        migrations.RunPython(default_templates)
+    ]
diff --git a/django_airavata/apps/auth/migrations/0018_merge_0014_auto_20220217_2255_0017_auto_20220616_1831.py b/django_airavata/apps/auth/migrations/0018_merge_0014_auto_20220217_2255_0017_auto_20220616_1831.py
new file mode 100644
index 0000000..ff67fdb
--- /dev/null
+++ b/django_airavata/apps/auth/migrations/0018_merge_0014_auto_20220217_2255_0017_auto_20220616_1831.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.2.14 on 2022-07-08 19:43
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('django_airavata_auth', '0014_auto_20220217_2255'),
+        ('django_airavata_auth', '0017_auto_20220616_1831'),
+    ]
+
+    operations = [
+    ]
diff --git a/django_airavata/apps/auth/models.py b/django_airavata/apps/auth/models.py
index 54395e0..6cc658e 100644
--- a/django_airavata/apps/auth/models.py
+++ b/django_airavata/apps/auth/models.py
@@ -11,6 +11,7 @@
 PASSWORD_RESET_EMAIL_TEMPLATE = 3
 USER_ADDED_TO_GROUP_TEMPLATE = 4
 VERIFY_EMAIL_CHANGE_TEMPLATE = 5
+USER_PROFILE_COMPLETED_TEMPLATE = 6
 
 
 class EmailVerification(models.Model):
@@ -29,6 +30,7 @@
         (PASSWORD_RESET_EMAIL_TEMPLATE, 'Password Reset Email Template'),
         (USER_ADDED_TO_GROUP_TEMPLATE, 'User Added to Group Template'),
         (VERIFY_EMAIL_CHANGE_TEMPLATE, 'Verify Email Change Template'),
+        (USER_PROFILE_COMPLETED_TEMPLATE, 'User Profile Completed Template'),
     )
     template_type = models.IntegerField(
         primary_key=True, choices=TEMPLATE_TYPE_CHOICES)
@@ -109,6 +111,19 @@
             result.append('last_name')
         return result
 
+    @property
+    def is_ext_user_profile_valid(self):
+        fields = ExtendedUserProfileField.objects.filter(deleted=False)
+        for field in fields:
+            try:
+                value = self.extended_profile_values.filter(ext_user_profile_field=field).get()
+                if not value.valid:
+                    return False
+            except ExtendedUserProfileValue.DoesNotExist:
+                if field.required:
+                    return False
+        return True
+
     def is_non_empty(self, value: str):
         return value is not None and value.strip() != ""
 
@@ -149,3 +164,230 @@
         max_length=36, unique=True, default=uuid.uuid4)
     created_date = models.DateTimeField(auto_now_add=True)
     verified = models.BooleanField(default=False)
+
+
+class ExtendedUserProfileField(models.Model):
+    name = models.CharField(max_length=64)
+    help_text = models.TextField(blank=True)
+    order = models.IntegerField()
+    created_date = models.DateTimeField(auto_now_add=True)
+    updated_date = models.DateTimeField(auto_now=True)
+    deleted = models.BooleanField(default=False)
+    required = models.BooleanField(default=True)
+
+    def __str__(self) -> str:
+        return f"{self.name} ({self.id})"
+
+    @property
+    def field_type(self):
+        if hasattr(self, 'text'):
+            return 'text'
+        elif hasattr(self, 'single_choice'):
+            return 'single_choice'
+        elif hasattr(self, 'multi_choice'):
+            return 'multi_choice'
+        elif hasattr(self, 'user_agreement'):
+            return 'user_agreement'
+        else:
+            raise Exception("Could not determine field_type")
+
+
+class ExtendedUserProfileTextField(ExtendedUserProfileField):
+    field_ptr = models.OneToOneField(ExtendedUserProfileField,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="text")
+
+
+class ExtendedUserProfileSingleChoiceField(ExtendedUserProfileField):
+    field_ptr = models.OneToOneField(ExtendedUserProfileField,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="single_choice")
+    other = models.BooleanField(default=False)
+
+
+class ExtendedUserProfileFieldChoice(models.Model):
+    display_text = models.CharField(max_length=255)
+    order = models.IntegerField()
+    deleted = models.BooleanField(default=False)
+
+    class Meta:
+        abstract = True
+
+    def __str__(self) -> str:
+        return f"{self.display_text} ({self.id})"
+
+
+class ExtendedUserProfileSingleChoiceFieldChoice(ExtendedUserProfileFieldChoice):
+    single_choice_field = models.ForeignKey(ExtendedUserProfileSingleChoiceField, on_delete=models.CASCADE, related_name="choices")
+
+
+class ExtendedUserProfileMultiChoiceField(ExtendedUserProfileField):
+    field_ptr = models.OneToOneField(ExtendedUserProfileField,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="multi_choice")
+    other = models.BooleanField(default=False)
+
+
+class ExtendedUserProfileMultiChoiceFieldChoice(ExtendedUserProfileFieldChoice):
+    multi_choice_field = models.ForeignKey(ExtendedUserProfileMultiChoiceField, on_delete=models.CASCADE, related_name="choices")
+
+
+class ExtendedUserProfileAgreementField(ExtendedUserProfileField):
+    field_ptr = models.OneToOneField(ExtendedUserProfileField,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="user_agreement")
+    # if no checkbox label, then some default text will be used
+    checkbox_label = models.TextField(blank=True)
+
+
+class ExtendedUserProfileFieldLink(models.Model):
+    label = models.TextField()
+    url = models.URLField()
+    order = models.IntegerField()
+    display_link = models.BooleanField(default=True)
+    display_inline = models.BooleanField(default=False)
+    # Technically any field can have links
+    field = models.ForeignKey(ExtendedUserProfileField, on_delete=models.CASCADE, related_name="links")
+
+    def __str__(self) -> str:
+        return f"{self.label} {self.url}"
+
+
+class ExtendedUserProfileValue(models.Model):
+    ext_user_profile_field = models.ForeignKey(ExtendedUserProfileField, on_delete=models.SET_NULL, null=True)
+    user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name="extended_profile_values")
+    created_date = models.DateTimeField(auto_now_add=True)
+    updated_date = models.DateTimeField(auto_now=True)
+
+    @property
+    def value_type(self):
+        if hasattr(self, 'text'):
+            return 'text'
+        elif hasattr(self, 'single_choice'):
+            return 'single_choice'
+        elif hasattr(self, 'multi_choice'):
+            return 'multi_choice'
+        elif hasattr(self, 'user_agreement'):
+            return 'user_agreement'
+        else:
+            raise Exception("Could not determine value_type")
+
+    @property
+    def value_display(self):
+        if self.value_type == 'text':
+            return self.text.text_value
+        elif self.value_type == 'single_choice':
+            if self.single_choice.choice:
+                try:
+                    choice = self.ext_user_profile_field.single_choice.choices.get(id=self.single_choice.choice)
+                    return choice.display_text
+                except ExtendedUserProfileSingleChoiceFieldChoice.DoesNotExist:
+                    return None
+            elif self.single_choice.other_value:
+                return f"Other: {self.single_choice.other_value}"
+        elif self.value_type == 'multi_choice':
+            result = []
+            if self.multi_choice.choices:
+                mc_field = self.ext_user_profile_field.multi_choice
+                for choice_value in self.multi_choice.choices.all():
+                    try:
+                        choice = mc_field.choices.get(id=choice_value.value)
+                        result.append(choice.display_text)
+                    except ExtendedUserProfileMultiChoiceFieldChoice.DoesNotExist:
+                        continue
+            if self.multi_choice.other_value:
+                result.append(f"Other: {self.multi_choice.other_value}")
+            return result
+        elif self.value_type == 'user_agreement':
+            if self.user_agreement.agreement_value:
+                return "Yes"
+            else:
+                return "No"
+        return None
+
+    @property
+    def value_display_list(self):
+        """Same as value_display except coerced always to a list."""
+        value_display = self.value_display
+        if value_display is not None and not isinstance(value_display, list):
+            return [value_display]
+        else:
+            return value_display
+
+    @property
+    def valid(self):
+        # if the field is deleted, whatever the value, consider it valid
+        if self.ext_user_profile_field.deleted:
+            return True
+        if self.ext_user_profile_field.required:
+            if self.value_type == 'text':
+                return self.text.text_value and len(self.text.text_value.strip()) > 0
+            if self.value_type == 'single_choice':
+                choice_exists = (self.single_choice.choice and
+                                 self.ext_user_profile_field.single_choice.choices
+                                 .filter(id=self.single_choice.choice).exists())
+                has_other = (self.ext_user_profile_field.single_choice.other and
+                             self.single_choice.other_value and
+                             len(self.single_choice.other_value.strip()) > 0)
+                return choice_exists or has_other
+            if self.value_type == 'multi_choice':
+                choice_ids = list(map(lambda c: c.value, self.multi_choice.choices.all()))
+                choice_exists = self.ext_user_profile_field.multi_choice.choices.filter(id__in=choice_ids).exists()
+                has_other = (self.ext_user_profile_field.multi_choice.other and
+                             self.multi_choice.other_value and
+                             len(self.multi_choice.other_value.strip()) > 0)
+                return choice_exists or has_other
+            if self.value_type == 'user_agreement':
+                return self.user_agreement.agreement_value is True
+        return True
+
+
+class ExtendedUserProfileTextValue(ExtendedUserProfileValue):
+    value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="text")
+    text_value = models.TextField()
+
+
+class ExtendedUserProfileSingleChoiceValue(ExtendedUserProfileValue):
+    value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="single_choice")
+    # Only one of value or other_value should be populated, not both
+    choice = models.BigIntegerField(null=True)
+    other_value = models.TextField(blank=True)
+
+
+class ExtendedUserProfileMultiChoiceValue(ExtendedUserProfileValue):
+    value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="multi_choice")
+    other_value = models.TextField(blank=True)
+
+
+class ExtendedUserProfileMultiChoiceValueChoice(models.Model):
+    value = models.BigIntegerField()
+    multi_choice_value = models.ForeignKey(ExtendedUserProfileMultiChoiceValue, on_delete=models.CASCADE, related_name="choices")
+
+
+class ExtendedUserProfileAgreementValue(ExtendedUserProfileValue):
+    value_ptr = models.OneToOneField(ExtendedUserProfileValue,
+                                     on_delete=models.CASCADE,
+                                     parent_link=True,
+                                     primary_key=True,
+                                     related_name="user_agreement")
+    agreement_value = models.BooleanField()
diff --git a/django_airavata/apps/auth/package.json b/django_airavata/apps/auth/package.json
index a136c92..bcc0445 100644
--- a/django_airavata/apps/auth/package.json
+++ b/django_airavata/apps/auth/package.json
@@ -17,7 +17,8 @@
     "django-airavata-api": "link:../api/",
     "django-airavata-common-ui": "link:../../static/common/",
     "vue": "^2.5.21",
-    "vuelidate": "^0.7.6"
+    "vuelidate": "^0.7.6",
+    "vuex": "^3.6.2"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "^3.1.1",
diff --git a/django_airavata/apps/auth/serializers.py b/django_airavata/apps/auth/serializers.py
index c10b918..06d8def 100644
--- a/django_airavata/apps/auth/serializers.py
+++ b/django_airavata/apps/auth/serializers.py
@@ -27,11 +27,13 @@
     pending_email_change = serializers.SerializerMethodField()
     complete = serializers.SerializerMethodField()
     username_valid = serializers.SerializerMethodField()
+    ext_user_profile_valid = serializers.SerializerMethodField()
 
     class Meta:
         model = get_user_model()
         fields = ['id', 'username', 'first_name', 'last_name', 'email',
-                  'pending_email_change', 'complete', 'username_valid']
+                  'pending_email_change', 'complete', 'username_valid',
+                  'ext_user_profile_valid']
         read_only_fields = ('username',)
 
     def get_pending_email_change(self, instance):
@@ -49,6 +51,9 @@
     def get_username_valid(self, instance):
         return instance.user_profile.is_username_valid
 
+    def get_ext_user_profile_valid(self, instance):
+        return instance.user_profile.is_ext_user_profile_valid
+
     @atomic
     def update(self, instance, validated_data):
         request = self.context['request']
@@ -98,3 +103,267 @@
             "url": verification_uri,
         })
         utils.send_email_to_user(models.VERIFY_EMAIL_CHANGE_TEMPLATE, context)
+
+
+class ExtendedUserProfileFieldChoiceSerializer(serializers.Serializer):
+    id = serializers.IntegerField(required=False)
+    display_text = serializers.CharField()
+    order = serializers.IntegerField()
+
+
+class ExtendedUserProfileFieldLinkSerializer(serializers.Serializer):
+    id = serializers.IntegerField(required=False)
+    label = serializers.CharField()
+    url = serializers.URLField()
+    order = serializers.IntegerField()
+    display_link = serializers.BooleanField(default=True)
+    display_inline = serializers.BooleanField(default=False)
+
+
+class ExtendedUserProfileFieldSerializer(serializers.ModelSerializer):
+    field_type = serializers.ChoiceField(choices=["text", "single_choice", "multi_choice", "user_agreement"])
+    other = serializers.BooleanField(required=False)
+    choices = ExtendedUserProfileFieldChoiceSerializer(required=False, many=True)
+    checkbox_label = serializers.CharField(allow_blank=True, required=False)
+    links = ExtendedUserProfileFieldLinkSerializer(required=False, many=True)
+
+    class Meta:
+        model = models.ExtendedUserProfileField
+        fields = ['id', 'name', 'help_text', 'order', 'created_date',
+                  'updated_date', 'field_type', 'other', 'choices', 'checkbox_label', 'links', 'required']
+        read_only_fields = ('created_date', 'updated_date')
+
+    def to_representation(self, instance):
+        result = super().to_representation(instance)
+        if instance.field_type == 'single_choice':
+            result['other'] = instance.single_choice.other
+            result['choices'] = ExtendedUserProfileFieldChoiceSerializer(instance.single_choice.choices.filter(deleted=False).order_by('order'), many=True).data
+        if instance.field_type == 'multi_choice':
+            result['other'] = instance.multi_choice.other
+            result['choices'] = ExtendedUserProfileFieldChoiceSerializer(instance.multi_choice.choices.filter(deleted=False).order_by('order'), many=True).data
+        if instance.field_type == 'user_agreement':
+            result['checkbox_label'] = instance.user_agreement.checkbox_label
+        result['links'] = ExtendedUserProfileFieldLinkSerializer(instance.links.order_by('order'), many=True).data
+        return result
+
+    def create(self, validated_data):
+        field_type = validated_data.pop('field_type')
+        other = validated_data.pop('other', False)
+        choices = validated_data.pop('choices', [])
+        checkbox_label = validated_data.pop('checkbox_label', '')
+        links = validated_data.pop('links', [])
+        if field_type == 'text':
+            instance = models.ExtendedUserProfileTextField.objects.create(**validated_data)
+        elif field_type == 'single_choice':
+            instance = models.ExtendedUserProfileSingleChoiceField.objects.create(**validated_data, other=other)
+            # add choices
+            for choice in choices:
+                choice.pop('id', None)
+                instance.choices.create(**choice)
+        elif field_type == 'multi_choice':
+            instance = models.ExtendedUserProfileMultiChoiceField.objects.create(**validated_data, other=other)
+            # add choices
+            for choice in choices:
+                choice.pop('id', None)
+                instance.choices.create(**choice)
+        elif field_type == 'user_agreement':
+            instance = models.ExtendedUserProfileAgreementField.objects.create(**validated_data, checkbox_label=checkbox_label)
+        else:
+            raise Exception(f"Unrecognized field type: {field_type}")
+        # create links
+        for link in links:
+            link.pop('id', None)
+            instance.links.create(**link)
+        return instance
+
+    def update(self, instance, validated_data):
+        instance.name = validated_data['name']
+        instance.help_text = validated_data['help_text']
+        instance.order = validated_data['order']
+        instance.required = validated_data.get('required', instance.required)
+        # logger.debug(f"instance.field_type={instance.field_type}, validated_data={validated_data}")
+        if instance.field_type == 'single_choice':
+            instance.single_choice.other = validated_data.get('other', instance.single_choice.other)
+            choices = validated_data.pop('choices', None)
+            if choices:
+                choice_ids = [choice['id'] for choice in choices if 'id' in choice]
+                # Soft delete any choices that are not in the list
+                instance.single_choice.choices.exclude(id__in=choice_ids).update(deleted=True)
+                for choice in choices:
+                    choice_id = choice.pop('id', None)
+                    models.ExtendedUserProfileSingleChoiceFieldChoice.objects.update_or_create(
+                        id=choice_id,
+                        defaults={**choice, "single_choice_field": instance.single_choice},
+                    )
+            instance.single_choice.save()
+        elif instance.field_type == 'multi_choice':
+            instance.multi_choice.other = validated_data.get('other', instance.multi_choice.other)
+            choices = validated_data.pop('choices', None)
+            if choices:
+                choice_ids = [choice['id'] for choice in choices if 'id' in choice]
+                # Soft delete any choices that are not in the list
+                instance.multi_choice.choices.exclude(id__in=choice_ids).update(deleted=True)
+                for choice in choices:
+                    choice_id = choice.pop('id', None)
+                    models.ExtendedUserProfileMultiChoiceFieldChoice.objects.update_or_create(
+                        id=choice_id,
+                        defaults={**choice, "multi_choice_field": instance.multi_choice},
+                    )
+            instance.multi_choice.save()
+        elif instance.field_type == 'user_agreement':
+            instance.user_agreement.checkbox_label = validated_data.pop('checkbox_label', instance.user_agreement.checkbox_label)
+            instance.user_agreement.save()
+
+        # update links
+        links = validated_data.pop('links', [])
+        link_ids = [link['id'] for link in links if 'id' in link]
+        instance.links.exclude(id__in=link_ids).delete()
+        for link in links:
+            link_id = link.pop('id', None)
+            link['field'] = instance
+            models.ExtendedUserProfileFieldLink.objects.update_or_create(
+                id=link_id,
+                defaults=link,
+            )
+
+        instance.save()
+        return instance
+
+
+class ExtendedUserProfileValueSerializer(serializers.ModelSerializer):
+    id = serializers.IntegerField(label='ID', required=False)
+    text_value = serializers.CharField(required=False, allow_blank=True)
+    # choices must be write_only so that DRF ignores trying to deserialized this related field
+    # deserialization is handled explicitly in to_representation, see below
+    choices = serializers.ListField(child=serializers.IntegerField(), required=False, write_only=True)
+    other_value = serializers.CharField(required=False, allow_blank=True)
+    agreement_value = serializers.BooleanField(required=False)
+
+    class Meta:
+        model = models.ExtendedUserProfileValue
+        fields = ['id', 'value_type', 'ext_user_profile_field', 'text_value',
+                  'choices', 'other_value', 'agreement_value', 'valid', 'value_display']
+        read_only_fields = ['value_type', 'value_display']
+
+    def to_representation(self, instance):
+        result = super().to_representation(instance)
+        if instance.value_type == 'text':
+            result['text_value'] = instance.text.text_value
+        elif instance.value_type == 'single_choice':
+            choices = []
+            if instance.single_choice.choice is not None:
+                choices.append(instance.single_choice.choice)
+            result['choices'] = choices
+            result['other_value'] = instance.single_choice.other_value
+        elif instance.value_type == 'multi_choice':
+            result['choices'] = list(map(lambda c: c.value, instance.multi_choice.choices.all()))
+            result['other_value'] = instance.multi_choice.other_value
+        elif instance.value_type == 'user_agreement':
+            result['agreement_value'] = instance.user_agreement.agreement_value
+        return result
+
+    def create(self, validated_data):
+        request = self.context['request']
+        user = request.user
+        user_profile = user.user_profile
+
+        # Support create/update in the many=True situation. When many=True and
+        # .save() is called, .create() will be called on each value. Here we
+        # need to see if there is an id and if so call .update() instead.
+        if "id" in validated_data:
+            instance = models.ExtendedUserProfileValue.objects.get(id=validated_data["id"])
+            return self.update(instance, validated_data)
+
+        ext_user_profile_field = validated_data.pop('ext_user_profile_field')
+        if ext_user_profile_field.field_type == 'text':
+            text_value = validated_data.pop('text_value')
+            return models.ExtendedUserProfileTextValue.objects.create(
+                ext_user_profile_field=ext_user_profile_field,
+                user_profile=user_profile,
+                text_value=text_value)
+        elif ext_user_profile_field.field_type == 'single_choice':
+            choices = validated_data.pop('choices', [])
+            choice = choices[0] if len(choices) > 0 else None
+            other_value = validated_data.pop('other_value', '')
+            return models.ExtendedUserProfileSingleChoiceValue.objects.create(
+                ext_user_profile_field=ext_user_profile_field,
+                user_profile=user_profile,
+                choice=choice,
+                other_value=other_value,
+            )
+        elif ext_user_profile_field.field_type == 'multi_choice':
+            choices = validated_data.pop('choices', [])
+            other_value = validated_data.pop('other_value', '')
+            value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+                ext_user_profile_field=ext_user_profile_field,
+                user_profile=user_profile,
+                other_value=other_value,
+            )
+            for choice in choices:
+                models.ExtendedUserProfileMultiChoiceValueChoice.objects.create(
+                    value=choice,
+                    multi_choice_value=value
+                )
+            return value
+        elif ext_user_profile_field.field_type == 'user_agreement':
+            agreement_value = validated_data.get('agreement_value')
+            return models.ExtendedUserProfileAgreementValue.objects.create(
+                ext_user_profile_field=ext_user_profile_field,
+                user_profile=user_profile,
+                agreement_value=agreement_value
+            )
+
+    def update(self, instance, validated_data):
+        if instance.value_type == 'text':
+            text_value = validated_data.pop('text_value')
+            instance.text.text_value = text_value
+            instance.text.save()
+        elif instance.value_type == 'single_choice':
+            choices = validated_data.pop('choices', [])
+            choice = choices[0] if len(choices) > 0 else None
+            other_value = validated_data.pop('other_value', '')
+            instance.single_choice.choice = choice
+            instance.single_choice.other_value = other_value
+            instance.single_choice.save()
+        elif instance.value_type == 'multi_choice':
+            choices = validated_data.pop('choices', [])
+            other_value = validated_data.pop('other_value', '')
+            # Delete any that are no longer in the set
+            instance.multi_choice.choices.exclude(value__in=choices).delete()
+            # Create records as needed for new entries
+            for choice in choices:
+                models.ExtendedUserProfileMultiChoiceValueChoice.objects.update_or_create(
+                    value=choice, multi_choice_value=instance.multi_choice)
+            instance.multi_choice.other_value = other_value
+            instance.multi_choice.save()
+        elif instance.value_type == 'user_agreement':
+            agreement_value = validated_data.pop('agreement_value')
+            instance.user_agreement.agreement_value = agreement_value
+            instance.user_agreement.save()
+        instance.save()
+        return instance
+
+    def validate(self, attrs):
+        ext_user_profile_field = attrs['ext_user_profile_field']
+        # validate that id_value is only provided for choice fields, and 'text_value' only for the others
+        if ext_user_profile_field.field_type == 'single_choice':
+            choices = attrs.get('choices', [])
+            other_value = attrs.get('other_value', '')
+            # Check that choices are valid
+            for choice in choices:
+                if not ext_user_profile_field.single_choice.choices.filter(id=choice, deleted=False).exists():
+                    raise serializers.ValidationError({'choices': 'Invalid choice.'})
+            if len(choices) > 1:
+                raise serializers.ValidationError({'choices': "Must specify only a single choice."})
+            if len(choices) == 1 and other_value != '':
+                raise serializers.ValidationError("Must specify only a single choice or the other choice, but not both.")
+            if len(choices) == 0 and other_value == '':
+                raise serializers.ValidationError("Must specify one of a single choice or the other choice (but not both).")
+        elif ext_user_profile_field.field_type == 'multi_choice':
+            choices = attrs.get('choices', [])
+            other_value = attrs.get('other_value', '')
+            # Check that choices are valid
+            for choice in choices:
+                if not ext_user_profile_field.multi_choice.choices.filter(id=choice, deleted=False).exists():
+                    raise serializers.ValidationError({'choices': 'Invalid choice.'})
+        return attrs
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue
new file mode 100644
index 0000000..aaf9fc7
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileEditor.vue
@@ -0,0 +1,57 @@
+<template>
+  <div>
+    <template v-for="extendedUserProfileField in extendedUserProfileFields">
+      <component
+        ref="extendedUserProfileFieldComponents"
+        :key="extendedUserProfileField.id"
+        :is="getEditor(extendedUserProfileField)"
+        :extended-user-profile-field="extendedUserProfileField"
+        @valid="recordValidChildComponent(extendedUserProfileField.id)"
+        @invalid="recordInvalidChildComponent(extendedUserProfileField.id)"
+      />
+    </template>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from "vuex";
+import ExtendedUserProfileMultiChoiceValueEditor from "./ExtendedUserProfileMultiChoiceValueEditor.vue";
+import ExtendedUserProfileSingleChoiceValueEditor from "./ExtendedUserProfileSingleChoiceValueEditor.vue";
+import ExtendedUserProfileTextValueEditor from "./ExtendedUserProfileTextValueEditor.vue";
+import ExtendedUserProfileUserAgreementValueEditor from "./ExtendedUserProfileUserAgreementValueEditor.vue";
+import { mixins } from "django-airavata-common-ui";
+export default {
+  mixins: [mixins.ValidationParent],
+  computed: {
+    ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]),
+    valid() {
+      return this.childComponentsAreValid;
+    },
+  },
+  methods: {
+    getEditor(extendedUserProfileField) {
+      const fieldTypeEditors = {
+        text: ExtendedUserProfileTextValueEditor,
+        single_choice: ExtendedUserProfileSingleChoiceValueEditor,
+        multi_choice: ExtendedUserProfileMultiChoiceValueEditor,
+        user_agreement: ExtendedUserProfileUserAgreementValueEditor,
+      };
+
+      if (extendedUserProfileField.field_type in fieldTypeEditors) {
+        return fieldTypeEditors[extendedUserProfileField.field_type];
+      } else {
+        // eslint-disable-next-line no-console
+        console.error(
+          "Unexpected field_type",
+          extendedUserProfileField.field_type
+        );
+      }
+    },
+    touch() {
+      this.$refs.extendedUserProfileFieldComponents.forEach((c) => c.touch());
+    },
+  },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileMultiChoiceValueEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileMultiChoiceValueEditor.vue
new file mode 100644
index 0000000..9905c89
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileMultiChoiceValueEditor.vue
@@ -0,0 +1,157 @@
+<template>
+  <extended-user-profile-value-editor v-bind="$props">
+    <b-form-checkbox-group
+      v-model="value"
+      :options="options"
+      stacked
+      @change="onChange"
+      :state="validateStateErrorOnly($v.value)"
+    >
+      <b-form-checkbox
+        :value="otherOptionValue"
+        v-if="extendedUserProfileField.other"
+        >Other (please specify)</b-form-checkbox
+      >
+
+      <b-form-invalid-feedback :state="validateState($v.value)"
+        >This field is required.</b-form-invalid-feedback
+      >
+    </b-form-checkbox-group>
+    <template v-if="showOther">
+      <b-form-input
+        class="mt-2"
+        v-model="other"
+        placeholder="Please specify"
+        :state="validateState($v.other)"
+        @input="onInput"
+      />
+      <b-form-invalid-feedback :state="validateState($v.other)"
+        >Please specify a value for 'Other'.</b-form-invalid-feedback
+      >
+    </template>
+  </extended-user-profile-value-editor>
+</template>
+
+<script>
+import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { required, requiredIf } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
+import ExtendedUserProfileValueEditor from "./ExtendedUserProfileValueEditor.vue";
+const OTHER_OPTION = new Object(); // sentinel value
+export default {
+  mixins: [validationMixin],
+  components: { ExtendedUserProfileValueEditor },
+  props: ["extendedUserProfileField"],
+  data() {
+    return {
+      otherOptionSelected: false,
+    };
+  },
+  computed: {
+    ...mapGetters("extendedUserProfile", [
+      "getMultiChoiceValue",
+      "getMultiChoiceOther",
+    ]),
+    value: {
+      get() {
+        const copy = this.getMultiChoiceValue(
+          this.extendedUserProfileField.id
+        ).slice();
+        if (this.showOther) {
+          copy.push(this.otherOptionValue);
+        }
+        return copy;
+      },
+      set(value) {
+        const values = value.filter((v) => v !== this.otherOptionValue);
+        this.setMultiChoiceValue({
+          value: values,
+          id: this.extendedUserProfileField.id,
+        });
+        this.$v.value.$touch();
+      },
+    },
+    other: {
+      get() {
+        return this.getMultiChoiceOther(this.extendedUserProfileField.id);
+      },
+      set(value) {
+        this.setMultiChoiceOther({
+          value,
+          id: this.extendedUserProfileField.id,
+        });
+        this.$v.other.$touch();
+      },
+    },
+    showOther() {
+      return this.other || this.otherOptionSelected;
+    },
+    options() {
+      return this.extendedUserProfileField &&
+        this.extendedUserProfileField.choices
+        ? this.extendedUserProfileField.choices.map((choice) => {
+            return {
+              value: choice.id,
+              text: choice.display_text,
+            };
+          })
+        : [];
+    },
+    otherOptionValue() {
+      return OTHER_OPTION;
+    },
+    valid() {
+      return !this.$v.$invalid;
+    },
+    required() {
+      return this.extendedUserProfileField.required;
+    },
+  },
+  validations() {
+    const validations = {
+      value: {
+        required: requiredIf("required"),
+      },
+      other: {},
+    };
+    if (this.showOther) {
+      validations.other = { required };
+    }
+    return validations;
+  },
+  methods: {
+    ...mapMutations("extendedUserProfile", [
+      "setMultiChoiceValue",
+      "setMultiChoiceOther",
+    ]),
+    onChange(value) {
+      this.otherOptionSelected = value.includes(this.otherOptionValue);
+      if (!this.otherOptionSelected) {
+        this.other = "";
+      }
+    },
+    onInput() {
+      // Handle case where initially there is an other value. If the user
+      // deletes the other value, then we still want to keep the other text box
+      // until the user unchecks the other option.
+      this.otherOptionSelected = true;
+    },
+    validateState: errors.vuelidateHelpers.validateState,
+    validateStateErrorOnly: errors.vuelidateHelpers.validateStateErrorOnly,
+    touch() {
+      this.$v.$touch();
+    },
+  },
+  watch: {
+    valid: {
+      handler(valid) {
+        this.$emit(valid ? "valid" : "invalid");
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceValueEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceValueEditor.vue
new file mode 100644
index 0000000..1ca3ef4
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileSingleChoiceValueEditor.vue
@@ -0,0 +1,159 @@
+<template>
+  <extended-user-profile-value-editor v-bind="$props">
+    <b-form-select
+      v-model="value"
+      :options="options"
+      @change="onChange"
+      :state="validateStateErrorOnly($v.value)"
+    >
+      <template #first>
+        <b-form-select-option :value="null" disabled
+          >-- Please select an option --</b-form-select-option
+        >
+      </template>
+
+      <b-form-select-option
+        :value="otherOptionValue"
+        v-if="extendedUserProfileField.other"
+        >Other (please specify)</b-form-select-option
+      >
+    </b-form-select>
+    <b-form-invalid-feedback :state="validateState($v.value)"
+      >This field is required.</b-form-invalid-feedback
+    >
+    <template v-if="showOther">
+      <b-form-input
+        class="mt-2"
+        v-model="other"
+        placeholder="Please specify"
+        :state="validateState($v.other)"
+        @input="onInput"
+      />
+      <b-form-invalid-feedback :state="validateState($v.other)"
+        >Please specify a value for 'Other'.</b-form-invalid-feedback
+      >
+    </template>
+  </extended-user-profile-value-editor>
+</template>
+
+<script>
+import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { required, requiredIf } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
+import ExtendedUserProfileValueEditor from "./ExtendedUserProfileValueEditor.vue";
+const OTHER_OPTION = new Object(); // sentinel value
+
+export default {
+  mixins: [validationMixin],
+  components: { ExtendedUserProfileValueEditor },
+  props: ["extendedUserProfileField"],
+  data() {
+    return {
+      otherOptionSelected: false,
+    };
+  },
+  computed: {
+    ...mapGetters("extendedUserProfile", [
+      "getSingleChoiceValue",
+      "getSingleChoiceOther",
+    ]),
+    value: {
+      get() {
+        if (this.showOther) {
+          return this.otherOptionValue;
+        } else {
+          return this.getSingleChoiceValue(this.extendedUserProfileField.id);
+        }
+      },
+      set(value) {
+        if (value !== this.otherOptionValue) {
+          this.setSingleChoiceValue({
+            value,
+            id: this.extendedUserProfileField.id,
+          });
+          this.$v.value.$touch();
+        }
+      },
+    },
+    other: {
+      get() {
+        return this.getSingleChoiceOther(this.extendedUserProfileField.id);
+      },
+      set(value) {
+        this.setSingleChoiceOther({
+          value,
+          id: this.extendedUserProfileField.id,
+        });
+        this.$v.other.$touch();
+      },
+    },
+    showOther() {
+      const value = this.getSingleChoiceValue(this.extendedUserProfileField.id);
+      return (value === null && this.other) || this.otherOptionSelected;
+    },
+    options() {
+      return this.extendedUserProfileField &&
+        this.extendedUserProfileField.choices
+        ? this.extendedUserProfileField.choices.map((choice) => {
+            return {
+              value: choice.id,
+              text: choice.display_text,
+            };
+          })
+        : [];
+    },
+    otherOptionValue() {
+      return OTHER_OPTION;
+    },
+    valid() {
+      return !this.$v.$invalid;
+    },
+    required() {
+      return this.extendedUserProfileField.required;
+    },
+  },
+  validations() {
+    const validations = {
+      value: {},
+      other: {},
+    };
+    if (this.showOther) {
+      validations.other = { required };
+    } else {
+      validations.value = { required: requiredIf("required") };
+    }
+    return validations;
+  },
+  methods: {
+    ...mapMutations("extendedUserProfile", [
+      "setSingleChoiceValue",
+      "setSingleChoiceOther",
+    ]),
+    onChange(value) {
+      this.otherOptionSelected = value === this.otherOptionValue;
+    },
+    onInput() {
+      // Handle case where initially there is an other value. If the user
+      // deletes the other value, then we still want to keep the other text box
+      // until the user unchecks the other option.
+      this.otherOptionSelected = true;
+    },
+    validateState: errors.vuelidateHelpers.validateState,
+    validateStateErrorOnly: errors.vuelidateHelpers.validateStateErrorOnly,
+    touch() {
+      this.$v.$touch();
+    },
+  },
+  watch: {
+    valid: {
+      handler(valid) {
+        this.$emit(valid ? "valid" : "invalid");
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextValueEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextValueEditor.vue
new file mode 100644
index 0000000..644ed82
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileTextValueEditor.vue
@@ -0,0 +1,63 @@
+<template>
+  <extended-user-profile-value-editor v-bind="$props">
+    <b-form-input v-model="value" :state="validateState($v.value)" />
+    <b-form-invalid-feedback :state="validateState($v.value)"
+      >This field is required.</b-form-invalid-feedback
+    >
+  </extended-user-profile-value-editor>
+</template>
+
+<script>
+import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { requiredIf } from "vuelidate/lib/validators";
+import { errors } from "django-airavata-common-ui";
+import ExtendedUserProfileValueEditor from "./ExtendedUserProfileValueEditor.vue";
+export default {
+  mixins: [validationMixin],
+  components: { ExtendedUserProfileValueEditor },
+  props: ["extendedUserProfileField"],
+  computed: {
+    ...mapGetters("extendedUserProfile", ["getTextValue"]),
+    value: {
+      get() {
+        return this.getTextValue(this.extendedUserProfileField.id);
+      },
+      set(value) {
+        this.setTextValue({ value, id: this.extendedUserProfileField.id });
+        this.$v.$touch();
+      },
+    },
+    valid() {
+      return !this.$v.$invalid;
+    },
+    required() {
+      return this.extendedUserProfileField.required;
+    },
+  },
+  validations() {
+    return {
+      value: {
+        required: requiredIf("required"),
+      },
+    };
+  },
+  methods: {
+    ...mapMutations("extendedUserProfile", ["setTextValue"]),
+    validateState: errors.vuelidateHelpers.validateState,
+    touch() {
+      this.$v.$touch();
+    },
+  },
+  watch: {
+    valid: {
+      handler(valid) {
+        this.$emit(valid ? "valid" : "invalid");
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
+
+<style></style>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileUserAgreementValueEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileUserAgreementValueEditor.vue
new file mode 100644
index 0000000..035044d
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileUserAgreementValueEditor.vue
@@ -0,0 +1,81 @@
+<template>
+  <extended-user-profile-value-editor v-bind="$props">
+    <b-form-checkbox
+      v-model="value"
+      :unchecked-value="false"
+      :value="true"
+      :state="validateStateErrorOnly($v.value)"
+    >
+      {{ extendedUserProfileField.checkbox_label }}
+    </b-form-checkbox>
+    <b-form-invalid-feedback :state="validateState($v.value)"
+      >This field is required.</b-form-invalid-feedback
+    >
+  </extended-user-profile-value-editor>
+</template>
+
+<script>
+import { mapGetters, mapMutations } from "vuex";
+import { validationMixin } from "vuelidate";
+import { errors } from "django-airavata-common-ui";
+import ExtendedUserProfileValueEditor from "./ExtendedUserProfileValueEditor.vue";
+
+export default {
+  mixins: [validationMixin],
+  components: { ExtendedUserProfileValueEditor },
+  props: ["extendedUserProfileField"],
+  computed: {
+    ...mapGetters("extendedUserProfile", ["getUserAgreementValue"]),
+    value: {
+      get() {
+        return this.getUserAgreementValue(this.extendedUserProfileField.id);
+      },
+      set(value) {
+        this.setUserAgreementValue({
+          value,
+          id: this.extendedUserProfileField.id,
+        });
+        this.$v.value.$touch();
+      },
+    },
+    valid() {
+      return !this.$v.$invalid;
+    },
+    required() {
+      return this.extendedUserProfileField.required;
+    },
+  },
+  validations() {
+    const validations = {
+      value: {
+        mustBeTrue: this.mustBeTrue,
+      },
+    };
+    return validations;
+  },
+  methods: {
+    ...mapMutations("extendedUserProfile", ["setUserAgreementValue"]),
+    mustBeTrue(value) {
+      if (this.required) {
+        return value === true;
+      } else {
+        // If not required, always valid
+        return true;
+      }
+    },
+    validateState: errors.vuelidateHelpers.validateState,
+    validateStateErrorOnly: errors.vuelidateHelpers.validateStateErrorOnly,
+    touch() {
+      this.$v.$touch();
+    },
+  },
+  watch: {
+    valid: {
+      handler(valid) {
+        this.$emit(valid ? "valid" : "invalid");
+      },
+      immediate: true,
+    },
+  },
+};
+</script>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileValueEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileValueEditor.vue
new file mode 100644
index 0000000..d9e3ca4
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/ExtendedUserProfileValueEditor.vue
@@ -0,0 +1,47 @@
+<template>
+  <b-form-group
+    :label="extendedUserProfileField.name"
+    :description="extendedUserProfileField.help_text"
+  >
+    <template #label>
+      {{ extendedUserProfileField.name }}
+      <small
+        v-if="!extendedUserProfileField.required"
+        class="text-muted text-small"
+        >(Optional)</small
+      >
+    </template>
+    <b-card
+      v-for="link in extendedUserProfileField.links"
+      :key="link.id"
+      :header="link.label"
+      class="ml-3 mb-3"
+    >
+      <b-card-text v-if="link.display_inline">
+        <iframe :src="link.url" />
+      </b-card-text>
+      <a
+        v-if="link.display_link"
+        :href="link.url"
+        target="_blank"
+        class="card-link"
+        >Open '{{ link.label }}' in separate tab.</a
+      >
+    </b-card>
+    <slot />
+  </b-form-group>
+</template>
+
+<script>
+export default {
+  props: ["extendedUserProfileField"],
+};
+</script>
+
+<style scoped>
+iframe {
+  border: none;
+  width: 100%;
+  height: 50vh;
+}
+</style>
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
index 669d356..bbac76a 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/components/UserProfileEditor.vue
@@ -1,30 +1,34 @@
 <template>
-  <b-card>
-    <b-form-group label="Username" :disabled="true" description="Only administrators can update a username.">
+  <div v-if="user">
+    <b-form-group
+      label="Username"
+      :disabled="true"
+      description="Only administrators can update a username."
+    >
       <b-form-input v-model="user.username" />
     </b-form-group>
     <b-form-group label="First Name" :disabled="disabled">
       <b-form-input
-        v-model="$v.user.first_name.$model"
+        v-model="$v.first_name.$model"
         @keydown.native.enter="save"
-        :state="validateState($v.user.first_name)"
+        :state="validateState($v.first_name)"
       />
     </b-form-group>
     <b-form-group label="Last Name" :disabled="disabled">
       <b-form-input
-        v-model="$v.user.last_name.$model"
+        v-model="$v.last_name.$model"
         @keydown.native.enter="save"
-        :state="validateState($v.user.last_name)"
+        :state="validateState($v.last_name)"
       />
     </b-form-group>
     <b-form-group label="Email" :disabled="disabled">
       <b-form-input
-        v-model="$v.user.email.$model"
+        v-model="$v.email.$model"
         @keydown.native.enter="save"
-        :state="validateState($v.user.email)"
+        :state="validateState($v.email)"
       />
-      <b-form-invalid-feedback v-if="!$v.user.email.email">
-        {{ user.email }} is not a valid email address.
+      <b-form-invalid-feedback v-if="!$v.email.email">
+        {{ email }} is not a valid email address.
       </b-form-invalid-feedback>
       <b-alert class="mt-1" show v-if="user.pending_email_change"
         >Once you verify your email address at
@@ -36,26 +40,19 @@
         ></b-alert
       >
     </b-form-group>
-    <b-button variant="primary" @click="save" :disabled="$v.$invalid || disabled"
-      >Save</b-button
-    >
-  </b-card>
+  </div>
 </template>
 
 <script>
-import { models } from "django-airavata-api";
 import { errors } from "django-airavata-common-ui";
 import { validationMixin } from "vuelidate";
 import { email, required } from "vuelidate/lib/validators";
+import { mapGetters, mapMutations } from "vuex";
 
 export default {
   name: "user-profile-editor",
   mixins: [validationMixin],
   props: {
-    value: {
-      type: models.User,
-      required: true,
-    },
     disabled: {
       type: Boolean,
       default: false,
@@ -63,46 +60,63 @@
   },
   created() {
     if (!this.disabled) {
-      this.$v.user.$touch();
+      this.$v.$touch();
     }
   },
   data() {
-    return {
-      user: this.cloneValue(),
-    };
+    return {};
+  },
+  computed: {
+    ...mapGetters("userProfile", ["user"]),
+    first_name: {
+      get() {
+        return this.user.first_name;
+      },
+      set(first_name) {
+        this.setFirstName({ first_name });
+      },
+    },
+    last_name: {
+      get() {
+        return this.user.last_name;
+      },
+      set(last_name) {
+        this.setLastName({ last_name });
+      },
+    },
+    email: {
+      get() {
+        return this.user.email;
+      },
+      set(email) {
+        this.setEmail({ email });
+      },
+    },
+    valid() {
+      return !this.$v.$invalid;
+    },
   },
   validations() {
     return {
-      user: {
-        first_name: {
-          required,
-        },
-        last_name: {
-          required,
-        },
-        email: {
-          required,
-          email,
-        },
+      first_name: {
+        required,
+      },
+      last_name: {
+        required,
+      },
+      email: {
+        required,
+        email,
       },
     };
   },
   methods: {
-    cloneValue() {
-      return JSON.parse(JSON.stringify(this.value));
-    },
+    ...mapMutations("userProfile", ["setFirstName", "setLastName", "setEmail"]),
     save() {
-      if (!this.$v.$invalid) {
-        this.$emit("save", this.user);
-      }
+      this.$emit("save");
     },
     validateState: errors.vuelidateHelpers.validateState,
   },
-  watch: {
-    value() {
-      this.user = this.cloneValue();
-    },
-  },
 };
 </script>
 
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue b/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue
index 647a60a..c70772c 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/containers/UserProfileContainer.vue
@@ -13,17 +13,27 @@
         In the meantime, please complete as much of your profile as possible.
       </p>
     </b-alert>
-    <b-alert v-else-if="user && !user.complete" show>
+    <b-alert v-else-if="mustComplete" show
       >Please complete your user profile before continuing.</b-alert
     >
-    <user-profile-editor
-      v-if="user"
-      v-model="user"
-      @save="onSave"
-      @resend-email-verification="resendEmailVerification"
-    />
+    <b-card>
+      <user-profile-editor
+        ref="userProfileEditor"
+        @save="onSave"
+        @resend-email-verification="handleResendEmailVerification"
+      />
+      <!-- include extended-user-profile-editor if there are extendedUserProfileFields -->
+      <template
+        v-if="extendedUserProfileFields && extendedUserProfileFields.length > 0"
+      >
+        <hr />
+        <extended-user-profile-editor ref="extendedUserProfileEditor" />
+      </template>
+
+      <b-button variant="primary" @click="onSave">Save</b-button>
+    </b-card>
     <b-link
-      v-if="user && user.complete"
+      v-if="!mustComplete"
       class="text-muted small"
       href="/workspace/dashboard"
       >Return to Dashboard</b-link
@@ -32,36 +42,66 @@
 </template>
 
 <script>
-import { services } from "django-airavata-api";
 import UserProfileEditor from "../components/UserProfileEditor.vue";
 import { notifications } from "django-airavata-common-ui";
+import { mapActions, mapGetters } from "vuex";
+import ExtendedUserProfileEditor from "../components/ExtendedUserProfileEditor.vue";
 
 export default {
-  components: { UserProfileEditor },
+  components: { UserProfileEditor, ExtendedUserProfileEditor },
   name: "user-profile-container",
-  created() {
-    services.UserService.current()
-      .then((user) => {
-        this.user = user;
-      })
-      .then(() => {
-        const queryParams = new URLSearchParams(window.location.search);
-        if (queryParams.has("code")) {
-          this.verifyEmailChange(queryParams.get("code"));
-        }
-      });
+  async created() {
+    await this.loadCurrentUser();
+    await this.loadExtendedUserProfileFields();
+    await this.loadExtendedUserProfileValues();
+
+    const queryParams = new URLSearchParams(window.location.search);
+    if (queryParams.has("code")) {
+      await this.verifyEmailChange({ code: queryParams.get("code") });
+      notifications.NotificationList.add(
+        new notifications.Notification({
+          type: "SUCCESS",
+          message: "Email address verified and updated",
+          duration: 5,
+        })
+      );
+      // Update URL, removing the code from the query string
+      window.history.replaceState({}, "", "/auth/user-profile/");
+    }
   },
   data() {
     return {
-      user: null,
+      invalidForm: false,
     };
   },
+  computed: {
+    ...mapGetters("userProfile", ["user"]),
+    ...mapGetters("extendedUserProfile", ["extendedUserProfileFields"]),
+    mustComplete() {
+      return (
+        this.user && (!this.user.complete || !this.user.ext_user_profile_valid)
+      );
+    },
+  },
   methods: {
-    onSave(value) {
-      services.UserService.update({
-        lookup: value.id,
-        data: value,
-      }).then((user) => {
+    ...mapActions("userProfile", [
+      "loadCurrentUser",
+      "verifyEmailChange",
+      "updateUser",
+      "resendEmailVerification",
+    ]),
+    ...mapActions("extendedUserProfile", [
+      "loadExtendedUserProfileFields",
+      "loadExtendedUserProfileValues",
+      "saveExtendedUserProfileValues",
+    ]),
+    async onSave() {
+      if (
+        this.$refs.userProfileEditor.valid &&
+        this.$refs.extendedUserProfileEditor.valid
+      ) {
+        await this.updateUser();
+        await this.saveExtendedUserProfileValues();
         notifications.NotificationList.add(
           new notifications.Notification({
             type: "SUCCESS",
@@ -69,39 +109,19 @@
             duration: 5,
           })
         );
-        this.user = user;
-      });
+      } else {
+        this.$refs.extendedUserProfileEditor.touch();
+      }
     },
-    resendEmailVerification() {
-      services.UserService.resendEmailVerification({
-        lookup: this.user.id,
-      }).then(() => {
-        notifications.NotificationList.add(
-          new notifications.Notification({
-            type: "SUCCESS",
-            message: "Verification link sent",
-            duration: 5,
-          })
-        );
-      });
-    },
-    verifyEmailChange(code) {
-      services.UserService.verifyEmailChange({
-        lookup: this.user.id,
-        data: { code: code },
-      }).then((user) => {
-        // User now updated with email change
-        this.user = user;
-        notifications.NotificationList.add(
-          new notifications.Notification({
-            type: "SUCCESS",
-            message: "Email address verified and updated",
-            duration: 5,
-          })
-        );
-        // Update URL, removing the code from the query string
-        window.history.replaceState({}, "", "/auth/user-profile/");
-      });
+    async handleResendEmailVerification() {
+      await this.resendEmailVerification();
+      notifications.NotificationList.add(
+        new notifications.Notification({
+          type: "SUCCESS",
+          message: "Verification link sent",
+          duration: 5,
+        })
+      );
     },
   },
 };
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/entry-user-profile.js b/django_airavata/apps/auth/static/django_airavata_auth/js/entry-user-profile.js
index 1052e61..382bfcd 100644
--- a/django_airavata/apps/auth/static/django_airavata_auth/js/entry-user-profile.js
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/entry-user-profile.js
@@ -1,8 +1,11 @@
 import { components, entry } from "django-airavata-common-ui";
 import UserProfileContainer from "./containers/UserProfileContainer.vue";
+import createStore from "./store";
 
-entry(Vue => {
-    new Vue({
-        render: h => h(components.MainLayout, [h(UserProfileContainer)])
-    }).$mount("#user-profile");
+entry((Vue) => {
+  const store = createStore(Vue);
+  new Vue({
+    store,
+    render: (h) => h(components.MainLayout, [h(UserProfileContainer)]),
+  }).$mount("#user-profile");
 });
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/store/index.js b/django_airavata/apps/auth/static/django_airavata_auth/js/store/index.js
new file mode 100644
index 0000000..8dd262a
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/store/index.js
@@ -0,0 +1,18 @@
+import Vuex from "vuex";
+import userProfile from "./modules/userProfile";
+import extendedUserProfile from "./modules/extendedUserProfile";
+
+const debug = process.env.NODE_ENV !== "production";
+
+function createStore(Vue) {
+  Vue.use(Vuex);
+  return new Vuex.Store({
+    modules: {
+      userProfile,
+      extendedUserProfile,
+    },
+    strict: debug,
+  });
+}
+
+export default createStore;
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/extendedUserProfile.js b/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/extendedUserProfile.js
new file mode 100644
index 0000000..084731b
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/extendedUserProfile.js
@@ -0,0 +1,188 @@
+import { services } from "django-airavata-api";
+
+const state = () => ({
+  extendedUserProfileFields: null,
+  extendedUserProfileValues: [],
+});
+
+const getters = {
+  extendedUserProfileFields: (state) => state.extendedUserProfileFields,
+  extendedUserProfileValues: (state) => state.extendedUserProfileValues,
+  getTextValue: (state) => (id) => {
+    const value = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    return value ? value.text_value : null;
+  },
+  getSingleChoiceValue: (state) => (id) => {
+    const value = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    if (value && value.choices && value.choices.length === 1) {
+      return value.choices[0];
+    } else {
+      return null;
+    }
+  },
+  getSingleChoiceOther: (state) => (id) => {
+    const value = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    return value ? value.other_value : null;
+  },
+  getMultiChoiceValue: (state) => (id) => {
+    const value = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    if (value && value.choices) {
+      return value.choices;
+    } else {
+      return [];
+    }
+  },
+  getMultiChoiceOther: (state) => (id) => {
+    const value = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    return value ? value.other_value : null;
+  },
+  getUserAgreementValue: (state) => (id) => {
+    const value = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    return value ? value.agreement_value : false;
+  },
+};
+
+const actions = {
+  async loadExtendedUserProfileFields({ commit }) {
+    const extendedUserProfileFields = await services.ExtendedUserProfileFieldService.list();
+    commit("setExtendedUserProfileFields", { extendedUserProfileFields });
+  },
+  async loadExtendedUserProfileValues({ commit }) {
+    const extendedUserProfileValues = await services.ExtendedUserProfileValueService.list();
+    commit("setExtendedUserProfileValues", { extendedUserProfileValues });
+  },
+  async saveExtendedUserProfileValues({ state, commit }) {
+    const extendedUserProfileValues = await services.ExtendedUserProfileValueService.saveAll(
+      { data: state.extendedUserProfileValues }
+    );
+    commit("updateExtendedUserProfileValues", { extendedUserProfileValues });
+  },
+};
+
+const mutations = {
+  setExtendedUserProfileFields(state, { extendedUserProfileFields }) {
+    state.extendedUserProfileFields = extendedUserProfileFields;
+  },
+  setExtendedUserProfileValues(state, { extendedUserProfileValues }) {
+    state.extendedUserProfileValues = extendedUserProfileValues;
+  },
+  setTextValue(state, { value, id }) {
+    const profileValue = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    if (profileValue) {
+      profileValue.text_value = value;
+    } else {
+      state.extendedUserProfileValues.push({
+        value_type: "text",
+        ext_user_profile_field: id,
+        text_value: value,
+      });
+    }
+  },
+  setSingleChoiceValue(state, { value, id }) {
+    const profileValue = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    if (profileValue) {
+      profileValue.choices = [value];
+      profileValue.other_value = "";
+    } else {
+      state.extendedUserProfileValues.push({
+        value_type: "single_choice",
+        ext_user_profile_field: id,
+        choices: [value],
+      });
+    }
+  },
+  setSingleChoiceOther(state, { value, id }) {
+    const profileValue = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    if (profileValue) {
+      profileValue.choices = [];
+      profileValue.other_value = value;
+    } else {
+      state.extendedUserProfileValues.push({
+        value_type: "single_choice",
+        ext_user_profile_field: id,
+        choices: [],
+        other_value: value,
+      });
+    }
+  },
+  setMultiChoiceValue(state, { value, id }) {
+    const profileValue = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    if (profileValue) {
+      profileValue.choices = value;
+    } else {
+      state.extendedUserProfileValues.push({
+        value_type: "multi_choice",
+        ext_user_profile_field: id,
+        choices: value,
+      });
+    }
+  },
+  setMultiChoiceOther(state, { value, id }) {
+    const profileValue = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    if (profileValue) {
+      profileValue.other_value = value;
+    } else {
+      state.extendedUserProfileValues.push({
+        value_type: "multi_choice",
+        ext_user_profile_field: id,
+        choices: [],
+        other_value: value,
+      });
+    }
+  },
+  setUserAgreementValue(state, { value, id }) {
+    const profileValue = state.extendedUserProfileValues.find(
+      (v) => v.ext_user_profile_field === id
+    );
+    if (profileValue) {
+      profileValue.agreement_value = value;
+    } else {
+      state.extendedUserProfileValues.push({
+        value_type: "user_agreement",
+        ext_user_profile_field: id,
+        agreement_value: value,
+      });
+    }
+  },
+  updateExtendedUserProfileValue(state, { extendedUserProfileValue }) {
+    const index = state.extendedUserProfileValues.findIndex(
+      (v) =>
+        v.ext_user_profile_field ===
+        extendedUserProfileValue.ext_user_profile_field
+    );
+    state.extendedUserProfileValues.splice(index, 1, extendedUserProfileValue);
+  },
+  updateExtendedUserProfileValues(state, { extendedUserProfileValues }) {
+    state.extendedUserProfileValues = extendedUserProfileValues;
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  getters,
+  actions,
+  mutations,
+};
diff --git a/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/userProfile.js b/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/userProfile.js
new file mode 100644
index 0000000..9b629ef
--- /dev/null
+++ b/django_airavata/apps/auth/static/django_airavata_auth/js/store/modules/userProfile.js
@@ -0,0 +1,64 @@
+import { services } from "django-airavata-api";
+
+const state = () => ({
+  user: null,
+});
+
+const getters = {
+  user: (state) => state.user,
+};
+
+const actions = {
+  async loadCurrentUser({ commit }) {
+    const user = await services.UserService.current();
+    commit("setUser", { user });
+  },
+
+  async verifyEmailChange({ commit, state }, { code }) {
+    const user = await services.UserService.verifyEmailChange({
+      lookup: state.user.id,
+      data: { code },
+    });
+    commit("setUser", { user });
+  },
+
+  async updateUser({ commit, state }) {
+    const user = await services.UserService.update({
+      lookup: state.user.id,
+      data: state.user,
+    });
+    commit("setUser", { user });
+  },
+
+  async resendEmailVerification({ state }) {
+    await services.UserService.resendEmailVerification({
+      lookup: state.user.id,
+    });
+  },
+};
+
+const mutations = {
+  setUser(state, { user }) {
+    state.user = user;
+  },
+
+  setFirstName(state, { first_name }) {
+    state.user.first_name = first_name;
+  },
+
+  setLastName(state, { last_name }) {
+    state.user.last_name = last_name;
+  },
+
+  setEmail(state, { email }) {
+    state.user.email = email;
+  },
+};
+
+export default {
+  namespaced: true,
+  state,
+  getters,
+  actions,
+  mutations,
+};
diff --git a/django_airavata/apps/auth/tests/test_middleware.py b/django_airavata/apps/auth/tests/test_middleware.py
new file mode 100644
index 0000000..81091af
--- /dev/null
+++ b/django_airavata/apps/auth/tests/test_middleware.py
@@ -0,0 +1,123 @@
+
+from unittest.mock import MagicMock, sentinel
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AnonymousUser
+from django.http import HttpResponseRedirect
+from django.test import RequestFactory, TestCase
+from django.urls import reverse
+
+from django_airavata.apps.auth import models
+from django_airavata.apps.auth.middleware import (
+    user_profile_completeness_check
+)
+
+
+class UserProfileCompletenessCheckTestCase(TestCase):
+
+    def setUp(self):
+        User = get_user_model()
+        self.user: User = User.objects.create_user("testuser")
+        self.user_profile: models.UserProfile = models.UserProfile.objects.create(user=self.user)
+        self.factory = RequestFactory()
+
+    def _middleware_passes_through(self, request):
+        get_response = MagicMock(return_value=sentinel.response)
+        response = user_profile_completeness_check(get_response)(request)
+        get_response.assert_called()
+        self.assertIs(response, sentinel.response)
+
+    def _middleware_redirects_to_user_profile(self, request):
+        get_response = MagicMock(return_value=sentinel.response)
+        response = user_profile_completeness_check(get_response)(request)
+        get_response.assert_not_called()
+        self.assertIsInstance(response, HttpResponseRedirect)
+        self.assertEqual(response.url, reverse('django_airavata_auth:user_profile'))
+
+    def test_not_authenticated(self):
+        """Test that completeness check is skipped when not authenticated."""
+        request = self.factory.get(reverse('django_airavata_workspace:dashboard'))
+        request.user = AnonymousUser()
+        self.assertFalse(request.user.is_authenticated)
+        self._middleware_passes_through(request)
+
+    def test_user_profile_is_incomplete(self):
+        """Test user profile incomplete, should redirect to user_profile view."""
+        request = self.factory.get(reverse('django_airavata_workspace:dashboard'), HTTP_ACCEPT=['text/html'])
+        request.user = self.user
+        self.assertTrue(request.user.is_authenticated)
+        self.assertFalse(self.user_profile.is_complete)
+        self._middleware_redirects_to_user_profile(request)
+
+    def test_user_profile_is_incomplete_but_allowed(self):
+        """Test user profile incomplete, but should be able to access user_profile."""
+        request = self.factory.get(reverse('django_airavata_auth:user_profile'), HTTP_ACCEPT=['text/html'])
+        request.user = self.user
+        self.assertTrue(request.user.is_authenticated)
+        self.assertFalse(self.user_profile.is_complete)
+        self._middleware_passes_through(request)
+
+    def test_user_profile_is_complete(self):
+        """Test user profile is complete, should pass through."""
+        request = self.factory.get(reverse('django_airavata_workspace:dashboard'), HTTP_ACCEPT=['text/html'])
+        request.user = self.user
+        self.user.first_name = "Test"
+        self.user.last_name = "User"
+        self.user.email = "testuser@gateway.edu"
+        self.assertTrue(request.user.is_authenticated)
+        self.assertTrue(self.user_profile.is_complete)
+        self.assertTrue(self.user_profile.is_ext_user_profile_valid)
+        self._middleware_passes_through(request)
+
+    def test_user_profile_is_complete_but_ext_up_is_invalid(self):
+        """Test user profile is complete, but ext user prof is invalid."""
+        request = self.factory.get(reverse('django_airavata_workspace:dashboard'), HTTP_ACCEPT=['text/html'])
+        request.user = self.user
+        self.user.first_name = "Test"
+        self.user.last_name = "User"
+        self.user.email = "testuser@gateway.edu"
+        models.ExtendedUserProfileTextField.objects.create(name="test1", order=1, required=True)
+        self.assertTrue(request.user.is_authenticated)
+        self.assertTrue(self.user_profile.is_complete)
+        self.assertFalse(self.user_profile.is_ext_user_profile_valid)
+        self._middleware_redirects_to_user_profile(request)
+
+    def test_user_profile_is_complete_and_ext_up_is_valid(self):
+        """Test user profile is complete and ext user prof is valid."""
+        request = self.factory.get(reverse('django_airavata_workspace:dashboard'), HTTP_ACCEPT=['text/html'])
+        request.user = self.user
+        self.user.first_name = "Test"
+        self.user.last_name = "User"
+        self.user.email = "testuser@gateway.edu"
+        field1 = models.ExtendedUserProfileTextField.objects.create(name="test1", order=1, required=True)
+        models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field1, user_profile=self.user_profile,
+            text_value="Answer #1"
+        )
+        self.assertTrue(self.user_profile.is_ext_user_profile_valid)
+        self.assertTrue(request.user.is_authenticated)
+        self.assertTrue(self.user_profile.is_complete)
+        self.assertEqual(1, len(self.user_profile.extended_profile_values.all()))
+        self._middleware_passes_through(request)
+
+    def test_user_profile_is_complete_ext_up_is_invalid_but_user_is_admin(self):
+        """Test user profile is complete, ext user prof is invalid, but user is gateway admin."""
+        request = self.factory.get(reverse('django_airavata_workspace:dashboard'), HTTP_ACCEPT=['text/html'])
+        request.user = self.user
+        request.is_gateway_admin = True
+        self.user.first_name = "Admin"
+        self.user.last_name = "User"
+        self.user.email = "admin@gateway.edu"
+        models.ExtendedUserProfileTextField.objects.create(name="test1", order=1, required=True)
+        self.assertFalse(self.user_profile.is_ext_user_profile_valid)
+        self.assertTrue(request.user.is_authenticated)
+        self.assertTrue(self.user_profile.is_complete)
+        self._middleware_passes_through(request)
+
+    def test_user_profile_is_incomplete_but_logout_allowed(self):
+        """Test user profile incomplete, but should be able to access logout."""
+        request = self.factory.get(reverse('django_airavata_auth:logout'), HTTP_ACCEPT=['text/html'])
+        request.user = self.user
+        self.assertTrue(request.user.is_authenticated)
+        self.assertFalse(self.user_profile.is_complete)
+        self._middleware_passes_through(request)
diff --git a/django_airavata/apps/auth/tests/test_models.py b/django_airavata/apps/auth/tests/test_models.py
new file mode 100644
index 0000000..677fc8f
--- /dev/null
+++ b/django_airavata/apps/auth/tests/test_models.py
@@ -0,0 +1,383 @@
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+
+from django_airavata.apps.auth import models
+
+
+class ExtendedUserProfileValueTestCase(TestCase):
+
+    def setUp(self) -> None:
+        User = get_user_model()
+        user = User.objects.create_user("testuser")
+        self.user_profile = models.UserProfile.objects.create(user=user)
+
+    def test_value_display_of_text_value(self):
+        field = models.ExtendedUserProfileTextField.objects.create(
+            name="test", order=1)
+        value = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            text_value="Some random answer.")
+        self.assertEqual(value.value_display, value.text_value)
+
+    def test_value_display_of_single_choice_with_choice(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        choice_two = field.choices.get(display_text="Choice #2")
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            choice=choice_two.id)
+        self.assertEqual(value.value_display, choice_two.display_text)
+
+    def test_value_display_of_single_choice_with_non_existent_choice(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            choice=-1)
+        self.assertEqual(value.value_display, None)
+
+    def test_value_display_of_single_choice_with_other(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Write-in value")
+        self.assertEqual(value.value_display, "Other: Write-in value")
+
+    def test_value_display_of_multi_choice_with_choices(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1)
+        choice_one = field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        choice_three = field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        value.choices.create(value=choice_one.id)
+        value.choices.create(value=choice_three.id)
+        self.assertListEqual(value.value_display, [choice_one.display_text, choice_three.display_text])
+
+    def test_value_display_of_multi_choice_with_other(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Some write-in value.")
+        self.assertListEqual(value.value_display, ["Other: Some write-in value."])
+
+    def test_value_display_of_multi_choice_with_choices_and_other(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1)
+        choice_one = field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        choice_three = field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Some write-in value.")
+        value.choices.create(value=choice_one.id)
+        value.choices.create(value=choice_three.id)
+        self.assertListEqual(value.value_display, [choice_one.display_text, choice_three.display_text, "Other: Some write-in value."])
+
+    def test_value_display_of_multi_choice_with_non_existent_choices(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1)
+        choice_one = field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        choice_three = field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        value.choices.create(value=choice_one.id)
+        value.choices.create(value=choice_three.id)
+        value.choices.create(value=-1)
+        self.assertListEqual(value.value_display, [choice_one.display_text, choice_three.display_text])
+
+    def test_value_display_of_user_agreement_with_no(self):
+        field = models.ExtendedUserProfileAgreementField.objects.create(
+            name="test", order=1)
+        value = models.ExtendedUserProfileAgreementValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            agreement_value=False)
+        self.assertEqual(value.value_display, "No")
+
+    def test_value_display_of_user_agreement_with_yes(self):
+        field = models.ExtendedUserProfileAgreementField.objects.create(
+            name="test", order=1)
+        value = models.ExtendedUserProfileAgreementValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            agreement_value=True)
+        self.assertEqual(value.value_display, "Yes")
+
+    def test_valid_of_text(self):
+        field = models.ExtendedUserProfileTextField.objects.create(
+            name="test", order=1, required=True)
+        value = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            text_value="Some value")
+        self.assertTrue(value.valid)
+
+    def test_valid_of_text_empty(self):
+        field = models.ExtendedUserProfileTextField.objects.create(
+            name="test", order=1, required=True)
+        value = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            text_value="")
+        self.assertFalse(value.valid)
+
+    def test_valid_of_text_empty_deleted(self):
+        """Invalid value but field is deleted so valid should be true."""
+        field = models.ExtendedUserProfileTextField.objects.create(
+            name="test", order=1, required=True, deleted=True)
+        value = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            text_value="")
+        self.assertTrue(value.valid, "Although value is empty but required, since the field is deleted, consider it not invalid")
+
+    def test_valid_of_text_empty_no_required(self):
+        field = models.ExtendedUserProfileTextField.objects.create(
+            name="test", order=1, required=False)
+        value = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            text_value="")
+        self.assertTrue(value.valid)
+
+    def test_valid_of_single_choice_none(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1, required=True)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        self.assertFalse(value.valid)
+
+    def test_valid_of_single_choice_with_choice(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1, required=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        choice_two = field.choices.get(display_text="Choice #2")
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            choice=choice_two.id)
+        self.assertTrue(value.valid)
+
+    def test_valid_of_single_choice_with_non_existent_choice(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1, required=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            choice=-1)
+        self.assertFalse(value.valid)
+
+    def test_valid_of_single_choice_with_other(self):
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1, required=True, other=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Some write-in value.")
+        self.assertTrue(value.valid)
+
+    def test_valid_of_single_choice_with_other_but_not_allowed(self):
+        # Configure field so that 'Other' isn't an option
+        field = models.ExtendedUserProfileSingleChoiceField.objects.create(
+            name="test", order=1, required=True, other=False)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileSingleChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Some write-in value.")
+        self.assertFalse(value.valid)
+
+    def test_valid_of_multi_choice_with_none(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1, required=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        self.assertFalse(value.valid)
+
+    def test_valid_of_multi_choice_with_some(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1, required=True)
+        choice_one = field.choices.create(display_text="Choice #1", order=1)
+        choice_two = field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        value.choices.create(value=choice_one.id)
+        value.choices.create(value=choice_two.id)
+        self.assertTrue(value.valid)
+
+    def test_valid_of_multi_choice_with_non_existent_choice(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1, required=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile)
+        value.choices.create(value=-1)
+        self.assertFalse(value.valid)
+
+    def test_valid_of_multi_choice_with_other(self):
+        field = models.ExtendedUserProfileMultiChoiceField.objects.create(
+            name="test", order=1, required=True, other=True)
+        field.choices.create(display_text="Choice #1", order=1)
+        field.choices.create(display_text="Choice #2", order=2)
+        field.choices.create(display_text="Choice #3", order=3)
+        value = models.ExtendedUserProfileMultiChoiceValue.objects.create(
+            ext_user_profile_field=field, user_profile=self.user_profile,
+            other_value="Some write-in value.")
+        self.assertTrue(value.valid)
+
+
+class UserProfileTestCase(TestCase):
+
+    def setUp(self) -> None:
+        User = get_user_model()
+        user = User.objects.create_user("testuser")
+        self.user_profile: models.UserProfile = models.UserProfile.objects.create(user=user)
+
+    def test_is_ext_user_profile_valid_no_fields(self):
+        self.assertTrue(self.user_profile.is_ext_user_profile_valid)
+
+    def test_is_ext_user_profile_valid_some_fields_some_values_valid(self):
+        """Values for all fields, but only some are valid"""
+        field1 = models.ExtendedUserProfileTextField.objects.create(
+            name="test1", order=1, required=True)
+        field2 = models.ExtendedUserProfileTextField.objects.create(
+            name="test2", order=2, required=True)
+        field3 = models.ExtendedUserProfileTextField.objects.create(
+            name="test3", order=3, required=True)
+        value1 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field1, user_profile=self.user_profile,
+            text_value="Answer #1"
+        )
+        value2 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field2, user_profile=self.user_profile,
+            text_value="Answer #2"
+        )
+        value3 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field3, user_profile=self.user_profile,
+            text_value=""  # intentionally blank
+        )
+        self.assertTrue(value1.valid)
+        self.assertTrue(value2.valid)
+        self.assertFalse(value3.valid)
+        self.assertFalse(self.user_profile.is_ext_user_profile_valid)
+
+    def test_is_ext_user_profile_valid_some_fields_all_values_valid(self):
+        """Values for all fields, and all values are valid."""
+        field1 = models.ExtendedUserProfileTextField.objects.create(
+            name="test1", order=1, required=True)
+        field2 = models.ExtendedUserProfileTextField.objects.create(
+            name="test2", order=2, required=True)
+        field3 = models.ExtendedUserProfileTextField.objects.create(
+            name="test3", order=3, required=True)
+        value1 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field1, user_profile=self.user_profile,
+            text_value="Answer #1"
+        )
+        value2 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field2, user_profile=self.user_profile,
+            text_value="Answer #2"
+        )
+        value3 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field3, user_profile=self.user_profile,
+            text_value="Answer #3"
+        )
+        self.assertTrue(value1.valid)
+        self.assertTrue(value2.valid)
+        self.assertTrue(value3.valid)
+        self.assertTrue(self.user_profile.is_ext_user_profile_valid)
+
+    def test_is_ext_user_profile_valid_some_fields_some_not_required_values_missing(self):
+        """Some values are missing but they are optional."""
+        field1 = models.ExtendedUserProfileTextField.objects.create(
+            name="test1", order=1, required=True)
+        field2 = models.ExtendedUserProfileTextField.objects.create(
+            name="test2", order=2, required=False)
+        field3 = models.ExtendedUserProfileTextField.objects.create(
+            name="test3", order=3, required=False)
+        value1 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field1, user_profile=self.user_profile,
+            text_value="Answer #1"
+        )
+        self.assertTrue(value1.valid)
+        self.assertFalse(models.ExtendedUserProfileValue.objects.filter(
+            user_profile=self.user_profile, ext_user_profile_field=field2).exists(),
+            "No value for field2")
+        self.assertFalse(models.ExtendedUserProfileValue.objects.filter(
+            user_profile=self.user_profile, ext_user_profile_field=field3).exists(),
+            "No value for field3")
+        self.assertTrue(self.user_profile.is_ext_user_profile_valid)
+
+    def test_is_ext_user_profile_valid_some_fields_some_required_values_missing(self):
+        """Some required values are missing."""
+        field1 = models.ExtendedUserProfileTextField.objects.create(
+            name="test1", order=1, required=True)
+        field2 = models.ExtendedUserProfileTextField.objects.create(
+            name="test2", order=2, required=True)
+        field3 = models.ExtendedUserProfileTextField.objects.create(
+            name="test3", order=3, required=False)
+        value1 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field1, user_profile=self.user_profile,
+            text_value="Answer #1"
+        )
+        self.assertTrue(value1.valid)
+        self.assertFalse(models.ExtendedUserProfileValue.objects.filter(
+            user_profile=self.user_profile, ext_user_profile_field=field2).exists(),
+            "No value for field2, but field2 is required")
+        self.assertFalse(models.ExtendedUserProfileValue.objects.filter(
+            user_profile=self.user_profile, ext_user_profile_field=field3).exists(),
+            "No value for field3")
+        self.assertFalse(self.user_profile.is_ext_user_profile_valid)
+
+    def test_is_ext_user_profile_valid_some_fields_invalid_value_but_field_deleted(self):
+        """Value is invalid but field is deleted so it shouldn't count."""
+        field1 = models.ExtendedUserProfileTextField.objects.create(
+            name="test1", order=1, required=True)
+        field2 = models.ExtendedUserProfileTextField.objects.create(
+            name="test2", order=2, required=True)
+        field3 = models.ExtendedUserProfileTextField.objects.create(
+            name="test3", order=3, required=True)
+
+        value1 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field1, user_profile=self.user_profile,
+            text_value="Answer #1"
+        )
+        value2 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field2, user_profile=self.user_profile,
+            text_value="Answer #2"
+        )
+        value3 = models.ExtendedUserProfileTextValue.objects.create(
+            ext_user_profile_field=field3, user_profile=self.user_profile,
+            text_value=""
+        )
+        self.assertTrue(value1.valid)
+        self.assertTrue(value2.valid)
+        self.assertFalse(value3.valid)
+        self.assertFalse(self.user_profile.is_ext_user_profile_valid)
+
+        field3.deleted = True
+        field3.save()
+        self.assertTrue(value3.valid)
+        self.assertTrue(self.user_profile.is_ext_user_profile_valid)
diff --git a/django_airavata/apps/auth/urls.py b/django_airavata/apps/auth/urls.py
index 89909a5..a762271 100644
--- a/django_airavata/apps/auth/urls.py
+++ b/django_airavata/apps/auth/urls.py
@@ -7,6 +7,8 @@
 
 router = routers.DefaultRouter()
 router.register(r'users', views.UserViewSet, basename='user')
+router.register(r'extended-user-profile-fields', views.ExtendedUserProfileFieldViewset, basename='extended-user-profile-field')
+router.register(r'extended-user-profile-values', views.ExtendedUserProfileValueViewset, basename='extended-user-profile-value')
 app_name = 'django_airavata_auth'
 urlpatterns = [
     re_path(r'^', include(router.urls)),
diff --git a/django_airavata/apps/auth/utils.py b/django_airavata/apps/auth/utils.py
index d7ebda8..cfa53c0 100644
--- a/django_airavata/apps/auth/utils.py
+++ b/django_airavata/apps/auth/utils.py
@@ -129,14 +129,7 @@
     })
     subject = Template(new_user_email_template.subject).render(context)
     body = Template(new_user_email_template.body).render(context)
-    msg = EmailMessage(subject=subject,
-                       body=body,
-                       from_email=f'"{settings.PORTAL_TITLE}" <{settings.SERVER_EMAIL}>',
-                       to=[f'"{a[0]}" <{a[1]}>' for a in getattr(settings,
-                                                                 'PORTAL_ADMINS',
-                                                                 settings.ADMINS)])
-    msg.content_subtype = 'html'
-    msg.send()
+    send_email_to_admins(subject, body)
 
 
 def send_admin_alert_about_uninitialized_username(request, username, email, first_name, last_name):
@@ -174,6 +167,33 @@
     portal.
     </p>
     """.strip()).render(context)
+    send_email_to_admins(subject, body)
+
+
+def send_admin_user_completed_profile(request, user_profile):
+    domain, port = split_domain_port(request.get_host())
+    user = user_profile.user
+    extended_profile_values = user_profile.extended_profile_values.filter(
+        ext_user_profile_field__deleted=False).order_by("ext_user_profile_field__order").all()
+    context = Context({
+        "username": user.username,
+        "email": user.email,
+        "first_name": user.first_name,
+        "last_name": user.last_name,
+        "portal_title": settings.PORTAL_TITLE,
+        "gateway_id": settings.GATEWAY_ID,
+        "http_host": domain,
+        "extended_profile_values": extended_profile_values
+    })
+
+    user_profile_completed_template = models.EmailTemplate.objects.get(
+        pk=models.USER_PROFILE_COMPLETED_TEMPLATE)
+    subject = Template(user_profile_completed_template.subject).render(context)
+    body = Template(user_profile_completed_template.body).render(context)
+    send_email_to_admins(subject, body)
+
+
+def send_email_to_admins(subject, body):
     msg = EmailMessage(subject=subject,
                        body=body,
                        from_email=f'"{settings.PORTAL_TITLE}" <{settings.SERVER_EMAIL}>',
diff --git a/django_airavata/apps/auth/views.py b/django_airavata/apps/auth/views.py
index edf8861..0b4fdd9 100644
--- a/django_airavata/apps/auth/views.py
+++ b/django_airavata/apps/auth/views.py
@@ -24,10 +24,14 @@
 from django.urls import reverse
 from django.views.decorators.debug import sensitive_variables
 from requests_oauthlib import OAuth2Session
-from rest_framework import permissions, viewsets
+from rest_framework import mixins, permissions, viewsets
 from rest_framework.decorators import action
 from rest_framework.response import Response
 
+from django_airavata.apps.api.view_utils import (
+    IsInAdminsGroupPermission,
+    ReadOnly
+)
 from django_airavata.apps.auth import serializers
 
 from . import forms, iam_admin_client, models, utils
@@ -701,3 +705,67 @@
     r = requests.get(client_endpoint + "/client-secret", headers=headers)
     r.raise_for_status()
     return r.json()['value']
+
+
+class ExtendedUserProfileFieldViewset(viewsets.ModelViewSet):
+    serializer_class = serializers.ExtendedUserProfileFieldSerializer
+    queryset = models.ExtendedUserProfileField.objects.all().order_by('order')
+    permission_classes = [permissions.IsAuthenticated, IsInAdminsGroupPermission | ReadOnly]
+
+    def get_queryset(self):
+        queryset = super().get_queryset()
+        if self.action == 'list':
+            queryset = queryset.filter(deleted=False)
+        return queryset
+
+    def perform_destroy(self, instance):
+        instance.deleted = True
+        instance.save()
+
+
+class IsExtendedUserProfileOwnerOrReadOnlyForAdmins(permissions.BasePermission):
+    def has_permission(self, request, view):
+        return request.user.is_authenticated
+
+    def has_object_permission(self, request, view, obj):
+        if (request.method in permissions.SAFE_METHODS and
+                request.is_gateway_admin):
+            return True
+        return obj.user_profile.user == request.user
+
+
+class ExtendedUserProfileValueViewset(mixins.CreateModelMixin,
+                                      mixins.RetrieveModelMixin,
+                                      mixins.UpdateModelMixin,
+                                      mixins.ListModelMixin,
+                                      viewsets.GenericViewSet):
+    serializer_class = serializers.ExtendedUserProfileValueSerializer
+    permission_classes = [IsExtendedUserProfileOwnerOrReadOnlyForAdmins]
+
+    def get_queryset(self):
+        user = self.request.user
+        if self.request.is_gateway_admin:
+            queryset = models.ExtendedUserProfileValue.objects.all()
+            username = self.request.query_params.get('username')
+            if username is not None:
+                queryset = queryset.filter(user_profile__user__username=username)
+        else:
+            queryset = user.user_profile.extended_profile_values.all()
+        return queryset
+
+    @action(methods=['POST'], detail=False, url_path="save-all")
+    @atomic
+    def save_all(self, request, format=None):
+        user = request.user
+        user_profile: models.UserProfile = user.user_profile
+        old_valid = user_profile.is_ext_user_profile_valid
+        serializer: serializers.ExtendedUserProfileValueSerializer = self.get_serializer(data=request.data, many=True)
+        serializer.is_valid(raise_exception=True)
+        values = serializer.save()
+
+        new_valid = user_profile.is_ext_user_profile_valid
+        if not old_valid and new_valid:
+            utils.send_admin_user_completed_profile(request, user_profile)
+
+        serializer = self.get_serializer(values, many=True)
+        return Response(serializer.data)
diff --git a/django_airavata/apps/auth/yarn.lock b/django_airavata/apps/auth/yarn.lock
index eadbe09..817a0c7 100644
--- a/django_airavata/apps/auth/yarn.lock
+++ b/django_airavata/apps/auth/yarn.lock
@@ -8272,6 +8272,11 @@
   resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.7.6.tgz#84100c13b943470660d0416642845cd2a1edf4b2"
   integrity sha512-suzIuet1jGcyZ4oUSW8J27R2tNrJ9cIfklAh63EbAkFjE380iv97BAiIeolRYoB9bF9usBXCu4BxftWN1Dkn3g==
 
+vuex@^3.6.2:
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
+  integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==
+
 watchpack-chokidar2@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"
diff --git a/django_airavata/apps/workspace/package.json b/django_airavata/apps/workspace/package.json
index 2535328..b32f710 100644
--- a/django_airavata/apps/workspace/package.json
+++ b/django_airavata/apps/workspace/package.json
@@ -51,9 +51,9 @@
     "cross-env": "^7.0.3",
     "eslint": "^5.8.0",
     "eslint-plugin-vue": "^5.0.0-0",
-    "node-sass": "^6",
     "npm-run-all": "^4.1.5",
     "prettier": "^2.1.2",
+    "sass": "^1.54.5",
     "sass-loader": "^10",
     "vue-loader": "^15.5.1",
     "vue-template-compiler": "^2.5.22",
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
index 79e04a2..4485733 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentEditor.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <unsaved-changes-guard :dirty="dirty"/>
+    <unsaved-changes-guard :dirty="dirty" />
     <div class="row">
       <div class="col-auto mr-auto">
         <h1 class="h4 mb-4">
@@ -89,9 +89,13 @@
       </div>
       <div class="row">
         <div class="col">
-          <workspace-notices-management-container class="mt-2"
+          <workspace-notices-management-container
+            class="mt-2"
             v-if="appInterface && appInterface.applicationDescription"
-            :data="[{notificationMessage: appInterface.applicationDescription}]"/>
+            :data="[
+              { notificationMessage: appInterface.applicationDescription },
+            ]"
+          />
         </div>
       </div>
       <div class="row">
@@ -148,12 +152,8 @@
       </div>
       <div class="row">
         <div class="col">
-          <b-form-group
-            label="Email Settings"
-          >
-            <b-form-checkbox
-              v-model="localExperiment.enableEmailNotification"
-            >
+          <b-form-group label="Email Settings">
+            <b-form-checkbox v-model="localExperiment.enableEmailNotification">
               Receive email notification of experiment status
             </b-form-checkbox>
           </b-form-group>
@@ -186,9 +186,10 @@
 import ExperimentDescriptionEditor from "./ExperimentDescriptionEditor.vue";
 import GroupResourceProfileSelector from "./GroupResourceProfileSelector.vue";
 import InputEditorContainer from "./input-editors/InputEditorContainer.vue";
-import {models, services} from "django-airavata-api";
-import {components, utils} from "django-airavata-common-ui";
+import { models, services } from "django-airavata-api";
+import { components, utils } from "django-airavata-common-ui";
 import WorkspaceNoticesManagementContainer from "../notices/WorkspaceNoticesManagementContainer";
+import _ from "lodash";
 
 export default {
   name: "edit-experiment",
@@ -348,6 +349,32 @@
     inputValueChanged: function () {
       this.localExperiment.evaluateInputDependencies();
     },
+    calculateQueueSettings: _.debounce(async function () {
+      const queueSettingsUpdate = await services.QueueSettingsCalculatorService.calculate(
+        {
+          lookup: this.appInterface.queueSettingsCalculatorId,
+          data: this.localExperiment,
+        },
+        { showSpinner: false }
+      );
+      // Override values in computationalResourceScheduling with the values
+      // returned from the queue settings calculator
+      Object.assign(
+        this.localExperiment.userConfigurationData
+          .computationalResourceScheduling,
+        queueSettingsUpdate
+      );
+    }, 500),
+    experimentInputsChanged() {
+      if (this.appInterface.queueSettingsCalculatorId) {
+        this.calculateQueueSettings();
+      }
+    },
+    resourceHostIdChanged() {
+      if (this.appInterface.queueSettingsCalculatorId) {
+        this.calculateQueueSettings();
+      }
+    },
   },
   watch: {
     experiment: function (newValue) {
@@ -359,6 +386,15 @@
       },
       deep: true,
     },
+    "experiment.experimentInputs": {
+      handler() {
+        this.experimentInputsChanged();
+      },
+      deep: true,
+    },
+    "experiment.userConfigurationData.computationalResourceScheduling.resourceHostId": function () {
+      this.resourceHostIdChanged();
+    },
   },
 };
 </script>
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/QueueSettingsEditor.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/QueueSettingsEditor.vue
index da86e07..12aa9ef 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/QueueSettingsEditor.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/QueueSettingsEditor.vue
@@ -2,10 +2,14 @@
   <div v-if="showQueueSettings">
     <div class="row">
       <div class="col">
-        <div class="card border-default" :class="{ 'border-danger': !valid }">
+        <div
+          class="card border-default"
+          :class="{ 'border-danger': !valid, 'is-disabled': disabled }"
+        >
           <b-link
             @click="showConfiguration = !showConfiguration"
             class="card-link text-dark"
+            :disabled="disabled"
           >
             <div class="card-body">
               <h5 class="card-title mb-4">
@@ -106,7 +110,8 @@
                   Max Allowed Cores = {{ maxCPUCount
                   }}<template
                     v-if="
-                      selectedQueueDefault && selectedQueueDefault.cpuPerNode > 0
+                      selectedQueueDefault &&
+                      selectedQueueDefault.cpuPerNode > 0
                     "
                     >. There are {{ selectedQueueDefault.cpuPerNode }} cores per
                     node.
@@ -114,16 +119,47 @@
                 </div>
               </b-form-group>
             </div>
-            <div class="d-flex flex-column" v-if="selectedQueueDefault && selectedQueueDefault.cpuPerNode > 0">
-              <div class="flex-fill"
-                   style="border: 1px solid #6c757d;border-top-right-radius: 10px;margin-top: 51px;border-left-width: 0px;border-bottom-width: 0px;margin-right: 15px;"></div>
-              <b-button size="sm" pill variant="outline-secondary"
-                        v-on:click="enableNodeCountToCpuCheck = !enableNodeCountToCpuCheck">
-                <i v-if="enableNodeCountToCpuCheck" class="fa fa-lock" aria-hidden="true"></i>
+            <div
+              class="d-flex flex-column"
+              v-if="selectedQueueDefault && selectedQueueDefault.cpuPerNode > 0"
+            >
+              <div
+                class="flex-fill"
+                style="
+                  border: 1px solid #6c757d;
+                  border-top-right-radius: 10px;
+                  margin-top: 51px;
+                  border-left-width: 0px;
+                  border-bottom-width: 0px;
+                  margin-right: 15px;
+                "
+              ></div>
+              <b-button
+                size="sm"
+                pill
+                variant="outline-secondary"
+                v-on:click="
+                  enableNodeCountToCpuCheck = !enableNodeCountToCpuCheck
+                "
+              >
+                <i
+                  v-if="enableNodeCountToCpuCheck"
+                  class="fa fa-lock"
+                  aria-hidden="true"
+                ></i>
                 <i v-else class="fa fa-unlock" aria-hidden="true"></i>
               </b-button>
-              <div class="flex-fill"
-                   style="border: 1px solid #6c757d;border-bottom-right-radius: 10px;margin-bottom: 57px;border-left-width: 0px;border-top-width: 0px;margin-right: 15px;"></div>
+              <div
+                class="flex-fill"
+                style="
+                  border: 1px solid #6c757d;
+                  border-bottom-right-radius: 10px;
+                  margin-bottom: 57px;
+                  border-left-width: 0px;
+                  border-top-width: 0px;
+                  margin-right: 15px;
+                "
+              ></div>
             </div>
           </div>
           <b-form-group
@@ -337,6 +373,12 @@
         ? this.applicationInterface.showQueueSettings
         : false;
     },
+    disabled() {
+      return (
+        this.applicationInterface &&
+        !!this.applicationInterface.queueSettingsCalculatorId
+      );
+    },
   },
   methods: {
     queueChanged: function (queueName) {
@@ -449,7 +491,10 @@
       }
     },
     nodeCountChanged() {
-      if (this.enableNodeCountToCpuCheck && this.selectedQueueDefault.cpuPerNode > 0) {
+      if (
+        this.enableNodeCountToCpuCheck &&
+        this.selectedQueueDefault.cpuPerNode > 0
+      ) {
         const nodeCount = parseInt(this.data.nodeCount);
         this.data.totalCPUCount = Math.min(
           nodeCount * this.selectedQueueDefault.cpuPerNode,
@@ -458,7 +503,10 @@
       }
     },
     cpuCountChanged() {
-      if (this.enableNodeCountToCpuCheck && this.selectedQueueDefault.cpuPerNode > 0) {
+      if (
+        this.enableNodeCountToCpuCheck &&
+        this.selectedQueueDefault.cpuPerNode > 0
+      ) {
         const cpuCount = parseInt(this.data.totalCPUCount);
         if (cpuCount > 0) {
           this.data.nodeCount = Math.min(
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/ExperimentStoragePathViewer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/ExperimentStoragePathViewer.vue
index 96a2ffe..e480ba3 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/ExperimentStoragePathViewer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/ExperimentStoragePathViewer.vue
@@ -24,8 +24,8 @@
           {{ data.item.name }}</b-link
         >
       </template>
-      <template slot="cell(createdTimestamp)" slot-scope="data">
-        <human-date :date="data.item.createdTime" />
+      <template slot="cell(modifiedTimestamp)" slot-scope="data">
+        <human-date :date="data.item.modifiedTime" />
       </template>
       <template slot="cell(actions)" slot-scope="data">
         <b-link
@@ -87,8 +87,8 @@
           formatter: (value) => this.getFormattedSize(value),
         },
         {
-          label: "Created Time",
-          key: "createdTimestamp",
+          label: "Last Modified",
+          key: "modifiedTimestamp",
           sortable: true,
         },
         {
@@ -106,8 +106,8 @@
               name: d.name,
               path: d.path,
               type: "dir",
-              createdTime: d.createdTime,
-              createdTimestamp: d.createdTime.getTime(), // for sorting
+              modifiedTime: d.modifiedTime,
+              modifiedTimestamp: d.modifiedTime.getTime(), // for sorting
               size: d.size,
             };
           });
@@ -118,8 +118,8 @@
             type: "file",
             dataProductURI: f.dataProductURI,
             downloadURL: f.downloadURL,
-            createdTime: f.createdTime,
-            createdTimestamp: f.createdTime.getTime(), // for sorting
+            modifiedTime: f.modifiedTime,
+            modifiedTimestamp: f.modifiedTime.getTime(), // for sorting
             size: f.size,
           };
         });
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue
index 71f6991..974c27c 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue
@@ -44,8 +44,8 @@
           :allow-preview="allowPreview"
         />
       </template>
-      <template slot="cell(createdTimestamp)" slot-scope="data">
-        <human-date :date="data.item.createdTime" />
+      <template slot="cell(modifiedTimestamp)" slot-scope="data">
+        <human-date :date="data.item.modifiedTime" />
       </template>
       <template slot="cell(actions)" slot-scope="data">
         <b-button
@@ -159,8 +159,8 @@
           formatter: (value) => this.getFormattedSize(value),
         },
         {
-          label: "Created Time",
-          key: "createdTimestamp",
+          label: "Last Modified",
+          key: "modifiedTimestamp",
           sortable: true,
         },
         {
@@ -178,8 +178,8 @@
               name: d.name,
               path: d.path,
               type: "dir",
-              createdTime: d.createdTime,
-              createdTimestamp: d.createdTime.getTime(), // for sorting
+              modifiedTime: d.modifiedTime,
+              modifiedTimestamp: d.modifiedTime.getTime(), // for sorting
               size: d.size,
             };
           });
@@ -190,8 +190,8 @@
             type: "file",
             dataProductURI: f.dataProductURI,
             downloadURL: f.downloadURL,
-            createdTime: f.createdTime,
-            createdTimestamp: f.createdTime.getTime(), // for sorting
+            modifiedTime: f.modifiedTime,
+            modifiedTimestamp: f.modifiedTime.getTime(), // for sorting
             size: f.size,
           };
         });
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue
index 40f6671..bd0e83d 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/containers/UserStorageContainer.vue
@@ -81,6 +81,7 @@
               files: [
                 {
                   createdTime: dataProduct.creationTime,
+                  modifiedTime: dataProduct.lastModifiedTime,
                   dataProductURI: this.dataProductUri,
                   downloadURL: dataProduct.downloadURL,
                   mimeType: dataProduct.productMetadata["mime-type"],
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 1e1053d..1322499 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
@@ -264,7 +264,7 @@
     } else {
       commit("updateGroupResourceProfileId", { groupResourceProfileId });
     }
-    if (oldValue !== groupResourceProfileId) {
+    if (groupResourceProfileId && oldValue !== groupResourceProfileId) {
       await dispatch("loadApplicationDeployments");
       await dispatch("applyGroupResourceProfile");
     }
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/web-components/store.spec.js b/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/web-components/store.spec.js
index 1b8346f..b9e4a01 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/web-components/store.spec.js
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/tests/unit/web-components/store.spec.js
@@ -38,7 +38,14 @@
     }
 
     mutationCount++;
-    if (mutationCount >= expectedMutations.length) {
+    checkIfDone();
+  };
+
+  const checkIfDone = () => {
+    if (
+      mutationCount >= expectedMutations.length &&
+      actionCount >= expectedActions.length
+    ) {
       done();
     }
   };
@@ -55,9 +62,7 @@
     }
 
     actionCount++;
-    if (actionCount >= expectedActions.length) {
-      done();
-    }
+    checkIfDone();
     return action.result;
   };
 
@@ -881,3 +886,50 @@
     done,
   });
 });
+
+test("updateGroupResourceProfileId: test normal case where updated to a GRP id", (done) => {
+  const mockGetters = {
+    groupResourceProfileId: "old_grp_id",
+  };
+  const groupResourceProfileId = "new_grp_id";
+  const expectedMutations = [
+    {
+      type: "updateGroupResourceProfileId",
+      payload: { groupResourceProfileId },
+    },
+  ];
+  const expectedActions = [
+    { type: "loadApplicationDeployments" },
+    { type: "applyGroupResourceProfile" },
+  ];
+  testAction(actions.updateGroupResourceProfileId, {
+    payload: {
+      groupResourceProfileId,
+    },
+    getters: mockGetters,
+    expectedMutations,
+    done,
+    expectedActions,
+  });
+});
+
+test("updateGroupResourceProfileId: test case where GRP id is updated to null", (done) => {
+  const mockGetters = {
+    groupResourceProfileId: "old_grp_id",
+  };
+  const groupResourceProfileId = null;
+  const expectedMutations = [
+    {
+      type: "updateGroupResourceProfileId",
+      payload: { groupResourceProfileId },
+    },
+  ];
+  testAction(actions.updateGroupResourceProfileId, {
+    payload: {
+      groupResourceProfileId,
+    },
+    getters: mockGetters,
+    expectedMutations,
+    done,
+  });
+});
diff --git a/django_airavata/apps/workspace/vue.config.js b/django_airavata/apps/workspace/vue.config.js
index c08ea40..73a8dc6 100644
--- a/django_airavata/apps/workspace/vue.config.js
+++ b/django_airavata/apps/workspace/vue.config.js
@@ -28,6 +28,12 @@
           path: __dirname,
         },
       },
+      sass: {
+        sassOptions: {
+          // Turn off deprecation warnings for sass dependencies
+          quietDeps: true,
+        },
+      },
     },
   },
   configureWebpack: {
diff --git a/django_airavata/apps/workspace/yarn.lock b/django_airavata/apps/workspace/yarn.lock
index 550fba2..059c14a 100644
--- a/django_airavata/apps/workspace/yarn.lock
+++ b/django_airavata/apps/workspace/yarn.lock
@@ -812,11 +812,6 @@
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
 
-"@types/minimist@^1.2.0":
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
-  integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
-
 "@types/node@*":
   version "12.12.9"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.9.tgz#0b5ae05516b757cbff2e82c04500190aef986c7b"
@@ -1415,7 +1410,7 @@
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ajv@^6.12.3, ajv@^6.12.5:
+ajv@^6.12.5:
   version "6.12.6"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
   integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -1430,11 +1425,6 @@
   resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
   integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
 
-amdefine@>=0.0.4:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
-  integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
-
 ansi-colors@^3.0.0:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
@@ -1503,6 +1493,14 @@
     micromatch "^3.1.4"
     normalize-path "^2.1.1"
 
+anymatch@~3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
 append-transform@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
@@ -1643,11 +1641,6 @@
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
   integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
 
-async-foreach@^0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
-  integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=
-
 async-limiter@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
@@ -1977,6 +1970,11 @@
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
   integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
 
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
 bluebird@^3.1.1, bluebird@^3.5.1, bluebird@^3.5.5:
   version "3.7.1"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de"
@@ -2074,6 +2072,13 @@
     split-string "^3.0.2"
     to-regex "^3.0.1"
 
+braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
 brorand@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -2336,21 +2341,12 @@
     no-case "^2.2.0"
     upper-case "^1.1.1"
 
-camelcase-keys@^6.2.2:
-  version "6.2.2"
-  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
-  integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
-  dependencies:
-    camelcase "^5.3.1"
-    map-obj "^4.0.0"
-    quick-lru "^4.0.1"
-
 camelcase@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
   integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
 
-camelcase@^5.0.0, camelcase@^5.3.1:
+camelcase@^5.0.0:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -2387,7 +2383,7 @@
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
 
-chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
@@ -2438,6 +2434,21 @@
   resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
   integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==
 
+"chokidar@>=3.0.0 <4.0.0":
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
 chokidar@^2.0.2, chokidar@^2.1.8:
   version "2.1.8"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -2462,11 +2473,6 @@
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
   integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==
 
-chownr@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
-  integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
-
 chrome-trace-event@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
@@ -2950,7 +2956,7 @@
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-cross-spawn@^7.0.1, cross-spawn@^7.0.3:
+cross-spawn@^7.0.1:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -3233,15 +3239,7 @@
   dependencies:
     ms "^2.1.1"
 
-decamelize-keys@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
-  integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
-  dependencies:
-    decamelize "^1.1.0"
-    map-obj "^1.0.0"
-
-decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.2.0:
+decamelize@^1.1.1, decamelize@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@@ -3675,11 +3673,6 @@
   resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
   integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
 
-env-paths@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
-  integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
-
 errno@^0.1.3, errno@~0.1.7:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
@@ -4329,6 +4322,13 @@
     repeat-string "^1.6.1"
     to-regex-range "^2.1.0"
 
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
 finalhandler@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@@ -4518,13 +4518,6 @@
   dependencies:
     minipass "^2.6.0"
 
-fs-minipass@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
-  integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
-  dependencies:
-    minipass "^3.0.0"
-
 fs-write-stream-atomic@^1.0.8:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
@@ -4548,6 +4541,11 @@
     nan "^2.12.1"
     node-pre-gyp "^0.12.0"
 
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
 function-bind@^1.0.2, function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -4572,13 +4570,6 @@
     strip-ansi "^3.0.1"
     wide-align "^1.1.0"
 
-gaze@^1.0.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
-  integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==
-  dependencies:
-    globule "^1.0.0"
-
 get-caller-file@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
@@ -4589,11 +4580,6 @@
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
-get-stdin@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
-  integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
-
 get-stream@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -4648,12 +4634,19 @@
     is-glob "^3.1.0"
     path-dirname "^1.0.0"
 
+glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
 glob-to-regexp@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
   integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
 
-glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.1:
+glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -4712,15 +4705,6 @@
     pify "^4.0.1"
     slash "^2.0.0"
 
-globule@^1.0.0:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.2.tgz#d8bdd9e9e4eef8f96e245999a5dee7eb5d8529c4"
-  integrity sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==
-  dependencies:
-    glob "~7.1.1"
-    lodash "~4.17.10"
-    minimatch "~3.0.2"
-
 good-listener@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
@@ -4733,11 +4717,6 @@
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
   integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
 
-graceful-fs@^4.2.3:
-  version "4.2.6"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
-  integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==
-
 growly@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -4780,19 +4759,6 @@
     ajv "^6.5.5"
     har-schema "^2.0.0"
 
-har-validator@~5.1.3:
-  version "5.1.5"
-  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd"
-  integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==
-  dependencies:
-    ajv "^6.12.3"
-    har-schema "^2.0.0"
-
-hard-rejection@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
-  integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
-
 has-ansi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -4926,13 +4892,6 @@
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c"
   integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==
 
-hosted-git-info@^4.0.1:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961"
-  integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==
-  dependencies:
-    lru-cache "^6.0.0"
-
 hpack.js@^2.1.6:
   version "2.1.6"
   resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
@@ -5139,6 +5098,11 @@
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
   integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
 
+immutable@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
+  integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
+
 import-cwd@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -5190,11 +5154,6 @@
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
   integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
 
-indent-string@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
-  integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
-
 indexes-of@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -5358,6 +5317,13 @@
   dependencies:
     binary-extensions "^1.0.0"
 
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
 is-buffer@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -5387,13 +5353,6 @@
     rgb-regex "^1.0.1"
     rgba-regex "^1.0.0"
 
-is-core-module@^2.5.0:
-  version "2.8.0"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548"
-  integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==
-  dependencies:
-    has "^1.0.3"
-
 is-data-descriptor@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -5520,6 +5479,13 @@
   dependencies:
     is-extglob "^2.1.1"
 
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
 is-number@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
@@ -5539,6 +5505,11 @@
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
   integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
 
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
 is-obj@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@@ -5563,7 +5534,7 @@
   dependencies:
     path-is-inside "^1.0.2"
 
-is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
+is-plain-obj@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
   integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
@@ -6099,11 +6070,6 @@
   resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2"
   integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==
 
-js-base64@^2.1.8:
-  version "2.6.4"
-  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
-  integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
-
 js-base64@^2.4.9:
   version "2.5.1"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
@@ -6314,11 +6280,6 @@
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
   integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
 
-kind-of@^6.0.3:
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
-  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
-
 kleur@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300"
@@ -6549,11 +6510,6 @@
     lodash._baseiteratee "~4.7.0"
     lodash._baseuniq "~4.6.0"
 
-lodash@^4.0.0, lodash@~4.17.10:
-  version "4.17.20"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
-  integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
-
 lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0:
   version "4.17.15"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
@@ -6644,16 +6600,6 @@
   resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
   integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
 
-map-obj@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
-  integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
-
-map-obj@^4.0.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
-  integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
-
 map-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@@ -6715,24 +6661,6 @@
   resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
   integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
 
-meow@^9.0.0:
-  version "9.0.0"
-  resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
-  integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==
-  dependencies:
-    "@types/minimist" "^1.2.0"
-    camelcase-keys "^6.2.2"
-    decamelize "^1.2.0"
-    decamelize-keys "^1.1.0"
-    hard-rejection "^2.1.0"
-    minimist-options "4.1.0"
-    normalize-package-data "^3.0.0"
-    read-pkg-up "^7.0.1"
-    redent "^3.0.0"
-    trim-newlines "^3.0.0"
-    type-fest "^0.18.0"
-    yargs-parser "^20.2.3"
-
 merge-descriptors@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -6857,11 +6785,6 @@
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
-min-indent@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
-  integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
-
 mini-css-extract-plugin@^0.8.0:
   version "0.8.0"
   resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1"
@@ -6882,22 +6805,13 @@
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
   integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
 
-minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
+minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist-options@4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
-  integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
-  dependencies:
-    arrify "^1.0.1"
-    is-plain-obj "^1.1.0"
-    kind-of "^6.0.3"
-
 minimist@0.0.8:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@@ -6926,13 +6840,6 @@
     safe-buffer "^5.1.2"
     yallist "^3.0.0"
 
-minipass@^3.0.0:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
-  integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==
-  dependencies:
-    yallist "^4.0.0"
-
 minizlib@^1.2.1:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
@@ -6940,14 +6847,6 @@
   dependencies:
     minipass "^2.9.0"
 
-minizlib@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
-  integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
-  dependencies:
-    minipass "^3.0.0"
-    yallist "^4.0.0"
-
 mississippi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
@@ -6995,11 +6894,6 @@
   dependencies:
     minimist "0.0.8"
 
-mkdirp@^1.0.3:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
-  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-
 moment@^2.21.0, moment@^2.24.0:
   version "2.24.0"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -7069,11 +6963,6 @@
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
   integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
 
-nan@^2.13.2:
-  version "2.14.2"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
-  integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
-
 nanomatch@^1.2.9:
   version "1.2.13"
   resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -7150,22 +7039,6 @@
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
   integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
 
-node-gyp@^7.1.0:
-  version "7.1.2"
-  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae"
-  integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==
-  dependencies:
-    env-paths "^2.2.0"
-    glob "^7.1.4"
-    graceful-fs "^4.2.3"
-    nopt "^5.0.0"
-    npmlog "^4.1.2"
-    request "^2.88.2"
-    rimraf "^3.0.2"
-    semver "^7.3.2"
-    tar "^6.0.2"
-    which "^2.0.2"
-
 node-int64@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -7248,27 +7121,6 @@
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
   integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==
 
-node-sass@^6:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-6.0.1.tgz#cad1ccd0ce63e35c7181f545d8b986f3a9a887fe"
-  integrity sha512-f+Rbqt92Ful9gX0cGtdYwjTrWAaGURgaK5rZCWOgCNyGWusFYHhbqCCBoFBeat+HKETOU02AyTxNhJV0YZf2jQ==
-  dependencies:
-    async-foreach "^0.1.3"
-    chalk "^1.1.1"
-    cross-spawn "^7.0.3"
-    gaze "^1.0.0"
-    get-stdin "^4.0.1"
-    glob "^7.0.3"
-    lodash "^4.17.15"
-    meow "^9.0.0"
-    nan "^2.13.2"
-    node-gyp "^7.1.0"
-    npmlog "^4.0.0"
-    request "^2.88.0"
-    sass-graph "2.2.5"
-    stdout-stream "^1.4.0"
-    "true-case-path" "^1.0.2"
-
 nopt@^4.0.1, nopt@~4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
@@ -7277,13 +7129,6 @@
     abbrev "1"
     osenv "^0.1.4"
 
-nopt@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
-  integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
-  dependencies:
-    abbrev "1"
-
 normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -7294,16 +7139,6 @@
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
-normalize-package-data@^3.0.0:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
-  integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==
-  dependencies:
-    hosted-git-info "^4.0.1"
-    is-core-module "^2.5.0"
-    semver "^7.3.4"
-    validate-npm-package-license "^3.0.1"
-
 normalize-path@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379"
@@ -7316,7 +7151,7 @@
   dependencies:
     remove-trailing-separator "^1.0.1"
 
-normalize-path@^3.0.0:
+normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
@@ -7383,7 +7218,7 @@
   dependencies:
     path-key "^3.0.0"
 
-npmlog@^4.0.0, npmlog@^4.0.2, npmlog@^4.1.2:
+npmlog@^4.0.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -7900,6 +7735,11 @@
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
+picomatch@^2.0.4, picomatch@^2.2.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
 pidtree@^0.3.0:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a"
@@ -8556,11 +8396,6 @@
   resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
   integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
 
-quick-lru@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
-  integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
-
 randomatic@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
@@ -8618,15 +8453,6 @@
     find-up "^1.0.0"
     read-pkg "^1.0.0"
 
-read-pkg-up@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
-  integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
-  dependencies:
-    find-up "^4.1.0"
-    read-pkg "^5.2.0"
-    type-fest "^0.8.1"
-
 read-pkg@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
@@ -8645,7 +8471,7 @@
     normalize-package-data "^2.3.2"
     path-type "^3.0.0"
 
-read-pkg@^5.0.0, read-pkg@^5.2.0:
+read-pkg@^5.0.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
   integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
@@ -8686,6 +8512,13 @@
     micromatch "^3.1.10"
     readable-stream "^2.0.2"
 
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
 realpath-native@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"
@@ -8693,14 +8526,6 @@
   dependencies:
     util.promisify "^1.0.0"
 
-redent@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
-  integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
-  dependencies:
-    indent-string "^4.0.0"
-    strip-indent "^3.0.0"
-
 regenerate-unicode-properties@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
@@ -8887,32 +8712,6 @@
     tunnel-agent "^0.6.0"
     uuid "^3.3.2"
 
-request@^2.88.0, request@^2.88.2:
-  version "2.88.2"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
-  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
-  dependencies:
-    aws-sign2 "~0.7.0"
-    aws4 "^1.8.0"
-    caseless "~0.12.0"
-    combined-stream "~1.0.6"
-    extend "~3.0.2"
-    forever-agent "~0.6.1"
-    form-data "~2.3.2"
-    har-validator "~5.1.3"
-    http-signature "~1.2.0"
-    is-typedarray "~1.0.0"
-    isstream "~0.1.2"
-    json-stringify-safe "~5.0.1"
-    mime-types "~2.1.19"
-    oauth-sign "~0.9.0"
-    performance-now "^2.1.0"
-    qs "~6.5.2"
-    safe-buffer "^5.1.2"
-    tough-cookie "~2.5.0"
-    tunnel-agent "^0.6.0"
-    uuid "^3.3.2"
-
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -9032,13 +8831,6 @@
   dependencies:
     glob "^7.1.3"
 
-rimraf@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
-  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
-  dependencies:
-    glob "^7.1.3"
-
 ripemd160@^2.0.0, ripemd160@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -9123,16 +8915,6 @@
   optionalDependencies:
     fsevents "^1.2.3"
 
-sass-graph@2.2.5:
-  version "2.2.5"
-  resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8"
-  integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==
-  dependencies:
-    glob "^7.0.0"
-    lodash "^4.0.0"
-    scss-tokenizer "^0.2.3"
-    yargs "^13.3.2"
-
 sass-loader@^10:
   version "10.2.0"
   resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.2.0.tgz#3d64c1590f911013b3fa48a0b22a83d5e1494716"
@@ -9144,6 +8926,15 @@
     schema-utils "^3.0.0"
     semver "^7.3.2"
 
+sass@^1.54.5:
+  version "1.54.5"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.5.tgz#93708f5560784f6ff2eab8542ade021a4a947b3a"
+  integrity sha512-p7DTOzxkUPa/63FU0R3KApkRHwcVZYC0PLnLm5iyZACyp15qSi32x7zVUhRdABAATmkALqgGrjCJAcWvobmhHw==
+  dependencies:
+    chokidar ">=3.0.0 <4.0.0"
+    immutable "^4.0.0"
+    source-map-js ">=0.6.2 <2.0.0"
+
 sax@^1.2.4, sax@~1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -9167,14 +8958,6 @@
     ajv "^6.12.5"
     ajv-keywords "^3.5.2"
 
-scss-tokenizer@^0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
-  integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE=
-  dependencies:
-    js-base64 "^2.1.8"
-    source-map "^0.4.2"
-
 select-hose@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -9209,13 +8992,6 @@
   dependencies:
     lru-cache "^6.0.0"
 
-semver@^7.3.4:
-  version "7.3.5"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
-  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
-  dependencies:
-    lru-cache "^6.0.0"
-
 send@0.17.1:
   version "0.17.1"
   resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -9445,6 +9221,11 @@
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
   integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
 
+"source-map-js@>=0.6.2 <2.0.0":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
 source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
@@ -9481,13 +9262,6 @@
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
   integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
 
-source-map@^0.4.2:
-  version "0.4.4"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
-  integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
-  dependencies:
-    amdefine ">=0.0.4"
-
 source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -9640,13 +9414,6 @@
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
 
-stdout-stream@^1.4.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de"
-  integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==
-  dependencies:
-    readable-stream "^2.0.1"
-
 stealthy-require@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
@@ -9835,13 +9602,6 @@
   resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
   integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
 
-strip-indent@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
-  integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
-  dependencies:
-    min-indent "^1.0.0"
-
 strip-json-comments@^2.0.0, strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -9958,18 +9718,6 @@
     safe-buffer "^5.1.2"
     yallist "^3.0.3"
 
-tar@^6.0.2:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
-  integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==
-  dependencies:
-    chownr "^2.0.0"
-    fs-minipass "^2.0.0"
-    minipass "^3.0.0"
-    minizlib "^2.1.1"
-    mkdirp "^1.0.3"
-    yallist "^4.0.0"
-
 terser-webpack-plugin@^1.2.3, terser-webpack-plugin@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4"
@@ -10115,6 +9863,13 @@
     is-number "^3.0.0"
     repeat-string "^1.6.1"
 
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
 to-regex@^3.0.1, to-regex@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@@ -10135,7 +9890,7 @@
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
   integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk=
 
-tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
+tough-cookie@^2.3.3, tough-cookie@^2.3.4:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
   integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
@@ -10158,23 +9913,11 @@
   dependencies:
     punycode "^2.1.0"
 
-trim-newlines@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
-  integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
-
 trim-right@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
   integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
 
-"true-case-path@^1.0.2":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d"
-  integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==
-  dependencies:
-    glob "^7.1.2"
-
 tryer@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
@@ -10232,21 +9975,11 @@
   dependencies:
     prelude-ls "~1.1.2"
 
-type-fest@^0.18.0:
-  version "0.18.1"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
-  integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
-
 type-fest@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
   integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
 
-type-fest@^0.8.1:
-  version "0.8.1"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
-  integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
-
 type-is@~1.6.17, type-is@~1.6.18:
   version "1.6.18"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -10855,7 +10588,7 @@
   dependencies:
     isexe "^2.0.0"
 
-which@^2.0.1, which@^2.0.2:
+which@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
@@ -11002,14 +10735,6 @@
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
-yargs-parser@^13.1.2:
-  version "13.1.2"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
-  integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
-  dependencies:
-    camelcase "^5.0.0"
-    decamelize "^1.2.0"
-
 yargs-parser@^16.1.0:
   version "16.1.0"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1"
@@ -11018,11 +10743,6 @@
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
-yargs-parser@^20.2.3:
-  version "20.2.9"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
-  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
 yargs-parser@^9.0.2:
   version "9.0.2"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
@@ -11066,22 +10786,6 @@
     y18n "^3.2.1"
     yargs-parser "^9.0.2"
 
-yargs@^13.3.2:
-  version "13.3.2"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
-  integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
-  dependencies:
-    cliui "^5.0.0"
-    find-up "^3.0.0"
-    get-caller-file "^2.0.1"
-    require-directory "^2.1.1"
-    require-main-filename "^2.0.0"
-    set-blocking "^2.0.0"
-    string-width "^3.0.0"
-    which-module "^2.0.0"
-    y18n "^4.0.0"
-    yargs-parser "^13.1.2"
-
 yargs@^15.0.0:
   version "15.0.2"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.0.2.tgz#4248bf218ef050385c4f7e14ebdf425653d13bd3"
diff --git a/django_airavata/static/common/js/errors/vuelidateHelpers.js b/django_airavata/static/common/js/errors/vuelidateHelpers.js
index 3f43183..1332e77 100644
--- a/django_airavata/static/common/js/errors/vuelidateHelpers.js
+++ b/django_airavata/static/common/js/errors/vuelidateHelpers.js
@@ -2,3 +2,17 @@
   const { $dirty, $error } = validation;
   return $dirty ? !$error : null;
 }
+
+/**
+ * Return false if there is a validation error, null otherwise.
+ *
+ * This is just like validateState except it doesn't return true when valid
+ * which is useful if you only want to show invalid feedback.
+ *
+ * @param {*} validation
+ * @returns
+ */
+export function validateStateErrorOnly(validation) {
+  const { $dirty, $error } = validation;
+  return $dirty && $error ? false : null;
+}
diff --git a/django_airavata/static/common/js/index.js b/django_airavata/static/common/js/index.js
index b4bcc5e..68c9719 100644
--- a/django_airavata/static/common/js/index.js
+++ b/django_airavata/static/common/js/index.js
@@ -31,6 +31,7 @@
 
 import ListLayout from "./layouts/ListLayout.vue";
 
+import ValidationParent from "./mixins/ValidationParent";
 import VModelMixin from "./mixins/VModelMixin";
 
 import Notification from "./notifications/Notification";
@@ -80,6 +81,7 @@
 };
 
 const mixins = {
+  ValidationParent,
   VModelMixin,
 };
 
diff --git a/django_airavata/static/common/js/mixins/ValidationParent.js b/django_airavata/static/common/js/mixins/ValidationParent.js
new file mode 100644
index 0000000..6e89563
--- /dev/null
+++ b/django_airavata/static/common/js/mixins/ValidationParent.js
@@ -0,0 +1,30 @@
+/**
+ * Aggregate validation state of child components. Child components should
+ * dispatch 'valid' and 'invalid' events and component using this mixin should
+ * call recordValidChildComponent or recordInvalidChildComponent, respectively.
+ */
+export default {
+  data: function () {
+    return {
+      invalidChildComponents: [],
+    };
+  },
+  computed: {
+    childComponentsAreValid() {
+      return this.invalidChildComponents.length === 0;
+    },
+  },
+  methods: {
+    recordInvalidChildComponent(childComponentId) {
+      if (!this.invalidChildComponents.includes(childComponentId)) {
+        this.invalidChildComponents.push(childComponentId);
+      }
+    },
+    recordValidChildComponent(childComponentId) {
+      if (this.invalidChildComponents.includes(childComponentId)) {
+        const index = this.invalidChildComponents.indexOf(childComponentId);
+        this.invalidChildComponents.splice(index, 1);
+      }
+    },
+  },
+};
diff --git a/django_airavata/static/common/package.json b/django_airavata/static/common/package.json
index eced286..27e9970 100644
--- a/django_airavata/static/common/package.json
+++ b/django_airavata/static/common/package.json
@@ -44,8 +44,8 @@
     "eslint": "^5.8.0",
     "eslint-plugin-vue": "^5.0.0-0",
     "file-loader": "^3.0.1",
-    "node-sass": "^6.0.1",
     "prettier": "^2.0.5",
+    "sass": "^1.54.5",
     "sass-loader": "^10",
     "vue-loader": "^15.5.1",
     "vue-template-compiler": "^2.5.22",
diff --git a/django_airavata/static/common/scss/main.scss b/django_airavata/static/common/scss/main.scss
index 72698d5..3314aa3 100644
--- a/django_airavata/static/common/scss/main.scss
+++ b/django_airavata/static/common/scss/main.scss
@@ -253,6 +253,9 @@
 .fade-leave-to {
   opacity: 0;
 }
+.fade-move {
+  transition: transform 0.3s;
+}
 
 /**
  * spacing between buttons. .btn-container is a class applied to a wrapper
@@ -357,6 +360,8 @@
   bottom: 0px;
   left: 70px;
   right: 0px;
+  // keep fixed-footer above form controls
+  z-index: 3;
   padding-left: calc(1rem + 15px);
   padding-right: calc(1rem + 15px);
   padding-top: 1rem;
diff --git a/django_airavata/static/common/yarn.lock b/django_airavata/static/common/yarn.lock
index 2fa3c48..00b6b4e 100644
--- a/django_airavata/static/common/yarn.lock
+++ b/django_airavata/static/common/yarn.lock
@@ -788,11 +788,6 @@
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
 
-"@types/minimist@^1.2.0":
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
-  integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
-
 "@types/node@*":
   version "12.12.9"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.9.tgz#0b5ae05516b757cbff2e82c04500190aef986c7b"
@@ -1356,11 +1351,6 @@
   resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
   integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
 
-amdefine@>=0.0.4:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
-  integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
-
 ansi-colors@^3.0.0:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
@@ -1434,6 +1424,14 @@
     micromatch "^3.1.4"
     normalize-path "^2.1.1"
 
+anymatch@~3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
 aproba@^1.0.3, aproba@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@@ -1501,11 +1499,6 @@
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
   integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
 
-arrify@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
-  integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
-
 asn1.js@^4.0.0:
   version "4.10.1"
   resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@@ -1550,11 +1543,6 @@
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
   integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
 
-async-foreach@^0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
-  integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=
-
 async-limiter@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
@@ -1709,6 +1697,11 @@
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
   integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
 
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
 bluebird@^3.1.1, bluebird@^3.5.1, bluebird@^3.5.5:
   version "3.7.1"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de"
@@ -1797,6 +1790,13 @@
     split-string "^3.0.2"
     to-regex "^3.0.1"
 
+braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
 brorand@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -2040,16 +2040,7 @@
     no-case "^2.2.0"
     upper-case "^1.1.1"
 
-camelcase-keys@^6.2.2:
-  version "6.2.2"
-  resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
-  integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
-  dependencies:
-    camelcase "^5.3.1"
-    map-obj "^4.0.0"
-    quick-lru "^4.0.1"
-
-camelcase@^5.0.0, camelcase@^5.3.1:
+camelcase@^5.0.0:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -2079,7 +2070,7 @@
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
   integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
 
-chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
@@ -2130,6 +2121,21 @@
   resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
   integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==
 
+"chokidar@>=3.0.0 <4.0.0":
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
 chokidar@^2.0.2, chokidar@^2.1.8:
   version "2.1.8"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -2154,11 +2160,6 @@
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
   integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==
 
-chownr@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
-  integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
-
 chrome-trace-event@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
@@ -2615,7 +2616,7 @@
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-cross-spawn@^7.0.1, cross-spawn@^7.0.3:
+cross-spawn@^7.0.1:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
   integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -2867,15 +2868,7 @@
   dependencies:
     ms "^2.1.1"
 
-decamelize-keys@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
-  integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
-  dependencies:
-    decamelize "^1.1.0"
-    map-obj "^1.0.0"
-
-decamelize@^1.1.0, decamelize@^1.2.0:
+decamelize@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@@ -3255,11 +3248,6 @@
   resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
   integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
 
-env-paths@^2.2.0:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
-  integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
-
 errno@^0.1.3, errno@~0.1.7:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
@@ -3809,6 +3797,13 @@
     repeat-string "^1.6.1"
     to-regex-range "^2.1.0"
 
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
 finalhandler@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
@@ -3986,13 +3981,6 @@
   dependencies:
     minipass "^2.6.0"
 
-fs-minipass@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
-  integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
-  dependencies:
-    minipass "^3.0.0"
-
 fs-write-stream-atomic@^1.0.8:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
@@ -4016,6 +4004,11 @@
     nan "^2.12.1"
     node-pre-gyp "^0.12.0"
 
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
 function-bind@^1.0.2, function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -4040,13 +4033,6 @@
     strip-ansi "^3.0.1"
     wide-align "^1.1.0"
 
-gaze@^1.0.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
-  integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==
-  dependencies:
-    globule "^1.0.0"
-
 get-caller-file@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
@@ -4057,11 +4043,6 @@
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
-get-stdin@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
-  integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
-
 get-stream@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -4101,12 +4082,19 @@
     is-glob "^3.1.0"
     path-dirname "^1.0.0"
 
+glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
 glob-to-regexp@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
   integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
 
-glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3:
+glob@^7.0.3, glob@^7.1.2, glob@^7.1.3:
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
   integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
@@ -4130,18 +4118,6 @@
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@~7.1.1:
-  version "7.1.7"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
-  integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.4"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
 globals@^11.0.1, globals@^11.1.0, globals@^11.7.0:
   version "11.12.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -4184,15 +4160,6 @@
     pify "^4.0.1"
     slash "^2.0.0"
 
-globule@^1.0.0:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.3.tgz#811919eeac1ab7344e905f2e3be80a13447973c2"
-  integrity sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg==
-  dependencies:
-    glob "~7.1.1"
-    lodash "~4.17.10"
-    minimatch "~3.0.2"
-
 good-listener@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
@@ -4205,7 +4172,7 @@
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
   integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
 
-graceful-fs@^4.1.2, graceful-fs@^4.2.3:
+graceful-fs@^4.1.2:
   version "4.2.8"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
   integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
@@ -4236,11 +4203,6 @@
     ajv "^6.12.3"
     har-schema "^2.0.0"
 
-hard-rejection@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
-  integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
-
 has-ansi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -4361,13 +4323,6 @@
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
   integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
 
-hosted-git-info@^4.0.1:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961"
-  integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==
-  dependencies:
-    lru-cache "^6.0.0"
-
 hpack.js@^2.1.6:
   version "2.1.6"
   resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
@@ -4567,6 +4522,11 @@
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
   integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
 
+immutable@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
+  integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
+
 import-cwd@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -4610,11 +4570,6 @@
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
   integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
 
-indent-string@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
-  integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
-
 indexes-of@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -4778,6 +4733,13 @@
   dependencies:
     binary-extensions "^1.0.0"
 
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
 is-buffer@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -4807,7 +4769,7 @@
     rgb-regex "^1.0.1"
     rgba-regex "^1.0.0"
 
-is-core-module@^2.2.0, is-core-module@^2.5.0:
+is-core-module@^2.2.0:
   version "2.8.0"
   resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548"
   integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==
@@ -4904,6 +4866,13 @@
   dependencies:
     is-extglob "^2.1.1"
 
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
 is-number@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@@ -4911,6 +4880,11 @@
   dependencies:
     kind-of "^3.0.2"
 
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
 is-obj@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@@ -4935,7 +4909,7 @@
   dependencies:
     path-is-inside "^1.0.2"
 
-is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
+is-plain-obj@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
   integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
@@ -5040,11 +5014,6 @@
   resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2"
   integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==
 
-js-base64@^2.1.8:
-  version "2.6.4"
-  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
-  integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
-
 js-base64@^2.4.9:
   version "2.5.1"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121"
@@ -5207,11 +5176,6 @@
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
   integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
 
-kind-of@^6.0.3:
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
-  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
-
 klona@^2.0.4:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
@@ -5401,16 +5365,16 @@
     lodash._baseiteratee "~4.7.0"
     lodash._baseuniq "~4.6.0"
 
-lodash@^4.0.0, lodash@^4.17.15, lodash@~4.17.10:
-  version "4.17.21"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
-  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
 lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0:
   version "4.17.15"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
 
+lodash@^4.17.15:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
 log-symbols@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
@@ -5489,16 +5453,6 @@
   resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
   integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
 
-map-obj@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
-  integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
-
-map-obj@^4.0.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
-  integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
-
 map-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
@@ -5550,24 +5504,6 @@
     errno "^0.1.3"
     readable-stream "^2.0.1"
 
-meow@^9.0.0:
-  version "9.0.0"
-  resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
-  integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==
-  dependencies:
-    "@types/minimist" "^1.2.0"
-    camelcase-keys "^6.2.2"
-    decamelize "^1.2.0"
-    decamelize-keys "^1.1.0"
-    hard-rejection "^2.1.0"
-    minimist-options "4.1.0"
-    normalize-package-data "^3.0.0"
-    read-pkg-up "^7.0.1"
-    redent "^3.0.0"
-    trim-newlines "^3.0.0"
-    type-fest "^0.18.0"
-    yargs-parser "^20.2.3"
-
 merge-descriptors@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -5673,11 +5609,6 @@
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
 
-min-indent@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
-  integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
-
 mini-css-extract-plugin@^0.8.0:
   version "0.8.0"
   resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1"
@@ -5698,22 +5629,13 @@
   resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
   integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
 
-minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
+minimatch@^3.0.2, minimatch@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist-options@4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
-  integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
-  dependencies:
-    arrify "^1.0.1"
-    is-plain-obj "^1.1.0"
-    kind-of "^6.0.3"
-
 minimist@0.0.8:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@@ -5737,13 +5659,6 @@
     safe-buffer "^5.1.2"
     yallist "^3.0.0"
 
-minipass@^3.0.0:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732"
-  integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==
-  dependencies:
-    yallist "^4.0.0"
-
 minizlib@^1.2.1:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
@@ -5751,14 +5666,6 @@
   dependencies:
     minipass "^2.9.0"
 
-minizlib@^2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
-  integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
-  dependencies:
-    minipass "^3.0.0"
-    yallist "^4.0.0"
-
 mississippi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
@@ -5813,11 +5720,6 @@
   dependencies:
     minimist "^1.2.5"
 
-mkdirp@^1.0.3:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
-  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-
 moment@^2.24.0:
   version "2.24.0"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
@@ -5887,11 +5789,6 @@
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
   integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
 
-nan@^2.13.2:
-  version "2.15.0"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
-  integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
-
 nanomatch@^1.2.9:
   version "1.2.13"
   resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -5960,22 +5857,6 @@
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
   integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
 
-node-gyp@^7.1.0:
-  version "7.1.2"
-  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae"
-  integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ==
-  dependencies:
-    env-paths "^2.2.0"
-    glob "^7.1.4"
-    graceful-fs "^4.2.3"
-    nopt "^5.0.0"
-    npmlog "^4.1.2"
-    request "^2.88.2"
-    rimraf "^3.0.2"
-    semver "^7.3.2"
-    tar "^6.0.2"
-    which "^2.0.2"
-
 node-ipc@^9.1.1:
   version "9.1.1"
   resolved "https://registry.yarnpkg.com/node-ipc/-/node-ipc-9.1.1.tgz#4e245ed6938e65100e595ebc5dc34b16e8dd5d69"
@@ -6042,27 +5923,6 @@
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
   integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==
 
-node-sass@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-6.0.1.tgz#cad1ccd0ce63e35c7181f545d8b986f3a9a887fe"
-  integrity sha512-f+Rbqt92Ful9gX0cGtdYwjTrWAaGURgaK5rZCWOgCNyGWusFYHhbqCCBoFBeat+HKETOU02AyTxNhJV0YZf2jQ==
-  dependencies:
-    async-foreach "^0.1.3"
-    chalk "^1.1.1"
-    cross-spawn "^7.0.3"
-    gaze "^1.0.0"
-    get-stdin "^4.0.1"
-    glob "^7.0.3"
-    lodash "^4.17.15"
-    meow "^9.0.0"
-    nan "^2.13.2"
-    node-gyp "^7.1.0"
-    npmlog "^4.0.0"
-    request "^2.88.0"
-    sass-graph "2.2.5"
-    stdout-stream "^1.4.0"
-    "true-case-path" "^1.0.2"
-
 nopt@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
@@ -6071,13 +5931,6 @@
     abbrev "1"
     osenv "^0.1.4"
 
-nopt@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
-  integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
-  dependencies:
-    abbrev "1"
-
 normalize-package-data@^2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -6088,16 +5941,6 @@
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
-normalize-package-data@^3.0.0:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
-  integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==
-  dependencies:
-    hosted-git-info "^4.0.1"
-    is-core-module "^2.5.0"
-    semver "^7.3.4"
-    validate-npm-package-license "^3.0.1"
-
 normalize-path@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379"
@@ -6110,7 +5953,7 @@
   dependencies:
     remove-trailing-separator "^1.0.1"
 
-normalize-path@^3.0.0:
+normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
@@ -6162,7 +6005,7 @@
   dependencies:
     path-key "^3.0.0"
 
-npmlog@^4.0.0, npmlog@^4.0.2, npmlog@^4.1.2:
+npmlog@^4.0.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -6634,6 +6477,11 @@
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
+picomatch@^2.0.4, picomatch@^2.2.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
 pify@^2.0.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -7245,11 +7093,6 @@
   resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
   integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
 
-quick-lru@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
-  integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
-
 randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -7290,16 +7133,7 @@
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
-read-pkg-up@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
-  integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
-  dependencies:
-    find-up "^4.1.0"
-    read-pkg "^5.2.0"
-    type-fest "^0.8.1"
-
-read-pkg@^5.0.0, read-pkg@^5.2.0:
+read-pkg@^5.0.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
   integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
@@ -7353,13 +7187,12 @@
     micromatch "^3.1.10"
     readable-stream "^2.0.2"
 
-redent@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
-  integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
   dependencies:
-    indent-string "^4.0.0"
-    strip-indent "^3.0.0"
+    picomatch "^2.2.1"
 
 regenerate-unicode-properties@^8.1.0:
   version "8.1.0"
@@ -7502,7 +7335,7 @@
     stealthy-require "^1.1.1"
     tough-cookie "^2.3.3"
 
-request@^2.87.0, request@^2.88.0, request@^2.88.2:
+request@^2.87.0:
   version "2.88.2"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
   integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -7650,13 +7483,6 @@
   dependencies:
     glob "^7.1.3"
 
-rimraf@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
-  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
-  dependencies:
-    glob "^7.1.3"
-
 ripemd160@^2.0.0, ripemd160@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -7725,16 +7551,6 @@
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
-sass-graph@2.2.5:
-  version "2.2.5"
-  resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8"
-  integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==
-  dependencies:
-    glob "^7.0.0"
-    lodash "^4.0.0"
-    scss-tokenizer "^0.2.3"
-    yargs "^13.3.2"
-
 sass-loader@^10:
   version "10.2.0"
   resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.2.0.tgz#3d64c1590f911013b3fa48a0b22a83d5e1494716"
@@ -7746,6 +7562,15 @@
     schema-utils "^3.0.0"
     semver "^7.3.2"
 
+sass@^1.54.5:
+  version "1.54.5"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.5.tgz#93708f5560784f6ff2eab8542ade021a4a947b3a"
+  integrity sha512-p7DTOzxkUPa/63FU0R3KApkRHwcVZYC0PLnLm5iyZACyp15qSi32x7zVUhRdABAATmkALqgGrjCJAcWvobmhHw==
+  dependencies:
+    chokidar ">=3.0.0 <4.0.0"
+    immutable "^4.0.0"
+    source-map-js ">=0.6.2 <2.0.0"
+
 sax@^1.2.4, sax@~1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -7769,14 +7594,6 @@
     ajv "^6.12.5"
     ajv-keywords "^3.5.2"
 
-scss-tokenizer@^0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
-  integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE=
-  dependencies:
-    js-base64 "^2.1.8"
-    source-map "^0.4.2"
-
 select-hose@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -7804,7 +7621,7 @@
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
-semver@^7.3.2, semver@^7.3.4:
+semver@^7.3.2:
   version "7.3.5"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
   integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
@@ -8030,6 +7847,11 @@
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
   integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
 
+"source-map-js@>=0.6.2 <2.0.0":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
 source-map-resolve@^0.5.0:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
@@ -8059,13 +7881,6 @@
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
   integrity sha1-dc449SvwczxafwwRjYEzSiu19BI=
 
-source-map@^0.4.2:
-  version "0.4.4"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
-  integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
-  dependencies:
-    amdefine ">=0.0.4"
-
 source-map@^0.5.0, source-map@^0.5.6:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -8213,13 +8028,6 @@
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
 
-stdout-stream@^1.4.0:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de"
-  integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==
-  dependencies:
-    readable-stream "^2.0.1"
-
 stealthy-require@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
@@ -8404,13 +8212,6 @@
   resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
   integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
 
-strip-indent@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
-  integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
-  dependencies:
-    min-indent "^1.0.0"
-
 strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -8515,18 +8316,6 @@
     safe-buffer "^5.1.2"
     yallist "^3.0.3"
 
-tar@^6.0.2:
-  version "6.1.11"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
-  integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
-  dependencies:
-    chownr "^2.0.0"
-    fs-minipass "^2.0.0"
-    minipass "^3.0.0"
-    minizlib "^2.1.1"
-    mkdirp "^1.0.3"
-    yallist "^4.0.0"
-
 terser-webpack-plugin@^1.2.3, terser-webpack-plugin@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4"
@@ -8646,6 +8435,13 @@
     is-number "^3.0.0"
     repeat-string "^1.6.1"
 
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
 to-regex@^3.0.1, to-regex@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@@ -8674,18 +8470,6 @@
     psl "^1.1.28"
     punycode "^2.1.1"
 
-trim-newlines@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
-  integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
-
-"true-case-path@^1.0.2":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d"
-  integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==
-  dependencies:
-    glob "^7.1.2"
-
 tryer@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
@@ -8733,21 +8517,11 @@
   dependencies:
     prelude-ls "~1.1.2"
 
-type-fest@^0.18.0:
-  version "0.18.1"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
-  integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
-
 type-fest@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
   integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
 
-type-fest@^0.8.1:
-  version "0.8.1"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
-  integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
-
 type-is@~1.6.17, type-is@~1.6.18:
   version "1.6.18"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -9243,7 +9017,7 @@
   dependencies:
     isexe "^2.0.0"
 
-which@^2.0.1, which@^2.0.2:
+which@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
@@ -9364,14 +9138,6 @@
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
-yargs-parser@^13.1.2:
-  version "13.1.2"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
-  integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
-  dependencies:
-    camelcase "^5.0.0"
-    decamelize "^1.2.0"
-
 yargs-parser@^16.1.0:
   version "16.1.0"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-16.1.0.tgz#73747d53ae187e7b8dbe333f95714c76ea00ecf1"
@@ -9380,11 +9146,6 @@
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
-yargs-parser@^20.2.3:
-  version "20.2.9"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
-  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
 yargs@12.0.5:
   version "12.0.5"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
@@ -9403,22 +9164,6 @@
     y18n "^3.2.1 || ^4.0.0"
     yargs-parser "^11.1.1"
 
-yargs@^13.3.2:
-  version "13.3.2"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
-  integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
-  dependencies:
-    cliui "^5.0.0"
-    find-up "^3.0.0"
-    get-caller-file "^2.0.1"
-    require-directory "^2.1.1"
-    require-main-filename "^2.0.0"
-    set-blocking "^2.0.0"
-    string-width "^3.0.0"
-    which-module "^2.0.0"
-    y18n "^4.0.0"
-    yargs-parser "^13.1.2"
-
 yargs@^15.0.0:
   version "15.0.2"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.0.2.tgz#4248bf218ef050385c4f7e14ebdf425653d13bd3"
diff --git a/docs/dev/developing_frontend.md b/docs/dev/developing_frontend.md
index 41913c1..f324c7b 100644
--- a/docs/dev/developing_frontend.md
+++ b/docs/dev/developing_frontend.md
@@ -2,7 +2,8 @@
 
 Make sure you have
 [the latest version of Node.js LTS installed](https://nodejs.org/en/download/).
-You also need to install [the Yarn package manager](https://yarnpkg.com).
+You also need to install
+[the Yarn 1 (Classic) package manager](https://classic.yarnpkg.com/en/docs/install).
 
 Start the Django portal (`python manage.py runserver`). Navigate to the Django
 app directory and run `yarn` and then `yarn` to start up the dev server. Now you
@@ -166,6 +167,6 @@
 
 ## Recommended tools
 
--   <https://github.com/vuejs/vue-devtools> - debugging/inspection in Firefox
-    or Chrome
+-   <https://github.com/vuejs/vue-devtools> - debugging/inspection in Firefox or
+    Chrome
 -   <https://vuejs.github.io/vetur/> - Vue tooling for Visual Studio Code
diff --git a/docs/dev/queue_settings_calculator.md b/docs/dev/queue_settings_calculator.md
new file mode 100644
index 0000000..55073f8
--- /dev/null
+++ b/docs/dev/queue_settings_calculator.md
@@ -0,0 +1,104 @@
+# Queue Settings Calculator
+
+A _Queue Settings Calculator_ is a Python function that computes the queue
+settings for an experiment. The function takes an instance of an experiment and
+returns:
+
+-   queue name
+-   core count
+-   node count
+-   walltime limit
+
+The Airavata Django Portal then uses these values to populate the Queue Settings
+fields when users are creating/editing experiments. This can greatly simplify
+experiment configuration for users and also lead to a better use of resources.
+
+## Getting started
+
+To add a Queue Settings Calculator function, you first will need a custom Django
+App. See the [Custom Django App](./custom_django_app.md) notes for how to create
+one.
+
+Next, add a `queue_settings_calculators.py` file to your custom Django app
+package if it doesn't already exist. You should add this file to the same folder
+that contains the `apps.py` file.
+
+In this file, you'll need to import the `@queue_settings_calculators` decorator.
+You'll use this to mark the queue settings calculator functions (you can define
+more than one).
+
+```python
+from airavata.model.experiment.ttypes import ExperimentModel
+from airavata_django_portal_sdk.decorators import queue_settings_calculator
+
+@queue_settings_calculator(
+    id="gateway_name-queue-settings-for-my-app", name="My Gateway: Queue Settings for My App"
+)
+def my_queue_settings_calculator(request, experiment_model: ExperimentModel):
+    # See https://airavata.apache.org/api-docs/master/experiment_model.html#Struct_ExperimentModel for ExperimentModel fields
+
+    total_core_count = 4
+    queue_name = "shared"
+    node_count = 1
+    walltime_limit = 30
+
+    # Return a dictionary with the queue settings values
+    result = {}
+    result["totalCPUCount"] = total_core_count
+    result["queueName"] = queue_name
+    result["nodeCount"] = node_count
+    result["wallTimeLimit"] = walltime_limit
+    return result
+```
+
+The `id` and `name` that are passed to the `@queue_settings_calculator`
+decorator are optional but highly recommended. The `id` will be used internally
+to associate applications with this function. Set the id to something that is
+unique to your gateway. The `name` is the value that will be displayed in the
+Settings UI for selecting this queue settings calculator.
+
+The queue settings calculator function is passed the Django `request` object and
+the `experiment_model`, an
+[ExperimentModel](https://airavata.apache.org/api-docs/master/experiment_model.html#Struct_ExperimentModel)
+instance.
+
+Primarily your function will inspect the `experiment_model` to determine the
+appropriate queue settings. For example you might look at one of the
+experiment's input files to determine how many cores are optimal for the job.
+
+Next, add the following import line to the `ready()` function of your apps.py
+AppConfig class. It will look something like this although the names of your
+AppConfig class and packages will be different:
+
+```python
+class CustomDjangoAppConfig(AppConfig):
+    name = 'custom_django_app'
+    label = name
+    verbose_name = "Custom Django App"
+    fa_icon_class = "fa-comment"
+    url_home = "custom_django_app:hello_world"
+
+    def ready(self) -> None:
+        from custom_django_app import queue_settings_calculators  # noqa
+```
+
+Add the `ready(self)` function to your AppConfig if it is missing. Then add an
+import of your queue_settings_calculators module. Importing the module will
+register the calculator functions at startup time.
+
+## Configuring an application's queue settings calculator
+
+To have one of your applications use your queue settings calculator, first make
+sure to install the custom Django app in your portal instance. Then, go to
+**Settings > Application Catalog** and click on your application. Select the
+**Interface** tab. Under _Queue Settings Calculator_, select your queue settings
+calculator from the drop down. It should be listed with the name that you gave
+to it in your function's decorator
+(`@queue_settings_calculator(id='...', name='...')`).
+
+When an application is configured to use a queue settings calculator, the Queue
+Settings UI in the Create/Edit Experiment views is disabled and made read only.
+
+## Examples
+
+-   <https://github.com/bio-miga/miga-autocomplete>
diff --git a/docs/tutorial/custom_ui_tutorial.md b/docs/tutorial/custom_ui_tutorial.md
index 6c5290a..755e8ec 100644
--- a/docs/tutorial/custom_ui_tutorial.md
+++ b/docs/tutorial/custom_ui_tutorial.md
@@ -15,7 +15,8 @@
 -   [Docker Desktop](https://www.docker.com/products/docker-desktop)
 -   If you don't have Docker installed or can't install it, you'll also need:
     -   [Node LTS](https://nodejs.org/en/download/),
-    -   and [Yarn package manager](https://yarnpkg.com/getting-started/install).
+    -   and
+        [Yarn 1 (Classic) package manager](https://classic.yarnpkg.com/en/docs/install).
 
 ### Installing Python
 
@@ -768,17 +769,13 @@
 import io
 import os
 
-import numpy as np
-from cclib.parser import ccopen
-from django.conf import settings
-from matplotlib.figure import Figure
+import numpy as np from cclib.parser import ccopen from django.conf import
+settings from matplotlib.figure import Figure
 
 from airavata_django_portal_sdk import user_storage
 
-
-class GaussianEigenvaluesViewProvider:
-    display_type = 'image'
-    name = "Gaussian Eigenvalues"
+class GaussianEigenvaluesViewProvider: display_type = 'image' name = "Gaussian
+Eigenvalues"
 
     def generate_data(self, request, experiment_output, experiment, output_file=None, **kwargs):
 
@@ -825,7 +822,7 @@
             'mime-type': 'image/png'
         }
 
-```
+````
 </div>
 
 5. Let's take a look at the implementation. First we added some imports at the
@@ -841,7 +838,7 @@
 from matplotlib.figure import Figure
 
 from airavata_django_portal_sdk import user_storage
-```
+````
 
 6.  Next we implemented the
     [`generate_data` function](../dev/custom_output_view_provider.md#output-view-provider-interface).
@@ -1140,12 +1137,10 @@
 
 from . import views
 
-app_name = 'custom_ui_tutorial_app'
-urlpatterns = [
-    path('home/', views.home, name='home'),
-    path('hello/', views.hello_world, name='hello_world'),
-]
-```
+app_name = 'custom_ui_tutorial_app' urlpatterns = [ path('home/', views.home,
+name='home'), path('hello/', views.hello_world, name='hello_world'), ]
+
+````
 </div>
 
 This maps the `/hello/` URL to the `hello_world` view.
@@ -1168,7 +1163,8 @@
     verbose_name = "Custom UI Tutorial App"
     fa_icon_class = "fa-comment"
     url_home = "custom_ui_tutorial_app:hello_world"
-```
+````
+
 </div>
 
 This the main metadata for this custom Django app. Besides the normal metadata
@@ -1245,13 +1241,11 @@
 
 from . import views
 
-app_name = 'custom_ui_tutorial_app'
-urlpatterns = [
-    path('home/', views.home, name='home'),
-    path('hello/', views.hello_world, name="hello_world"),
-    path('languages/', views.languages, name="languages"),
-]
-```
+app_name = 'custom_ui_tutorial_app' urlpatterns = [ path('home/', views.home,
+name='home'), path('hello/', views.hello_world, name="hello_world"),
+path('languages/', views.languages, name="languages"), ]
+
+````
 </div>
 
 4. In
@@ -1282,7 +1276,7 @@
     </main>
 </div>
 ...
-```
+````
 
 <button class="btn" data-clipboard-target="#greeting-select">
     Copy to clipboard
@@ -1311,7 +1305,6 @@
     Add to `hello.html` the code between the **STARTING HERE** and **ENDING
     HERE** comments.
 
-
 ```html
 {% block scripts %}
 <script src="{% static 'django_airavata_api/dist/airavata-api.js' %}"></script>
@@ -1472,7 +1465,8 @@
 
     loadExperiments();
     $("#refresh-button").click(loadExperiments);
-```
+
+````
 </div>
 
 The user interface should now look something like:
@@ -1515,7 +1509,8 @@
     }
 
     $("#run-button").click(runClickHandler);
-```
+````
+
 </div>
 
 2. Going line by line we'll now take a look at this code. We added a click
@@ -1688,7 +1683,9 @@
             const stdoutInput = experiment.getExperimentOutput("Echo-STDOUT");
             const dataProductURI = stdoutInput.value;
             try {
-                const stdout = await utils.ExperimentUtils.readDataProduct(dataProductURI);
+                const stdout = await utils.ExperimentUtils.readDataProduct(
+                    dataProductURI
+                );
                 // if stdout is null, it means the file wasn't found
                 if (stdout !== null) {
                     $(`#output_${index}`).text(stdout);
diff --git a/mkdocs.yml b/mkdocs.yml
index 3a0b01e..9ecc3b0 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -16,6 +16,7 @@
     - Custom UI Tutorial: tutorial/custom_ui_tutorial.md
     - Adding a Custom Django App: dev/custom_django_app.md
     - Adding a Custom Output View Provider: dev/custom_output_view_provider.md
+    - Adding a Queue Settings Calculator: dev/queue_settings_calculator.md
     - HOWTOs: dev/customization_howto.md
   - Administrator Guide:
     - Application Input Customization: admin/app_inputs.md
diff --git a/requirements.txt b/requirements.txt
index e910a59..6fb3756 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 # Pin these dependencies
-Django==3.2.11
+Django==3.2.15
 requests==2.25.1
 requests-oauthlib==0.7.0
 thrift==0.10.0
@@ -21,7 +21,7 @@
 grpcio-tools==1.34.1
 grpcio==1.34.1
 
-airavata-django-portal-sdk==1.4.0
+airavata-django-portal-sdk==1.6.0
 airavata-python-sdk==1.0.2
 
 -e "."