@rancher/shell 3.0.12-rc.5 → 3.0.12-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/apis/intf/resources-api/resource-base.ts +42 -2
  2. package/apis/intf/resources-api/resource-instance.ts +101 -0
  3. package/apis/intf/resources-api/resources-api.ts +177 -23
  4. package/apis/intf/resources.ts +8 -6
  5. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  6. package/apis/resources/resources-api-class.ts +232 -43
  7. package/assets/images/providers/traefik.png +0 -0
  8. package/assets/translations/en-us.yaml +10 -4
  9. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  10. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  11. package/cloud-credential/__tests__/generic.test.ts +132 -0
  12. package/cloud-credential/generic.vue +33 -3
  13. package/components/EtcdInfoBanner.vue +2 -12
  14. package/components/GrafanaDashboard.vue +2 -8
  15. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  16. package/components/Resource/Detail/Masthead/index.vue +8 -3
  17. package/components/form/Security.vue +1 -1
  18. package/components/formatter/Translate.vue +22 -1
  19. package/components/nav/Group.vue +5 -10
  20. package/components/nav/TopLevelMenu.vue +1 -30
  21. package/components/nav/__tests__/Group.test.ts +61 -0
  22. package/config/labels-annotations.js +1 -0
  23. package/config/table-headers.js +5 -2
  24. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  25. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  26. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  27. package/core/__tests__/plugin-products-new.test.ts +389 -0
  28. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  29. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  30. package/core/plugin-products-base.ts +201 -93
  31. package/core/plugin-products-extending.ts +1 -1
  32. package/core/plugin-products-external.ts +380 -0
  33. package/core/plugin-products-helpers.ts +28 -24
  34. package/core/plugin-products-internal.ts +207 -0
  35. package/core/plugin-products-top-level.ts +10 -5
  36. package/core/plugin-products-type-guards.ts +17 -6
  37. package/core/plugin-products.ts +5 -7
  38. package/core/plugin-types.ts +1 -389
  39. package/core/plugin.ts +10 -11
  40. package/core/types.ts +6 -202
  41. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  42. package/detail/pod.vue +1 -1
  43. package/detail/provisioning.cattle.io.cluster.vue +0 -2
  44. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  45. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  46. package/edit/auth/__tests__/azuread.test.ts +1 -0
  47. package/edit/auth/__tests__/github.test.ts +1 -0
  48. package/edit/auth/__tests__/oidc.test.ts +1 -0
  49. package/edit/auth/__tests__/saml.test.ts +1 -0
  50. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  51. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  52. package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
  53. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  54. package/list/management.cattle.io.user.vue +24 -5
  55. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  56. package/machine-config/__tests__/generic.test.ts +163 -0
  57. package/machine-config/components/EC2Networking.vue +31 -7
  58. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  59. package/machine-config/generic.vue +32 -5
  60. package/mixins/resource-fetch.js +1 -1
  61. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  62. package/models/pod.js +12 -10
  63. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  64. package/package.json +1 -1
  65. package/pages/auth/login.vue +1 -1
  66. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
  67. package/pages/c/_cluster/apps/charts/install.vue +29 -0
  68. package/pages/c/_cluster/auth/config/index.vue +11 -4
  69. package/pages/c/_cluster/monitoring/index.vue +6 -1
  70. package/plugins/dashboard-store/resource-class.js +10 -3
  71. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  72. package/plugins/steve/steve-class.js +43 -0
  73. package/plugins/steve/subscribe.js +11 -0
  74. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  75. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  76. package/store/__tests__/auth.test.ts +18 -0
  77. package/store/__tests__/notifications.test.ts +466 -1
  78. package/store/__tests__/ui-context.test.ts +255 -0
  79. package/store/auth.js +11 -1
  80. package/store/features.js +1 -0
  81. package/store/plugins.js +6 -0
  82. package/types/rancher/steve.api.ts +2 -2
  83. package/types/shell/index.d.ts +58 -0
  84. package/types/store/dashboard-store.types.ts +2 -2
  85. package/types/store/type-map.ts +262 -1
  86. package/utils/async.ts +2 -0
  87. package/utils/grafana.js +2 -17
  88. package/utils/monitoring.js +38 -1
  89. package/core/__tests__/plugin-products.test.ts +0 -4694
