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

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 (116) 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/intf/shell-api/slide-in.ts +46 -17
  6. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  7. package/apis/resources/resources-api-class.ts +232 -43
  8. package/apis/shell/__tests__/slide-in.test.ts +83 -2
  9. package/apis/shell/slide-in.ts +20 -0
  10. package/assets/images/providers/traefik.png +0 -0
  11. package/assets/translations/en-us.yaml +10 -4
  12. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  13. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  14. package/cloud-credential/__tests__/generic.test.ts +132 -0
  15. package/cloud-credential/generic.vue +33 -3
  16. package/components/EtcdInfoBanner.vue +2 -12
  17. package/components/ExplorerProjectsNamespaces.vue +2 -2
  18. package/components/GrafanaDashboard.vue +2 -8
  19. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  20. package/components/Resource/Detail/Masthead/index.vue +8 -3
  21. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +9 -0
  22. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +5 -1
  23. package/components/SlideInPanelManager.vue +103 -25
  24. package/components/__tests__/SlideInPanelManager.spec.ts +153 -10
  25. package/components/auth/AuthBanner.vue +1 -1
  26. package/components/form/Security.vue +1 -1
  27. package/components/formatter/Translate.vue +22 -1
  28. package/components/nav/Group.vue +5 -10
  29. package/components/nav/TopLevelMenu.vue +1 -30
  30. package/components/nav/__tests__/Group.test.ts +61 -0
  31. package/config/labels-annotations.js +1 -0
  32. package/config/table-headers.js +5 -2
  33. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  34. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  35. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  36. package/core/__tests__/plugin-products-new.test.ts +389 -0
  37. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  38. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  39. package/core/plugin-products-base.ts +201 -93
  40. package/core/plugin-products-extending.ts +1 -1
  41. package/core/plugin-products-external.ts +380 -0
  42. package/core/plugin-products-helpers.ts +28 -24
  43. package/core/plugin-products-internal.ts +207 -0
  44. package/core/plugin-products-top-level.ts +10 -5
  45. package/core/plugin-products-type-guards.ts +17 -6
  46. package/core/plugin-products.ts +5 -7
  47. package/core/plugin-types.ts +1 -389
  48. package/core/plugin.ts +10 -11
  49. package/core/plugins-loader.js +2 -0
  50. package/core/types.ts +6 -202
  51. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  52. package/detail/pod.vue +1 -1
  53. package/detail/provisioning.cattle.io.cluster.vue +0 -2
  54. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  55. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  56. package/edit/auth/__tests__/azuread.test.ts +1 -0
  57. package/edit/auth/__tests__/github.test.ts +1 -0
  58. package/edit/auth/__tests__/oidc.test.ts +1 -0
  59. package/edit/auth/__tests__/saml.test.ts +1 -0
  60. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  61. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  62. package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
  63. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  64. package/list/management.cattle.io.user.vue +24 -5
  65. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  66. package/machine-config/__tests__/generic.test.ts +163 -0
  67. package/machine-config/components/EC2Networking.vue +31 -7
  68. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  69. package/machine-config/generic.vue +32 -5
  70. package/mixins/resource-fetch.js +1 -1
  71. package/models/__tests__/namespace.test.ts +133 -8
  72. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +57 -1
  73. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  74. package/models/namespace.js +34 -2
  75. package/models/pod.js +12 -10
  76. package/models/provisioning.cattle.io.cluster.js +20 -13
  77. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  78. package/package.json +1 -1
  79. package/pages/auth/login.vue +1 -1
  80. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
  81. package/pages/c/_cluster/apps/charts/install.vue +29 -0
  82. package/pages/c/_cluster/auth/config/index.vue +11 -4
  83. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +136 -1
  84. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +69 -1
  85. package/pages/c/_cluster/fleet/index.vue +5 -9
  86. package/pages/c/_cluster/monitoring/index.vue +6 -1
  87. package/pkg/vue.config.js +4 -3
  88. package/plugins/codemirror.js +2 -0
  89. package/plugins/dashboard-store/resource-class.js +13 -9
  90. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  91. package/plugins/steve/steve-class.js +43 -0
  92. package/plugins/steve/subscribe.js +11 -0
  93. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  94. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  95. package/store/__tests__/action-menu.test.ts +622 -0
  96. package/store/__tests__/auth.test.ts +18 -0
  97. package/store/__tests__/notifications.test.ts +466 -1
  98. package/store/__tests__/slideInPanel.test.ts +143 -43
  99. package/store/__tests__/ui-context.test.ts +255 -0
  100. package/store/__tests__/wm.test.ts +503 -0
  101. package/store/auth.js +11 -1
  102. package/store/aws.js +19 -4
  103. package/store/features.js +1 -0
  104. package/store/plugins.js +6 -0
  105. package/store/slideInPanel.ts +15 -3
  106. package/types/rancher/steve.api.ts +2 -2
  107. package/types/shell/index.d.ts +84 -2
  108. package/types/store/dashboard-store.types.ts +2 -2
  109. package/types/store/type-map.ts +262 -1
  110. package/utils/__tests__/fleet-appco.test.ts +23 -0
  111. package/utils/async.ts +2 -0
  112. package/utils/fleet-appco.ts +6 -1
  113. package/utils/grafana.js +2 -17
  114. package/utils/monitoring.js +38 -1
  115. package/utils/sort.js +1 -1
  116. package/core/__tests__/plugin-products.test.ts +0 -4694
