@rancher/shell 0.3.17 → 0.3.18

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 (110) hide show
  1. package/assets/translations/en-us.yaml +8 -4
  2. package/assets/translations/zh-hans.yaml +64 -8
  3. package/components/AsyncButton.vue +1 -1
  4. package/components/Inactivity.vue +10 -0
  5. package/components/LazyImage.vue +2 -2
  6. package/components/PromptRestore.vue +7 -5
  7. package/components/ResourceDetail/Masthead.vue +1 -1
  8. package/components/ResourceDetail/index.vue +4 -2
  9. package/components/__tests__/PromptRestore.test.ts +72 -0
  10. package/components/auth/AzureWarning.vue +1 -1
  11. package/components/fleet/FleetResources.vue +3 -64
  12. package/components/form/FileImageSelector.vue +9 -0
  13. package/components/form/FileSelector.vue +2 -1
  14. package/components/form/MatchExpressions.vue +1 -3
  15. package/components/form/__tests__/FileImageSelector.test.ts +42 -0
  16. package/components/form/__tests__/FileSelector.test.ts +76 -0
  17. package/components/formatter/ClusterProvider.vue +3 -1
  18. package/components/formatter/__tests__/ClusterProvider.test.ts +24 -0
  19. package/components/nav/WindowManager/ContainerShell.vue +60 -36
  20. package/components/nav/WindowManager/__tests__/ContainerShell.test.ts +561 -0
  21. package/config/labels-annotations.js +2 -1
  22. package/config/persistentVolume.ts +108 -0
  23. package/config/product/manager.js +5 -1
  24. package/config/types.js +2 -0
  25. package/core/plugin-helpers.js +19 -3
  26. package/core/types.ts +4 -0
  27. package/detail/fleet.cattle.io.gitrepo.vue +10 -2
  28. package/detail/pod.vue +36 -3
  29. package/detail/workload/index.vue +40 -9
  30. package/edit/__tests__/ui.cattle.io.navlink.test.ts +110 -0
  31. package/edit/fleet.cattle.io.clustergroup.vue +14 -3
  32. package/edit/persistentvolume/__tests__/persistentvolume.test.ts +82 -0
  33. package/edit/persistentvolume/index.vue +2 -1
  34. package/edit/persistentvolume/plugins/csi.vue +3 -1
  35. package/edit/persistentvolume/plugins/longhorn.vue +12 -12
  36. package/edit/provisioning.cattle.io.cluster/RegistryConfigs.vue +15 -11
  37. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  38. package/edit/provisioning.cattle.io.cluster/rke2.vue +5 -1
  39. package/edit/storage.k8s.io.storageclass/index.vue +1 -2
  40. package/edit/ui.cattle.io.navlink.vue +213 -187
  41. package/layouts/default.vue +1 -1
  42. package/list/group.principal.vue +1 -1
  43. package/middleware/authenticated.js +12 -4
  44. package/mixins/create-edit-view/impl.js +2 -2
  45. package/models/chart.js +1 -1
  46. package/models/fleet.cattle.io.cluster.js +33 -4
  47. package/models/fleet.cattle.io.gitrepo.js +112 -38
  48. package/models/management.cattle.io.kontainerdriver.js +14 -0
  49. package/models/persistentvolume.js +2 -111
  50. package/models/pod.js +30 -0
  51. package/models/rke.cattle.io.etcdsnapshot.js +10 -7
  52. package/package.json +1 -1
  53. package/pages/c/_cluster/auth/group.principal/assign-edit.vue +1 -1
  54. package/pages/c/_cluster/auth/roles/index.vue +1 -1
  55. package/pages/c/_cluster/explorer/index.vue +1 -1
  56. package/pages/c/_cluster/manager/cloudCredential/_id.vue +0 -1
  57. package/pages/c/_cluster/manager/cloudCredential/create.vue +0 -1
  58. package/pages/c/_cluster/settings/brand.vue +11 -8
  59. package/pages/c/_cluster/uiplugins/index.vue +9 -4
  60. package/pages/home.vue +1 -1
  61. package/plugins/dashboard-store/__tests__/actions.spec.ts +165 -0
  62. package/plugins/dashboard-store/__tests__/getters.spec.ts +100 -0
  63. package/plugins/dashboard-store/__tests__/{mutations.spec.js → mutations.spec.ts} +2 -2
  64. package/plugins/dashboard-store/actions.js +1 -1
  65. package/plugins/dashboard-store/resource-class.js +4 -0
  66. package/plugins/steve/__tests__/getters.spec.ts +93 -0
  67. package/plugins/steve/getters.js +21 -1
  68. package/plugins/steve/subscribe.js +1 -3
  69. package/rancher-components/components/BadgeState/BadgeState.spec.ts +12 -0
  70. package/rancher-components/components/BadgeState/BadgeState.vue +111 -0
  71. package/rancher-components/components/BadgeState/index.ts +1 -0
  72. package/rancher-components/components/Banner/Banner.test.ts +63 -0
  73. package/rancher-components/components/Banner/Banner.vue +244 -0
  74. package/rancher-components/components/Banner/index.ts +1 -0
  75. package/rancher-components/components/Card/Card.test.ts +37 -0
  76. package/rancher-components/components/Card/Card.vue +167 -0
  77. package/rancher-components/components/Card/index.ts +1 -0
  78. package/rancher-components/components/Form/Checkbox/Checkbox.test.ts +68 -0
  79. package/rancher-components/components/Form/Checkbox/Checkbox.vue +420 -0
  80. package/rancher-components/components/Form/Checkbox/index.ts +1 -0
  81. package/rancher-components/components/Form/LabeledInput/LabeledInput.test.ts +23 -0
  82. package/rancher-components/components/Form/LabeledInput/LabeledInput.vue +355 -0
  83. package/rancher-components/components/Form/LabeledInput/index.ts +1 -0
  84. package/rancher-components/components/Form/Radio/RadioButton.test.ts +31 -0
  85. package/rancher-components/components/Form/Radio/RadioButton.vue +287 -0
  86. package/rancher-components/components/Form/Radio/RadioGroup.vue +254 -0
  87. package/rancher-components/components/Form/Radio/index.ts +2 -0
  88. package/rancher-components/components/Form/TextArea/TextAreaAutoGrow.vue +170 -0
  89. package/rancher-components/components/Form/TextArea/index.ts +1 -0
  90. package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.test.ts +94 -0
  91. package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.vue +149 -0
  92. package/rancher-components/components/Form/ToggleSwitch/index.ts +1 -0
  93. package/rancher-components/components/Form/index.ts +5 -0
  94. package/rancher-components/components/LabeledTooltip/LabeledTooltip.vue +151 -0
  95. package/rancher-components/components/LabeledTooltip/index.ts +1 -0
  96. package/rancher-components/components/StringList/StringList.test.ts +484 -0
  97. package/rancher-components/components/StringList/StringList.vue +611 -0
  98. package/rancher-components/components/StringList/index.ts +1 -0
  99. package/scripts/typegen.sh +10 -2
  100. package/store/index.js +1 -3
  101. package/store/store-types.js +2 -0
  102. package/types/api.d.ts +1 -0
  103. package/types/fleet.d.ts +1 -0
  104. package/types/shell/index.d.ts +695 -2
  105. package/types/userPreferences.d.ts +1 -1
  106. package/utils/__mocks__/socket.js +21 -0
  107. package/utils/grafana.js +23 -11
  108. package/utils/selector.js +2 -1
  109. package/utils/validators/formRules/index.ts +3 -3
  110. package/plugins/steve/urloptions.js +0 -47