@@ -0,0 +1,269 @@
1
+ import { shallowMount, flushPromises } from '@vue/test-utils';
2
+ import RancherMonitoringDashboards from '@shell/chart/rancher-monitoring-dashboards/index.vue';
3
+
4
+ // The component keeps a module-level Map of `chartName -> last selected
5
+ // clusterType.id` so it can restore the user's pick after a Form → YAML → Form
6
+ // remount. To stop this cache from leaking between tests we generate a unique
7
+ // chart name per test.
8
+ let chartNameCounter = 0;
9
+ const uniqueChartName = (prefix = 'chart') => `${ prefix }-${ ++chartNameCounter }`;
10
+
11
+ describe('rancher-monitoring-dashboards: index', () => {
12
+ const createWrapper = (
13
+ {
14
+ provider = 'rke2',
15
+ value = {} as Record<string, any>,
16
+ chart,
17
+ mode = 'create',
18
+ }: {
19
+ provider?: string;
20
+ value?: Record<string, any>;
21
+ chart?: Record<string, any>;
22
+ mode?: string;
23
+ } = {}
24
+ ) => {
25
+ return shallowMount(RancherMonitoringDashboards as any, {
26
+ props: {
27
+ value,
28
+ chart: chart ?? { chartName: uniqueChartName() },
29
+ mode,
30
+ },
31
+ global: {
32
+ mocks: {
33
+ $store: {
34
+ getters: {
35
+ currentCluster: { status: { provider } },
36
+ 'i18n/t': (k: string) => k,
37
+ t: (k: string) => k,
38
+ },
39
+ },
40
+ t: (k: string) => k,
41
+ },
42
+ stubs: { LabeledSelect: true },
43
+ },
44
+ });
45
+ };
46
+
47
+ describe('initial cluster type selection', () => {
48
+ it.each([
49
+ ['aks'],
50
+ ['eks'],
51
+ ['gke'],
52
+ ['k3s'],
53
+ ['kubeadm'],
54
+ ['rke2'],
55
+ ])('defaults clusterType to the provider %s', (provider) => {
56
+ const wrapper = createWrapper({ provider });
57
+
58
+ expect((wrapper.vm as any).clusterType.id).toBe(provider);
59
+ });
60
+
61
+ it('falls back to "other" when the provider does not match any cluster type', () => {
62
+ const wrapper = createWrapper({ provider: 'unknown-provider' });
63
+
64
+ expect((wrapper.vm as any).clusterType.id).toBe('other');
65
+ });
66
+
67
+ it('lowercases the provider before matching', () => {
68
+ const wrapper = createWrapper({ provider: 'RKE2' });
69
+
70
+ expect((wrapper.vm as any).clusterType.id).toBe('rke2');
71
+ });
72
+ });
73
+
74
+ describe('applyClusterTypeValues', () => {
75
+ it('sets k3sServer.enabled=true and etcd.enabled=false for k3s', async() => {
76
+ const value: Record<string, any> = {};
77
+
78
+ createWrapper({ provider: 'k3s', value });
79
+ await flushPromises();
80
+
81
+ expect(value.k3sServer.enabled).toBe(true);
82
+ expect(value.etcd.enabled).toBe(false);
83
+ });
84
+
85
+ it.each([
86
+ ['rke2'],
87
+ ['eks'],
88
+ ['kubeadm'],
89
+ ['unknown-provider'],
90
+ ])('sets etcd.enabled=true and k3sServer.enabled=false for non-k3s provider %s', async(provider) => {
91
+ const value: Record<string, any> = {};
92
+
93
+ createWrapper({ provider, value });
94
+ await flushPromises();
95
+
96
+ expect(value.etcd.enabled).toBe(true);
97
+ expect(value.k3sServer.enabled).toBe(false);
98
+ });
99
+
100
+ it('creates the etcd and k3sServer objects when missing on value', async() => {
101
+ const value: Record<string, any> = {};
102
+
103
+ createWrapper({ provider: 'rke2', value });
104
+ await flushPromises();
105
+
106
+ expect(value.etcd).toStrictEqual({ enabled: true });
107
+ expect(value.k3sServer).toStrictEqual({ enabled: false });
108
+ });
109
+
110
+ it('preserves unrelated fields on value', async() => {
111
+ const value: Record<string, any> = {
112
+ global: { foo: 'bar' },
113
+ etcd: { extraField: 'keep-me' },
114
+ };
115
+
116
+ createWrapper({ provider: 'k3s', value });
117
+ await flushPromises();
118
+
119
+ expect(value.global).toStrictEqual({ foo: 'bar' });
120
+ expect(value.etcd.extraField).toBe('keep-me');
121
+ expect(value.etcd.enabled).toBe(false);
122
+ });
123
+
124
+ it('flips values when the user changes the selection', async() => {
125
+ const value: Record<string, any> = {};
126
+ const wrapper = createWrapper({ provider: 'rke2', value });
127
+
128
+ await flushPromises();
129
+ expect(value.etcd.enabled).toBe(true);
130
+ expect(value.k3sServer.enabled).toBe(false);
131
+
132
+ const k3sOption = (wrapper.vm as any).clusterTypes.find((c: any) => c.id === 'k3s');
133
+
134
+ (wrapper.vm as any).clusterType = k3sOption;
135
+ await flushPromises();
136
+
137
+ expect(value.etcd.enabled).toBe(false);
138
+ expect(value.k3sServer.enabled).toBe(true);
139
+ });
140
+ });
141
+
142
+ describe('watch: clusterType', () => {
143
+ it('ignores empty cluster type and does not mutate value', async() => {
144
+ const value: Record<string, any> = {};
145
+ const wrapper = createWrapper({ provider: 'rke2', value });
146
+
147
+ await flushPromises();
148
+
149
+ const snapshot = JSON.stringify(value);
150
+
151
+ (wrapper.vm as any).clusterType = null;
152
+ await flushPromises();
153
+
154
+ expect(JSON.stringify(value)).toBe(snapshot);
155
+ });
156
+ });
157
+
158
+ describe('cached selection across remounts', () => {
159
+ it('restores the last user selection when the same chart remounts', async() => {
160
+ const chart = { chartName: uniqueChartName('same-chart') };
161
+
162
+ const first = createWrapper({
163
+ provider: 'rke2', value: {}, chart
164
+ });
165
+
166
+ await flushPromises();
167
+ expect((first.vm as any).clusterType.id).toBe('rke2');
168
+
169
+ const k3sOption = (first.vm as any).clusterTypes.find((c: any) => c.id === 'k3s');
170
+
171
+ (first.vm as any).clusterType = k3sOption;
172
+ await flushPromises();
173
+ first.unmount();
174
+
175
+ // Simulate a remount on the same chart (e.g. user toggled to YAML and back).
176
+ const second = createWrapper({
177
+ provider: 'rke2',
178
+ value: { etcd: { enabled: false }, k3sServer: { enabled: true } },
179
+ chart,
180
+ });
181
+
182
+ expect((second.vm as any).clusterType.id).toBe('k3s');
183
+ });
184
+
185
+ it('preserves a non-k3s selection across remounts (the case value-derivation alone cannot recover)', async() => {
186
+ const chart = { chartName: uniqueChartName('non-k3s-pick') };
187
+
188
+ const first = createWrapper({
189
+ provider: 'k3s', value: {}, chart
190
+ });
191
+
192
+ await flushPromises();
193
+ expect((first.vm as any).clusterType.id).toBe('k3s');
194
+
195
+ const rke2Option = (first.vm as any).clusterTypes.find((c: any) => c.id === 'rke2');
196
+
197
+ (first.vm as any).clusterType = rke2Option;
198
+ await flushPromises();
199
+ first.unmount();
200
+
201
+ // Both rke2 and the k3s-provider default would write the same value
202
+ // object, so derivation from `value` alone cannot tell them apart — the
203
+ // module-level cache is the only thing that keeps "rke2" here.
204
+ const second = createWrapper({
205
+ provider: 'k3s',
206
+ value: { etcd: { enabled: true }, k3sServer: { enabled: false } },
207
+ chart,
208
+ });
209
+
210
+ expect((second.vm as any).clusterType.id).toBe('rke2');
211
+ });
212
+
213
+ it('does not leak a selection across different chart names', async() => {
214
+ const first = createWrapper({
215
+ provider: 'rke2',
216
+ value: {},
217
+ chart: { chartName: uniqueChartName('chart-a') },
218
+ });
219
+
220
+ await flushPromises();
221
+
222
+ const k3sOption = (first.vm as any).clusterTypes.find((c: any) => c.id === 'k3s');
223
+
224
+ (first.vm as any).clusterType = k3sOption;
225
+ await flushPromises();
226
+ first.unmount();
227
+
228
+ const second = createWrapper({
229
+ provider: 'rke2',
230
+ value: {},
231
+ chart: { chartName: uniqueChartName('chart-b') },
232
+ });
233
+
234
+ // Different cache key, so chart-b should still default from its provider.
235
+ expect((second.vm as any).clusterType.id).toBe('rke2');
236
+ });
237
+
238
+ it('uses chart.name as the cache key when chartName is absent', async() => {
239
+ const chart = { name: uniqueChartName('fallback-name') };
240
+
241
+ const first = createWrapper({
242
+ provider: 'rke2', value: {}, chart
243
+ });
244
+
245
+ await flushPromises();
246
+
247
+ const k3sOption = (first.vm as any).clusterTypes.find((c: any) => c.id === 'k3s');
248
+
249
+ (first.vm as any).clusterType = k3sOption;
250
+ await flushPromises();
251
+ first.unmount();
252
+
253
+ const second = createWrapper({
254
+ provider: 'rke2', value: {}, chart
255
+ });
256
+
257
+ expect((second.vm as any).clusterType.id).toBe('k3s');
258
+ });
259
+ });
260
+
261
+ describe('clusterTypes data', () => {
262
+ it('sorts cluster types by id for the dropdown', () => {
263
+ const wrapper = createWrapper();
264
+ const ids = (wrapper.vm as any).clusterTypes.map((c: any) => c.id);
265
+
266
+ expect(ids).toStrictEqual([...ids].sort());
267
+ });
268
+ });
269
+ });
@@ -0,0 +1,155 @@
1
+ <script>
2
+ import { mapGetters } from 'vuex';
3
+ import isEmpty from 'lodash/isEmpty';
4
+ import sortBy from 'lodash/sortBy';
5
+ import LabeledSelect from '@shell/components/form/LabeledSelect';
6
+ import { findBy } from '@shell/utils/array';
7
+
8
+ // Survives Form → YAML → Form tab switches (which unmount/remount this
9
+ // component) because the module is loaded once. Keyed by chart name so a
10
+ // different chart's install doesn't restore a stale selection.
11
+ const cachedSelections = new Map();
12
+
13
+ const CLUSTER_TYPES = [
14
+ {
15
+ group: 'managed',
16
+ id: 'aks',
17
+ label: 'cluster.provider.azureaks',
18
+ },
19
+ {
20
+ group: 'managed',
21
+ id: 'eks',
22
+ label: 'cluster.provider.amazoneks',
23
+ },
24
+ {
25
+ group: 'managed',
26
+ id: 'gke',
27
+ label: 'cluster.provider.googlegke',
28
+ },
29
+ {
30
+ group: 'k3s',
31
+ id: 'k3s',
32
+ label: 'cluster.provider.k3s',
33
+ },
34
+ {
35
+ group: 'kubeadm',
36
+ id: 'kubeadm',
37
+ label: 'cluster.provider.kubeAdmin',
38
+ },
39
+ {
40
+ group: 'other',
41
+ id: 'other',
42
+ label: 'cluster.provider.other',
43
+ },
44
+ {
45
+ group: 'rke',
46
+ id: 'rke2',
47
+ label: 'cluster.provider.rke2',
48
+ },
49
+ ];
50
+
51
+ export default {
52
+ emits: ['input'],
53
+
54
+ components: { LabeledSelect },
55
+
56
+ props: {
57
+ chart: {
58
+ type: Object,
59
+ default: () => ({}),
60
+ },
61
+
62
+ mode: {
63
+ type: String,
64
+ default: 'create',
65
+ },
66
+
67
+ value: {
68
+ type: Object,
69
+ default: () => ({}),
70
+ },
71
+ },
72
+
73
+ data() {
74
+ return {
75
+ clusterType: null,
76
+ clusterTypes: sortBy(CLUSTER_TYPES, 'id'),
77
+ };
78
+ },
79
+
80
+ computed: {
81
+ ...mapGetters(['currentCluster']),
82
+ provider() {
83
+ return this.currentCluster.status.provider.toLowerCase();
84
+ },
85
+ cacheKey() {
86
+ return this.chart?.chartName || this.chart?.name || '__default__';
87
+ },
88
+ },
89
+
90
+ watch: {
91
+ clusterType(clusterType) {
92
+ if (isEmpty(clusterType)) {
93
+ return;
94
+ }
95
+
96
+ cachedSelections.set(this.cacheKey, clusterType.id);
97
+ this.applyClusterTypeValues(clusterType);
98
+ },
99
+ },
100
+
101
+ created() {
102
+ const cachedId = cachedSelections.get(this.cacheKey);
103
+ const cached = cachedId ? findBy(this.clusterTypes, 'id', cachedId) : null;
104
+
105
+ if (cached) {
106
+ this.clusterType = cached;
107
+
108
+ return;
109
+ }
110
+
111
+ const matched = findBy(this.clusterTypes, 'id', this.provider);
112
+
113
+ this.clusterType = matched || findBy(this.clusterTypes, 'id', 'other');
114
+ },
115
+
116
+ methods: {
117
+ applyClusterTypeValues(clusterType) {
118
+ const isK3s = clusterType.group === 'k3s';
119
+
120
+ if (!this.value.etcd) {
121
+ this.value.etcd = {};
122
+ }
123
+ if (!this.value.k3sServer) {
124
+ this.value.k3sServer = {};
125
+ }
126
+
127
+ this.value.etcd.enabled = !isK3s;
128
+ this.value.k3sServer.enabled = isK3s;
129
+ },
130
+ },
131
+ };
132
+ </script>
133
+
134
+ <template>
135
+ <div class="config-monitoring-dashboards-container">
136
+ <div class="row">
137
+ <div class="col span-6">
138
+ <LabeledSelect
139
+ v-model:value="clusterType"
140
+ :label="t('monitoring.clusterType.label')"
141
+ :placeholder="t('monitoring.clusterType.placeholder')"
142
+ :localized-label="true"
143
+ :options="clusterTypes"
144
+ :mode="mode"
145
+ />
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </template>
150
+
151
+ <style lang="scss" scoped>
152
+ .config-monitoring-dashboards-container {
153
+ padding: 10px 0;
154
+ }
155
+ </style>
@@ -0,0 +1,132 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import Generic, { INPUT_TYPES } from '@shell/cloud-credential/generic.vue';
3
+ import { SCHEMA } from '@shell/config/types';
4
+
5
+ const DRIVER_NAME = 'testdriver';
6
+ const CONFIG_ID = `testdrivercredentialconfig`;
7
+
8
+ /**
9
+ * Build a normanSchema whose resourceFields include one field per INPUT_TYPE
10
+ * plus a plain "string" field (which should fall back to "text").
11
+ */
12
+ const buildResourceFields = () => {
13
+ const fields: Record<string, { type: string }> = {};
14
+
15
+ INPUT_TYPES.forEach((t) => {
16
+ fields[`${ t }Field`] = { type: t };
17
+ });
18
+ fields.stringField = { type: 'string' };
19
+
20
+ return fields;
21
+ };
22
+
23
+ const normanSchema = { resourceFields: buildResourceFields() };
24
+
25
+ const mockedStore = () => ({
26
+ getters: {
27
+ 'i18n/t': (key: string) => key,
28
+ t: (key: string) => key,
29
+ 'plugins/credentialFieldForDriver': () => DRIVER_NAME,
30
+ 'plugins/fieldNamesForDriver': () => [],
31
+ },
32
+ dispatch: jest.fn((_action: string, args: any) => {
33
+ if (args?.type === SCHEMA && args?.id === CONFIG_ID) {
34
+ return Promise.resolve(normanSchema);
35
+ }
36
+
37
+ return Promise.resolve(null);
38
+ }),
39
+ });
40
+
41
+ const mountGeneric = () => {
42
+ const store = mockedStore();
43
+ const value = {
44
+ decodedData: {} as Record<string, string>,
45
+ setData: jest.fn(),
46
+ };
47
+
48
+ return shallowMount(Generic, {
49
+ props: {
50
+ driverName: DRIVER_NAME,
51
+ value,
52
+ mode: 'edit',
53
+ },
54
+ global: {
55
+ mocks: {
56
+ $store: store,
57
+ $fetchState: { pending: false },
58
+ },
59
+ },
60
+ data() {
61
+ return {
62
+ normanSchema,
63
+ keyOptions: Object.keys(normanSchema.resourceFields),
64
+ } as any;
65
+ },
66
+ });
67
+ };
68
+
69
+ describe('generic cloud credential', () => {
70
+ describe('typeForField', () => {
71
+ it.each(INPUT_TYPES)('should return "%s" for a field with type "%s"', (inputType) => {
72
+ const wrapper = mountGeneric();
73
+
74
+ expect((wrapper.vm as any).typeForField(`${ inputType }Field`)).toBe(inputType);
75
+ });
76
+
77
+ it('should fall back to "text" for a field type not in INPUT_TYPES', () => {
78
+ const wrapper = mountGeneric();
79
+
80
+ expect((wrapper.vm as any).typeForField('stringField')).toBe('text');
81
+ });
82
+
83
+ it('should fall back to "text" when no normanSchema could be located', () => {
84
+ const store = {
85
+ ...mockedStore(),
86
+ dispatch: jest.fn(() => Promise.resolve(null)),
87
+ };
88
+
89
+ const wrapper = shallowMount(Generic, {
90
+ props: {
91
+ driverName: DRIVER_NAME,
92
+ value: { decodedData: {}, setData: jest.fn() },
93
+ mode: 'edit',
94
+ },
95
+ global: {
96
+ mocks: {
97
+ $store: store,
98
+ $fetchState: { pending: false },
99
+ },
100
+ },
101
+ data() {
102
+ return { normanSchema: null, keyOptions: [] };
103
+ },
104
+ });
105
+
106
+ expect((wrapper.vm as any).typeForField('passwordField')).toBe('text');
107
+ expect((wrapper.vm as any).typeForField('cronField')).toBe('text');
108
+ expect((wrapper.vm as any).typeForField('anyField')).toBe('text');
109
+ });
110
+ });
111
+
112
+ describe('LabeledInput rendering', () => {
113
+ it('should render a LabeledInput inside the KeyValue value slot', () => {
114
+ const wrapper = mountGeneric();
115
+ const keyValue = wrapper.findComponent({ name: 'KeyValue' });
116
+
117
+ expect(keyValue.exists()).toBe(true);
118
+
119
+ // The component registers LabeledInput and passes typeForField via the slot template
120
+ expect((wrapper.vm as any).$options.components.LabeledInput).toBeDefined();
121
+ });
122
+
123
+ it('should pass typeForField result as the type prop to LabeledInput', () => {
124
+ const wrapper = mountGeneric();
125
+
126
+ // Validate the integration: typeForField returns the correct type for each INPUT_TYPE field
127
+ INPUT_TYPES.forEach((inputType) => {
128
+ expect((wrapper.vm as any).typeForField(`${ inputType }Field`)).toBe(inputType);
129
+ });
130
+ });
131
+ });
132
+ });
@@ -5,13 +5,24 @@ import { Banner } from '@components/Banner';
5
5
  import { simplify, iffyFields, likelyFields } from '@shell/store/plugins';
