Repository: rancher/dashboard
Pull request #13703: Refactor form components so data prop no longer contains init logic
Author: login: rak-phillip
Created: 2025-03-11T22:45:17Z | Merged: 2025-03-28T22:10:44Z

<!-- This template is for Devs to give QA details before moving the issue To-Test -->
### Summary
This refactors form components that contain initialization logic in their data props so that they use patterns that are more idiomatic to Vue.

Fixes #13702 
<!-- Define findings related to the feature or bug issue. -->

### Occurred changes and/or fixed issues
<!-- Include information of the changes, including collateral areas which have been affected by this PR as requirement or for convenience. -->

### Technical notes summary
<!-- Outline technical changes which may pass unobserved or may help to understand the process of solving the issue -->

🗒️ NOTE: I recommend reviewing this PR commit-by-commit to reduce the complexity of reviewing some larger changes. 

🗒️ NOTE: This is a large PR that touches many forms throughout Dashboard. I am open to splitting this into smaller PRs if we find that validating and testing these changes takes too much effort.

Some form components require a larger refactor than others because of watcher usage. Watchers will trigger when watched values are updated in the created hook; this is undesirable because this modifies component behavior on initial render, potentially introducing regressions in forms. Refactoring these components to use setup() has the advantage of allowing us to retain a similar data initialization pattern to what we previously used before. I kept these refactors limited to components that utilize watchers on data props that require initialization in order to reduce the amount of complexity involved in this refactor. I think that the tradeoff is worth it in order solve the issue while maintaining existing behavior.  

### Areas or cases that should be tested
<!-- Areas that should be tested can include Airgap checks, Rancher upgrades, K8s upgrade, etc. -->
<!-- Which browser did you use for local testing? The reviewer should test with a different browser. -->
<!-- Add missing steps or rewrite them if have been missed or to complement existing information. This should define a clear way to reproduce it and not an approximation. -->

In effect, this touches all of our forms. Components like `NameNsDescription.vue` are widely used throughout Dashboard. Focus on core form functionality and pay attention to larger refactors with components:

- Probe.vue
- ValueFromResource.vue
- NameNsDescription.vue
- ArrayList.vue

### Areas which could experience regressions
<!-- Create a detailed list of areas to be analyzed which may be affected by the changes, which would require a prior research to avoid regressions. -->

Forms have different modes: Create, Edit, View. these should not regress as a result of this change.

### Screenshot/Video
<!-- Attach screenshot or video of the changes and eventual comparison if you find it necessary -->

NA

### Checklist
- [x] The PR is linked to an issue and the linked issue has a Milestone, or no issue is needed
- [x] The PR has a Milestone <!-- The milestone should automatically be assigned if the linked issue has one, but does not always happen (incorrectly linked, issue has no milestone, etc) -->
- [x] The PR template has been filled out
- [x] The PR has been self reviewed <!-- There are no TODOs, no incorrect files in the PR, all the required files are there, no commented out code, etc-->
- [x] The PR has a reviewer assigned
- [x] The PR has automated tests or clear instructions for manual tests and the linked issue has appropriate QA labels, or tests are not needed
- [x] The PR has reviewed with UX and tested in light and dark mode, or there are no UX changes

Linked issue #13702: Data exceptions: Form Components
Refactor Form Components to replace data props that have complex init logic with safe patterns.

## Acceptance criteria

The following Form Components are refactored so that the `data` prop no longer contains logic and only specifies component state: 

- [ ] ./shell/components/form/ResourceQuota/Project.vue
- [ ] ./shell/components/form/Command.vue
- [ ] ./shell/components/form/EnvVars.vue
- [ ] ./shell/components/form/HealthCheck.vue
- [ ] ./shell/components/form/HookOption.vue
- [ ] ./shell/components/form/LifecycleHooks.vue
- [ ] ./shell/components/form/MatchExpressions.vue
- [ ] ./shell/components/form/NameNsDescription.vue
- [ ] ./shell/components/form/Networking.vue
- [ ] ./shell/components/form/NodeAffinity.vue
- [ ] ./shell/components/form/NodeScheduling.vue
- [ ] ./shell/components/form/PodAffinity.vue
- [ ] ./shell/components/form/Probe.vue
- [ ] ./shell/components/form/ResourceSelector.vue
- [ ] ./shell/components/form/ShellInput.vue
- [ ] ./shell/components/form/Tolerations.vue
- [ ] ./shell/components/form/ValueFromResource.vue
- [ ] ./shell/components/form/WorkloadPorts.vue
- [ ] ./shell/components/form/ArrayList.vue
- [ ] ./shell/components/form/Security.vue


Diff:
diff --git a/jest.setup.js b/jest.setup.js
index 736c3dc2aa8..e0cbf318e29 100644
--- a/jest.setup.js
+++ b/jest.setup.js
@@ -68,7 +68,18 @@ jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => (key) => key }))
 // eslint-disable-next-line no-console
 jest.spyOn(console, 'warn').mockImplementation((warning) => warning.includes('[Vue warn]') ? null : console.log(warning));
 
-jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => (key) => key }));
+// jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => (key) => key }));
+jest.mock('@shell/composables/useI18n', () => {
+  return {
+    useI18n() {
+      return {
+        t(key) {
+          return key;
+        }
+      };
+    }
+  };
+});
 // eslint-disable-next-line no-console
 jest.spyOn(console, 'warn').mockImplementation((warning) => warning.includes('[Vue warn]') ? null : console.log(warning));
 
