@rancher/shell 2.0.1 → 2.0.2

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 (112) hide show
  1. package/assets/translations/en-us.yaml +73 -34
  2. package/assets/translations/zh-hans.yaml +1 -0
  3. package/components/AssignTo.vue +2 -0
  4. package/components/PromptRemove.vue +8 -3
  5. package/components/Questions/index.vue +2 -2
  6. package/components/ResourceDetail/Masthead.vue +1 -0
  7. package/components/auth/RoleDetailEdit.vue +5 -4
  8. package/components/fleet/FleetClusters.vue +0 -3
  9. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  10. package/components/form/ProjectMemberEditor.vue +1 -1
  11. package/components/form/ResourceLabeledSelect.vue +11 -3
  12. package/components/form/labeled-select-utils/labeled-select.utils.ts +1 -1
  13. package/components/formatter/CloudCredExpired.vue +69 -0
  14. package/components/formatter/Date.vue +1 -1
  15. package/components/nav/Header.vue +9 -5
  16. package/components/nav/TopLevelMenu.vue +115 -51
  17. package/components/nav/__tests__/TopLevelMenu.test.ts +53 -27
  18. package/config/labels-annotations.js +2 -0
  19. package/config/pagination-table-headers.js +5 -4
  20. package/config/roles.ts +34 -19
  21. package/config/router/navigation-guards/attempt-first-login.js +1 -1
  22. package/config/router/navigation-guards/authentication.js +1 -1
  23. package/config/router/navigation-guards/i18n.js +1 -1
  24. package/config/router/navigation-guards/index.js +2 -1
  25. package/config/router/navigation-guards/load-initial-settings.js +1 -1
  26. package/config/router/navigation-guards/runtime-extension-route.js +31 -0
  27. package/config/router/routes.js +10 -1
  28. package/config/uiplugins.js +130 -61
  29. package/core/plugin.ts +5 -0
  30. package/core/plugins.js +7 -1
  31. package/detail/catalog.cattle.io.app.vue +17 -4
  32. package/detail/fleet.cattle.io.cluster.vue +11 -9
  33. package/detail/fleet.cattle.io.gitrepo.vue +1 -1
  34. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +86 -13
  35. package/edit/provisioning.cattle.io.cluster/__tests__/DirectoryConfig.test.ts +3 -134
  36. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +209 -0
  37. package/edit/provisioning.cattle.io.cluster/index.vue +8 -4
  38. package/edit/provisioning.cattle.io.cluster/rke2.vue +128 -17
  39. package/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest.vue +50 -0
  40. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +29 -64
  41. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +42 -3
  42. package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +22 -86
  43. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +8 -2
  44. package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +61 -0
  45. package/initialize/entry-helpers.js +4 -21
  46. package/list/provisioning.cattle.io.cluster.vue +56 -5
  47. package/mixins/__tests__/chart.test.ts +4 -1
  48. package/mixins/chart.js +36 -16
  49. package/models/__tests__/apps.deployment.test.ts +93 -0
  50. package/models/apps.deployment.js +18 -4
  51. package/models/catalog.cattle.io.app.js +108 -21
  52. package/models/cloudcredential.js +159 -2
  53. package/models/fleet.cattle.io.gitrepo.js +4 -13
  54. package/models/management.cattle.io.cluster.js +15 -4
  55. package/models/management.cattle.io.user.js +3 -3
  56. package/models/nodedriver.js +5 -0
  57. package/models/provisioning.cattle.io.cluster.js +41 -3
  58. package/package.json +1 -1
  59. package/pages/404.vue +15 -0
  60. package/pages/auth/login.vue +4 -1
  61. package/pages/auth/setup.vue +4 -1
  62. package/pages/c/_cluster/apps/charts/install.vue +2 -1
  63. package/pages/c/_cluster/explorer/__tests__/index.test.ts +1 -1
  64. package/pages/c/_cluster/explorer/index.vue +6 -2
  65. package/pages/c/_cluster/fleet/index.vue +11 -5
  66. package/pages/c/_cluster/manager/cloudCredential/index.vue +68 -4
  67. package/pages/c/_cluster/manager/jwt.authentication/index.vue +10 -4
  68. package/pages/c/_cluster/settings/performance.vue +2 -2
  69. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +7 -10
  70. package/pages/c/_cluster/uiplugins/index.vue +28 -18
  71. package/pages/home.vue +2 -13
  72. package/plugins/dashboard-store/actions.js +1 -1
  73. package/plugins/dashboard-store/getters.js +1 -1
  74. package/plugins/steve/__tests__/getters.test.ts +5 -5
  75. package/plugins/steve/getters.js +6 -4
  76. package/plugins/steve/hybrid-class.js +1 -5
  77. package/scripts/extension/bundle +1 -1
  78. package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +1 -1
  79. package/scripts/publish-shell.sh +56 -59
  80. package/scripts/test-plugins-build.sh +45 -39
  81. package/scripts/typegen.sh +26 -23
  82. package/store/type-map.js +4 -2
  83. package/types/shell/index.d.ts +10 -0
  84. package/types/store/pagination.types.ts +1 -1
  85. package/utils/cluster.js +9 -0
  86. package/utils/settings.ts +3 -1
  87. package/utils/string.js +9 -0
  88. package/utils/v-sphere.ts +251 -0
  89. package/creators/app/app.package.json +0 -14
  90. package/creators/app/files/.eslintignore +0 -16
  91. package/creators/app/files/.eslintrc.js +0 -173
  92. package/creators/app/files/.gitignore +0 -70
  93. package/creators/app/files/.gitlab-ci.yml +0 -14
  94. package/creators/app/files/.vscode/settings.json +0 -21
  95. package/creators/app/files/babel.config.js +0 -1
  96. package/creators/app/files/tsconfig.json +0 -42
  97. package/creators/app/files/vue.config.js +0 -6
  98. package/creators/app/init +0 -120
  99. package/creators/app/package.json +0 -25
  100. package/creators/pkg/files/.github/workflows/build-extension-catalog.yml +0 -24
  101. package/creators/pkg/files/.github/workflows/build-extension-charts.yml +0 -22
  102. package/creators/pkg/files/babel.config.js +0 -1
  103. package/creators/pkg/files/index.ts +0 -14
  104. package/creators/pkg/files/tsconfig.json +0 -53
  105. package/creators/pkg/files/vue.config.js +0 -1
  106. package/creators/pkg/init +0 -286
  107. package/creators/pkg/package.json +0 -19
  108. package/creators/pkg/pkg.package.json +0 -21
  109. package/creators/pkg/vue-shim.ts +0 -4
  110. package/creators/update/init +0 -56
  111. package/creators/update/package.json +0 -20
  112. package/creators/update/upgrade +0 -56