package/config/types.js CHANGED
@@ -151,6 +151,8 @@ export const LONGHORN = {
151
151
  VOLUMES: 'longhorn.io.volume',
152
152
  };
153
153
 
154
+ export const LONGHORN_DRIVER = 'driver.longhorn.io';
155
+
154
156
  export const SNAPSHOT = 'rke.cattle.io.etcdsnapshot';
155
157
 
156
158
  // --------------------------------------
@@ -1,7 +1,9 @@
1
1
  import { ActionLocation, CardLocation, ExtensionPoint } from '@shell/core/types';
2
2
  import { isMac } from '@shell/utils/platform';
3
3
  import { ucFirst, randomStr } from '@shell/utils/string';
4
- import { _EDIT, _CONFIG, _DETAIL, _LIST } from '@shell/config/query-params';
4
+ import {
5
+ _EDIT, _CONFIG, _DETAIL, _LIST, _CREATE
6
+ } from '@shell/config/query-params';
5
7
  import { getProductFromRoute } from '@shell/middleware/authenticated';
6
8
  import { isEqual } from '@shell/utils/object';
7
9
 
@@ -21,15 +23,18 @@ function checkRouteProduct({ name, params, query }, locationConfigParam) {
21
23
  }
22
24
 
23
25
  function checkRouteMode({ name, query }, locationConfigParam) {
24
- if (locationConfigParam === _EDIT && query.mode && query.mode === _EDIT) {
26
+ if (locationConfigParam === _EDIT && query.mode && query.mode === _EDIT && !query.as) {
25
27
  return true;
26
28
  } else if (locationConfigParam === _CONFIG && query.as && query.as === _CONFIG) {
27
29
  return true;
28
- } else if (locationConfigParam === _DETAIL && name.includes('-id')) {
30
+ } else if (locationConfigParam === _DETAIL && !query.as && name.includes('-id') && (!query.mode || query?.mode !== _EDIT)) {
29
31
  return true;
30
32
  // alias to target all list views
31
33
  } else if (locationConfigParam === _LIST && !name.includes('-id') && name.includes('-resource')) {
32
34
  return true;
35
+ // alias to target create views
36
+ } else if (locationConfigParam === _CREATE && name.endsWith('-create')) {
37
+ return true;
33
38
  }
34
39
 
35
40
  return false;
@@ -52,6 +57,7 @@ function checkExtensionRouteBinding($route, locationConfig, context) {
52
57
  'cluster',
53
58
  'id',
54
59
  'mode',
60
+ 'path',
55
61
  // url query params
56
62
  'queryParam',
57
63
  // Custom context specific params provided by the extension, not to be confused with location params
@@ -79,8 +85,18 @@ function checkExtensionRouteBinding($route, locationConfig, context) {
79
85
  } else if (param === 'context') {
80
86
  // Need all keys and values to match
81
87
  res = isEqual(locationConfigParam, context);
88
+ // evaluate queryParam in route
82
89
  } else if (param === 'queryParam') {
83
90
  res = isEqual(locationConfigParam, $route.query);
91
+ // evaluate path in route
92
+ } else if (param === 'path' && locationConfigParam.urlPath) {
93
+ if (locationConfigParam.endsWith) {
94
+ res = $route.path.endsWith(locationConfigParam.urlPath);
95
+ } else if (!Object.keys(locationConfigParam).includes('exact') || locationConfigParam.exact) {
96
+ res = locationConfigParam.urlPath === $route.path;
97
+ } else {
98
+ res = $route.path.includes(locationConfigParam.urlPath);
99
+ }
84
100
  } else if (locationConfigParam === params[param]) {
85
101
  res = true;
86
102
  } else {
package/core/types.ts CHANGED
@@ -139,6 +139,10 @@ export type LocationConfig = {
139
139
  cluster?: string[],
140
140
  id?: string[],
141
141
  mode?: string[],
142
+ /**
143
+ * path match from URL (excludes host address)
144
+ */
145
+ path?: { [key: string]: string | boolean}[],
142
146
  /**
143
147
  * Query Params from URL
144
148
  */
@@ -31,8 +31,9 @@ export default {
31
31
 
32
32
  data() {
33
33
  return {
34
- allFleet: [],
35
- allBundles: []
34
+ allFleet: [],
35
+ allBundles: [],
36
+ allBundleDeployments: [],
36
37
  };
37
38
  },
38
39
 
@@ -79,6 +80,12 @@ export default {
79
80
  inStoreType: 'management',
80
81
  type: FLEET.BUNDLE
81
82
  },
83
+
84
+ allBundleDeployments: {
85
+ inStoreType: 'management',
86
+ type: FLEET.BUNDLE_DEPLOYMENT
87
+ },
88
+
82
89
  allFleet: {
83
90
  inStoreType: 'management',
84
91
  type: FLEET.CLUSTER
@@ -89,6 +96,7 @@ export default {
89
96
  }
90
97
  }, this.$store);
91
98
 
99
+ this.allBundleDeployments = allDispatches.allBundleDeployments || [];
92
100
  this.allBundles = allDispatches.allBundles || [];
93
101
  this.allFleet = allDispatches.allFleet || [];
94
102
  },
package/detail/pod.vue CHANGED
@@ -14,6 +14,8 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
14
14
  import day from 'dayjs';
15
15
  import { DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
16
16
  import { escapeHtml } from '@shell/utils/string';
17
+ import { NAMESPACE } from '@shell/config/types';
18
+ import { PROJECT } from '@shell/config/labels-annotations';
17
19
 
18
20
  const POD_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-pod-containers-1/rancher-pod-containers?orgId=1';
19
21
  const POD_METRICS_SUMMARY_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-pod-1/rancher-pod?orgId=1';
@@ -33,6 +35,18 @@ export default {
33
35
 
34
36
  async fetch() {
35
37
  this.showMetrics = await allDashboardsExist(this.$store, this.currentCluster.id, [POD_METRICS_DETAIL_URL, POD_METRICS_SUMMARY_URL]);
38
+ if (!this.showMetrics) {
39
+ const namespace = await this.$store.dispatch('cluster/find', { type: NAMESPACE, id: this.value.metadata.namespace });
40
+
41
+ const projectId = namespace?.metadata?.labels[PROJECT];
42
+
43
+ if (projectId) {
44
+ this.POD_PROJECT_METRICS_DETAIL_URL = `/api/v1/namespaces/cattle-project-${ projectId }-monitoring/services/http:cattle-project-${ projectId }-monitoring-grafana:80/proxy/d/rancher-pod-containers-1/rancher-pod-containers?orgId=1'`;
45
+ this.POD_PROJECT_METRICS_SUMMARY_URL = `/api/v1/namespaces/cattle-project-${ projectId }-monitoring/services/http:cattle-project-${ projectId }-monitoring-grafana:80/proxy/d/rancher-pod-1/rancher-pod?orgId=1`;
46
+
47
+ this.showProjectMetrics = await allDashboardsExist(this.$store, this.currentCluster.id, [this.POD_PROJECT_METRICS_DETAIL_URL, this.POD_PROJECT_METRICS_SUMMARY_URL], 'cluster', projectId);
48
+ }
49
+ }
36
50
  },
37
51
 
38
52
  data() {
@@ -45,10 +59,13 @@ export default {
45
59
  return {
46
60
  POD_METRICS_DETAIL_URL,
47
61
  POD_METRICS_SUMMARY_URL,
62
+ POD_PROJECT_METRICS_DETAIL_URL: '',
63
+ POD_PROJECT_METRICS_SUMMARY_URL: '',
48
64
  POD_OPTION,
49
- showMetrics: false,
50
- selection: POD_OPTION,
51
- metricsID: null,
65
+ showMetrics: false,
66
+ showProjectMetrics: false,
67
+ selection: POD_OPTION,
68
+ metricsID: null,
52
69
  };
53
70
  },
54
71
 
@@ -278,6 +295,22 @@ export default {
278
295
  />
279
296
  </template>
280
297
  </Tab>
298
+ <Tab
299
+ v-if="showProjectMetrics"
300
+ :label="t('workload.container.titles.metrics')"
301
+ name="pod-metrics"
302
+ :weight="2.5"
303
+ >
304
+ <template #default="props">
305
+ <DashboardMetrics
306
+ v-if="props.active"
307
+ :detail-url="POD_PROJECT_METRICS_DETAIL_URL"
308
+ :summary-url="POD_PROJECT_METRICS_SUMMARY_URL"
309
+ :vars="graphVars"
310
+ graph-height="550px"
311
+ />
312
+ </template>
313
+ </Tab>
281
314
  </ResourceTabs>
282
315
  </template>
283
316
  <style scoped>
@@ -2,7 +2,7 @@
2
2
  import CreateEditView from '@shell/mixins/create-edit-view';
3
3
  import { NAMESPACE as NAMESPACE_COL } from '@shell/config/table-headers';
4
4
  import {
5
- POD, WORKLOAD_TYPES, SCALABLE_WORKLOAD_TYPES, SERVICE, INGRESS, NODE
5
+ POD, WORKLOAD_TYPES, SCALABLE_WORKLOAD_TYPES, SERVICE, INGRESS, NODE, NAMESPACE,
6
6
  } from '@shell/config/types';
7
7
  import ResourceTable from '@shell/components/ResourceTable';
8
8
  import Tab from '@shell/components/Tabbed/Tab';
@@ -16,6 +16,7 @@ import { mapGetters } from 'vuex';
16
16
  import { allDashboardsExist } from '@shell/utils/grafana';
17
17
  import PlusMinus from '@shell/components/form/PlusMinus';
18
18
  import { matches } from '@shell/utils/selector';
19
+ import { PROJECT } from '@shell/config/labels-annotations';
19
20
 
20
21
  const SCALABLE_TYPES = Object.values(SCALABLE_WORKLOAD_TYPES);
21
22
  const WORKLOAD_METRICS_DETAIL_URL = '/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/d/rancher-workload-pods-1/rancher-workload-pods?orgId=1';
@@ -85,23 +86,37 @@ export default {
85
86
  const isMetricsSupportedKind = METRICS_SUPPORTED_KINDS.includes(this.value.type);
86
87
 
87
88
  this.showMetrics = isMetricsSupportedKind && await allDashboardsExist(this.$store, this.currentCluster.id, [WORKLOAD_METRICS_DETAIL_URL, WORKLOAD_METRICS_SUMMARY_URL]);
89
+ if (!this.showMetrics) {
90
+ const namespace = await this.$store.dispatch('cluster/find', { type: NAMESPACE, id: this.value.metadata.namespace });
88
91
 
92
+ const projectId = namespace?.metadata?.labels[PROJECT];
93
+
94
+ if (projectId) {
95
+ this.WORKLOAD_PROJECT_METRICS_DETAIL_URL = `/api/v1/namespaces/cattle-project-${ projectId }-monitoring/services/http:cattle-project-${ projectId }-monitoring-grafana:80/proxy/d/rancher-pod-containers-1/rancher-workload-pods?orgId=1'`;
96
+ this.WORKLOAD_PROJECT_METRICS_SUMMARY_URL = `/api/v1/namespaces/cattle-project-${ projectId }-monitoring/services/http:cattle-project-${ projectId }-monitoring-grafana:80/proxy/d/rancher-pod-1/rancher-workload?orgId=1`;
97
+
98
+ this.showProjectMetrics = await allDashboardsExist(this.$store, this.currentCluster.id, [this.WORKLOAD_PROJECT_METRICS_DETAIL_URL, this.WORKLOAD_PROJECT_METRICS_SUMMARY_URL], 'cluster', projectId);
99
+ }
100
+ }
89
101
  this.findMatchingServices();
90
102
  this.findMatchingIngresses();
91
103
  },
92
104
 
93
105
  data() {
94
106
  return {
95
- allPods: [],
96
- allServices: [],
97
- allIngresses: [],
98
- matchingServices: [],
99
- matchingIngresses: [],
100
- allJobs: [],
101
- allNodes: [],
107
+ allPods: [],
108
+ allServices: [],
109
+ allIngresses: [],
110
+ matchingServices: [],
111
+ matchingIngresses: [],
112
+ allJobs: [],
113
+ allNodes: [],
102
114
  WORKLOAD_METRICS_DETAIL_URL,
103
115
  WORKLOAD_METRICS_SUMMARY_URL,
104
- showMetrics: false,
116
+ POD_PROJECT_METRICS_DETAIL_URL: '',
117
+ POD_PROJECT_METRICS_SUMMARY_URL: '',
118
+ showMetrics: false,
119
+ showProjectMetrics: false,
105
120
  };
106
121
  },
107
122
 
@@ -421,6 +436,22 @@ export default {
421
436
  />
422
437
  </template>
423
438
  </Tab>
439
+ <Tab
440
+ v-if="showProjectMetrics"
441
+ :label="t('workload.container.titles.metrics')"
442
+ name="workload-metrics"
443
+ :weight="3"
444
+ >
445
+ <template #default="props">
446
+ <DashboardMetrics
447
+ v-if="props.active"
448
+ :detail-url="WORKLOAD_PROJECT_METRICS_DETAIL_URL"
449
+ :summary-url="WORKLOAD_PROJECT_METRICS_SUMMARY_URL"
450
+ :vars="graphVars"
451
+ graph-height="550px"
452
+ />
453
+ </template>
454
+ </Tab>
424
455
  <Tab
425
456
  v-if="v1MonitoringUrl"
426
457
  name="v1Metrics"
@@ -0,0 +1,110 @@
1
+ /* eslint-disable jest/no-hooks */
2
+ import { mount } from '@vue/test-utils';
3
+ import Navlink from '@shell/edit/ui.cattle.io.navlink.vue';
4
+ import { _CREATE } from '@shell/config/query-params';
5
+ import CruResource from '@shell/components/CruResource';
6
+
7
+ describe('view: ui.cattle.io.navlink should', () => {
8
+ const name = 'test';
9
+ const url = 'http://test.com';
10
+ let wrapper: any;
11
+
12
+ const requiredSetup = () => ({
13
+ // Remove all these mocks after migration to Vue 2.7/3 due mixin logic
14
+ mocks: {
15
+ $store: {
16
+ getters: {
17
+ currentStore: () => 'current_store',
18
+ 'current_store/schemaFor': jest.fn(),
19
+ 'current_store/all': jest.fn(),
20
+ 'i18n/t': (val) => val,
21
+ 'i18n/exists': jest.fn(),
22
+ }
23
+ },
24
+ $route: { query: { AS: '' } },
25
+ $router: { applyQuery: jest.fn() },
26
+ },
27
+ propsData: {
28
+ metadata: { namespace: 'test' },
29
+ spec: { template: {} },
30
+ targetInfo: { mode: 'all' },
31
+ value: {},
32
+ mode: _CREATE,
33
+ },
34
+
35
+ });
36
+
37
+ beforeEach(() => {
38
+ wrapper = mount(Navlink, { ...requiredSetup() });
39
+ });
40
+
41
+ afterEach(() => {
42
+ wrapper.destroy();
43
+ });
44
+
45
+ it('have "Create" button disabled before fields are filled in', () => {
46
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
47
+
48
+ expect(saveButton.disabled).toBe(true);
49
+ });
50
+ it('have "Create" button disabled when Link type is URL and only name is filled in', async() => {
51
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
52
+ const nameField = wrapper.find('[data-testid="Navlink-name-field"]').find('input');
53
+
54
+ nameField.setValue(name);
55
+
56
+ await wrapper.vm.$nextTick();
57
+
58
+ expect(saveButton.disabled).toBe(true);
59
+ });
60
+ it('have "Create" button enabled when Link type is URL and all required fields are filled in', async() => {
61
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
62
+ const nameField = wrapper.find('[data-testid="Navlink-name-field"]').find('input');
63
+ const urlField = wrapper.find('[data-testid="Navlink-url-field"]');
64
+
65
+ nameField.setValue(name);
66
+ urlField.setValue(url);
67
+
68
+ await wrapper.vm.$nextTick();
69
+
70
+ expect(saveButton.disabled).toBe(false);
71
+ });
72
+
73
+ it('have "Create" button disabled when Link type is Service and and only name is filled in', async() => {
74
+ const saveButton = wrapper.find('[data-testid="form-save"]').element as HTMLInputElement;
75
+ const nameField = wrapper.find('[data-testid="Navlink-name-field"]').find('input');
76
+ const rg = wrapper.find('[data-testid="Navlink-link-radiogroup"]');
77
+
78
+ const serviceBttn = rg.findAll('.radio-label').at(1);
79
+
80
+ nameField.setValue(name);
81
+ serviceBttn.trigger('click');
82
+ await wrapper.vm.$nextTick();
83
+
84
+ expect(saveButton.disabled).toBe(true);
85
+ });
86
+
87
+ it('have "Create" button enabled when Link type is Service and and all required fields are filled in', async() => {
88
+ const nameField = wrapper.find('[data-testid="Navlink-name-field"]').find('input');
89
+ const rg = wrapper.find('[data-testid="Navlink-link-radiogroup"]');
90
+
91
+ const serviceBttn = rg.findAll('.radio-label').at(1);
92
+
93
+ nameField.setValue(name);
94
+ serviceBttn.trigger('click');
95
+ await wrapper.vm.$nextTick();
96
+
97
+ const schemeField = wrapper.find('[data-testid="Navlink-scheme-field"]');
98
+ const serviceField = wrapper.find('[data-testid="Navlink-currentService-field"]');
99
+
100
+ schemeField.find('button').trigger('click');
101
+ await wrapper.trigger('keydown.down');
102
+ await wrapper.trigger('keydown.enter');
103
+
104
+ serviceField.find('button').trigger('click');
105
+ await wrapper.trigger('keydown.down');
106
+ await wrapper.trigger('keydown.enter');
107
+
108
+ expect(CruResource.computed.canSave()).toBe(true);
109
+ });
110
+ });
@@ -11,6 +11,7 @@ import { set } from '@shell/utils/object';
11
11
  import { FLEET } from '@shell/config/types';
12
12
  import { convert, matching, simplify } from '@shell/utils/selector';
13
13
  import throttle from 'lodash/throttle';
14
+ import { allHash } from '@shell/utils/promise';
14
15
 
15
16
  export default {
16
17
  name: 'CruClusterGroup',
@@ -27,10 +28,20 @@ export default {
27
28
  mixins: [CreateEditView],
28
29
 
29
30
  async fetch() {
30
- if (this.$store.getters['management/schemaFor']( FLEET.CLUSTER )) {
31
- this.allClusters = await this.$store.getters['management/all'](FLEET.CLUSTER);
31
+ const _hash = {};
32
+
33
+ if (this.$store.getters['management/schemaFor'](FLEET.CLUSTER)) {
34
+ _hash.allClusters = await this.$store.dispatch('management/findAll', { type: FLEET.CLUSTER });
35
+ }
36
+
37
+ if (this.$store.getters['management/schemaFor'](FLEET.WORKSPACE)) {
38
+ _hash.allWorkspaces = this.$store.dispatch('management/findAll', { type: FLEET.WORKSPACE });
32
39
  }
33
- this.allWorkspaces = await this.$store.dispatch('management/findAll', { type: FLEET.WORKSPACE });
40
+
41
+ const hash = await allHash(_hash);
42
+
43
+ this.allClusters = hash.allClusters || [];
44
+ this.allWorkspaces = hash.allWorkspaces || [];
34
45
 
35
46
  if ( !this.value.spec?.selector ) {
36
47
  this.value.spec = this.value.spec || {};
@@ -0,0 +1,82 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import PersistentVolume from '@shell/edit/persistentvolume/index';
3
+ import { ExtendedVue, Vue } from 'vue/types/vue';
4
+
5
+ describe('view: PersistentVolume', () => {
6
+ it('should list enabled PV storage option if supported', () => {
7
+ const plugin = {
8
+ labelKey: 'persistentVolume.csi.label', supported: true, value: 'csi'
9
+ };
10
+ const resource = 'PersistentVolume';
11
+ const wrapper = mount(PersistentVolume as ExtendedVue<Vue, {}, {}, {}, PersistentVolume>, {
12
+ propsData: { value: { spec: { } } },
13
+ mocks: {
14
+ $store: {
15
+ dispatch: () => jest.fn(),
16
+ getters: {
17
+ 'i18n/t': jest.fn(),
18
+ 'i18n/exists': jest.fn(),
19
+ currentStore: () => 'cluster',
20
+ 'features/get': () => jest.fn(),
21
+ 'prefs/get': () => resource,
22
+ 'cluster/schemaFor': () => {},
23
+ 'cluster/all': () => [{}],
24
+ }
25
+ },
26
+ $fetchState: {
27
+ pending: false, error: true, timestamp: Date.now()
28
+ },
29
+ $route: {
30
+ params: { resource },
31
+ query: { AS: '' },
32
+ hash: '',
33
+ },
34
+ $router: {
35
+ currentRoute: {},
36
+ replace: jest.fn(),
37
+ }
38
+ },
39
+ stubs: { LabeledSelect: true }
40
+ });
41
+
42
+ const select = wrapper.find('[data-testid="persistent-volume-plugin-select"]');
43
+
44
+ expect((select.vm as unknown as any).options).toStrictEqual(expect.arrayContaining([plugin]));
45
+ });
46
+
47
+ it('should select current plugin', () => {
48
+ const plugin = 'csi';
49
+ const resource = 'PersistentVolume';
50
+ const wrapper = mount(PersistentVolume as ExtendedVue<Vue, {}, {}, {}, PersistentVolume>, {
51
+ propsData: { value: { spec: { [plugin]: { value: plugin } } } },
52
+ mocks: {
53
+ $store: {
54
+ dispatch: () => jest.fn(),
55
+ getters: {
56
+ 'i18n/t': jest.fn(),
57
+ 'i18n/exists': jest.fn(),
58
+ currentStore: () => 'cluster',
59
+ 'features/get': () => jest.fn(),
60
+ 'prefs/get': () => resource,
61
+ 'cluster/schemaFor': () => {},
62
+ 'cluster/all': () => [{}],
63
+ }
64
+ },
65
+ $fetchState: {
66
+ pending: false, error: true, timestamp: Date.now()
67
+ },
68
+ $route: {
69
+ params: { resource },
70
+ query: { AS: '' },
71
+ hash: '',
72
+ },
73
+ $router: {
74
+ currentRoute: {},
75
+ replace: jest.fn(),
76
+ }
77
+ }
78
+ });
79
+
80
+ expect(wrapper.vm.plugin).toBe(plugin);
81
+ });
82
+ });
@@ -13,9 +13,9 @@ import NodeAffinity from '@shell/components/form/NodeAffinity';
13
13
  import { Checkbox } from '@components/Form/Checkbox';
14
14
  import uniq from 'lodash/uniq';
15
15
  import UnitInput from '@shell/components/form/UnitInput';
16
+ import { VOLUME_PLUGINS, LONGHORN_PLUGIN } from '@shell/config/persistentVolume';
16
17
  import { NODE, PVC, STORAGE_CLASS } from '@shell/config/types';
17
18
  import Loading from '@shell/components/Loading';
18
- import { LONGHORN_PLUGIN, VOLUME_PLUGINS } from '@shell/models/persistentvolume';
19
19
  import { _CREATE, _VIEW } from '@shell/config/query-params';
20
20
  import { clone } from '@shell/utils/object';
21
21
  import InfoBox from '@shell/components/InfoBox';
@@ -258,6 +258,7 @@ export default {
258
258
  :label="t('persistentVolume.plugin.label')"
259
259
  :localized-label="true"
260
260
  option-label="labelKey"
261
+ data-testid="persistent-volume-plugin-select"
261
262
  :options="plugins"
262
263
  :mode="modeOverride"
263
264
  :required="true"
@@ -70,7 +70,9 @@ export default {
70
70
  },
71
71
  methods: {
72
72
  selectDriver(e) {
73
- set(this.value, 'spec.csi.driver', e.value ? e.value : e.label);
73
+ const name = e.value || e.label || e;
74
+
75
+ set(this.value, 'spec.csi.driver', name);
74
76
  }
75
77
  }
76
78
  };
@@ -3,7 +3,7 @@ import KeyValue from '@shell/components/form/KeyValue';
3
3
  import { LabeledInput } from '@components/Form/LabeledInput';
4
4
  import { RadioGroup } from '@components/Form/Radio';
5
5
  import { _CREATE } from '@shell/config/query-params';
6
- import { LONGHORN_DRIVER } from '@shell/models/persistentvolume';
6
+ import { LONGHORN_DRIVER } from '@shell/config/types';
7
7
 
8
8
  export default {
9
9
  components: {
@@ -54,20 +54,20 @@ export default {
54
54
  <div>
55
55
  <div class="row mb-20">
56
56
  <div class="col span-6">
57
- <LabeledInput
58
- v-model="value.spec.csi.fsType"
59
- :mode="mode"
60
- :label="t('persistentVolume.shared.filesystemType.label')"
61
- :placeholder="t('persistentVolume.shared.filesystemType.placeholder')"
57
+ <LabeledInput
58
+ v-model="value.spec.csi.fsType"
59
+ :mode="mode"
60
+ :label="t('persistentVolume.shared.filesystemType.label')"
61
+ :placeholder="t('persistentVolume.shared.filesystemType.placeholder')"
62
62
  />
63
63
  </div>
64
64
  <div class="col span-6">
65
- <LabeledInput
66
- v-model="value.spec.csi.volumeHandle"
67
- :mode="mode"
68
- :label="t('persistentVolume.longhorn.volumeHandle.label')"
69
- :placeholder="t('persistentVolume.longhorn.volumeHandle.placeholder')"
70
- :required="true"
65
+ <LabeledInput
66
+ v-model="value.spec.csi.volumeHandle"
67
+ :mode="mode"
68
+ :label="t('persistentVolume.longhorn.volumeHandle.label')"
69
+ :placeholder="t('persistentVolume.longhorn.volumeHandle.placeholder')"
70
+ :required="true"
71
71
  />
72
72
  </div>
73
73
  </div>
@@ -43,14 +43,28 @@ export default {
43
43
  const configMap = this.value.spec.rkeConfig?.registries?.configs || {};
44
44
  const entries = [];
45
45
 
46
+ const defaultAddValue = {
47
+ hostname: '',
48
+ authConfigSecretName: null,
49
+ caBundle: '',
50
+ insecureSkipVerify: false,
51
+ tlsSecretName: null,
52
+ };
53
+
46
54
  for ( const hostname in configMap ) {
55
+ if (configMap[hostname]) {
56
+ configMap[hostname].insecureSkipVerify = configMap[hostname].insecureSkipVerify ?? defaultAddValue.insecureSkipVerify;
57
+ configMap[hostname].authConfigSecretName = configMap[hostname].authConfigSecretName ?? defaultAddValue.authConfigSecretName;
58
+ configMap[hostname].caBundle = configMap[hostname].caBundle ?? defaultAddValue.caBundle;
59
+ configMap[hostname].tlsSecretName = configMap[hostname].tlsSecretName ?? defaultAddValue.tlsSecretName;
60
+ }
47
61
  entries.push({
48
62
  hostname,
49
63
  ...configMap[hostname],
50
64
  });
51
65
  }
52
66
 
53
- return { entries };
67
+ return { entries, defaultAddValue };
54
68
  },
55
69
 
56
70
  computed: {
@@ -59,16 +73,6 @@ export default {
59
73
  return TYPES.TLS;
60
74
  },
61
75
  },
62
-
63
- defaultAddValue() {
64
- return {
65
- hostname: '',
66
- authConfigSecretName: null,
67
- caBundle: '',
68
- insecureSkipVerify: false,
69
- tlsSecretName: null,
70
- };
71
- },
72
76
  },
73
77
 
74
78
  mounted() {
@@ -17,7 +17,7 @@ import { CATALOG } from '@shell/config/labels-annotations';
17
17
  import { CAPI, MANAGEMENT, DEFAULT_WORKSPACE } from '@shell/config/types';
18
18
  import { mapFeature, RKE2 as RKE2_FEATURE } from '@shell/store/features';
19
19
  import { allHash } from '@shell/utils/promise';
20
- import { BLANK_CLUSTER } from '@shell/store';
20
+ import { BLANK_CLUSTER } from '@shell/store/store-types.js';
21
21
  import { ELEMENTAL_PRODUCT_NAME, ELEMENTAL_CLUSTER_PROVIDER } from '../../config/elemental-types';
22
22
  import Rke2Config from './rke2';
23
23
  import Import from './import';
@@ -1645,7 +1645,11 @@ export default {
1645
1645
  const harvesterKubeconfigSecret = await this.createKubeconfigSecret(kubeconfig);
1646
1646
 
1647
1647
  set(this.agentConfig, 'cloud-provider-config', `secret://fleet-default:${ harvesterKubeconfigSecret?.metadata?.name }`);
1648
- set(this.chartValues, `${ HARVESTER_CLOUD_PROVIDER }.clusterName`, this.value.metadata.name);
1648
+
1649
+ if (this.isCreate) {
1650
+ set(this.chartValues, `${ HARVESTER_CLOUD_PROVIDER }.global.cattle.clusterName`, this.value.metadata.name);
1651
+ }
1652
+
1649
1653
  set(this.chartValues, `${ HARVESTER_CLOUD_PROVIDER }.cloudConfigPath`, '/var/lib/rancher/rke2/etc/config-files/cloud-provider-config');
1650
1654
  }
1651
1655
  } catch (err) {
@@ -11,8 +11,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
11
11
  import { _CREATE, _VIEW } from '@shell/config/query-params';
12
12
  import { PROVISIONER_OPTIONS } from '@shell/models/storage.k8s.io.storageclass';
13
13
  import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
14
- import { CSI_DRIVER } from '@shell/config/types';
15
- import { LONGHORN_DRIVER } from '@shell/models/persistentvolume';
14
+ import { CSI_DRIVER, LONGHORN_DRIVER } from '@shell/config/types';
16
15
 
17
16
  export default {
18
17
  name: 'StorageClass',