@@ -0,0 +1,163 @@
1
+ import Generic from '@shell/machine-config/generic.vue';
2
+ import { shallowMount } from '@vue/test-utils';
3
+ import { MANAGEMENT } from '@shell/config/types';
4
+ import { NODE_DRIVER_FIELD_HINTS } from '@shell/config/labels-annotations';
5
+ import { Banner } from '@components/Banner';
6
+ import Questions from '@shell/components/Questions';
7
+
8
+ const PROVIDER = 'testdriver';
9
+
10
+ const buildFields = () => ({
11
+ zone: { type: 'string' },
12
+ diskSize: { type: 'string' },
13
+ enginePort: { type: 'string' },
14
+ });
15
+
16
+ const mockedStore = (fieldHints: Record<string, { type: string }> | null = null) => {
17
+ const driver = { metadata: { annotations: { ...(fieldHints ? { [NODE_DRIVER_FIELD_HINTS]: JSON.stringify(fieldHints) } : {}) } } };
18
+
19
+ return {
20
+ getters: {
21
+ 'i18n/t': (key: string) => key,
22
+ t: (key: string) => key,
23
+ 'plugins/fieldsForDriver': () => Promise.resolve(buildFields()),
24
+ 'plugins/credentialFieldForDriver': () => PROVIDER,
25
+ 'plugins/fieldNamesForDriver': () => [],
26
+ 'rancher/schemaFor': () => null,
27
+ },
28
+ dispatch: jest.fn((_action: string, args: any) => {
29
+ if (args?.type === MANAGEMENT.NODE_DRIVER && args?.id === PROVIDER) {
30
+ return Promise.resolve(driver);
31
+ }
32
+
33
+ return Promise.resolve(null);
34
+ }),
35
+ };
36
+ };
37
+
38
+ const mountAndFetch = async(storeOverrides: Record<string, any> = {}) => {
39
+ const store = {
40
+ ...mockedStore(null),
41
+ ...storeOverrides,
42
+ getters: {
43
+ ...mockedStore(null).getters,
44
+ ...(storeOverrides.getters || {}),
45
+ },
46
+ };
47
+
48
+ if (storeOverrides.dispatch) {
49
+ store.dispatch = storeOverrides.dispatch;
50
+ }
51
+
52
+ const wrapper = shallowMount(Generic, {
53
+ props: {
54
+ credentialId: 'cattle-global-data:cc-12345',
55
+ provider: PROVIDER,
56
+ value: { metadata: { namespace: 'fleet-default' } },
57
+ mode: 'edit',
58
+ },
59
+ global: {
60
+ mocks: {
61
+ $store: store,
62
+ $fetchState: { pending: false },
63
+ },
64
+ },
65
+ });
66
+
67
+ await (Generic as any).fetch.call(wrapper.vm);
68
+ await wrapper.vm.$nextTick();
69
+
70
+ return wrapper;
71
+ };
72
+
73
+ describe('generic machine config', () => {
74
+ describe('io.cattle.nodedriver/ui-field-hints annotation', () => {
75
+ it('should override field types when type hints are provided', async() => {
76
+ const hints = {
77
+ zone: { type: 'multiline' },
78
+ diskSize: { type: 'multiline' },
79
+ };
80
+ const wrapper = await mountAndFetch({
81
+ ...mockedStore(hints),
82
+ getters: { ...mockedStore(hints).getters },
83
+ });
84
+
85
+ expect((wrapper.vm as any).fields.zone.type).toBe('multiline');
86
+ expect((wrapper.vm as any).fields.diskSize.type).toBe('multiline');
87
+ });
88
+
89
+ it('should not modify fields that are not in the hints', async() => {
90
+ const hints = { zone: { type: 'multiline' } };
91
+ const wrapper = await mountAndFetch({
92
+ ...mockedStore(hints),
93
+ getters: { ...mockedStore(hints).getters },
94
+ });
95
+
96
+ expect((wrapper.vm as any).fields.enginePort.type).toBe('string');
97
+ });
98
+
99
+ it('should not modify fields when no hints annotation exists', async() => {
100
+ const wrapper = await mountAndFetch();
101
+
102
+ expect((wrapper.vm as any).fields.zone.type).toBe('string');
103
+ expect((wrapper.vm as any).fields.diskSize.type).toBe('string');
104
+ expect((wrapper.vm as any).fields.enginePort.type).toBe('string');
105
+ });
106
+
107
+ it('should ignore hint entries for fields not present in the schema', async() => {
108
+ const hints = { nonExistentField: { type: 'multiline' } };
109
+ const wrapper = await mountAndFetch({
110
+ ...mockedStore(hints),
111
+ getters: { ...mockedStore(hints).getters },
112
+ });
113
+
114
+ expect((wrapper.vm as any).fields.zone.type).toBe('string');
115
+ expect((wrapper.vm as any).fields.nonExistentField).toBeUndefined();
116
+ });
117
+
118
+ it('should ignore hint entries that do not specify a type', async() => {
119
+ const hints = { zone: { otherKey: 'blah' } } as any;
120
+ const wrapper = await mountAndFetch({
121
+ ...mockedStore(hints),
122
+ getters: { ...mockedStore(hints).getters },
123
+ });
124
+
125
+ expect((wrapper.vm as any).fields.zone.type).toBe('string');
126
+ });
127
+
128
+ it('should handle malformed JSON in the annotation gracefully', async() => {
129
+ const driver = { metadata: { annotations: { [NODE_DRIVER_FIELD_HINTS]: 'testing bad json{' } } };
130
+ const wrapper = await mountAndFetch({ dispatch: jest.fn(() => Promise.resolve(driver)) });
131
+
132
+ expect((wrapper.vm as any).fields.zone.type).toBe('string');
133
+ expect((wrapper.vm as any).errors.length).toBe(1);
134
+ });
135
+ });
136
+
137
+ describe('data fetching failures', () => {
138
+ it('should render an error banner and not render Questions when unable to determine fields for the driver', async() => {
139
+ const wrapper = await mountAndFetch({ getters: { 'plugins/fieldsForDriver': () => Promise.resolve(null) } });
140
+
141
+ expect((wrapper.vm as any).errors.length).toBe(1);
142
+ expect((wrapper.vm as any).errors[0].message).toContain(`Machine Driver config schema not found for rke-machine-config.cattle.io.${ PROVIDER }config`);
143
+
144
+ const banners = wrapper.findAllComponents(Banner);
145
+
146
+ expect(banners.length).toBe(1);
147
+ expect(wrapper.findComponent(Questions).exists()).toBe(false);
148
+ });
149
+
150
+ it('should render an error banner but still render Questions when unable to retrieve node driver field hints', async() => {
151
+ const driverError = 'node driver not found';
152
+ const wrapper = await mountAndFetch({ dispatch: jest.fn(() => Promise.reject(new Error(driverError))) });
153
+
154
+ expect((wrapper.vm as any).errors.length).toBe(1);
155
+ expect((wrapper.vm as any).errors[0].message).toContain(driverError);
156
+
157
+ const banners = wrapper.findAllComponents(Banner);
158
+
159
+ expect(banners.length).toBe(1);
160
+ expect(wrapper.findComponent(Questions).exists()).toBe(true);
161
+ });
162
+ });
163
+ });
@@ -10,6 +10,7 @@ import { Banner } from '@components/Banner';
10
10
  import { scrollToBottom } from '@shell/utils/scroll';