diff --git a/shell/components/auth/__tests__/RoleDetailEdit.test.ts b/shell/components/auth/__tests__/RoleDetailEdit.test.ts
index a9e82001a40..96fbef9420f 100644
--- a/shell/components/auth/__tests__/RoleDetailEdit.test.ts
+++ b/shell/components/auth/__tests__/RoleDetailEdit.test.ts
@@ -51,8 +51,9 @@ describe('component: RoleDetailEdit', () => {
     const wrapper = mount(RoleDetailEdit, {
       props: {
         value: {
-          rules:   [{ verbs }],
-          subtype: 'GLOBAL'
+          rules:    [{ verbs }],
+          subtype:  'GLOBAL',
+          metadata: { name: 'global-role-with-inherited' },
         },
       },
 
diff --git a/shell/components/form/ArrayList.vue b/shell/components/form/ArrayList.vue
index efc7df865a3..a363b34d830 100644
--- a/shell/components/form/ArrayList.vue
+++ b/shell/components/form/ArrayList.vue
@@ -1,4 +1,5 @@
 <script>
+import { ref, watch, computed } from 'vue';
 import debounce from 'lodash/debounce';
 import { _EDIT, _VIEW } from '@shell/config/query-params';
 import { removeAt } from '@shell/utils/array';
@@ -95,21 +96,78 @@ export default {
       validator: (rules) => rules.every((rule) => ['function'].includes(typeof rule))
     },
   },
-  data() {
-    const input = (Array.isArray(this.value) ? this.value : []).slice();
-    const rows = [];
+
+  setup(props, { emit }) {
+    const input = (Array.isArray(props.value) ? props.value : []).slice();
+    const rows = ref([]);
 
     for ( const value of input ) {
-      rows.push({ value });
+      rows.value.push({ value });
     }
-    if ( !rows.length && this.initialEmptyRow ) {
-      const value = this.defaultAddValue ? clone(this.defaultAddValue) : '';
+    if ( !rows.value.length && props.initialEmptyRow ) {
+      const value = props.defaultAddValue ? clone(props.defaultAddValue) : '';
 
-      rows.push({ value });
+      rows.value.push({ value });
     }
 
-    return { rows, lastUpdateWasFromValue: false };
+    const isView = computed(() => {
+      return props.mode === _VIEW;
+    });
+
+    /**
+     * Cleanup rows and emit input
+     */
+    const update = () => {
+      if ( isView.value ) {
+        return;
+      }
+      const out = [];
+
+      for ( const row of rows.value ) {
+        const trim = !props.valueMultiline && (typeof row.value === 'string');
+        const value = trim ? row.value.trim() : row.value;
+
+        if ( typeof value !== 'undefined' ) {
+          out.push(value);
+        }
+      }
+      emit('update:value', out);
+    };
+
+    const lastUpdateWasFromValue = ref(false);
+    const queueUpdate = debounce(update, 50);
+
+    watch(
+      rows,
+      () => {
+        // lastUpdateWasFromValue is used to break a cycle where when rows are updated
+        // this was called which then forced rows to updated again
+        if (!lastUpdateWasFromValue.value) {
+          queueUpdate();
+        }
+        lastUpdateWasFromValue.value = false;
+      },
+      { deep: true }
+    );
+
+    watch(
+      () => props.value,
+      () => {
+        lastUpdateWasFromValue.value = true;
+        rows.value = (props.value || []).map((v) => ({ value: v }));
+      },
+      { deep: true }
+    );
+
+    return {
+      rows,
+      lastUpdateWasFromValue,
+      queueUpdate,
+      isView,
+      update,
+    };
   },
+
   computed: {
     _addLabel() {
       return this.addLabel || this.t('generic.add');
@@ -117,10 +175,6 @@ export default {
     _removeLabel() {
       return this.removeLabel || this.t('generic.remove');
     },
-
-    isView() {
-      return this.mode === _VIEW;
-    },
     showAdd() {
       return this.addAllowed;
     },
@@ -141,29 +195,7 @@ export default {
       return !this.valueMultiline && this.protip;
     }
   },
-  watch: {
-    value: {
-      deep: true,
-      handler() {
-        this.lastUpdateWasFromValue = true;
-        this.rows = (this.value || []).map((v) => ({ value: v }));
-      }
-    },
-
-    rows: {
-      deep: true,
-      handler(newValue, oldValue) {
-        // lastUpdateWasFromValue is used to break a cycle where when rows are updated
-        // this was called which then forced rows to updated again
-        if (!this.lastUpdateWasFromValue) {
-          this.queueUpdate();
-        }
-        this.lastUpdateWasFromValue = false;
-      }
-    }
-  },
   created() {
-    this.queueUpdate = debounce(this.update, 50);
   },
   methods: {
     add() {
@@ -189,26 +221,6 @@ export default {
       this.queueUpdate();
     },
 
-    /**
-     * Cleanup rows and emit input
-     */
-    update() {
-      if ( this.isView ) {
-        return;
-      }
-      const out = [];
-
-      for ( const row of this.rows ) {
-        const trim = !this.valueMultiline && (typeof row.value === 'string');
-        const value = trim ? row.value.trim() : row.value;
-
-        if ( typeof value !== 'undefined' ) {
-          out.push(value);
-        }
-      }
-      this.$emit('update:value', out);
-    },
-
     /**
      * Handle paste event, e.g. split multiple lines in rows
      */
diff --git a/shell/components/form/Command.vue b/shell/components/form/Command.vue
index 11a0ed10fe3..e786bad2943 100644
--- a/shell/components/form/Command.vue
+++ b/shell/components/form/Command.vue
@@ -43,23 +43,14 @@ export default {
   },
 
   data() {
-    const {
-      command,
-      args,
-      workingDir,
-      stdin = false,
-      stdinOnce = false,
-      tty = false,
-    } = this.value;
-
     return {
-      args,
-      command,
+      args:           this.value.args,
+      command:        this.value.command,
       commandOptions: ['No', 'Once', 'Yes'],
-      stdin,
-      stdinOnce,
-      tty,
-      workingDir,
+      stdin:          this.value.stdin || false,
+      stdinOnce:      this.value.stdin || false,
+      tty:            this.value.tty || false,
+      workingDir:     this.value.workingDir,
     };
   },
 
diff --git a/shell/components/form/EnvVars.vue b/shell/components/form/EnvVars.vue
index 97b31a69955..198f793425d 100644
--- a/shell/components/form/EnvVars.vue
+++ b/shell/components/form/EnvVars.vue
@@ -39,14 +39,10 @@ export default {
   },
 
   data() {
-    const { env = [], envFrom = [] } = this.value;
-
-    const allEnv = [...env, ...envFrom].map((row) => {
-      return { value: row, id: randomStr(4) };
-    });
-
     return {
-      env, envFrom, allEnv
+      env:     [],
+      envFrom: [],
+      allEnv:  [],
     };
   },
 
@@ -63,7 +59,18 @@ export default {
       }
     }
   },
+
   created() {
+    const { env = [], envFrom = [] } = this.value;
+
+    const allEnv = [...env, ...envFrom].map((row) => {
+      return { value: row, id: randomStr(4) };
+    });
+
+    this.env = env;
+    this.envFrom = envFrom;
+    this.allEnv = allEnv;
+
     this.queueUpdate = debounce(this.update, 500);
   },
 
diff --git a/shell/components/form/HealthCheck.vue b/shell/components/form/HealthCheck.vue
index 10f82aa8c19..1a21e081c76 100644
--- a/shell/components/form/HealthCheck.vue
+++ b/shell/components/form/HealthCheck.vue
@@ -18,10 +18,10 @@ export default {
   },
 
   data() {
-    const { readinessProbe, livenessProbe, startupProbe } = this.value;
-
     return {
-      readinessProbe, livenessProbe, startupProbe
+      readinessProbe: this.value.readinessProbe,
+      livenessProbe:  this.value.livenessProbe,
+      startupProbe:   this.value.startupProbe,
     };
   },
 
diff --git a/shell/components/form/HookOption.vue b/shell/components/form/HookOption.vue
index e265b48bd0a..6e913949e20 100644
--- a/shell/components/form/HookOption.vue
+++ b/shell/components/form/HookOption.vue
@@ -28,23 +28,18 @@ export default {
   },
 
   data() {
-    const selectHook = null;
-
-    const defaultExec = { exec: { command: [] } };
-    const defaultHttpGet = {
-      httpGet: {
-        host:        '',
-        path:        '',
-        port:        null,
-        scheme:      '',
-        httpHeaders: null
-      }
-    };
-
     return {
-      selectHook,
-      defaultExec,
-      defaultHttpGet,
+      selectHook:     null,
+      defaultExec:    { exec: { command: [] } },
+      defaultHttpGet: {
+        httpGet: {
+          host:        '',
+          path:        '',
+          port:        null,
+          scheme:      '',
+          httpHeaders: null
+        }
+      },
       schemeOptions: ['HTTP', 'HTTPS']
     };
   },
diff --git a/shell/components/form/LifecycleHooks.vue b/shell/components/form/LifecycleHooks.vue
index 2cfaecab430..2f05cc83c9a 100644
--- a/shell/components/form/LifecycleHooks.vue
+++ b/shell/components/form/LifecycleHooks.vue
@@ -24,10 +24,10 @@ export default {
   },
 
   data() {
-    const { postStart, preStop } = this.value;
-
     return {
-      postStart, preStop, hookOptions: ['postStart', 'preStop']
+      postStart:   this.value.postStart,
+      preStop:     this.value.preStop,
+      hookOptions: ['postStart', 'preStop']
     };
   },
 
diff --git a/shell/components/form/MatchExpressions.vue b/shell/components/form/MatchExpressions.vue
index 04e52104eac..60a250eb1cc 100644
--- a/shell/components/form/MatchExpressions.vue
+++ b/shell/components/form/MatchExpressions.vue
@@ -70,6 +70,14 @@ export default {
   },
 
   data() {
+    return {
+      ops:    [],
+      rules:  [],
+      custom: []
+    };
+  },
+
+  created() {
     const t = this.$store.getters['i18n/t'];
 
     const podOptions = [
@@ -88,8 +96,6 @@ export default {
       { label: t('workload.scheduling.affinity.matchExpressions.greaterThan'), value: 'Gt' },
     ];
 
-    const ops = this.type === NODE ? nodeOptions : podOptions;
-
     let rules;
 
     // special case for matchFields and matchExpressions
@@ -127,11 +133,8 @@ export default {
       rules.push(newRule);
     }
 
-    return {
-      ops,
-      rules,
-      custom: []
-    };
+    this.rules = rules;
+    this.ops = this.type === NODE ? nodeOptions : podOptions;
   },
 
   computed: {
diff --git a/shell/components/form/NameNsDescription.vue b/shell/components/form/NameNsDescription.vue
index 7e799dc4cd7..aa2866da2e3 100644
--- a/shell/components/form/NameNsDescription.vue
+++ b/shell/components/form/NameNsDescription.vue
@@ -1,5 +1,7 @@
 <script>
-import { mapGetters, mapActions } from 'vuex';
+import { computed, ref, toRef, watch } from 'vue';
+import { mapActions, useStore } from 'vuex';
+
 import { get, set } from '@shell/utils/object';
 import { sortBy } from '@shell/utils/sort';
 import { NAMESPACE } from '@shell/config/types';
@@ -8,6 +10,7 @@ import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
 import { LabeledInput } from '@components/Form/LabeledInput';
 import LabeledSelect from '@shell/components/form/LabeledSelect';
 import { normalizeName } from '@shell/utils/kube';
+import { useI18n } from '@shell/composables/useI18n';
 
 export default {
   name: 'NameNsDescription',
@@ -169,81 +172,74 @@ export default {
   },
 
   data() {
-    const v = this.value;
-    const metadata = v.metadata;
-    let namespace, name, description;
+    return { createNamespace: false };
+  },
 
-    if (this.nameKey) {
-      name = get(v, this.nameKey);
-    } else {
-      name = metadata?.name;
-    }
+  setup(props, { emit }) {
+    const v = toRef(props.value);
+    const metadata = v.value.metadata;
+    const namespace = ref(null);
+    const name = ref(null);
+    const description = ref(null);
 
-    if (this.namespaced) {
-      if (this.forceNamespace) {
-        namespace = this.forceNamespace;
-        this.updateNamespace(namespace);
-      } else if (this.namespaceKey) {
-        namespace = get(v, this.namespaceKey);
-      } else {
-        namespace = metadata?.namespace;
+    watch(name, (val) => {
+      if (props.normalizeName) {
+        val = normalizeName(val);
       }
 
-      if (!namespace && !this.noDefaultNamespace) {
-        namespace = this.$store.getters['defaultNamespace'];
-        if (metadata) {
-          metadata.namespace = namespace;
-        }
+      if (props.nameKey) {
+        set(props.value, props.nameKey, val);
+      } else {
+        props.value.metadata['name'] = val;
       }
-    }
+      emit('update:value', props.value);
+    });
 
-    if (this.descriptionKey) {
-      description = get(v, this.descriptionKey);
+    if (props.nameKey) {
+      name.value = get(v, props.nameKey);
     } else {
-      description = metadata?.annotations?.[DESCRIPTION];
+      name.value = metadata?.name || '';
     }
 
-    const inStore = this.$store.getters['currentStore']();
-    const nsSchema = this.$store.getters[`${ inStore }/schemaFor`](NAMESPACE);
+    const isCreate = computed(() => {
+      return props.mode === _CREATE;
+    });
 
-    return {
-      namespace,
-      name,
-      description,
-      createNamespace: false,
-      nsSchema
-    };
-  },
+    const store = useStore();
+    const { t } = useI18n(store);
+    const allowedNamespaces = computed(() => store.getters.allowedNamespaces());
+    const storeNamespaces = computed(() => store.getters.namespaces());
+    const currentCluster = computed(() => store.getters.currentCluster);
 
-  computed: {
-    ...mapGetters(['currentProduct', 'currentCluster', 'namespaces', 'allowedNamespaces']),
-    ...mapActions('cru-resource', ['setCreateNamespace']),
-    namespaceReallyDisabled() {
-      return (
-        !!this.forceNamespace || this.namespaceDisabled || this.mode === _EDIT
-      ); // namespace is never editable
-    },
+    const inStore = computed(() => {
+      return store.getters['currentStore']();
+    });
 
-    nameReallyDisabled() {
-      return this.nameDisabled || (this.mode === _EDIT && !this.nameEditable);
-    },
+    const nsSchema = computed(() => {
+      return store.getters[`${ inStore.value }/schemaFor`](NAMESPACE);
+    });
+
+    const canCreateNamespace = computed(() => {
+      // Check if user can push to namespaces... and as the ns is outside of a project restrict to admins and cluster owners
+      return (nsSchema.value?.collectionMethods || []).includes('POST') && currentCluster.value?.canUpdate;
+    });
 
     /**
      * Map namespaces from the store to options, adding divider and create button
      */
-    options() {
+    const options = computed(() => {
       let namespaces;
 
-      if (this.namespacesOverride) {
+      if (props.namespacesOverride) {
         // Use the resources provided
-        namespaces = this.namespacesOverride;
+        namespaces = props.namespacesOverride;
       } else {
-        if (this.namespaceOptions) {
+        if (props.namespaceOptions) {
           // Use the namespaces provided
-          namespaces = (this.namespaceOptions.map((ns) => ns.name) || []).sort();
+          namespaces = (props.namespaceOptions.map((ns) => ns.name) || []).sort();
         } else {
           // Determine the namespaces
-          const namespaceObjs = this.isCreate ? this.allowedNamespaces() : this.namespaces();
+          const namespaceObjs = isCreate.value ? allowedNamespaces.value : storeNamespaces.value;
 
           namespaces = Object.keys(namespaceObjs);
         }
@@ -251,22 +247,22 @@ export default {
 
       const options = namespaces
         .map((namespace) => ({ nameDisplay: namespace, id: namespace }))
-        .map(this.namespaceMapper || ((obj) => ({
+        .map(props.namespaceMapper || ((obj) => ({
           label: obj.nameDisplay,
           value: obj.id,
         })));
 
       const sortedByLabel = sortBy(options, 'label');
 
-      if (this.forceNamespace) {
+      if (props.forceNamespace) {
         sortedByLabel.unshift({
-          label: this.forceNamespace,
-          value: this.forceNamespace,
+          label: props.forceNamespace,
+          value: props.forceNamespace,
         });
       }
 
       const createButton = {
-        label: this.t('namespace.createNamespace'),
+        label: t('namespace.createNamespace'),
         value: '',
         kind:  'highlighted'
       };
@@ -276,20 +272,78 @@ export default {
         kind:     'divider'
       };
 
-      const createOverhead = this.canCreateNamespace || this.createNamespaceOverride ? [createButton, divider] : [];
+      const createOverhead = canCreateNamespace.value || props.createNamespaceOverride ? [createButton, divider] : [];
 
       return [
         ...createOverhead,
         ...sortedByLabel
       ];
+    });
+
+    const updateNamespace = (val) => {
+      if (props.forceNamespace) {
+        val = props.forceNamespace;
+      }
+
+      if (props.namespaced) {
+        emit('isNamespaceNew', !val || (options.value && !options.value.find((n) => n.value === val)));
+      }
+
+      if (props.namespaceKey) {
+        set(props.value, props.namespaceKey, val);
+      } else {
+        props.value.metadata.namespace = val;
+      }
+    };
+
+    if (props.namespaced) {
+      if (props.forceNamespace) {
+        namespace.value = toRef(props.forceNamespace);
+        updateNamespace(namespace);
+      } else if (props.namespaceKey) {
+        namespace.value = get(v, props.namespaceKey);
+      } else {
+        namespace.value = metadata?.namespace;
+      }
+
+      if (!namespace.value && !props.noDefaultNamespace) {
+        namespace.value = store.getters['defaultNamespace'];
+        if (metadata) {
+          metadata.namespace = namespace;
+        }
+      }
+    }
+
+    if (props.descriptionKey) {
+      description.value = get(v, props.descriptionKey);
+    } else {
+      description.value = metadata?.annotations?.[DESCRIPTION];
+    }
+
+    return {
+      namespace,
+      name,
+      description,
+      isCreate,
+      options,
+      updateNamespace,
+    };
+  },
+
+  computed: {
+    ...mapActions('cru-resource', ['setCreateNamespace']),
+    namespaceReallyDisabled() {
+      return (
+        !!this.forceNamespace || this.namespaceDisabled || this.mode === _EDIT
+      ); // namespace is never editable
     },
 
-    isView() {
-      return this.mode === _VIEW;
+    nameReallyDisabled() {
+      return this.nameDisabled || (this.mode === _EDIT && !this.nameEditable);
     },
 
-    isCreate() {
-      return this.mode === _CREATE;
+    isView() {
+      return this.mode === _VIEW;
     },
 
     showCustomize() {
@@ -308,27 +362,9 @@ export default {
 
       return `span-${ span }`;
     },
-
-    canCreateNamespace() {
-      // Check if user can push to namespaces... and as the ns is outside of a project restrict to admins and cluster owners
-      return (this.nsSchema?.collectionMethods || []).includes('POST') && this.currentCluster?.canUpdate;
-    }
   },
 
   watch: {
-    name(val) {
-      if (this.normalizeName) {
-        val = normalizeName(val);
-      }
-
-      if (this.nameKey) {
-        set(this.value, this.nameKey, val);
-      } else {
-        this.value.metadata['name'] = val;
-      }
-      this.$emit('update:value', this.value);
-    },
-
     namespace(val) {
       this.updateNamespace(val);
       this.$emit('update:value', this.value);
@@ -346,29 +382,13 @@ export default {
 
   mounted() {
     this.$nextTick(() => {
-      if (this.$refs.name) {
-        this.$refs.name.focus();
+      if (this.$refs.nameInput) {
+        this.$refs.nameInput.focus();
       }
     });
   },
 
   methods: {
-    updateNamespace(val) {
-      if (this.forceNamespace) {
-        val = this.forceNamespace;
-      }
-
-      if (this.namespaced) {
-        this.$emit('isNamespaceNew', !val || (this.options && !this.options.find((n) => n.value === val)));
-      }
-
-      if (this.namespaceKey) {
-        set(this.value, this.namespaceKey, val);
-      } else {
-        this.value.metadata.namespace = val;
-      }
-    },
-
     changeNameAndNamespace(e) {
       this.name = (e.text || '').toLowerCase();
       this.namespace = e.selected;
@@ -389,7 +409,7 @@ export default {
           true,
         );
         this.$emit('isNamespaceNew', true);
-        this.$nextTick(() => this.$refs.namespace.focus());
+        this.$nextTick(() => this.$refs.namespaceInput.focus());
       } else {
         this.createNamespace = false;
         this.$store.dispatch(
@@ -411,7 +431,7 @@ export default {
       class="col span-3"
     >
       <LabeledInput
-        ref="namespace"
+        ref="namespaceInput"
         v-model:value="namespace"
         :label="t('namespace.label')"
         :placeholder="t('namespace.createNamespace')"
@@ -459,7 +479,7 @@ export default {
       class="col span-3"
     >
       <LabeledInput
-        ref="name"
+        ref="nameInput"
         key="name"
         v-model:value="name"
         :label="t(nameLabel)"
@@ -473,7 +493,6 @@ export default {
     </div>
 
     <slot name="customize" />
-    <!-- // TODO: here goes the custom component -->
     <div
       v-show="!descriptionHidden"
       :data-testid="componentTestid + '-description'"
diff --git a/shell/components/form/Networking.vue b/shell/components/form/Networking.vue
index fcad5eb2ea1..7d77e264e9b 100644
--- a/shell/components/form/Networking.vue
+++ b/shell/components/form/Networking.vue
@@ -29,6 +29,20 @@ export default {
 
   data() {
     const t = this.$store.getters['i18n/t'];
+
+    return {
+      dnsPolicy:   this.value.dnsPolicy || 'ClusterFirst',
+      networkMode: this.value.hostNetwork ? { label: t('workload.networking.networkMode.options.hostNetwork'), value: true } : { label: t('workload.networking.networkMode.options.normal'), value: false },
+      hostAliases: [],
+      nameservers: null,
+      searches:    null,
+      hostname:    null,
+      subdomain:   null,
+      options:     null,
+    };
+  },
+
+  created() {
     const hostAliases = (this.value.hostAliases || []).map((entry) => {
       return {
         ip:        entry.ip,
@@ -38,18 +52,12 @@ export default {
     const { dnsConfig = {}, hostname, subdomain } = this.value;
     const { nameservers, searches, options } = dnsConfig;
 
-    const out = {
-      dnsPolicy:   this.value.dnsPolicy || 'ClusterFirst',
-      networkMode: this.value.hostNetwork ? { label: t('workload.networking.networkMode.options.hostNetwork'), value: true } : { label: t('workload.networking.networkMode.options.normal'), value: false },
-      hostAliases,
-      nameservers,
-      searches,
-      hostname,
-      subdomain,
-      options
-    };
-
-    return out;
+    this.hostAliases = hostAliases;
+    this.nameservers = nameservers;
+    this.searches = searches;
+    this.hostname = hostname;
+    this.subdomain = subdomain;
+    this.options = options;
   },
 
   computed: {
diff --git a/shell/components/form/NodeAffinity.vue b/shell/components/form/NodeAffinity.vue
index 3d27c4781c4..68cc26f1932 100644
--- a/shell/components/form/NodeAffinity.vue
+++ b/shell/components/form/NodeAffinity.vue
@@ -42,31 +42,18 @@ export default {
   data() {
     // VolumeNodeAffinity only has 'required' field
     if (this.value.required) {
-      return { nodeSelectorTerms: this.value.required.nodeSelectorTerms };
     } else {
-      const { preferredDuringSchedulingIgnoredDuringExecution = [], requiredDuringSchedulingIgnoredDuringExecution = {} } = this.value;
-      const { nodeSelectorTerms = [] } = requiredDuringSchedulingIgnoredDuringExecution;
-      const allSelectorTerms = [...preferredDuringSchedulingIgnoredDuringExecution, ...nodeSelectorTerms].map((term) => {
-        const neu = clone(term);
-
-        neu._id = randomStr(4);
-        if (term.preference) {
-          Object.assign(neu, term.preference);
-          delete neu.preference;
-        }
-
-        return neu;
-      });
-
-      return {
-        allSelectorTerms,
-        weightedNodeSelectorTerms: preferredDuringSchedulingIgnoredDuringExecution,
-        defaultWeight:             1,
-        // rules in MatchExpressions.vue can not catch changes what happens on parent component
-        // we need re-render it via key changing
-        rerenderNums:              randomStr(4)
-      };
     }
+
+    return {
+      nodeSelectorTerms:         null,
+      allSelectorTerms:          null,
+      weightedNodeSelectorTerms: null,
+      defaultWeight:             1,
+      // rules in MatchExpressions.vue can not catch changes what happens on parent component
+      // we need re-render it via key changing
+      rerenderNums:              randomStr(4)
+    };
   },
 
   computed: {
@@ -88,6 +75,27 @@ export default {
   },
 
   created() {
+    if (this.value.required) {
+      this.nodeSelectorTerms = this.value.required.nodeSelectorTerms;
+    } else {
+      const { preferredDuringSchedulingIgnoredDuringExecution = [], requiredDuringSchedulingIgnoredDuringExecution = {} } = this.value;
+      const { nodeSelectorTerms = [] } = requiredDuringSchedulingIgnoredDuringExecution;
+      const allSelectorTerms = [...preferredDuringSchedulingIgnoredDuringExecution, ...nodeSelectorTerms].map((term) => {
+        const neu = clone(term);
+
+        neu._id = randomStr(4);
+        if (term.preference) {
+          Object.assign(neu, term.preference);
+          delete neu.preference;
+        }
+
+        return neu;
+      });
+
+      this.allSelectorTerms = allSelectorTerms;
+      this.weightedNodeSelectorTerms = preferredDuringSchedulingIgnoredDuringExecution;
+    }
+
     this.queueUpdate = debounce(this.update, 500);
   },
 
diff --git a/shell/components/form/NodeScheduling.vue b/shell/components/form/NodeScheduling.vue
index 99055787546..ccd9c9ad826 100644
--- a/shell/components/form/NodeScheduling.vue
+++ b/shell/components/form/NodeScheduling.vue
@@ -40,6 +40,15 @@ export default {
   },
 
   data() {
+    return {
+      selectNode:   null,
+      nodeName:     '',
+      nodeAffinity: {},
+      nodeSelector: {},
+    };
+  },
+
+  created() {
     const isHarvester = this.$store.getters['currentProduct'].inStore === VIRTUAL;
 
     let { nodeName = '' } = this.value;
@@ -65,9 +74,10 @@ export default {
       nodeAffinity['preferredDuringSchedulingIgnoredDuringExecution'] = [];
     }
 
-    return {
-      selectNode, nodeName, nodeAffinity, nodeSelector
-    };
+    this.selectNode = selectNode;
+    this.nodeName = nodeName;
+    this.nodeAffinity = nodeAffinity;
+    this.nodeSelector = nodeSelector;
   },
 
   computed: {
diff --git a/shell/components/form/PodAffinity.vue b/shell/components/form/PodAffinity.vue
index b70ed063ca2..e2fa4c9210b 100644
--- a/shell/components/form/PodAffinity.vue
+++ b/shell/components/form/PodAffinity.vue
@@ -84,53 +84,14 @@ export default {
   },
 
   data() {
-    if (!this.value[this.field]) {
-      this.value[this.field] = {};
-    }
-    const { podAffinity = {}, podAntiAffinity = {} } = this.value[this.field];
-    const allAffinityTerms = [...(podAffinity.preferredDuringSchedulingIgnoredDuringExecution || []), ...(podAffinity.requiredDuringSchedulingIgnoredDuringExecution || [])].map((term) => {
-      let out = clone(term);
-
-      out._id = randomStr(4);
-      out._anti = false;
-      if (term.podAffinityTerm) {
-        Object.assign(out, term.podAffinityTerm);
-        out = this.parsePodAffinityTerm(out);
-
-        delete out.podAffinityTerm;
-      } else {
-        out = this.parsePodAffinityTerm(out);
-      }
-
-      return out;
-    });
-    const allAntiTerms = [...(podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution || []), ...(podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution || [])].map((term) => {
-      let out = clone(term);
-
-      out._id = randomStr(4);
-      out._anti = true;
-      if (term.podAffinityTerm) {
-        Object.assign(out, term.podAffinityTerm);
-        out = this.parsePodAffinityTerm(out);
-
-        delete out.podAffinityTerm;
-      } else {
-        out = this.parsePodAffinityTerm(out);
-      }
-
-      return out;
-    });
-
-    const allSelectorTerms = [...allAffinityTerms, ...allAntiTerms];
-
     return {
-      allSelectorTerms,
-      defaultWeight:   1,
+      allSelectorTerms: [],
+      defaultWeight:    1,
       // rules in MatchExpressions.vue can not catch changes what happens on parent component
       // we need re-render it via key changing
-      rerenderNums:    randomStr(4),
+      rerenderNums:     randomStr(4),
       NAMESPACE_SELECTION_OPTION_VALUES,
-      defaultAddValue: {
+      defaultAddValue:  {
         _namespaceOption: NAMESPACE_SELECTION_OPTION_VALUES.POD,
         matchExpressions: [],
         namespaces:       null,
@@ -138,6 +99,7 @@ export default {
       }
     };
   },
+
   computed: {
     ...mapGetters({ t: 'i18n/t' }),
     isView() {
@@ -226,6 +188,44 @@ export default {
   },
 
   created() {
+    if (!this.value[this.field]) {
+      this.value[this.field] = {};
+    }
+    const { podAffinity = {}, podAntiAffinity = {} } = this.value[this.field];
+    const allAffinityTerms = [...(podAffinity.preferredDuringSchedulingIgnoredDuringExecution || []), ...(podAffinity.requiredDuringSchedulingIgnoredDuringExecution || [])].map((term) => {
+      let out = clone(term);
+
+      out._id = randomStr(4);
+      out._anti = false;
+      if (term.podAffinityTerm) {
+        Object.assign(out, term.podAffinityTerm);
+        out = this.parsePodAffinityTerm(out);
+
+        delete out.podAffinityTerm;
+      } else {
+        out = this.parsePodAffinityTerm(out);
+      }
+
+      return out;
+    });
+    const allAntiTerms = [...(podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution || []), ...(podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution || [])].map((term) => {
+      let out = clone(term);
+
+      out._id = randomStr(4);
+      out._anti = true;
+      if (term.podAffinityTerm) {
+        Object.assign(out, term.podAffinityTerm);
+        out = this.parsePodAffinityTerm(out);
+
+        delete out.podAffinityTerm;
+      } else {
+        out = this.parsePodAffinityTerm(out);
+      }
+
+      return out;
+    });
+
+    this.allSelectorTerms = [...allAffinityTerms, ...allAntiTerms];
     this.queueUpdate = debounce(this.update, 500);
   },
 
diff --git a/shell/components/form/Probe.vue b/shell/components/form/Probe.vue
index bff12687b62..68973a773f5 100644
--- a/shell/components/form/Probe.vue
+++ b/shell/components/form/Probe.vue
@@ -6,6 +6,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
 import LabeledSelect from '@shell/components/form/LabeledSelect';
 import ShellInput from '@shell/components/form/ShellInput';
 import KeyValue from '@shell/components/form/KeyValue';
+import { computed, ref, watch } from 'vue';
 
 const KINDS = [
   'none',
@@ -43,29 +44,29 @@ export default {
     },
   },
 
-  data() {
-    let kind = 'none';
-    let probe = null;
-    let exec = null;
-    let httpGet = null;
-    let tcpSocket = null;
-
-    if ( this.value ) {
-      probe = clone(this.value);
-
-      if ( probe.exec ) {
-        kind = 'exec';
-      } else if ( probe.httpGet ) {
-        if ( (probe.httpGet.scheme || '').toLowerCase() === 'https' ) {
-          kind = 'HTTPS';
+  setup(props, { emit }) {
+    const kind = ref('none');
+    const probe = ref(null);
+    const exec = ref(null);
+    const httpGet = ref(null);
+    const tcpSocket = ref(null);
+
+    if ( props.value ) {
+      probe.value = clone(props.value);
+
+      if ( probe.value.exec ) {
+        kind.value = 'exec';
+      } else if ( probe.value.httpGet ) {
+        if ( (probe.value.httpGet.scheme || '').toLowerCase() === 'https' ) {
+          kind.value = 'HTTPS';
         } else {
-          kind = 'HTTP';
+          kind.value = 'HTTP';
         }
-      } else if ( probe.tcpSocket ) {
-        kind = 'tcp';
+      } else if ( probe.value.tcpSocket ) {
+        kind.value = 'tcp';
       }
     } else {
-      probe = {
+      probe.value = {
         failureThreshold:    3,
         successThreshold:    1,
         initialDelaySeconds: 0,
@@ -77,68 +78,68 @@ export default {
       };
     }
 
-    exec = probe.exec || {};
-    httpGet = probe.httpGet || {};
-    tcpSocket = probe.tcpSocket || {};
+    exec.value = probe.value.exec || {};
+    httpGet.value = probe.value.httpGet || {};
+    tcpSocket.value = probe.value.tcpSocket || {};
 
-    return {
-      probe, kind, exec, httpGet, tcpSocket
-    };
-  },
-
-  computed: {
-    isView() {
-      return this.mode === _VIEW;
-    },
-
-    isNone() {
-      return this.kind === 'none';
-    },
+    const isNone = computed(() => {
+      return kind.value === 'none';
+    });
 
-    kindOptions() {
-      return KINDS.map((k) => {
-        return { label: this.t(`workload.container.healthCheck.kind.${ k }`), value: k };
-      });
-    }
-  },
-
-  watch: {
-    kind() {
-      this.update();
-    }
-  },
-
-  methods: {
-    update() {
-      const probe = this.probe;
-
-      if ( this.isNone ) {
-        this.$emit('update:value', null);
+    const update = () => {
+      if ( isNone.value ) {
+        emit('update:value', null);
 
         return;
       }
 
-      switch ( this.kind ) {
+      switch ( kind.value ) {
       case 'HTTP':
       case 'HTTPS':
-        this.httpGet.scheme = this.kind;
-        probe.httpGet = this.httpGet;
-        probe.tcpSocket = null;
-        probe.exec = null;
+        httpGet.value.scheme = kind.value;
+        probe.value.httpGet = httpGet.value;
+        probe.value.tcpSocket = null;
+        probe.value.exec = null;
         break;
       case 'tcp':
-        probe.httpGet = null;
-        probe.tcpSocket = this.tcpSocket;
-        probe.exec = null;
+        probe.value.httpGet = null;
+        probe.value.tcpSocket = tcpSocket.value;
+        probe.value.exec = null;
         break;
       case 'exec':
-        probe.httpGet = null;
-        probe.tcpSocket = null;
-        probe.exec = this.exec;
+        probe.value.httpGet = null;
+        probe.value.tcpSocket = null;
+        probe.value.exec = exec.value;
         break;
       }
 
-      this.$emit('update:value', probe);
+      emit('update:value', probe.value);
+    };
+
+    watch(kind, () => {
+      update();
+    });
+
+    return {
+      probe,
+      kind,
+      exec,
+      httpGet,
+      tcpSocket,
+      update,
+      isNone,
+    };
+  },
+
+  computed: {
+    isView() {
+      return this.mode === _VIEW;
+    },
+
+    kindOptions() {
+      return KINDS.map((k) => {
+        return { label: this.t(`workload.container.healthCheck.kind.${ k }`), value: k };
+      });
     }
   },
 };
diff --git a/shell/components/form/ResourceQuota/Project.vue b/shell/components/form/ResourceQuota/Project.vue
index e603548a520..3332bf1ce96 100644
--- a/shell/components/form/ResourceQuota/Project.vue
+++ b/shell/components/form/ResourceQuota/Project.vue
@@ -28,11 +28,15 @@ export default {
   },
 
   data() {
+    return { typeValues: null };
+  },
+
+  created() {
     this.value['spec'] = this.value.spec || {};
     this.value.spec['namespaceDefaultResourceQuota'] = this.value.spec.namespaceDefaultResourceQuota || { limit: {} };
     this.value.spec['resourceQuota'] = this.value.spec.resourceQuota || { limit: {} };
 
-    return { typeValues: Object.keys(this.value.spec.resourceQuota.limit) };
+    this.typeValues = Object.keys(this.value.spec.resourceQuota.limit);
   },
 
   computed: { ...QUOTA_COMPUTED },
diff --git a/shell/components/form/ResourceSelector.vue b/shell/components/form/ResourceSelector.vue
index 2a50136ec25..6df5611e086 100644
--- a/shell/components/form/ResourceSelector.vue
+++ b/shell/components/form/ResourceSelector.vue
@@ -45,16 +45,14 @@ export default {
   },
 
   data() {
-    const matchingResources = {
-      matched: 0,
-      matches: [],
-      none:    true,
-      sample:  null,
-      total:   0,
-    };
-
     return {
-      matchingResources,
+      matchingResources: {
+        matched: 0,
+        matches: [],
+        none:    true,
+        sample:  null,
+        total:   0,
+      },
       allResources:        [],
       allResourcesInScope: [],
       tableHeaders:        this.$store.getters['type-map/headersFor'](
diff --git a/shell/components/form/Security.vue b/shell/components/form/Security.vue
index 7d0e901403d..85ef034f79a 100644
--- a/shell/components/form/Security.vue
+++ b/shell/components/form/Security.vue
@@ -5,6 +5,45 @@ import { _VIEW } from '@shell/config/query-params';
 import { mapGetters } from 'vuex';
 import LabeledSelect from '@shell/components/form/LabeledSelect';
 
+const allCapabilities = ['ALL',
+  'AUDIT_CONTROL',
+  'AUDIT_WRITE',
+  'BLOCK_SUSPEND',
+  'CHOWN',
+  'DAC_OVERRIDE',
+  'DAC_READ_SEARCH',
+  'FOWNER',
+  'FSETID',
+  'IPC_LOCK',
+  'IPC_OWNER',
+  'KILL',
+  'LEASE',
+  'LINUX_IMMUTABLE',
+  'MAC_ADMIN',
+  'MAC_OVERRIDE',
+  'MKNOD',
+  'NET_ADMIN',
+  'NET_BIND_SERVICE',
+  'NET_BROADCAST',
+  'NET_RAW',
+  'SETFCAP',
+  'SETGID',
+  'SETPCAP',
+  'SETUID',
+  'SYSLOG',
+  'SYS_ADMIN',
+  'SYS_BOOT',
+  'SYS_CHROOT',
+  'SYS_MODULE',
+  'SYS_NICE',
+  'SYS_PACCT',
+  'SYS_PTRACE',
+  'SYS_RAWIO',
+  'SYS_RESOURCE',
+  'SYS_TIME',
+  'SYS_TTY_CONFIG',
+  'WAKE_ALARM'];
+
 export default {
   emits: ['update:value'],
 
@@ -25,68 +64,27 @@ export default {
   },
 
   data() {
-    const allCapabilities = ['ALL',
-      'AUDIT_CONTROL',
-      'AUDIT_WRITE',
-      'BLOCK_SUSPEND',
-      'CHOWN',
-      'DAC_OVERRIDE',
-      'DAC_READ_SEARCH',
-      'FOWNER',
-      'FSETID',
-      'IPC_LOCK',
-      'IPC_OWNER',
-      'KILL',
-      'LEASE',
-      'LINUX_IMMUTABLE',
-      'MAC_ADMIN',
-      'MAC_OVERRIDE',
-      'MKNOD',
-      'NET_ADMIN',
-      'NET_BIND_SERVICE',
-      'NET_BROADCAST',
-      'NET_RAW',
-      'SETFCAP',
-      'SETGID',
-      'SETPCAP',
-      'SETUID',
-      'SYSLOG',
-      'SYS_ADMIN',
-      'SYS_BOOT',
-      'SYS_CHROOT',
-      'SYS_MODULE',
-      'SYS_NICE',
-      'SYS_PACCT',
-      'SYS_PTRACE',
-      'SYS_RAWIO',
-      'SYS_RESOURCE',
-      'SYS_TIME',
-      'SYS_TTY_CONFIG',
-      'WAKE_ALARM'];
+    return {
+      privileged:               this.value.privileged || false,
+      allowPrivilegeEscalation: this.value.allowPrivilegeEscalation || false,
+      allCapabilities,
+      runAsNonRoot:             this.value.runAsNonRoot || false,
+      readOnlyRootFilesystem:   this.value.readOnlyRootFilesystem || false,
+      add:                      [],
+      drop:                     [],
+      runAsUser:                this.value.runAsUser
+    };
+  },
 
-    const {
-      capabilities = {},
-      runAsNonRoot = false,
-      readOnlyRootFilesystem = false,
-      privileged = false,
-      allowPrivilegeEscalation = false,
-      runAsUser
-    } = this.value;
+  created() {
+    const { capabilities = {} } = this.value;
     const {
       add = [],
       drop = []
     } = capabilities;
 
-    return {
-      privileged,
-      allowPrivilegeEscalation,
-      allCapabilities,
-      runAsNonRoot,
-      readOnlyRootFilesystem,
-      add,
-      drop,
-      runAsUser
-    };
+    this.add = add;
+    this.drop = drop;
   },
 
   computed: {
diff --git a/shell/components/form/ShellInput.vue b/shell/components/form/ShellInput.vue
index 48c1339a42c..8bda13eddf4 100644
--- a/shell/components/form/ShellInput.vue
+++ b/shell/components/form/ShellInput.vue
@@ -21,6 +21,10 @@ export default {
       causes $emit 'input' of ["-c", "sleep 600"]
   */
   data() {
+    return { userValue: '' };
+  },
+
+  created() {
     let userValue = '';
 
     if ( this.value ) {
@@ -36,7 +40,7 @@ export default {
       }, '').trim();
     }
 
-    return { userValue };
+    this.userValue = userValue;
   },
 
   methods: {
diff --git a/shell/components/form/Tolerations.vue b/shell/components/form/Tolerations.vue
index ce9b412e44e..71313d87468 100644
--- a/shell/components/form/Tolerations.vue
+++ b/shell/components/form/Tolerations.vue
@@ -29,6 +29,10 @@ export default {
   },
 
   data() {
+    return { rules: [] };
+  },
+
+  created() {
     const rules = [];
 
     // on creation in agent configuration, the backend "eats"
@@ -49,7 +53,7 @@ export default {
       });
     }
 
-    return { rules };
+    this.rules = rules;
   },
 
   computed: {
diff --git a/shell/components/form/ValueFromResource.vue b/shell/components/form/ValueFromResource.vue
index bbbec61f21e..94a48806e0d 100644
--- a/shell/components/form/ValueFromResource.vue
+++ b/shell/components/form/ValueFromResource.vue
@@ -4,6 +4,7 @@ import { get } from '@shell/utils/object';
 import { _VIEW } from '@shell/config/query-params';
 import LabeledSelect from '@shell/components/form/LabeledSelect';
 import { LabeledInput } from '@components/Form/LabeledInput';
+import { ref, toRef, watch } from 'vue';
 
 export default {
   emits: ['update:value', 'remove'],
@@ -44,84 +45,164 @@ export default {
   },
 
   data() {
-    const typeOpts = [
-      { value: 'simple', label: 'Key/Value Pair' },
-      { value: 'resourceFieldRef', label: 'Resource' },
-      { value: 'configMapKeyRef', label: 'ConfigMap Key' },
-      { value: 'secretKeyRef', label: 'Secret key' },
-      { value: 'fieldRef', label: 'Pod Field' },
-      { value: 'secretRef', label: 'Secret' },
-      { value: 'configMapRef', label: 'ConfigMap' },
-    ];
-
-    const resourceKeyOpts = ['limits.cpu', 'limits.ephemeral-storage', 'limits.memory', 'requests.cpu', 'requests.ephemeral-storage', 'requests.memory'];
-    let type;
-
-    if (this.value.secretRef) {
-      type = 'secretRef';
-    } else if (this.value.configMapRef) {
-      type = 'configMapRef';
-    } else if (this.value.value) {
-      type = 'simple';
-    } else if (this.value.valueFrom) {
-      type = Object.keys((this.value.valueFrom))[0] || 'simple';
+    return {
+      typeOpts: [
+        { value: 'simple', label: 'Key/Value Pair' },
+        { value: 'resourceFieldRef', label: 'Resource' },
+        { value: 'configMapKeyRef', label: 'ConfigMap Key' },
+        { value: 'secretKeyRef', label: 'Secret key' },
+        { value: 'fieldRef', label: 'Pod Field' },
+        { value: 'secretRef', label: 'Secret' },
+        { value: 'configMapRef', label: 'ConfigMap' },
+      ],
+      secrets:         this.allSecrets,
+      resourceKeyOpts: ['limits.cpu', 'limits.ephemeral-storage', 'limits.memory', 'requests.cpu', 'requests.ephemeral-storage', 'requests.memory'],
+    };
+  },
+
+  setup(props, { emit }) {
+    const type = ref(null);
+
+    if (props.value.secretRef) {
+      type.value = 'secretRef';
+    } else if (props.value.configMapRef) {
+      type.value = 'configMapRef';
+    } else if (props.value.value) {
+      type.value = 'simple';
+    } else if (props.value.valueFrom) {
+      type.value = Object.keys((props.value.valueFrom))[0] || 'simple';
     }
 
-    let refName;
-    let name;
-    let fieldPath;
-    let referenced;
-    let key;
-    let valStr;
-    const keys = [];
+    const refName = ref('');
+    const name = ref('');
+    const fieldPath = ref('');
+    const referenced = ref(null);
+    const key = ref(null);
+    const valStr = ref('');
+    const keys = ref([]);
 
-    switch (type) {
+    switch (type.value) {
     case 'resourceFieldRef':
-      name = this.value.name;
-      refName = this.value.valueFrom[type].containerName;
-      key = this.value.valueFrom[type].resource || '';
+      name.value = toRef(props.value.name);
+      refName.value = toRef(props.value.valueFrom[type.value].containerName);
+      key.value = props.value.valueFrom[type.value].resource || '';
       break;
     case 'configMapKeyRef':
-      name = this.value.name;
-      key = this.value.valueFrom[type].key || '';
-      refName = this.value.valueFrom[type].name;
-      referenced = this.allConfigMaps.filter((resource) => {
-        return resource.metadata.name === refName;
+      name.value = toRef(props.value.name);
+      key.value = props.value.valueFrom[type.value].key || '';
+      refName.value = toRef(props.value.valueFrom[type.value].name);
+      referenced.value = props.allConfigMaps.filter((resource) => {
+        return resource.metadata.name === refName.value;
       })[0];
-      if (referenced && referenced.data) {
-        keys.push(...Object.keys(referenced.data));
+      if (referenced.value && referenced.value.data) {
+        keys.value.push(...Object.keys(referenced.value.data));
       }
       break;
     case 'secretRef':
     case 'configMapRef':
-      name = this.value.prefix;
-      refName = this.value[type].name;
+      name.value = toRef(props.value.prefix);
+      refName.value = toRef(props.value[type.value].name);
       break;
     case 'secretKeyRef':
-      name = this.value.name;
-      key = this.value.valueFrom[type].key || '';
-      refName = this.value.valueFrom[type].name;
-      referenced = this.allSecrets.filter((resource) => {
-        return resource.metadata.name === refName;
+      name.value = toRef(props.value.name);
+      key.value = props.value.valueFrom[type.value].key || '';
+      refName.value = toRef(props.value.valueFrom[type.value].name);
+      referenced.value = props.allSecrets.filter((resource) => {
+        return resource.metadata.name === refName.value;
       })[0];
-      if (referenced && referenced.data) {
-        keys.push(...Object.keys(referenced.data));
+      if (referenced.value && referenced.value.data) {
+        keys.value.push(...Object.keys(referenced.value.data));
       }
       break;
     case 'fieldRef':
-      fieldPath = get(this.value.valueFrom, `${ type }.fieldPath`) || '';
-      name = this.value.name;
+      fieldPath.value = get(props.value.valueFrom, `${ type.value }.fieldPath`) || '';
+      name.value = toRef(props.value.name);
       break;
     default:
-      name = this.value.name;
-      valStr = this.value.value;
+      name.value = toRef(props.value.name);
+      valStr.value = toRef(props.value.value);
       break;
     }
 
+    referenced.value = refName.value;
+
+    const updateRow = () => {
+      if (!name.value?.length && !refName.value?.length) {
+        if (type.value !== 'fieldRef') {
+          emit('update:value', null);
+
+          return;
+        }
+      }
+      let out = { name: name.value || refName.value };
+
+      switch (type.value) {
+      case 'configMapKeyRef':
+      case 'secretKeyRef':
+        out.valueFrom = {
+          [type.value]: {
+            key: key.value, name: refName.value, optional: false
+          }
+        };
+        break;
+      case 'resourceFieldRef':
+        out.valueFrom = {
+          [type.value]: {
+            containerName: refName.value, divisor: 1, resource: key.value
+          }
+        };
+        break;
+      case 'fieldRef':
+        if (!fieldPath.value || !fieldPath.value.length) {
+          out = null; break;
+        }
+        out.valueFrom = { [type.value]: { apiVersion: 'v1', fieldPath: fieldPath.value } };
+        break;
+      case 'simple':
+        out.value = valStr.value;
+        break;
+      default:
+        delete out.name;
+        out.prefix = name.value;
+        out[type.value] = { name: refName.value, optional: false };
+      }
+      emit('update:value', out);
+    };
+
+    watch(type, () => {
+      referenced.value = null;
+      key.value = '';
+      refName.value = '';
+      keys.value = [];
+      key.value = '';
+      valStr.value = '';
+      fieldPath.value = '';
+    });
+
+    watch(referenced, (neu, old) => {
+      if (neu) {
+        if ((neu.type === SECRET || neu.type === CONFIG_MAP) && neu.data) {
+          keys.value = Object.keys(neu.data);
+        }
+        refName.value = neu?.metadata?.name;
+      }
+      updateRow();
+    });
+
     return {
-      typeOpts, type, refName, referenced: refName, secrets: this.allSecrets, keys, key, fieldPath, name, resourceKeyOpts, valStr
+      type,
+      refName,
+      referenced,
+      keys,
+      key,
+      fieldPath,
+      name,
+      valStr,
+      updateRow,
+      get,
     };
   },
+
   computed: {
     isView() {
       return this.mode === _VIEW;
@@ -189,74 +270,6 @@ export default {
       return ['resourceFieldRef', 'configMapKeyRef', 'secretKeyRef'].includes(this.type);
     },
   },
-
-  watch: {
-    type() {
-      this.referenced = null;
-      this.key = '';
-      this.refName = '';
-      this.keys = [];
-      this.key = '';
-      this.valStr = '';
-      this.fieldPath = '';
-    },
-
-    referenced(neu, old) {
-      if (neu) {
-        if ((neu.type === SECRET || neu.type === CONFIG_MAP) && neu.data) {
-          this.keys = Object.keys(neu.data);
-        }
-        this.refName = neu?.metadata?.name;
-      }
-      this.updateRow();
-    },
-  },
-
-  methods: {
-    updateRow() {
-      if (!this.name?.length && !this.refName?.length) {
-        if (this.type !== 'fieldRef') {
-          this.$emit('update:value', null);
-
-          return;
-        }
-      }
-      let out = { name: this.name || this.refName };
-
-      switch (this.type) {
-      case 'configMapKeyRef':
-      case 'secretKeyRef':
-        out.valueFrom = {
-          [this.type]: {
-            key: this.key, name: this.refName, optional: false
-          }
-        };
-        break;
-      case 'resourceFieldRef':
-        out.valueFrom = {
-          [this.type]: {
-            containerName: this.refName, divisor: 1, resource: this.key
-          }
-        };
-        break;
-      case 'fieldRef':
-        if (!this.fieldPath || !this.fieldPath.length) {
-          out = null; break;
-        }
-        out.valueFrom = { [this.type]: { apiVersion: 'v1', fieldPath: this.fieldPath } };
-        break;
-      case 'simple':
-        out.value = this.valStr;
-        break;
-      default:
-        delete out.name;
-        out.prefix = this.name;
-        out[this.type] = { name: this.refName, optional: false };
-      }
-      this.$emit('update:value', out);
-    },
-    get
-  }
 };
 </script>
 
diff --git a/shell/components/form/WorkloadPorts.vue b/shell/components/form/WorkloadPorts.vue
index 2ba7a98a020..4d35b10999a 100644
--- a/shell/components/form/WorkloadPorts.vue
+++ b/shell/components/form/WorkloadPorts.vue
@@ -44,25 +44,9 @@ export default {
   },
 
   data() {
-    const rows = clone(this.value || []).map((row) => {
-      row._showHost = false;
-      row._serviceType = row._serviceType || '';
-      row._name = row.name ? `${ row.name }` : `${ row.containerPort }${ row.protocol.toLowerCase() }${ row.hostPort || row._listeningPort || '' }`;
-      if (row.hostPort || row.hostIP) {
-        row._showHost = true;
-      }
-
-      row._ipam = '';
-
-      return row;
-    });
-
-    // show host port column if existing port data has any host ports defined
-    const showHostPorts = !!rows.some((row) => !!row.hostPort);
-
     return {
-      rows,
-      showHostPorts,
+      rows:                [],
+      showHostPorts:       false,
       workloadPortOptions: ['TCP', 'UDP']
     };
   },
@@ -168,6 +152,22 @@ export default {
   },
 
   created() {
+    const rows = clone(this.value || []).map((row) => {
+      row._showHost = false;
+      row._serviceType = row._serviceType || '';
+      row._name = row.name ? `${ row.name }` : `${ row.containerPort }${ row.protocol.toLowerCase() }${ row.hostPort || row._listeningPort || '' }`;
+      if (row.hostPort || row.hostIP) {
+        row._showHost = true;
+      }
+
+      row._ipam = '';
+
+      return row;
+    });
+
+    this.rows = rows;
+    // show host port column if existing port data has any host ports defined
+    this.showHostPorts = !!rows.some((row) => !!row.hostPort);
     this.queueUpdate = debounce(this.update, 500);
     this.rows.map((row) => {
       this.setServiceType(row);
diff --git a/shell/components/form/__tests__/ArrayList.test.ts b/shell/components/form/__tests__/ArrayList.test.ts
index e5c979d3096..8ff69ba168b 100644
--- a/shell/components/form/__tests__/ArrayList.test.ts
+++ b/shell/components/form/__tests__/ArrayList.test.ts
@@ -4,6 +4,8 @@ import { _EDIT, _VIEW } from '@shell/config/query-params';
 import { ExtendedVue, Vue } from 'vue/types/vue';
 import { DefaultProps } from 'vue/types/options';
 
+jest.mock('lodash/debounce', () => jest.fn((fn) => fn));
+
 describe('the ArrayList', () => {
   it('is empty', () => {
     const wrapper = mount(ArrayList, {
@@ -62,6 +64,7 @@ describe('the ArrayList', () => {
 
     expect(wrapper.find('[data-testid="remove-item-2"]').exists()).toBe(false);
     expect((wrapper.emitted('remove')![0][0] as any).row.value).toStrictEqual('string 1');
+    expect(wrapper.vm.rows).toStrictEqual([{ value: 'string 0' }, { value: 'string 2' }]);
     expect(wrapper.emitted('update:value')![0][0]).toStrictEqual(['string 0', 'string 2']);
   });
 
diff --git a/shell/components/form/__tests__/MatchExpressions.test.ts b/shell/components/form/__tests__/MatchExpressions.test.ts
index 50fe4824220..2bd10753957 100644
--- a/shell/components/form/__tests__/MatchExpressions.test.ts
+++ b/shell/components/form/__tests__/MatchExpressions.test.ts
@@ -6,9 +6,9 @@ import { nextTick } from 'vue';
 describe('component: MatchExpressions', () => {
   it('should display all the inputs', () => {
     const wrapper = mount(MatchExpressions, {
-      props: { mode: _CREATE },
-      data:  () => ({
-        rules: [
+      props: {
+        mode:  _CREATE,
+        value: [
           {
             id:       '123',
             key:      '123',
@@ -16,7 +16,7 @@ describe('component: MatchExpressions', () => {
             values:   '123'
           }
         ]
-      })
+      },
     });
 
     const inputWraps = wrapper.findAll('[data-testid^=input-match-expression-]');
@@ -29,9 +29,9 @@ describe('component: MatchExpressions', () => {
     'values',
   ])('should emit an update on %p input', async(field) => {
     const wrapper = mount(MatchExpressions, {
-      props: { mode: _CREATE },
-      data:  () => ({
-        rules: [
+      props: {
+        mode:  _CREATE,
+        value: [
           {
             id:       '123',
             key:      '123',
@@ -39,7 +39,7 @@ describe('component: MatchExpressions', () => {
             values:   '123'
           }
         ]
-      })
+      },
     });
     const input = wrapper.find(`[data-testid="input-match-expression-${ field }-0"]`).find('input');
     const newValue = 123;
@@ -54,9 +54,9 @@ describe('component: MatchExpressions', () => {
     'operator',
   ])('should emit an update on %p selection change', async(field) => {
     const wrapper = mount(MatchExpressions, {
-      props: { mode: _CREATE },
-      data:  () => ({
-        rules: [
+      props: {
+        mode:  _CREATE,
+        value: [
           {
             id:       '123',
             key:      '123',
@@ -64,7 +64,7 @@ describe('component: MatchExpressions', () => {
             values:   '123'
           }
         ]
-      })
+      },
     });
 
     const select = wrapper.find(`[data-testid="input-match-expression-${ field }-0"]`);
diff --git a/shell/components/form/__tests__/NameNsDescription.test.ts b/shell/components/form/__tests__/NameNsDescription.test.ts
index c11b38cdb31..05eb4e8f541 100644
--- a/shell/components/form/__tests__/NameNsDescription.test.ts
+++ b/shell/components/form/__tests__/NameNsDescription.test.ts
@@ -1,10 +1,18 @@
 import { mount } from '@vue/test-utils';
 import NameNsDescription from '@shell/components/form/NameNsDescription.vue';
+import { createStore } from 'vuex';
 
 describe('component: NameNsDescription', () => {
   // Accessing to computed value due code complexity
   it('should map namespaces to options', () => {
     const namespaceName = 'test';
+    const store = createStore({
+      getters: {
+        allowedNamespaces:   () => () => ({ [namespaceName]: true }),
+        currentStore:        () => () => 'cluster',
+        'cluster/schemaFor': () => jest.fn()
+      }
+    });
     const result = [
       {
         label: namespaceName,
@@ -13,20 +21,21 @@ describe('component: NameNsDescription', () => {
     ];
     const wrapper = mount(NameNsDescription, {
       props: {
-        value:   {},
+        value: {
+          setAnnotation: jest.fn(),
+          metadata:      {}
+        },
         mode:    'create',
         cluster: {},
       },
       global: {
-        mocks: {
+        provide: { store },
+        mocks:   {
           $store: {
             dispatch: jest.fn(),
             getters:  {
-              namespaces:          jest.fn(),
-              allowedNamespaces:   () => ({ [namespaceName]: true }),
-              currentStore:        () => 'cluster',
-              'cluster/schemaFor': jest.fn(),
-              'i18n/t':            jest.fn(),
+              namespaces: jest.fn(),
+              'i18n/t':   jest.fn(),
             },
           },
         },
@@ -38,27 +47,35 @@ describe('component: NameNsDescription', () => {
 
   it('should emit in case of new namespace', () => {
     const namespaceName = 'test';
+    const store = createStore({
+      getters: {
+        allowedNamespaces:   () => () => ({ [namespaceName]: true }),
+        currentStore:        () => () => 'cluster',
+        'cluster/schemaFor': () => jest.fn()
+      }
+    });
     const newNamespaceName = 'bananas';
     const wrapper = mount(NameNsDescription, {
       props: {
-        value: { metadata: {} },
-        mode:  'create',
+        value: {
+          setAnnotation: jest.fn(),
+          metadata:      {}
+        },
+        mode: 'create',
       },
       global: {
-        mocks: {
+        provide: { store },
+        mocks:   {
           $store: {
             dispatch: jest.fn(),
             getters:  {
               namespaces:                         jest.fn(),
-              allowedNamespaces:                  () => ({ [namespaceName]: true }),
               'customizations/getPreviewCluster': {
                 ready:   true,
                 isLocal: false,
                 badge:   {},
               },
-              currentStore:        () => 'cluster',
-              'cluster/schemaFor': jest.fn(),
-              'i18n/t':            jest.fn(),
+              'i18n/t': jest.fn(),
             },
           },
         },
diff --git a/shell/components/form/__tests__/Probe.test.ts b/shell/components/form/__tests__/Probe.test.ts
index 0cfb7638cbe..8827f32740c 100644
--- a/shell/components/form/__tests__/Probe.test.ts
+++ b/shell/components/form/__tests__/Probe.test.ts
@@ -6,18 +6,20 @@ import { DefaultProps } from 'vue/types/options';
 
 describe('component: Probe', () => {
   describe.each([
-    ['HTTPS', ['port', 'path']],
-    ['tcp', ['socket']],
-    ['exec', ['command']],
-  ])('given kind %p', (kind, extraFields) => {
+    [{ httpGet: { scheme: 'https' } }, ['port', 'path']],
+    [{ tcpSocket: {} }, ['socket']],
+    [{ exec: {} }, ['command']],
+  ])('given kind %p', (value, extraFields) => {
     it.each([
       ...extraFields,
       'successThreshold',
       'failureThreshold',
     ])('should emit an update on %p input', (field) => {
       const wrapper = mount(Probe as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, {
-        props: { mode: _EDIT },
-        data:  () => ({ kind })
+        props: {
+          mode: _EDIT,
+          value,
+        },
       });
       const input = wrapper.find(`[data-testid="input-probe-${ field }"]`).find('input');
       const newValue = 123;
@@ -33,8 +35,10 @@ describe('component: Probe', () => {
       'timeoutSeconds',
     ])('should emit an update on %p input and blur', (field) => {
       const wrapper = mount(Probe as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, {
-        props: { mode: _EDIT },
-        data:  () => ({ kind })
+        props: {
+          mode: _EDIT,
+          value
+        },
       });
       const input = wrapper.find(`[data-testid="input-probe-${ field }"]`).find('input');
       const newValue = 123;
diff --git a/shell/edit/__tests__/monitoring.coreos.com.prometheusrule.test.ts b/shell/edit/__tests__/monitoring.coreos.com.prometheusrule.test.ts
index dd741b5cb0b..09f8b1635aa 100644
--- a/shell/edit/__tests__/monitoring.coreos.com.prometheusrule.test.ts
+++ b/shell/edit/__tests__/monitoring.coreos.com.prometheusrule.test.ts
@@ -2,14 +2,23 @@ import { mount } from '@vue/test-utils';
 import FormValidation from '@shell/mixins/form-validation';
 import Monitoring from '@shell/edit/monitoring.coreos.com.prometheusrule/index.vue';
 import { _EDIT } from '@shell/config/query-params';
+import { createStore } from 'vuex';
 
 describe('edit: management.cattle.io.setting should', () => {
   const MOCKED_ERRORS = ['error1', 'error2', 'error3', 'error4', 'error5'];
   const ERROR_BANNER_SELECTOR = '[data-testid="banner-close"]';
+  const store = createStore({
+    getters: {
+      namespaces:                () => () => ({}),
+      currentStore:              () => () => 'current_store',
+      'current_store/schemaFor': () => jest.fn()
+    }
+  });
   const requiredSetup = () => ({
     // Remove all these mocks after migration to Vue 2.7/3 due mixin logic
     global: {
-      mocks: {
+      provide: { store },
+      mocks:   {
         $store: {
           dispatch: jest.fn(),
           getters:  {
@@ -35,8 +44,12 @@ describe('edit: management.cattle.io.setting should', () => {
         canYaml:  false,
         mode:     _EDIT,
         resource: {},
-        value:    { value: 'anything' },
-        name:     ''
+        value:    {
+          setAnnotation: jest.fn(),
+          value:         'anything',
+          metadata:      {},
+        },
+        name: ''
       },
       ...requiredSetup()
     });
diff --git a/shell/edit/logging.banzaicloud.io.output/__tests__/logging.banzaicloud.io.output.test.ts b/shell/edit/logging.banzaicloud.io.output/__tests__/logging.banzaicloud.io.output.test.ts
index 0cd668b1b9f..37ff8c346e0 100644
--- a/shell/edit/logging.banzaicloud.io.output/__tests__/logging.banzaicloud.io.output.test.ts
+++ b/shell/edit/logging.banzaicloud.io.output/__tests__/logging.banzaicloud.io.output.test.ts
@@ -1,5 +1,6 @@
 import { mount } from '@vue/test-utils';
 import Banzai from '@shell/edit/logging.banzaicloud.io.output/index.vue';
+import { createStore } from 'vuex';
 
 const outputSchema = {
   id:    'logging.banzaicloud.io.output',
@@ -109,16 +110,26 @@ describe('view: logging.banzaicloud.io.output', () => {
     ['http://localhost:3100', []],
     ['not a proper URL', ['logging.loki.urlInvalid']],
   ])('should validate Loki URL on save', (url, expectation) => {
+    const store = createStore({
+      getters: {
+        namespaces:          () => () => ({}),
+        currentStore:        () => () => 'cluster',
+        'cluster/schemaFor': () => jest.fn()
+      }
+    });
     const wrapper = mount(Banzai, {
       data:  () => ({ selectedProvider: 'loki' }),
       props: {
         value: {
-          save: jest.fn(),
-          spec: { loki: { url } }
+          save:          jest.fn(),
+          setAnnotation: jest.fn(),
+          spec:          { loki: { url } },
+          metadata:      {},
         }
       },
       global: {
-        mocks: {
+        provide: { store },
+        mocks:   {
           $fetchState: { pending: false },
           $store:      {
             dispatch: jest.fn(),
@@ -149,16 +160,26 @@ describe('view: logging.banzaicloud.io.output', () => {
   });
 
   it('should load the default YAML data for output buffer config (from schema) in a CREATE scenario', async() => {
+    const store = createStore({
+      getters: {
+        namespaces:          () => () => ({}),
+        currentStore:        () => () => 'cluster',
+        'cluster/schemaFor': () => jest.fn()
+      }
+    });
     const wrapper = mount(Banzai, {
       data:  () => ({ selectedProvider: 'awsElasticsearch' }),
       props: {
         value: {
-          save: jest.fn(),
-          spec: {}
+          save:          jest.fn(),
+          setAnnotation: jest.fn(),
+          spec:          {},
+          metadata:      {},
         }
       },
       global: {
-        mocks: {
+        provide: { store },
+        mocks:   {
           $fetchState: { pending: false },
           $store:      {
             dispatch(arg: any) {
@@ -230,16 +251,26 @@ describe('view: logging.banzaicloud.io.output', () => {
   });
 
   it('should load current output buffer config in an EDIT scenario', async() => {
+    const store = createStore({
+      getters: {
+        namespaces:          () => () => ({}),
+        currentStore:        () => () => 'cluster',
+        'cluster/schemaFor': () => jest.fn()
+      }
+    });
     const wrapper = mount(Banzai, {
       data:  () => ({ selectedProvider: 'awsElasticsearch' }),
       props: {
         value: {
-          save: jest.fn(),
-          spec: { awsElasticsearch: { buffer: '#chunk_limit_records: int' } }
+          save:          jest.fn(),
+          setAnnotation: jest.fn(),
+          spec:          { awsElasticsearch: { buffer: '#chunk_limit_records: int' } },
+          metadata:      {}
         }
       },
       global: {
-        mocks: {
+        provide: { store },
+        mocks:   {
           $fetchState: { pending: false },
           $store:      {
             dispatch(arg: any) {
diff --git a/shell/edit/persistentvolume/__tests__/persistentvolume.test.ts b/shell/edit/persistentvolume/__tests__/persistentvolume.test.ts
index 2a30867f980..98046154598 100644
--- a/shell/edit/persistentvolume/__tests__/persistentvolume.test.ts
+++ b/shell/edit/persistentvolume/__tests__/persistentvolume.test.ts
@@ -9,7 +9,13 @@ describe('view: PersistentVolume', () => {
     };
     const resource = 'PersistentVolume';
     const wrapper = mount(PersistentVolume as ExtendedVue<Vue, {}, {}, {}, PersistentVolume>, {
-      props: { value: { spec: { } } },
+      props: {
+        value: {
+          setAnnotation: jest.fn(),
+          spec:          {},
+          metadata:      {},
+        }
+      },
 
       global: {
         mocks: {
@@ -54,7 +60,13 @@ describe('view: PersistentVolume', () => {
     const plugin = 'csi';
     const resource = 'PersistentVolume';
     const wrapper = mount(PersistentVolume as ExtendedVue<Vue, {}, {}, {}, PersistentVolume>, {
-      props:  { value: { spec: { [plugin]: { value: plugin } } } },
+      props: {
+        value: {
+          setAnnotation: jest.fn(),
+          spec:          { [plugin]: { value: plugin } },
+          metadata:      {},
+        }
+      },
       global: {
         mocks: {
           $store: {


Reviews:
user: login: rak-phillip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T21:33:12Z
user: login: cnotv
body: Thanks for this PR, it covers some cases which were easily prone to bugs, which I encountered once in `Principal` recently (with a prop change in my case).

Usually would be nicer to have an automation to catch all these cases, as well as to prevent the pattern to occur again in the repository.
As alternative I would recommend to present the caused or possible issues related to this change.

I gave it 3 times a look to it, including by file and by commit. It does not seems to have any relevant issue blocking this PR.

I have added some notes mainly related to the tests. Feel free to discuss or resolve them. I already approve with the assumption things have been covered and are secondary.

LGTM
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-25T16:42:44Z
user: login: codyrancher
body: I know some of our code is now using the `<script setup>` tag. By any chance do we want to update these to make use of the same for consistency?
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-25T23:02:28Z
user: login: rak-phillip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:16:28Z
user: login: rak-phillip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:19:18Z
user: login: rak-phillip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:19:40Z
user: login: rak-phillip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:35:15Z
user: login: rak-phillip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:43:52Z
user: login: rak-phillip
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:45:16Z
user: login: cnotv
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T22:05:10Z
user: login: cnotv
body: LGTM
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-26T22:06:52Z
user: login: cnotv
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T23:22:43Z
user: login: codyrancher
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T22:02:23Z

Review comments:
diff_hunk: @@ -43,23 +43,14 @@ export default {
   },
 
   data() {
-    const {
-      command,
-      args,
-      workingDir,
-      stdin = false,
-      stdinOnce = false,
-      tty = false,
-    } = this.value;
-
     return {
-      args,
-      command,
+      args:           this.value.args,
+      command:        this.value.command,
       commandOptions: ['No', 'Once', 'Yes'],
-      stdin,
-      stdinOnce,
-      tty,
-      workingDir,
+      stdin:          this.value.stdin || false,
+      stdinOnce:      this.value.stdin || false,
+      tty:            this.value.tty || false,
+      workingDir:     this.value.workingDir,
     };
path: shell/components/form/Command.vue
user: login: rak-phillip
body: I don't think that this pattern is dangerous with regards to the issue as it is defined, but I opted to make refactors like this to simplify data prop usage and maintain consistency in our components.
created_at: 2025-03-18T21:32:30Z
author_association: MEMBER
diff_hunk: @@ -68,7 +68,18 @@ jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => (key) => key }))
 // eslint-disable-next-line no-console
 jest.spyOn(console, 'warn').mockImplementation((warning) => warning.includes('[Vue warn]') ? null : console.log(warning));
 
-jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => (key) => key }));
+// jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => (key) => key }));
+jest.mock('@shell/composables/useI18n', () => {
+  return {
+    useI18n() {
+      return {
+        t(key) {
path: jest.setup.js
user: login: cnotv
body: I think we should avoid the translation, since it has a separated test plus it would turn every test into integrations tests.
created_at: 2025-03-24T18:23:26Z
author_association: MEMBER
diff_hunk: @@ -38,27 +47,35 @@ describe('component: NameNsDescription', () => {
 
   it('should emit in case of new namespace', () => {
     const namespaceName = 'test';
+    const store = createStore({
+      getters: {
+        allowedNamespaces:   () => () => ({ [namespaceName]: true }),
+        currentStore:        () => () => 'cluster',
+        'cluster/schemaFor': () => jest.fn()
+      }
+    });
     const newNamespaceName = 'bananas';
     const wrapper = mount(NameNsDescription, {
       props: {
-        value: { metadata: {} },
-        mode:  'create',
+        value: {
+          setAnnotation: jest.fn(),
+          metadata:      {}
+        },
+        mode: 'create',
       },
       global: {
-        mocks: {
+        provide: { store },
path: shell/components/form/__tests__/NameNsDescription.test.ts
user: login: cnotv
body: We have 2 store definitions here, one provided and one mocked. Should we not stick to one only?
created_at: 2025-03-25T11:19:18Z
author_association: MEMBER
diff_hunk: @@ -2,14 +2,23 @@ import { mount } from '@vue/test-utils';
 import FormValidation from '@shell/mixins/form-validation';
 import Monitoring from '@shell/edit/monitoring.coreos.com.prometheusrule/index.vue';
 import { _EDIT } from '@shell/config/query-params';
+import { createStore } from 'vuex';
 
 describe('edit: management.cattle.io.setting should', () => {
   const MOCKED_ERRORS = ['error1', 'error2', 'error3', 'error4', 'error5'];
   const ERROR_BANNER_SELECTOR = '[data-testid="banner-close"]';
+  const store = createStore({
+    getters: {
+      namespaces:                () => () => ({}),
+      currentStore:              () => () => 'current_store',
+      'current_store/schemaFor': () => jest.fn()
+    }
+  });
   const requiredSetup = () => ({
     // Remove all these mocks after migration to Vue 2.7/3 due mixin logic
     global: {
-      mocks: {
+      provide: { store },
path: shell/edit/__tests__/monitoring.coreos.com.prometheusrule.test.ts
user: login: cnotv
body: As for the other case above, two patterns overlap when testing the state.
If you want to use a computed value from the store, you can mock the computed value directly.
created_at: 2025-03-25T11:20:46Z
author_association: MEMBER
diff_hunk: @@ -4,6 +4,8 @@ import { _EDIT, _VIEW } from '@shell/config/query-params';
 import { ExtendedVue, Vue } from 'vue/types/vue';
 import { DefaultProps } from 'vue/types/options';
 
+jest.mock('lodash/debounce', () => jest.fn((fn) => fn));
path: shell/components/form/__tests__/ArrayList.test.ts
user: login: cnotv
body: Seems like after mocking this we have 2 event emissions each time. Should this not happen and maybe create an issue for it?
created_at: 2025-03-25T15:30:46Z
author_association: MEMBER
diff_hunk: @@ -13,7 +13,10 @@ describe('component: NameNsDescription', () => {
     ];
     const wrapper = mount(NameNsDescription, {
       props: {
-        value:   {},
+        value: {
path: shell/components/form/__tests__/NameNsDescription.test.ts
user: login: cnotv
body: I see in many tests there's a lot of added data. Are these values now required in the UI? We should not add unnecessary values as they are confusing.

For instance `metadata` has a fallback if not defined, so we should handle with such case.
created_at: 2025-03-25T15:33:17Z
author_association: MEMBER
diff_hunk: @@ -92,7 +92,7 @@ describe('the ArrayList', () => {
 
       wrapper.vm.onPaste(0, event);
 
-      expect(wrapper.emitted('update:value')?.[1][0]).toStrictEqual(expectation);
+      expect(wrapper.emitted('update:value')?.[0][0]).toStrictEqual(expectation);
path: shell/components/form/__tests__/ArrayList.test.ts
user: login: cnotv
body: Does this mean we do not need the debounce anymore?
created_at: 2025-03-25T16:29:14Z
author_association: MEMBER
diff_hunk: @@ -169,106 +172,97 @@ export default {
   },
 
   data() {
-    return {
-      namespace:       '',
-      name:            '',
-      description:     '',
-      createNamespace: false,
-    };
+    return { createNamespace: false };
   },
 
-  created() {
-    const v = this.value;
-    const metadata = v.metadata;
-    let namespace, name, description;
-
-    if (this.nameKey) {
-      name = get(v, this.nameKey);
-    } else {
-      name = metadata?.name;
-    }
+  setup(props, { emit }) {
path: shell/components/form/NameNsDescription.vue
user: login: cnotv
body: May I recommend adding some code comments in a long code for the next time, considering the efforts to understand all the logic?
I have been struggling several times to code or review PRs for this very reason.

Kudos to you for refactoring this btw 🍺 
created_at: 2025-03-25T16:35:08Z
author_association: MEMBER
diff_hunk: @@ -68,7 +68,18 @@ jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => (key) => key }))
 // eslint-disable-next-line no-console
 jest.spyOn(console, 'warn').mockImplementation((warning) => warning.includes('[Vue warn]') ? null : console.log(warning));
 
-jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => (key) => key }));
+// jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => (key) => key }));
+jest.mock('@shell/composables/useI18n', () => {
+  return {
+    useI18n() {
+      return {
+        t(key) {
path: jest.setup.js
user: login: rak-phillip
body: We do avoid the translation - this change is fixing the mock for the `useI18n` composable so that it matches the global mock of `t` for the options api.
created_at: 2025-03-26T21:16:28Z
author_association: MEMBER
diff_hunk: @@ -38,27 +47,35 @@ describe('component: NameNsDescription', () => {
 
   it('should emit in case of new namespace', () => {
     const namespaceName = 'test';
+    const store = createStore({
+      getters: {
+        allowedNamespaces:   () => () => ({ [namespaceName]: true }),
+        currentStore:        () => () => 'cluster',
+        'cluster/schemaFor': () => jest.fn()
+      }
+    });
     const newNamespaceName = 'bananas';
     const wrapper = mount(NameNsDescription, {
       props: {
-        value: { metadata: {} },
-        mode:  'create',
+        value: {
+          setAnnotation: jest.fn(),
+          metadata:      {}
+        },
+        mode: 'create',
       },
       global: {
-        mocks: {
+        provide: { store },
path: shell/components/form/__tests__/NameNsDescription.test.ts
user: login: rak-phillip
body: The `provide` implementation utilizes `createStore`, which allows us to access the store from the `setup()` function[^1]

[^1]: https://test-utils.vuejs.org/guide/advanced/vuex.html#Example-for-providing-the-unkeyed-useStore
created_at: 2025-03-26T21:19:18Z
author_association: MEMBER
diff_hunk: @@ -2,14 +2,23 @@ import { mount } from '@vue/test-utils';
 import FormValidation from '@shell/mixins/form-validation';
 import Monitoring from '@shell/edit/monitoring.coreos.com.prometheusrule/index.vue';
 import { _EDIT } from '@shell/config/query-params';
+import { createStore } from 'vuex';
 
 describe('edit: management.cattle.io.setting should', () => {
   const MOCKED_ERRORS = ['error1', 'error2', 'error3', 'error4', 'error5'];
   const ERROR_BANNER_SELECTOR = '[data-testid="banner-close"]';
+  const store = createStore({
+    getters: {
+      namespaces:                () => () => ({}),
+      currentStore:              () => () => 'current_store',
+      'current_store/schemaFor': () => jest.fn()
+    }
+  });
   const requiredSetup = () => ({
     // Remove all these mocks after migration to Vue 2.7/3 due mixin logic
     global: {
-      mocks: {
+      provide: { store },
path: shell/edit/__tests__/monitoring.coreos.com.prometheusrule.test.ts
user: login: rak-phillip
body: See https://github.com/rancher/dashboard/pull/13703#discussion_r2015013412
created_at: 2025-03-26T21:19:39Z
author_association: MEMBER
diff_hunk: @@ -4,6 +4,8 @@ import { _EDIT, _VIEW } from '@shell/config/query-params';
 import { ExtendedVue, Vue } from 'vue/types/vue';
 import { DefaultProps } from 'vue/types/options';
 
+jest.mock('lodash/debounce', () => jest.fn((fn) => fn));
path: shell/components/form/__tests__/ArrayList.test.ts
user: login: rak-phillip
body: This is a good catch. The mock is is fine, what's happened is that the refactor has altered the behavior by triggering the watcher during initialization of the ArrayList component. This has been fixed with 5932cd9b7ee8eed3f5366769c157b06f163afb4a
created_at: 2025-03-26T21:35:14Z
author_association: MEMBER
diff_hunk: @@ -13,7 +13,10 @@ describe('component: NameNsDescription', () => {
     ];
     const wrapper = mount(NameNsDescription, {
       props: {
-        value:   {},
+        value: {
path: shell/components/form/__tests__/NameNsDescription.test.ts
user: login: rak-phillip
body: `value` is what drives the initialization for data in a lot of these components. If properties like `metadata` are omitted, we will throw errors in the tests:

```
TypeError: Cannot set properties of undefined (setting 'name')

@ [NameNsDescription.vue:193:9](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)
props.value.metadata['name'] = val;

Object.<anonymous> @ [NameNsDescription.test.ts:58:5](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)

const wrapper = mount(NameNsDescription, {
```

My best understanding is that enhanced reactivity that is provided by the composition api/setup method. I'm open to alternative approaches if we want to avoid passing values via the `value` prop in tests.
created_at: 2025-03-26T21:43:52Z
author_association: MEMBER
diff_hunk: @@ -169,106 +172,97 @@ export default {
   },
 
   data() {
-    return {
-      namespace:       '',
-      name:            '',
-      description:     '',
-      createNamespace: false,
-    };
+    return { createNamespace: false };
   },
 
-  created() {
-    const v = this.value;
-    const metadata = v.metadata;
-    let namespace, name, description;
-
-    if (this.nameKey) {
-      name = get(v, this.nameKey);
-    } else {
-      name = metadata?.name;
-    }
+  setup(props, { emit }) {
path: shell/components/form/NameNsDescription.vue
user: login: rak-phillip
body: I'm open to adding comments where they will help. Let me know if you would like to sync on where we can benefit with additional comments
created_at: 2025-03-26T21:45:15Z
author_association: MEMBER
diff_hunk: @@ -169,106 +172,97 @@ export default {
   },
 
   data() {
-    return {
-      namespace:       '',
-      name:            '',
-      description:     '',
-      createNamespace: false,
-    };
+    return { createNamespace: false };
   },
 
-  created() {
-    const v = this.value;
-    const metadata = v.metadata;
-    let namespace, name, description;
-
-    if (this.nameKey) {
-      name = get(v, this.nameKey);
-    } else {
-      name = metadata?.name;
-    }
+  setup(props, { emit }) {
path: shell/components/form/NameNsDescription.vue
user: login: cnotv
body: Nah, let's keep it for the next time :)
created_at: 2025-03-26T22:05:10Z
author_association: MEMBER

PR comments:
user: login: rak-phillip
created_at: 2025-03-24T18:01:38Z
body: This is quite an impactful change. I would like two approvals if possible.
author_association: MEMBER
user: login: cnotv
created_at: 2025-03-26T09:35:50Z
body: > I know some of our code is now using the `<script setup>` tag. By any chance do we want to update these to make use of the same for consistency?

`<script setup>` means using Composition API, which would require a whole refactoring, including the tests in some cases. I would suggest using a separate issue for it, since we want to tackle a problem at the time.
author_association: MEMBER
user: login: rak-phillip
created_at: 2025-03-26T21:53:40Z
body: > > I know some of our code is now using the `<script setup>` tag. By any chance do we want to update these to make use of the same for consistency?
> 
> `<script setup>` means using Composition API, which would require a whole refactoring, including the tests in some cases. I would suggest using a separate issue for it, since we want to tackle a problem at the time.

I totally agree. Further refactoring to use `script setup` will involve more changes that are outside the scope of resolving problematic data props. I think we should assess this work outside of this PR.
author_association: MEMBER