6
6
  import Loading from '@shell/components/Loading';
7
7
  import { SCHEMA } from '@shell/config/types';
8
+ import { LabeledInput } from '@components/Form/LabeledInput';
9
+
10
+ export const INPUT_TYPES = [
11
+ 'number',
12
+ 'password',
13
+ 'text',
14
+ ];
8
15
 
9
16
  export default {
10
17
  emits: ['validationChanged'],
11
18
 
12
19
  components: {
13
- KeyValue, Banner, Loading
20
+ KeyValue,
21
+ Banner,
22
+ Loading,
23
+ LabeledInput
14
24
  },
25
+
15
26
  mixins: [CreateEditView],
16
27
 
17
28
  props: {
@@ -60,6 +71,7 @@ export default {
60
71
  return {
61
72
  errors: null,
62
73
  normanSchema: null,
74
+ keyOptions: [],
63
75
  };
64
76
  },
65
77
 
@@ -72,7 +84,14 @@ export default {
72
84
  methods: {
73
85
  update(val) {
74
86
  this.value.setData(val);
75
- }
87
+ },
88
+
89
+ typeForField(fieldName = '') {
90
+ const fieldSchema = this.normanSchema?.resourceFields?.[fieldName] || {};
91
+ const fieldType = fieldSchema.type || '';
92
+
93
+ return INPUT_TYPES.includes(fieldType) ? fieldType : 'text';
94
+ },
76
95
  },
77
96
 
78
97
  computed: {
@@ -103,6 +122,17 @@ export default {
103
122
  :remove-allowed="!hasSupport"
104
123
  :initial-empty-row="true"
105
124
  @update:value="update"
106
- />
125
+ >
126
+ <template #value="{row, mode, queueUpdate}">
127
+ <LabeledInput
128
+ v-model:value="row.value"
129
+ :mode="mode"
130
+ :type="typeForField(row.key)"
131
+ :aria-label="row.key || t('generic.value')"
132
+ placeholder-key="keyValue.valuePlaceholder"
133
+ @update:value="queueUpdate"
134
+ />
135
+ </template>
136
+ </KeyValue>
107
137
  </div>
108
138
  </template>
@@ -3,23 +3,13 @@ import { Banner } from '@components/Banner';
3
3
  import Loading from '@shell/components/Loading';
4
4
  import { mapGetters } from 'vuex';
5
5
  import { hasLeader, leaderChanges, failedProposals } from '@shell/utils/grafana';
6
- import { CATALOG } from '@shell/config/types';
6
+ import { fetchMonitoringVersion } from '@shell/utils/monitoring';
7
7
 
8
8
  export default {
9
9
  components: { Banner, Loading },
10
10
  async fetch() {
11
11
  const inStore = this.$store.getters['currentProduct'].inStore;
12
- let monitoringVersion = '';
13
-
14
- if (this.$store.getters[`${ inStore }/canList`](CATALOG.APP)) {
15
- try {
16
- const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
17
-
18
- monitoringVersion = res?.currentVersion;
19
- } catch (err) {
20
-
21
- }
22
- }
12
+ const monitoringVersion = await fetchMonitoringVersion(this.$store, inStore);
23
13
 
24
14
  const leader = await hasLeader(monitoringVersion, this.$store.dispatch, this.currentCluster.id);
25
15
 
@@ -2,7 +2,7 @@
2
2
  import Loading from '@shell/components/Loading';
3
3
  import { Banner } from '@components/Banner';
4
4
  import { computeDashboardUrl } from '@shell/utils/grafana';
5
- import { CATALOG } from '@shell/config/types';
5
+ import { fetchMonitoringVersion } from '@shell/utils/monitoring';
6
6
 
7
7
  export default {
8
8
  components: { Banner, Loading },
@@ -41,13 +41,7 @@ export default {
41
41
  async fetch() {
42
42
  const inStore = this.$store.getters['currentProduct'].inStore;
43
43
 
44
- if (this.$store.getters[`${ inStore }/canList`](CATALOG.APP)) {
45
- try {
46
- const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
47
-
48
- this.monitoringVersion = res?.currentVersion;
49
- } catch (err) {}
50
- }
44
+ this.monitoringVersion = await fetchMonitoringVersion(this.$store, inStore);
51
45
  },
52
46
  data() {
53
47
  return {