@@ -1,16 +1,12 @@
1
1
 
2
2
  <script>
3
- import { Checkbox } from '@components/Form/Checkbox';
4
3
  import { LabeledInput } from '@components/Form/LabeledInput';
5
4
  import { _CREATE } from '@shell/config/query-params';
6
5
 
7
6
  export default {
8
7
  name: 'DirectoryConfig',
9
- components: {
10
- Checkbox,
11
- LabeledInput,
12
- },
13
- props: {
8
+ components: { LabeledInput },
9
+ props: {
14
10
  mode: {
15
11
  type: String,
16
12
  required: true,
@@ -21,50 +17,11 @@ export default {
21
17
  required: true,
22
18
  },
23
19
  },
24
- data() {
25
- let atLeastOneDirWithAnIdentifier = false;
26
- let allDirsWithSameIdentifier = false;
27
-
28
- if (this.value.systemAgent.length || this.value.provisioning.length || this.value.k8sDistro.length) {
29
- atLeastOneDirWithAnIdentifier = true;
30
- if (this.value.systemAgent === this.value.provisioning && this.value.provisioning === this.value.k8sDistro &&
31
- this.value.systemAgent === this.value.k8sDistro) {
32
- allDirsWithSameIdentifier = true;
33
- }
34
- }
35
-
36
- return {
37
- isSettingCommonConfig: !(atLeastOneDirWithAnIdentifier && !allDirsWithSameIdentifier),
38
- commonConfig: allDirsWithSameIdentifier ? this.value.systemAgent : '',
39
- };
40
- },
41
- watch: {
42
- commonConfig(neu) {
43
- if (neu && neu.length && this.isSettingCommonConfig) {
44
- this.value.systemAgent = neu;
45
- this.value.provisioning = neu;
46
- this.value.k8sDistro = neu;
47
- }
48
- }
49
- },
50
20
  computed: {
51
21
  disableEditInput() {
52
22
  return this.mode !== _CREATE;
53
23
  }
54
- },
55
- methods: {
56
- handleCommonConfig(val) {
57
- this.isSettingCommonConfig = val;
58
-
59
- if (val) {
60
- this.value.systemAgent = '';
61
- this.value.provisioning = '';
62
- this.value.k8sDistro = '';
63
- } else {
64
- this.commonConfig = '';
65
- }
66
- }
67
- },
24
+ }
68
25
  };
69
26
  </script>
70
27
 
@@ -74,54 +31,33 @@ export default {
74
31
  <h3 class="mb-20">
75
32
  {{ t('cluster.directoryConfig.title') }}
76
33
  </h3>
77
- <Checkbox
78
- class="mb-10"
79
- :value="isSettingCommonConfig"
34
+ <LabeledInput
35
+ v-model="value.systemAgent"
36
+ class="mb-20"
37
+ :mode="mode"
38
+ :label="t('cluster.directoryConfig.systemAgent.label')"
39
+ :tooltip="t('cluster.directoryConfig.systemAgent.tooltip')"
40
+ :disabled="disableEditInput"
41
+ data-testid="rke2-directory-config-systemAgent-data-dir"
42
+ />
43
+ <LabeledInput
44
+ v-model="value.provisioning"
45
+ class="mb-20"
80
46
  :mode="mode"
81
- :label="t('cluster.directoryConfig.checkboxText')"
47
+ :label="t('cluster.directoryConfig.provisioning.label')"
48
+ :tooltip="t('cluster.directoryConfig.provisioning.tooltip')"
82
49
  :disabled="disableEditInput"
83
- data-testid="rke2-directory-config-individual-config-checkbox"
84
- @input="handleCommonConfig"
50
+ data-testid="rke2-directory-config-provisioning-data-dir"
85
51
  />
86
52
  <LabeledInput
87
- v-if="isSettingCommonConfig"
88
- v-model="commonConfig"
53
+ v-model="value.k8sDistro"
89
54
  class="mb-20"
90
55
  :mode="mode"
91
- :label="t('cluster.directoryConfig.common.label')"
92
- :tooltip="t('cluster.directoryConfig.common.tooltip')"
56
+ :label="t('cluster.directoryConfig.k8sDistro.label')"
57
+ :tooltip="t('cluster.directoryConfig.k8sDistro.tooltip')"
93
58
  :disabled="disableEditInput"
94
- data-testid="rke2-directory-config-common-data-dir"
59
+ data-testid="rke2-directory-config-k8sDistro-data-dir"
95
60
  />
96
- <div v-if="!isSettingCommonConfig">
97
- <LabeledInput
98
- v-model="value.systemAgent"
99
- class="mb-20"
100
- :mode="mode"
101
- :label="t('cluster.directoryConfig.systemAgent.label')"
102
- :tooltip="t('cluster.directoryConfig.systemAgent.tooltip')"
103
- :disabled="disableEditInput"
104
- data-testid="rke2-directory-config-systemAgent-data-dir"
105
- />
106
- <LabeledInput
107
- v-model="value.provisioning"
108
- class="mb-20"
109
- :mode="mode"
110
- :label="t('cluster.directoryConfig.provisioning.label')"
111
- :tooltip="t('cluster.directoryConfig.provisioning.tooltip')"
112
- :disabled="disableEditInput"
113
- data-testid="rke2-directory-config-provisioning-data-dir"
114
- />
115
- <LabeledInput
116
- v-model="value.k8sDistro"
117
- class="mb-20"
118
- :mode="mode"
119
- :label="t('cluster.directoryConfig.k8sDistro.label')"
120
- :tooltip="t('cluster.directoryConfig.k8sDistro.tooltip')"
121
- :disabled="disableEditInput"
122
- data-testid="rke2-directory-config-k8sDistro-data-dir"
123
- />
124
- </div>
125
61
  <div class="mb-40" />
126
62
  </div>
127
63
  </div>
@@ -7,6 +7,7 @@ import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthS
7
7
  import CreateEditView from '@shell/mixins/create-edit-view';
8
8
  import SecretSelector from '@shell/components/form/SecretSelector';
9
9
  import { SECRET_TYPES as TYPES } from '@shell/config/secret';
10
+ import { base64Decode, base64Encode } from '@shell/utils/crypto';
10
11
 
11
12
  export default {
12
13
  components: {
@@ -55,7 +56,7 @@ export default {
55
56
  if (configMap[hostname]) {
56
57
  configMap[hostname].insecureSkipVerify = configMap[hostname].insecureSkipVerify ?? defaultAddValue.insecureSkipVerify;
57
58
  configMap[hostname].authConfigSecretName = configMap[hostname].authConfigSecretName ?? defaultAddValue.authConfigSecretName;
58
- configMap[hostname].caBundle = configMap[hostname].caBundle ?? defaultAddValue.caBundle;
59
+ configMap[hostname].caBundle = base64Decode(configMap[hostname].caBundle ?? defaultAddValue.caBundle);
59
60
  configMap[hostname].tlsSecretName = configMap[hostname].tlsSecretName ?? defaultAddValue.tlsSecretName;
60
61
  }
61
62
  entries.push({
@@ -94,7 +95,11 @@ export default {
94
95
  continue;
95
96
  }
96
97
 
97
- configs[h] = { ...entry };
98
+ configs[h] = {
99
+ ...entry,
100
+ caBundle: base64Encode(entry.caBundle)
101
+ };
102
+
98
103
  delete configs[h].hostname;
99
104
  }
100
105
 
@@ -177,6 +182,7 @@ export default {
177
182
 
178
183
  <LabeledInput
179
184
  v-model="row.value.caBundle"
185
+ :data-testid="`registry-caBundle-${i}`"
180
186
  class="mt-20"
181
187
  type="multiline"
182
188
  label="CA Cert Bundle"
@@ -0,0 +1,61 @@
1
+ import { mount, Wrapper } from '@vue/test-utils';
2
+ import { clone } from '@shell/utils/object';
3
+ import { _EDIT } from '@shell/config/query-params';
4
+ import { PROV_CLUSTER } from '@shell/edit/provisioning.cattle.io.cluster/__tests__/utils/cluster';
5
+ import RegistryConfigs from '@shell/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue';
6
+
7
+ describe('component: RegistryConfigs', () => {
8
+ let wrapper: Wrapper<InstanceType<typeof RegistryConfigs> & { [key: string]: any }>;
9
+
10
+ const mountOptions = {
11
+ propsData: {
12
+ value: {},
13
+ mode: _EDIT,
14
+ clusterRegisterBeforeHook: () => {}
15
+ },
16
+ stubs: {
17
+ SelectOrCreateAuthSecret: true,
18
+ SecretSelector: true,
19
+ },
20
+ mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
21
+ };
22
+
23
+ describe('key CA Cert Bundle', () => {
24
+ it('should display default key', () => {
25
+ const value = clone(PROV_CLUSTER);
26
+
27
+ value.spec.rkeConfig.registries.configs = { foo: { caBundle: 'Zm9vYmFy' } };
28
+
29
+ mountOptions.propsData.value = value;
30
+
31
+ wrapper = mount(
32
+ RegistryConfigs,
33
+ mountOptions
34
+ );
35
+
36
+ const registry = wrapper.find('[data-testid^="registry-caBundle"]').element as HTMLTextAreaElement;
37
+
38
+ expect(registry.value).toBe('foobar');
39
+ });
40
+
41
+ it('should update key in base64 format', async() => {
42
+ const value = clone(PROV_CLUSTER);
43
+
44
+ value.spec.rkeConfig.registries.configs = { foo: { caBundle: 'Zm9vYmFy' } };
45
+
46
+ mountOptions.propsData.value = value;
47
+
48
+ wrapper = mount(
49
+ RegistryConfigs,
50
+ mountOptions
51
+ );
52
+
53
+ const registry = wrapper.find('[data-testid^="registry-caBundle"]');
54
+
55
+ await registry.setValue('ssh key');
56
+ wrapper.vm.update();
57
+
58
+ expect(wrapper.emitted('updateConfigs')![0][0]['foo']['caBundle']).toBe('c3NoIGtleQ==');
59
+ });
60
+ });
61
+ });
@@ -3,7 +3,6 @@ import { updatePageTitle } from '@shell/utils/title';
3
3
  import { getVendor } from '@shell/config/private-label';
4
4
  import middleware from '@shell/config/middleware.js';
5
5
  import { withQuery } from 'ufo';
6
- import dynamicPluginLoader from '@shell/pkg/dynamic-plugin-loader';
7
6
 
8
7
  // Global variable used on mount, updated on route change and used in the render function
9
8
  let app;
@@ -191,30 +190,14 @@ async function render(to, from, next) {
191
190
  next: _next.bind(this)
192
191
  });
193
192
 
193
+ if (this.$loading.start && !this.$loading.manual) {
194
+ this.$loading.start();
195
+ }
196
+
194
197
  // Get route's matched components
195
198
  const matches = [];
196
199
  const Components = getMatchedComponents(to, matches);
197
200
 
198
- // If no Components matched, generate 404
199
- if (!Components.length) {
200
- // Handle the loading of dynamic plugins (Harvester) because we only want to attempt to load those plugins and routes if we first couldn't find a page.
201
- // We should probably get rid of this concept entirely and just load plugins at the start.
202
- await app.context.store.dispatch('loadManagement');
203
- const newLocation = await dynamicPluginLoader.check({ route: { path: window.location.pathname }, store: app.context.store });
204
-
205
- // If we have a new location, double check that it's actually valid
206
- const resolvedRoute = newLocation?.path ? app.context.store.app.router.resolve({ path: newLocation.path.replace(/^\/{0,1}dashboard/, '') }) : null;
207
-
208
- if (resolvedRoute?.route.matched.length) {
209
- // Note - don't use `redirect` or `store.app.route` (breaks feature by failing to run middleware in default layout)
210
- return next(resolvedRoute.resolved.path);
211
- }
212
-
213
- errorRedirect(this, new Error('404: This page could not be found'));
214
-
215
- return next();
216
- }
217
-
218
201
  try {
219
202
  // Call middleware
220
203
  await callMiddleware.call(this, Components, app.context);
@@ -10,10 +10,11 @@ import { mapFeature, HARVESTER as HARVESTER_FEATURE } from '@shell/store/feature
10
10
  import { NAME as EXPLORER } from '@shell/config/product/explorer';
11
11
  import ResourceFetch from '@shell/mixins/resource-fetch';
12
12
  import { BadgeState } from '@components/BadgeState';
13
+ import CloudCredExpired from '@shell/components/formatter/CloudCredExpired';
13
14
 
14
15
  export default {
15
16
  components: {
16
- Banner, ResourceTable, Masthead, BadgeState
17
+ Banner, ResourceTable, Masthead, BadgeState, CloudCredExpired
17
18
  },
18
19
  mixins: [ResourceFetch],
19
20
  props: {
@@ -41,6 +42,8 @@ export default {
41
42
  mgmtClusters: this.$fetchType(MANAGEMENT.CLUSTER),
42
43
  };
43
44
 
45
+ this.$store.dispatch('rancher/findAll', { type: NORMAN.CLOUD_CREDENTIAL });
46
+
44
47
  if ( this.$store.getters['management/canList'](SNAPSHOT) ) {
45
48
  hash.etcdSnapshots = this.$fetchType(SNAPSHOT);
46
49
  }
@@ -141,6 +144,29 @@ export default {
141
144
  // This will be used when there's clusters from extension based provisioners
142
145
  // We should re-visit this for scaling reasons
143
146
  return this.filteredRows.some((c) => c.metadata.namespace !== 'fleet-local' && c.metadata.namespace !== 'fleet-default');
147
+ },
148
+
149
+ tokenExpiredData() {
150
+ const counts = this.rows.reduce((res, provCluster) => {
151
+ const expireData = provCluster.cloudCredential?.expireData;
152
+
153
+ if (expireData?.expiring) {
154
+ res.expiring++;
155
+ }
156
+ if (expireData?.expired) {
157
+ res.expired++;
158
+ }
159
+
160
+ return res;
161
+ }, {
162
+ expiring: 0,
163
+ expired: 0
164
+ });
165
+
166
+ return {
167
+ expiring: counts.expiring ? this.t('cluster.cloudCredentials.banners.expiring', { count: counts.expiring }) : '',
168
+ expired: counts.expired ? this.t('cluster.cloudCredentials.banners.expired', { count: counts.expired }) : '',
169
+ };
144
170
  }
145
171
  },
146
172
 
@@ -186,6 +212,17 @@ export default {
186
212
  </template>
187
213
  </Masthead>
188
214
 
215
+ <Banner
216
+ v-if="tokenExpiredData.expiring"
217
+ color="warning"
218
+ :label="tokenExpiredData.expiring"
219
+ />
220
+ <Banner
221
+ v-if="tokenExpiredData.expired"
222
+ color="error"
223
+ :label="tokenExpiredData.expired"
224
+ />
225
+
189
226
  <ResourceTable
190
227
  :schema="schema"
191
228
  :rows="filteredRows"
@@ -194,6 +231,7 @@ export default {
194
231
  :use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
195
232
  :data-testid="'cluster-list'"
196
233
  :force-update-live-and-delayed="forceUpdateLiveAndDelayed"
234
+ :sub-rows="true"
197
235
  >
198
236
  <!-- Why are state column and subrow overwritten here? -->
199
237
  <!-- for rke1 clusters, where they try to use the mgmt cluster stateObj instead of prov cluster stateObj, -->
@@ -207,19 +245,32 @@ export default {
207
245
  </template>
208
246
  <template #sub-row="{fullColspan, row, keyField, componentTestid, i, onRowMouseEnter, onRowMouseLeave}">
209
247
  <tr
210
- v-if="row.stateDescription"
211
248
  :key="row[keyField] + '-description'"
212
249
  :data-testid="componentTestid + '-' + i + '-row-description'"
213
250
  class="state-description sub-row"
214
251
  @mouseenter="onRowMouseEnter"
215
252
  @mouseleave="onRowMouseLeave"
216
253
  >
217
- <td>&nbsp;</td>
254
+ <td v-if="row.cloudCredentialWarning || row.stateDescription">
255
+ &nbsp;
256
+ </td>
218
257
  <td
258
+ v-if="row.cloudCredentialWarning || row.stateDescription"
219
259
  :colspan="fullColspan - 1"
220
- :class="{ 'text-error' : row.stateObj.error }"
221
260
  >
222
- {{ row.stateDescription }}
261
+ <CloudCredExpired
262
+ v-if="row.cloudCredentialWarning"
263
+ :value="row.cloudCredential.expires"
264
+ :row="row.cloudCredential"
265
+ :verbose="true"
266
+ :class="{'mb-10': row.stateDescription}"
267
+ />
268
+ <div
269
+ v-if="row.stateDescription"
270
+ :class="{ 'text-error' : row.stateObj.error }"
271
+ >
272
+ {{ row.stateDescription }}
273
+ </div>
223
274
  </td>
224
275
  </tr>
225
276
  </template>
@@ -24,8 +24,9 @@ describe('chartMixin', () => {
24
24
  localVue.mixin(ChartMixin);
25
25
 
26
26
  it.each(testCases.opa)(
27
- 'should add OPA deprecation warning properly', (chartId, expected) => {
27
+ 'should add OPA deprecation warning properly', async(chartId, expected) => {
28
28
  const store = new Vuex.Store({
29
+ actions: { 'catalog/load': () => {} },
29
30
  getters: {
30
31
  currentCluster: () => {},
31
32
  isRancher: () => true,
@@ -44,6 +45,8 @@ describe('chartMixin', () => {
44
45
 
45
46
  instance.$route = { query: { chart: 'chart_name' } };
46
47
 
48
+ await instance.fetchChart();
49
+
47
50
  const warnings = instance.warnings;
48
51
 
49
52
  expect(warnings).toHaveLength(expected);
package/mixins/chart.js CHANGED
@@ -27,6 +27,8 @@ export default {
27
27
  existing: null,
28
28
 
29
29
  ignoreWarning: false,
30
+
31
+ chart: null,
30
32
  };
31
33
  },
32
34
 
@@ -35,20 +37,6 @@ export default {
35
37
 
36
38
  showPreRelease: mapPref(SHOW_PRE_RELEASE),
37
39
 
38
- chart() {
39
- if ( this.repo && this.query.chartName ) {
40
- return this.$store.getters['catalog/chart']({
41
- repoType: this.query.repoType,
42
- repoName: this.query.repoName,
43
- chartName: this.query.chartName,
44
- includeHidden: true,
45
- showDeprecated: this.showDeprecated
46
- });
47
- }
48
-
49
- return null;
50
- },
51
-
52
40
  repo() {
53
41
  return this.$store.getters['catalog/repo']({
54
42
  repoType: this.query.repoType,
@@ -258,11 +246,39 @@ export default {
258
246
  },
259
247
 
260
248
  methods: {
249
+ /**
250
+ * Populate `this.chart`
251
+ *
252
+ * `chart` used to be a computed property pointing at getter catalog/chart
253
+ *
254
+ * this however stopped recalculating given changes to the store
255
+ *
256
+ * (the store would populate a charts collection, which the getter uses to find the chart,
257
+ * however this did not kick off the computed property, so this.charts was not populated)
258
+ *
259
+ * Now we find and cache the chart
260
+ */
261
+ fetchStoreChart() {
262
+ if (!this.chart && this.repo && this.query.chartName) {
263
+ this.chart = this.$store.getters['catalog/chart']({
264
+ repoType: this.query.repoType,
265
+ repoName: this.query.repoName,
266
+ chartName: this.query.chartName,
267
+ includeHidden: true,
268
+ showDeprecated: this.showDeprecated
269
+ });
270
+ }
271
+
272
+ return this.chart;
273
+ },
274
+
261
275
  async fetchChart() {
262
276
  this.versionInfoError = null;
263
277
 
264
278
  await this.$store.dispatch('catalog/load'); // not the problem
265
279
 
280
+ this.fetchStoreChart();
281
+
266
282
  if ( this.query.appNamespace && this.query.appName ) {
267
283
  // First check the URL query for an app name and namespace.
268
284
  // Use those values to check for a catalog app resource.
@@ -275,6 +291,8 @@ export default {
275
291
  id: `${ this.query.appNamespace }/${ this.query.appName }`,
276
292
  });
277
293
 
294
+ await this.existing?.fetchValues(true);
295
+
278
296
  this.mode = _EDIT;
279
297
  } catch (e) {
280
298
  this.mode = _CREATE;
@@ -434,10 +452,12 @@ export default {
434
452
  }
435
453
  }
436
454
  if (existingCRDApp) {
455
+ await existingCRDApp.fetchValues(true);
456
+
437
457
  // spec.values are any non-default values the user configured
438
458
  // the installation form should show these, as well as any default values from the chart
439
- const existingValues = clone(existingCRDApp.spec?.values || {});
440
- const defaultValues = clone(existingCRDApp.spec?.chart?.values || {});
459
+ const existingValues = clone(existingCRDApp.values || {});
460
+ const defaultValues = clone(existingCRDApp.chartValues || {});
441
461
 
442
462
  crdVersionInfo.existingValues = existingValues;
443
463
  crdVersionInfo.allValues = merge(defaultValues, existingValues);
@@ -0,0 +1,93 @@
1
+ import Deployment from '@shell/models/apps.deployment';
2
+ import { WORKLOAD_TYPES } from '@shell/config/types';
3
+
4
+ describe('class Deployment', () => {
5
+ describe('replicaSetId', () => {
6
+ it.each([{
7
+ relationships: [],
8
+ expected: undefined,
9
+ }, {
10
+ relationships: [{
11
+ rel: 'owner',
12
+ toType: WORKLOAD_TYPES.REPLICA_SET,
13
+ toId: 'rel-id'
14
+ }],
15
+ expected: 'rel-id',
16
+ }, {
17
+ relationships: [{
18
+ rel: 'owner',
19
+ toType: WORKLOAD_TYPES.REPLICA_SET,
20
+ toId: 'rel-id-1',
21
+ message: 'ReplicaSet is available. Replicas: 1'
22
+ }],
23
+ expected: 'rel-id-1',
24
+ }, {
25
+ relationships: [{
26
+ rel: 'owner',
27
+ toType: WORKLOAD_TYPES.REPLICA_SET,
28
+ toId: 'rel-id-1',
29
+ message: 'ReplicaSet is available. Replicas: 0'
30
+ }, {
31
+ rel: 'owner',
32
+ toType: WORKLOAD_TYPES.REPLICA_SET,
33
+ toId: 'rel-id-2',
34
+ message: 'ReplicaSet is available. Replicas: 1'
35
+ }],
36
+ expected: 'rel-id-2',
37
+ }, {
38
+ relationships: [{
39
+ rel: 'owner',
40
+ toType: WORKLOAD_TYPES.REPLICA_SET,
41
+ toId: 'rel-id-1',
42
+ message: 'Message without replicas count'
43
+ }, {
44
+ rel: 'owner',
45
+ toType: WORKLOAD_TYPES.REPLICA_SET,
46
+ toId: 'rel-id-2',
47
+ message: 'Another message without replicas count'
48
+ }],
49
+ expected: 'rel-id-1',
50
+ }, {
51
+ relationships: [{
52
+ rel: 'owner',
53
+ toType: WORKLOAD_TYPES.REPLICA_SET,
54
+ toId: 'rel-id-1',
55
+ message: 'ReplicaSet is available. Replicas: 0'
56
+ }, {
57
+ rel: 'owner',
58
+ toType: WORKLOAD_TYPES.REPLICA_SET,
59
+ toId: 'rel-id-2',
60
+ message: 'ReplicaSet is available. Replicas: 0'
61
+ }],
62
+ expected: 'rel-id-1',
63
+ }, {
64
+ relationships: [{
65
+ rel: 'owner',
66
+ toType: WORKLOAD_TYPES.REPLICA_SET,
67
+ toId: 'rel-id-1',
68
+ message: 'Message without replicas count'
69
+ }, {
70
+ rel: 'owner',
71
+ toType: WORKLOAD_TYPES.REPLICA_SET,
72
+ toId: 'rel-id-2',
73
+ message: 'ReplicaSet is available. Replicas: 0'
74
+ }],
75
+ expected: 'rel-id-1',
76
+ }])('replicaSetId', ({ relationships, expected }) => {
77
+ const deploymentData = {
78
+ id: 'any-id',
79
+ type: WORKLOAD_TYPES.DEPLOYMENT,
80
+ metadata: {
81
+ name: 'any-name',
82
+ namespace: 'any-namespace',
83
+ uid: 'any-uid',
84
+ relationships,
85
+ },
86
+ };
87
+
88
+ const deployment = new Deployment(deploymentData);
89
+
90
+ expect(deployment.replicaSetId).toStrictEqual(expected);
91
+ });
92
+ });
93
+ });
@@ -10,14 +10,28 @@ const IGNORED_ANNOTATIONS = [
10
10
  'deprecated.deployment.rollback.to',
11
11
  ];
12
12
 
13
+ const replicasRegEx = /Replicas: (\d+)/;
14
+
13
15
  export default class Deployment extends Workload {
14
16
  get replicaSetId() {
15
- const set = this.metadata?.relationships?.find((relationship) => {
16
- return relationship.rel === 'owner' &&
17
- relationship.toType === WORKLOAD_TYPES.REPLICA_SET;
17
+ const relationships = this.metadata?.relationships || [];
18
+
19
+ // Find all relevant ReplicaSet relationships
20
+ const replicaSetRelationships = relationships.filter((relationship) => relationship.rel === 'owner' && relationship.toType === WORKLOAD_TYPES.REPLICA_SET
21
+ );
22
+
23
+ // Filter the ReplicaSets based on replicas > 0
24
+ const activeReplicaSet = replicaSetRelationships.find((relationship) => {
25
+ const replicasMatch = relationship.message?.match(replicasRegEx);
26
+ const replicas = replicasMatch ? parseInt(replicasMatch[1], 10) : 0;
27
+
28
+ return replicas > 0;
18
29
  });
19
30
 
20
- return set?.toId?.replace(`${ this.namespace }/`, '');
31
+ // If no active ReplicaSet is found, fall back to the first one from the list
32
+ const selectedReplicaSet = activeReplicaSet || replicaSetRelationships[0];
33
+
34
+ return selectedReplicaSet?.toId?.replace(`${ this.namespace }/`, '');
21
35
  }
22
36
 
23
37
  async rollBack(cluster, deployment, revision) {