11
11
  import { _CREATE } from '@shell/config/query-params';
12
12
  import { getSubnetDisplayName, getVpcDisplayName, isIpv4Network, isIpv6Network } from '@shell/utils/aws';
13
+ import { STACK_PREFS } from '@shell/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue';
13
14
 
14
15
  export default {
15
16
  name: 'Ec2MachinePoolNetworking',
@@ -297,10 +298,27 @@ export default {
297
298
  return this.mode === _CREATE || (this.isNew && this.somePoolisIpv6OrDual) || this.enableIpv6;
298
299
  },
299
300
 
300
- poolsInvalid() {
301
- const somePoolHasIpv4 = !!this.machinePools.find((p) => !p.isIpv6 && !p.isDualStack);
301
+ dualStackModeInvalid() {
302
+ const poolNetworkModes = new Set(this.machinePools.map((p) => {
303
+ if (p.isDualStack || p.isIpv6) {
304
+ return STACK_PREFS.DUAL;
305
+ }
306
+
307
+ return STACK_PREFS.IPV4;
308
+ }));
302
309
 
303
- return this.somePoolisIpv6OrDual && somePoolHasIpv4;
310
+ return poolNetworkModes.size > 1;
311
+ },
312
+ ipv6ModeInvalid() {
313
+ const poolNetworkModes = new Set(this.machinePools.map((p) => {
314
+ if (p.isIpv6) {
315
+ return STACK_PREFS.IPV6;
316
+ }
317
+
318
+ return 'other';
319
+ }));
320
+
321
+ return poolNetworkModes.size > 1;
304
322
  },
305
323
 
306
324
  addressCountInvalid() {
@@ -308,7 +326,7 @@ export default {
308
326
  },
309
327
 
310
328
  allValid() {
311
- return !this.poolsInvalid && !this.addressCountInvalid;
329
+ return !this.dualStackModeInvalid && !this.addressCountInvalid && !this.ipv6ModeInvalid;
312
330
  }
313
331
  },
314
332
 
@@ -356,10 +374,10 @@ export default {
356
374
 
357
375
  <template>
358
376
  <Banner
359
- v-if="poolsInvalid"
377
+ v-if="dualStackModeInvalid"
360
378
  color="error"
361
- :label="t('cluster.machineConfig.amazonEc2.ipv6ValidationWarning')"
362
- data-testid="amazonEc2__ipv6Warning"
379
+ :label="t('cluster.machineConfig.amazonEc2.dualStackValidationWarning')"
380
+ data-testid="amazonEc2__dualStackWarning"
363
381
  />
364
382
  <div
365
383
  v-if="showIpv6Options"
@@ -410,6 +428,12 @@ export default {
410
428
  </LabeledSelect>
411
429
  </div>
412
430
  </div>
431
+ <Banner
432
+ v-if="enableIpv6 && ipv6ModeInvalid"
433
+ color="error"
434
+ :label="t('cluster.machineConfig.amazonEc2.ipv6ValidationWarning')"
435
+ data-testid="amazonEc2__ipv6Warning"
436
+ />
413
437
  <div
414
438
  v-if="enableIpv6"
415
439
  class="row mb-10 ipv6-row"
@@ -1,4 +1,4 @@
1
- import { mount, shallowMount } from '@vue/test-utils';
1
+ import { shallowMount } from '@vue/test-utils';
2
2
  import EC2Networking from '@shell/machine-config/components/EC2Networking.vue';
3
3
 
4
4
  import { vpcInfo, subnetInfo } from './utils/vpcSubnetMockData';
@@ -16,6 +16,7 @@ describe('component: EC2Networking', () => {
16
16
  },
17
17
  global: {
18
18
  mocks: {
19
+ t: (key: string) => key,
19
20
  $fetchState: { pending: false },
20
21
  $store: { getters: defaultGetters },
21
22
  },
@@ -65,16 +66,9 @@ describe('component: EC2Networking', () => {
65
66
  });
66
67
 
67
68
  it('should render the ipv6 only checkbox when the selected network is dual-stack', async() => {
68
- const wrapper = mount(EC2Networking, defaultCreateSetup);
69
- const ipv6Checkbox = wrapper.findComponent('[data-testid="amazonEc2__enableIpv6"]');
70
-
71
- ipv6Checkbox.vm.$emit('update:value', true);
72
- await wrapper.vm.$nextTick();
73
-
74
- const networkDropdown = wrapper.findComponent('[data-testid="amazonEc2__selectedNetwork"]');
69
+ const wrapper = shallowMount(EC2Networking, defaultCreateSetup);
75
70
 
76
- networkDropdown.vm.$emit('selecting', 'subnet-321');
77
- await wrapper.vm.$nextTick();
71
+ await wrapper.setData({ enableIpv6: true, selectedNetwork: 'vpc-12345' });
78
72
 
79
73
  const ipv6OnlyCheckbox = wrapper.findComponent('[data-testid="amazonEc2__ipv6AddressOnly"]');
80
74
 
@@ -82,14 +76,19 @@ describe('component: EC2Networking', () => {
82
76
  });
83
77
 
84
78
  it.each([
85
- [[{ isIpv6: true }, { isIpv6: false }], true],
86
- [[{ isIpv6: false }, { isIpv6: false }], false],
87
- [[{ isIpv6: true }, { isIpv6: true }], false],
88
- ])('should show an error banner if pools do not either all have ipv6 or all have ipv4', (pools, shouldShowError) => {
79
+ [[{ isIpv6: true }, { isIpv6: false }], true, true],
80
+ [[{ isIpv6: false, isDualStack: true }, { isIpv6: false, isDualStack: false }], true, false],
81
+ [[{ isIpv6: true, isDualStack: false }, { isIpv6: false, isDualStack: true }], false, true],
82
+ [[{ isIpv6: false, isDualStack: false }, { isIpv6: false, isDualStack: false }], false, false],
83
+ [[{ isIpv6: true, isDualStack: false }, { isIpv6: true, isDualStack: false }], false, false],
84
+ [[{ isIpv6: false, isDualStack: true }, { isIpv6: false, isDualStack: true }], false, false],
85
+ ])('should show the correct warning banner based on mixed machine pool network modes', (pools, shouldShowDualStackWarning, shouldShowIpv6Warning) => {
89
86
  const wrapper = shallowMount(EC2Networking, { ...defaultCreateSetup, propsData: { ...defaultCreateSetup.propsData, machinePools: pools } });
87
+ const dualStackWarning = wrapper.findComponent('[data-testid="amazonEc2__dualStackWarning"]');
90
88
  const ipv6Warning = wrapper.findComponent('[data-testid="amazonEc2__ipv6Warning"]');
91
89
 
92
- expect(ipv6Warning.exists()).toBe(shouldShowError);
90
+ expect(dualStackWarning.exists()).toBe(shouldShowDualStackWarning);
91
+ expect(ipv6Warning.exists()).toBe(shouldShowIpv6Warning);
93
92
  });
94
93
 
95
94
  it('should show ipv6 inputs and automatically check the enable ipv6 checkbox when adding a new pool if some other pool in the cluster is using ipv6 or dual stack', () => {
@@ -145,4 +144,28 @@ describe('component: EC2Networking', () => {
145
144
 
146
145
  expect(wrapper.emitted('validationChanged')?.[0][0]).toBe(true);
147
146
  });
147
+
148
+ it('should emit a validationChanged: false event when ipv6-only and dual-stack pools are mixed', async() => {
149
+ const wrapper = shallowMount(EC2Networking, {
150
+ ...defaultCreateSetup,
151
+ propsData: {
152
+ ...defaultCreateSetup.propsData,
153
+ machinePools: [{ isIpv6: true, isDualStack: false }, { isIpv6: false, isDualStack: true }],
154
+ },
155
+ });
156
+
157
+ expect(wrapper.emitted('validationChanged')?.[0][0]).toBe(false);
158
+ });
159
+
160
+ it('should emit a validationChanged: true event when all pools are dual-stack', async() => {
161
+ const wrapper = shallowMount(EC2Networking, {
162
+ ...defaultCreateSetup,
163
+ propsData: {
164
+ ...defaultCreateSetup.propsData,
165
+ machinePools: [{ isIpv6: false, isDualStack: true }, { isIpv6: false, isDualStack: true }],
166
+ },
167
+ });
168
+
169
+ expect(wrapper.emitted('validationChanged')?.[0][0]).toBe(true);
170
+ });
148
171
  });
@@ -4,6 +4,10 @@ import { Banner } from '@components/Banner';
4
4
  import CreateEditView from '@shell/mixins/create-edit-view';
5
5
  import { exceptionToErrorsArray, stringify } from '@shell/utils/error';
6
6
  import Questions from '@shell/components/Questions';
7
+ import { MANAGEMENT } from '@shell/config/types';
8
+ import { NODE_DRIVER_FIELD_HINTS } from '@shell/config/labels-annotations';
9
+ import { isEmpty } from '@shell/utils/object';
10
+ import cloneDeep from 'lodash/cloneDeep';
7
11
 
8
12
  export default {
9
13
  emits: ['input'],
@@ -35,12 +39,36 @@ export default {
35
39
  this.errors = [];
36
40
 
37
41
  try {
38
- this.fields = await this.$store.getters['plugins/fieldsForDriver'](this.provider);
42
+ const fields = await this.$store.getters['plugins/fieldsForDriver'](this.provider);
43
+
44
+ // copy the result of the getter before modifying it to add hints
45
+ this.fields = cloneDeep(fields);
39
46
  const name = `rke-machine-config.cattle.io.${ this.provider }config`;
40
47
 
41
48
  if ( !this.fields ) {
42
49
  throw new Error(`Machine Driver config schema not found for ${ name }`);
43
50
  }
51
+ } catch (e) {
52
+ this.errors = exceptionToErrorsArray(e);
53
+
54
+ return;
55
+ }
56
+
57
+ try {
58
+ const driver = await this.$store.dispatch('management/find', { type: MANAGEMENT.NODE_DRIVER, id: this.provider } );
59
+ const fieldHints = driver?.metadata?.annotations?.[NODE_DRIVER_FIELD_HINTS];
60
+ let parsedHints;
61
+
62
+ if (fieldHints) {
63
+ parsedHints = JSON.parse(fieldHints);
64
+ }
65
+ if (parsedHints && !isEmpty(parsedHints)) {
66
+ Object.keys(this.fields).forEach((fieldKey) => {
67
+ if (parsedHints[fieldKey] && parsedHints[fieldKey].type) {
68
+ this.fields[fieldKey].type = parsedHints[fieldKey].type;
69
+ }
70
+ });
71
+ }
44
72
  } catch (e) {
45
73
  this.errors = exceptionToErrorsArray(e);
46
74
  }
@@ -77,7 +105,7 @@ export default {
77
105
  v-if="$fetchState.pending"
78
106
  :delayed="true"
79
107
  />
80
- <div v-else-if="errors.length">
108
+ <template v-else>
81
109
  <div
82
110
  v-for="(err, idx) in errors"
83
111
  :key="idx"
@@ -87,9 +115,8 @@ export default {
87
115
  :label="stringify(err)"
88
116
  />
89
117
  </div>
90
- </div>
91
- <div v-else>
92
118
  <Questions
119
+ v-if="fields"
93
120
  :value="value"
94
121
  :mode="mode"
95
122
  :tabbed="false"
@@ -99,5 +126,5 @@ export default {
99
126
  :disabled="disabled"
100
127
  @update:value="$emit('input', $event)"
101
128
  />
102
- </div>
129
+ </template>
103
130
  </template>
@@ -35,7 +35,7 @@ export default {
35
35
  // Normally owner components supply `resource` and `inStore` as part of their data, however these are needed here before parent data runs
36
36
  // So set up both here
37
37
  const params = { ...this.$route.params };
38
- const resource = params.resource || this.schema?.id; // Resource can either be on a page showing single list, or a page of a resource showing a list of another resource
38
+ const resource = this.schema?.id || params.resource; // Resource can either be on a page showing single list, or a page of a resource showing a list of another resource
39
39
  const inStore = this.$store.getters['currentStore'](resource);
40
40
 
41
41
  return {
@@ -1,6 +1,20 @@
1
1
  import Namespace from '@shell/models/namespace';
2
2
  import { SYSTEM_NAMESPACE } from '@shell/config/labels-annotations';
3
3
  import SYSTEM_NAMESPACES from '@shell/config/system-namespaces';
4
+ import { LOCAL_CLUSTER } from '@shell/config/types';
5
+ import { NAME as MANAGER } from '@shell/config/product/manager';
6
+ import { NAME as EXPLORER } from '@shell/config/product/explorer';
7
+ import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
8
+
9
+ jest.mock('@shell/components/nav/TopLevelMenu.helper', () => ({
10
+ __esModule: true,
11
+ default: {
12
+ helper: {
13
+ clustersPinned: [],
14
+ clustersOthers: [],
15
+ },
16
+ },
17
+ }));
4
18
 
5
19
  describe('class Namespace', () => {
6
20
  describe('checking if isSystem', () => {
@@ -201,24 +215,64 @@ describe('class Namespace', () => {
201
215
  expect(namespace.listLocation.name).toBe(name);
202
216
  });
203
217
 
204
- it('should return the name and resource if Harvester', () => {
218
+ it('should route to local cluster explorer when in manager product', () => {
205
219
  const namespace = new Namespace({});
206
220
 
207
221
  jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({
208
222
  isRancher: true,
209
- currentProduct: { inStore: 'harvester' }
223
+ productId: MANAGER,
224
+ clusterId: '_',
225
+ currentProduct: { inStore: '' },
210
226
  });
211
227
 
212
- const value = {
213
- name: 'harvester-c-cluster-projectsnamespaces',
214
- params: { resource: 'namespace' }
215
- };
228
+ expect(namespace.listLocation.params.cluster).toBe(LOCAL_CLUSTER);
229
+ expect(namespace.listLocation.params.product).toBe(EXPLORER);
230
+ });
231
+
232
+ it('should use current cluster and always use explorer product when not in manager', () => {
233
+ const namespace = new Namespace({});
216
234
 
217
- expect(namespace.listLocation).toStrictEqual(value);
235
+ jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({
236
+ isRancher: true,
237
+ productId: EXPLORER,
238
+ clusterId: 'c-abc',
239
+ currentProduct: { inStore: '' },
240
+ });
241
+
242
+ expect(namespace.listLocation.params.cluster).toBe('c-abc');
243
+ expect(namespace.listLocation.params.product).toBe(EXPLORER);
218
244
  });
219
245
  });
220
246
 
221
- it.todo('should return _detailLocation with a name');
247
+ describe('handling _detailLocation', () => {
248
+ const mockDetailLocation = (namespace: any, overrides: Record<string, any>) => {
249
+ jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue(overrides);
250
+ jest.spyOn(namespace, '$getters', 'get').mockReturnValue({ schemaFor: () => ({ attributes: { namespaced: false } }) });
251
+ Object.defineProperty(namespace, 'isProdRegistrationV2TopLevelProductResoure', { get: () => false });
252
+ };
253
+
254
+ it('should route to local cluster explorer when in manager product', () => {
255
+ const namespace = new Namespace({});
256
+
257
+ mockDetailLocation(namespace, { productId: MANAGER, clusterId: '_' });
258
+
259
+ const loc = namespace._detailLocation;
260
+
261
+ expect(loc.params.cluster).toBe(LOCAL_CLUSTER);
262
+ expect(loc.params.product).toBe(EXPLORER);
263
+ });
264
+
265
+ it('should use current cluster and product when not in manager product', () => {
266
+ const namespace = new Namespace({});
267
+
268
+ mockDetailLocation(namespace, { productId: EXPLORER, clusterId: 'c-abc' });
269
+
270
+ const loc = namespace._detailLocation;
271
+
272
+ expect(loc.params.cluster).toBe('c-abc');
273
+ expect(loc.params.product).toBe(EXPLORER);
274
+ });
275
+ });
222
276
  it.todo('should return the resourceQuota');
223
277
  it.todo('should set the resourceQuota as reactive Vue property');
224
278
  it.todo('should reset project with cleanForNew');
@@ -263,6 +317,7 @@ describe('class Namespace', () => {
263
317
  const namespace = new Namespace({});
264
318
 
265
319
  jest.spyOn(namespace, 'project', 'get').mockReturnValue(null);
320
+ jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({ productId: EXPLORER });
266
321
  Object.defineProperty(namespace, '_glance', { get: jest.fn(() => [{ name: 'namespace' }, { name: 'other' }]) });
267
322
 
268
323
  const result = namespace.glance;
@@ -270,6 +325,76 @@ describe('class Namespace', () => {
270
325
  expect(result).toHaveLength(1);
271
326
  expect(result[0].name).toBe('other');
272
327
  });
328
+
329
+ it('should remove type Link formatter when in manager product without local cluster access', () => {
330
+ const namespace = new Namespace({});
331
+
332
+ sideNavService.helper.clustersPinned.length = 0;
333
+ sideNavService.helper.clustersOthers.length = 0;
334
+
335
+ jest.spyOn(namespace, 'project', 'get').mockReturnValue(null);
336
+ jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({ productId: MANAGER });
337
+ Object.defineProperty(namespace, '_glance', {
338
+ get: jest.fn(() => [
339
+ {
340
+ name: 'type', formatter: 'Link', formatterOpts: { to: {}, row: {} }
341
+ },
342
+ { name: 'other' },
343
+ ])
344
+ });
345
+
346
+ const result = namespace.glance;
347
+ const typeItem = result.find((item: any) => item.name === 'type');
348
+
349
+ expect(typeItem.formatter).toBeUndefined();
350
+ expect(typeItem.formatterOpts).toBeUndefined();
351
+ });
352
+
353
+ it('should keep type Link formatter when in manager product with local cluster access', () => {
354
+ const namespace = new Namespace({});
355
+
356
+ sideNavService.helper.clustersPinned.length = 0;
357
+ sideNavService.helper.clustersPinned.push({ id: LOCAL_CLUSTER } as any);
358
+ sideNavService.helper.clustersOthers.length = 0;
359
+
360
+ jest.spyOn(namespace, 'project', 'get').mockReturnValue(null);
361
+ jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({ productId: MANAGER });
362
+ Object.defineProperty(namespace, '_glance', {
363
+ get: jest.fn(() => [
364
+ {
365
+ name: 'type', formatter: 'Link', formatterOpts: { to: {}, row: {} }
366
+ },
367
+ { name: 'other' },
368
+ ])
369
+ });
370
+
371
+ const result = namespace.glance;
372
+ const typeItem = result.find((item: any) => item.name === 'type');
373
+
374
+ expect(typeItem.formatter).toBe('Link');
375
+ expect(typeItem.formatterOpts).toBeDefined();
376
+ });
377
+
378
+ it('should keep type Link formatter when not in manager product', () => {
379
+ const namespace = new Namespace({});
380
+
381
+ jest.spyOn(namespace, 'project', 'get').mockReturnValue(null);
382
+ jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({ productId: EXPLORER });
383
+ Object.defineProperty(namespace, '_glance', {
384
+ get: jest.fn(() => [
385
+ {
386
+ name: 'type', formatter: 'Link', formatterOpts: { to: {}, row: {} }
387
+ },
388
+ { name: 'other' },
389
+ ])
390
+ });
391
+
392
+ const result = namespace.glance;
393
+ const typeItem = result.find((item: any) => item.name === 'type');
394
+
395
+ expect(typeItem.formatter).toBe('Link');
396
+ expect(typeItem.formatterOpts).toBeDefined();
397
+ });
273
398
  });
274
399
 
275
400
  describe('projectGlance', () => {
@@ -3,11 +3,23 @@ import MgmtCluster from '@shell/models/management.cattle.io.cluster';
3
3
  import { IMPORTED_DAY_2_OPS } from '@shell/config/features';
4
4
  import { OPERATION_ANNOTATIONS } from '@shell/config/labels-annotations';
5
5
  import { SETTING } from '@shell/config/settings';
6
- import { MANAGEMENT, OPERATION } from '@shell/config/types';
6
+ import { MANAGEMENT, OPERATION, LOCAL_CLUSTER, NAMESPACE } from '@shell/config/types';
7
7
  import { createOperationCR } from '@shell/utils/operation-cr';
8
+ import { NAME as EXPLORER } from '@shell/config/product/explorer';
9
+ import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
8
10
 
9
11
  jest.mock('@shell/utils/operation-cr', () => ({ createOperationCR: jest.fn() }));
10
12
 
13
+ jest.mock('@shell/components/nav/TopLevelMenu.helper', () => ({
14
+ __esModule: true,
15
+ default: {
16
+ helper: {
17
+ clustersPinned: [],
18
+ clustersOthers: []
19
+ }
20
+ }
21
+ }));
22
+
11
23
  jest.mock('@shell/utils/provider', () => ({
12
24
  isHostedProvider: jest.fn().mockImplementation((context, provider) => {
13
25
  return ['GKE', 'EKS', 'AKS'].includes(provider);
@@ -597,4 +609,48 @@ describe('class ProvCluster', () => {
597
609
  expect((createOperationCR as jest.Mock).mock.calls[0][4]).toBe('imported-cluster');
598
610
  });
599
611
  });
612
+
613
+ describe('namespaceLocation', () => {
614
+ const setLocalClusterAccess = ({ pinned, others }: { pinned: boolean, others: boolean }) => {
615
+ sideNavService.helper.clustersPinned.length = 0;
616
+ sideNavService.helper.clustersOthers.length = 0;
617
+
618
+ if (pinned) {
619
+ sideNavService.helper.clustersPinned.push({ id: LOCAL_CLUSTER } as any);
620
+ }
621
+ if (others) {
622
+ sideNavService.helper.clustersOthers.push({ id: LOCAL_CLUSTER } as any);
623
+ }
624
+ };
625
+
626
+ it('should route to the local cluster explorer when local is in the pinned side-nav clusters', () => {
627
+ setLocalClusterAccess({ pinned: true, others: false });
628
+ const cluster = new ProvCluster({ metadata: { namespace: 'fleet-default' } });
629
+
630
+ expect(cluster.namespaceLocation).toStrictEqual({
631
+ name: 'c-cluster-product-resource-id',
632
+ params: {
633
+ cluster: LOCAL_CLUSTER,
634
+ product: EXPLORER,
635
+ resource: NAMESPACE,
636
+ id: 'fleet-default',
637
+ },
638
+ });
639
+ });
640
+
641
+ it('should route to the local cluster explorer when local is in the unpinned side-nav clusters', () => {
642
+ setLocalClusterAccess({ pinned: false, others: true });
643
+ const cluster = new ProvCluster({ metadata: { namespace: 'fleet-default' } });
644
+
645
+ expect(cluster.namespaceLocation?.params.cluster).toBe(LOCAL_CLUSTER);
646
+ expect(cluster.namespaceLocation?.params.product).toBe(EXPLORER);
647
+ });
648
+
649
+ it('should return null when the user has no access to the local cluster', () => {
650
+ setLocalClusterAccess({ pinned: false, others: false });
651
+ const cluster = new ProvCluster({ metadata: { namespace: 'fleet-default' } });
652
+
653
+ expect(cluster.namespaceLocation).toBeNull();
654
+ });
655
+ });
600
656
  });