@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
@@ -4,7 +4,7 @@ import {
4
4
  import { CATALOG as CATALOG_ANNOTATIONS, FLEET } from '@shell/config/labels-annotations';
5
5
  import { compare, isPrerelease, sortable } from '@shell/utils/version';
6
6
  import { filterBy } from '@shell/utils/array';
7
- import { CATALOG, MANAGEMENT, NORMAN } from '@shell/config/types';
7
+ import { CATALOG, MANAGEMENT, NORMAN, SECRET } from '@shell/config/types';
8
8
  import { SHOW_PRE_RELEASE } from '@shell/store/prefs';
9
9
  import { set } from '@shell/utils/object';
10
10
 
@@ -279,28 +279,115 @@ export default class CatalogApp extends SteveModel {
279
279
  };
280
280
  }
281
281
 
282
- get deployedAsLegacy() {
283
- return async() => {
284
- if (this.spec?.values?.global) {
285
- const { clusterName, projectName } = this.spec.values.global;
286
-
287
- if (clusterName && projectName) {
288
- try {
289
- const legacyApp = await this.$dispatch('rancher/find', {
290
- type: NORMAN.APP,
291
- id: `${ projectName }:${ this.metadata?.name }`,
292
- opt: { url: `/v3/project/${ clusterName }:${ projectName }/apps/${ projectName }:${ this.metadata?.name }` }
293
- }, { root: true });
294
-
295
- if (legacyApp) {
296
- return legacyApp;
297
- }
298
- } catch (e) {}
299
- }
282
+ async deployedAsLegacy() {
283
+ await this.fetchValues();
284
+
285
+ if (this.values?.global) {
286
+ const { clusterName, projectName } = this.values.global;
287
+
288
+ if (clusterName && projectName) {
289
+ try {
290
+ const legacyApp = await this.$dispatch('rancher/find', {
291
+ type: NORMAN.APP,
292
+ id: `${ projectName }:${ this.metadata?.name }`,
293
+ opt: { url: `/v3/project/${ clusterName }:${ projectName }/apps/${ projectName }:${ this.metadata?.name }` }
294
+ }, { root: true });
295
+
296
+ if (legacyApp) {
297
+ return legacyApp;
298
+ }
299
+ } catch (e) {}
300
300
  }
301
+ }
301
302
 
302
- return false;
303
- };
303
+ return false;
304
+ }
305
+
306
+ /**
307
+ * User and Chart values live in a helm secret, so fetch it (with special param)
308
+ */
309
+ async fetchValues(force = false) {
310
+ if (!this.secretId) {
311
+ // If there's no secret id this isn't ever going to work, no need to carry on
312
+ return;
313
+ }
314
+
315
+ const haveValues = !!this._values && !!this._chartValues;
316
+
317
+ if (haveValues && !force) {
318
+ // If we already have the required values and we're not forced to re-fetch, no need to carry on
319
+ return;
320
+ }
321
+
322
+ try {
323
+ await this.$dispatch('find', {
324
+ type: SECRET,
325
+ id: this.secretId,
326
+ opt: {
327
+ force: force || (!!this._secret && !haveValues), // force if explicitly requested or there's ean existing secret without the required values we have a secret without the values in (Secret has been fetched another way)
328
+ watch: false, // Cannot watch with custom params (they are dropped on calls made when resyncing over socket)
329
+ params: { includeHelmData: true }
330
+ }
331
+ });
332
+ } catch (e) {
333
+ console.error(`Cannot find values for ${ this.id } (unable to fetch)`, e); // eslint-disable-line no-console
334
+ }
335
+ }
336
+
337
+ get secretId() {
338
+ const metadata = this.metadata;
339
+ const secretReference = metadata.ownerReferences?.find((ow) => ow.kind.toLowerCase() === SECRET);
340
+
341
+ const secretId = secretReference?.name;
342
+ const secretNamespace = metadata.namespace;
343
+
344
+ if (!secretNamespace || !secretId) {
345
+ console.warn(`Cannot find values for ${ this.id } (cannot find related secret namespace or id)`); // eslint-disable-line no-console
346
+
347
+ return null;
348
+ }
349
+
350
+ return `${ secretNamespace }/${ secretId }`;
351
+ }
352
+
353
+ get _secret() {
354
+ return this.secretId ? this.$getters['byId'](SECRET, this.secretId) : null;
355
+ }
356
+
357
+ _validateSecret(noun) {
358
+ if (this._secret === undefined) {
359
+ throw new Error(`Cannot find ${ noun } for ${ this.id } (chart secret has not been fetched via app \`fetchValues\`)`);
360
+ }
361
+
362
+ if (this._secret === null) {
363
+ throw new Error(`Cannot find ${ noun } for ${ this.id } (chart secret cannot or has failed to fetch) `);
364
+ }
365
+ }
366
+
367
+ /**
368
+ * The user's helm values
369
+ */
370
+ get values() {
371
+ this._validateSecret('values');
372
+
373
+ return this._values;
374
+ }
375
+
376
+ get _values() {
377
+ return this._secret?.data?.release?.config;
378
+ }
379
+
380
+ /**
381
+ * The Charts default helm values
382
+ */
383
+ get chartValues() {
384
+ this._validateSecret('chartValues');
385
+
386
+ return this._chartValues;
387
+ }
388
+
389
+ get _chartValues() {
390
+ return this._secret?.data?.release?.chart?.values;
304
391
  }
305
392
  }
306
393
 
@@ -1,11 +1,65 @@
1
- import { CAPI } from '@shell/config/labels-annotations';
1
+ import { CAPI, CLOUD_CREDENTIALS } from '@shell/config/labels-annotations';
2
2
  import { fullFields, prefixFields, simplify, suffixFields } from '@shell/store/plugins';
3
3
  import { isEmpty, set } from '@shell/utils/object';
4
- import { SECRET } from '@shell/config/types';
4
+ import { MANAGEMENT, SECRET } from '@shell/config/types';
5
5
  import { escapeHtml } from '@shell/utils/string';
6
6
  import NormanModel from '@shell/plugins/steve/norman-class';
7
+ import { DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
8
+ import day from 'dayjs';
9
+
10
+ const harvesterProvider = 'harvester';
11
+
12
+ const renew = {
13
+ [harvesterProvider]: {
14
+ renew: ({ cloudCredential, $ctx }) => {
15
+ return renew[harvesterProvider].renewBulk(
16
+ { cloudCredentials: [cloudCredential], $ctx }
17
+ );
18
+ },
19
+ renewBulk: async({ cloudCredentials, $ctx }) => {
20
+ // A harvester cloud credential (at the moment) is a kubeconfig complete with expiring token
21
+ // So to renew we just need to generate a new kubeconfig and save it to the cc (similar to shell/cloud-credential/harvester.vue)
22
+ await Promise.all(cloudCredentials.map(async(cc) => {
23
+ try {
24
+ if (!cc.harvestercredentialConfig?.clusterId) {
25
+ throw new Error(`credential has no matching harvester cluster`);
26
+ }
27
+ const mgmtCluster = $ctx.rootGetters['management/byId'](MANAGEMENT.CLUSTER, cc.harvestercredentialConfig.clusterId);
28
+
29
+ if (!mgmtCluster) {
30
+ throw new Error(`cannot find harvester cluster`);
31
+ }
32
+
33
+ const kubeconfigContent = await mgmtCluster.generateKubeConfig();
34
+
35
+ cc.setData('kubeconfigContent', kubeconfigContent);
36
+
37
+ await cc.save();
38
+ } catch (error) {
39
+ console.error(`Unable to refresh harvester cloud credential '${ cc.id }'`, error); // eslint-disable-line no-console
40
+ }
41
+ }));
42
+ }
43
+ }
44
+ };
7
45
 
8
46
  export default class CloudCredential extends NormanModel {
47
+ get _availableActions() {
48
+ const out = super._availableActions;
49
+
50
+ out.splice(0, 0, { divider: true });
51
+ out.splice(0, 0, {
52
+ action: 'renew',
53
+ enabled: this.canRenew,
54
+ bulkable: this.canBulkRenew,
55
+ bulkAction: 'renewBulk',
56
+ icon: 'icon icon-fw icon-refresh',
57
+ label: this.t('manager.cloudCredentials.renew'),
58
+ });
59
+
60
+ return out;
61
+ }
62
+
9
63
  get hasSensitiveData() {
10
64
  return true;
11
65
  }
@@ -177,4 +231,107 @@ export default class CloudCredential extends NormanModel {
177
231
  get doneRoute() {
178
232
  return 'c-cluster-manager-secret';
179
233
  }
234
+
235
+ get canRenew() {
236
+ return !!renew[this.provider]?.renew && this.expires !== undefined && this.canUpdate;
237
+ }
238
+
239
+ get canBulkRenew() {
240
+ return !!renew[this.provider]?.renewBulk;
241
+ }
242
+
243
+ get expiresForSort() {
244
+ // Why not just `expires`?
245
+ // Ensures the correct sort order:
246
+ // 'expired' --> 'expiring soon' --> 'never expires'
247
+ return this.expires !== undefined ? this.expires : Number.MAX_SAFE_INTEGER;
248
+ }
249
+
250
+ get expires() {
251
+ const expires = this.annotations[CLOUD_CREDENTIALS.EXPIRATION];
252
+
253
+ if (typeof expires === 'string') {
254
+ return parseInt(expires);
255
+ } else if (typeof expires === 'number') {
256
+ return expires;
257
+ }
258
+
259
+ return undefined; // Weird things happen if this isn't a number
260
+ }
261
+
262
+ get expireData() {
263
+ if (typeof this.expiresIn !== 'number') {
264
+ return null;
265
+ }
266
+
267
+ const sevenDays = 1000 * 60 * 60 * 24 * 7;
268
+
269
+ if (this.expiresIn === 0) {
270
+ return {
271
+ expired: true,
272
+ expiring: false,
273
+ };
274
+ } else if (this.expiresIn < sevenDays) {
275
+ return {
276
+ expired: false,
277
+ expiring: true,
278
+ };
279
+ }
280
+
281
+ return null;
282
+ }
283
+
284
+ get expiresString() {
285
+ if (this.expires === undefined) {
286
+ return '';
287
+ }
288
+
289
+ if (this.expireData.expired) {
290
+ return this.t('manager.cloudCredentials.expired');
291
+ }
292
+
293
+ const dateFormat = escapeHtml( this.$rootGetters['prefs/get'](DATE_FORMAT));
294
+ const timeFormat = escapeHtml( this.$rootGetters['prefs/get'](TIME_FORMAT));
295
+
296
+ return day(this.expires).format(`${ dateFormat } ${ timeFormat }`);
297
+ }
298
+
299
+ get expiresIn() {
300
+ if (this.expires === undefined) {
301
+ return null;
302
+ }
303
+
304
+ const timeThen = this.expires;
305
+ const timeNow = Date.now();
306
+
307
+ const expiresIn = timeThen - timeNow;
308
+
309
+ return expiresIn < 0 ? 0 : expiresIn;
310
+ }
311
+
312
+ renew() {
313
+ const renewFn = renew[this.provider]?.renew;
314
+
315
+ if (!renewFn) {
316
+ console.error('No fn renew function for ', this.provider); // eslint-disable-line no-console
317
+ }
318
+
319
+ return renewFn({
320
+ cloudCredential: this,
321
+ $ctx: this.$ctx
322
+ });
323
+ }
324
+
325
+ async renewBulk(cloudCredentials = []) {
326
+ const renewBulkFn = renew[this.provider]?.renewBulk;
327
+
328
+ if (!renewBulkFn) {
329
+ console.error('No fn renew bulk function for ', this.provider); // eslint-disable-line no-console
330
+ }
331
+
332
+ return renewBulkFn({
333
+ cloudCredentials,
334
+ $ctx: this.$ctx
335
+ });
336
+ }
180
337
  }
@@ -308,20 +308,11 @@ export default class GitRepo extends SteveModel {
308
308
  bundle.namespacedName.startsWith(`${ this.namespace }:${ this.name }`));
309
309
  }
310
310
 
311
+ /**
312
+ * Bundles with state of active
313
+ */
311
314
  get bundlesReady() {
312
- if (this.bundles && this.bundles.length) {
313
- return this.bundles.filter((bundle) => bundle.state === 'active');
314
- }
315
-
316
- return 0;
317
- }
318
-
319
- get targetClustersReady() {
320
- if (this.targetClusters && this.targetClusters.length) {
321
- return this.targetClusters.filter((cluster) => cluster.state === 'active');
322
- }
323
-
324
- return 0;
315
+ return this.bundles?.filter((bundle) => bundle.state === 'active');
325
316
  }
326
317
 
327
318
  get bundleDeployments() {
@@ -10,12 +10,14 @@ import { addParams } from '@shell/utils/url';
10
10
  import { isEmpty } from '@shell/utils/object';
11
11
  import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
12
12
  import { isHarvesterCluster } from '@shell/utils/cluster';
13
- import HybridModel from '@shell/plugins/steve/hybrid-class';
13
+ import SteveModel from '@shell/plugins/steve/steve-class';
14
14
  import { LINUX, WINDOWS } from '@shell/store/catalog';
15
15
  import { KONTAINER_TO_DRIVER } from './management.cattle.io.kontainerdriver';
16
16
  import { PINNED_CLUSTERS } from '@shell/store/prefs';
17
17
  import { copyTextToClipboard } from '@shell/utils/clipboard';
18
18
 
19
+ const DEFAULT_BADGE_COLOR = '#707070';
20
+
19
21
  // See translation file cluster.providers for list of providers
20
22
  // If the logo is not named with the provider name, add an override here
21
23
  const PROVIDER_LOGO_OVERRIDE = {};
@@ -27,7 +29,7 @@ function findRelationship(verb, type, relationships = []) {
27
29
  return relationships.find((r) => r[from] === type)?.[id];
28
30
  }
29
31
 
30
- export default class MgmtCluster extends HybridModel {
32
+ export default class MgmtCluster extends SteveModel {
31
33
  get details() {
32
34
  const out = [
33
35
  {
@@ -306,13 +308,22 @@ export default class MgmtCluster extends HybridModel {
306
308
  return undefined;
307
309
  }
308
310
 
309
- const color = this.metadata?.annotations[CLUSTER_BADGE.COLOR] || '#7f7f7f';
311
+ let color = this.metadata?.annotations[CLUSTER_BADGE.COLOR] || DEFAULT_BADGE_COLOR;
310
312
  const iconText = this.metadata?.annotations[CLUSTER_BADGE.ICON_TEXT] || '';
313
+ let foregroundColor;
314
+
315
+ try {
316
+ foregroundColor = textColor(parseColor(color.trim())); // Remove any whitespace
317
+ } catch (_e) {
318
+ // If we could not parse the badge color, use the defaults
319
+ color = DEFAULT_BADGE_COLOR;
320
+ foregroundColor = textColor(parseColor(color));
321
+ }
311
322
 
312
323
  return {
313
324
  text: comment || undefined,
314
325
  color,
315
- textColor: textColor(parseColor(color)),
326
+ textColor: foregroundColor,
316
327
  iconText: iconText.substr(0, 3)
317
328
  };
318
329
  }
@@ -105,7 +105,7 @@ export default class User extends HybridModel {
105
105
  * @returns {number}
106
106
  */
107
107
  get userLastLogin() {
108
- return this.metadata?.labels?.['cattle.io/last-login'] * 1000;
108
+ return this.metadata?.labels?.['cattle.io/last-login'] * 1000 || 0;
109
109
  }
110
110
 
111
111
  /**
@@ -113,7 +113,7 @@ export default class User extends HybridModel {
113
113
  * @returns {number}
114
114
  */
115
115
  get userDisabledIn() {
116
- return this.metadata?.labels?.['cattle.io/disable-after'] * 1000;
116
+ return this.metadata?.labels?.['cattle.io/disable-after'] * 1000 || 0;
117
117
  }
118
118
 
119
119
  /**
@@ -129,7 +129,7 @@ export default class User extends HybridModel {
129
129
  * @returns {number}
130
130
  */
131
131
  get userDeletedIn() {
132
- return this.metadata?.labels?.['cattle.io/delete-after'] * 1000;
132
+ return this.metadata?.labels?.['cattle.io/delete-after'] * 1000 || 0;
133
133
  }
134
134
 
135
135
  get state() {
@@ -1,5 +1,10 @@
1
1
  import Driver from '@shell/models/driver';
2
2
 
3
+ /**
4
+ * Overrides for spec.addCloudCredential
5
+ */
6
+ export const CLOUD_CREDENTIAL_OVERRIDE = { nutanix: true };
7
+
3
8
  export default class NodeDriver extends Driver {
4
9
  get doneRoute() {
5
10
  return 'c-cluster-manager-driver-nodedriver';
@@ -10,7 +10,6 @@ import { compare } from '@shell/utils/version';
10
10
  import { AS, MODE, _VIEW, _YAML } from '@shell/config/query-params';
11
11
  import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
12
12
  import { CAPI as CAPI_ANNOTATIONS, NODE_ARCHITECTURE } from '@shell/config/labels-annotations';
13
- import capitalize from 'lodash/capitalize';
14
13
 
15
14
  /**
16
15
  * Class representing Cluster resource.
@@ -166,6 +165,19 @@ export default class ProvCluster extends SteveModel {
166
165
  enabled: canSaveRKETemplate,
167
166
  }, { divider: true }];
168
167
 
168
+ // Harvester Cluster 1:1 Harvester Cloud Cred
169
+ if (this.cloudCredential?.canRenew || this.cloudCredential?.canBulkRenew) {
170
+ out.splice(0, 0, { divider: true });
171
+ out.splice(0, 0, {
172
+ action: 'renew',
173
+ enabled: this.cloudCredential?.canRenew,
174
+ bulkable: this.cloudCredential?.canBulkRenew,
175
+ bulkAction: 'renewBulk',
176
+ icon: 'icon icon-fw icon-refresh',
177
+ label: this.$rootGetters['i18n/t']('cluster.cloudCredentials.renew'),
178
+ });
179
+ }
180
+
169
181
  return actions.concat(out);
170
182
  }
171
183
 
@@ -411,7 +423,7 @@ export default class ProvCluster extends SteveModel {
411
423
  if (!node.metadata?.state?.transitioning) {
412
424
  const architecture = node.status?.nodeLabels?.[NODE_ARCHITECTURE];
413
425
 
414
- const key = architecture ? capitalize(architecture) : this.t('cluster.architecture.label.unknown');
426
+ const key = architecture || this.t('cluster.architecture.label.unknown');
415
427
 
416
428
  obj[key] = (obj[key] || 0) + 1;
417
429
  }
@@ -885,7 +897,8 @@ export default class ProvCluster extends SteveModel {
885
897
  get agentConfig() {
886
898
  // The one we want is the first one with no selector.
887
899
  // If there are multiple with no selector, that will fall under the unsupported message below.
888
- return this.spec.rkeConfig.machineSelectorConfig.find((x) => !x.machineLabelSelector)?.config;
900
+ return this.spec.rkeConfig?.machineSelectorConfig
901
+ ?.find((x) => !x.machineLabelSelector)?.config || { };
889
902
  }
890
903
 
891
904
  get cloudProvider() {
@@ -986,4 +999,29 @@ export default class ProvCluster extends SteveModel {
986
999
  'spec.rkeConfig.machinePools.dynamicSchemaSpec',
987
1000
  ];
988
1001
  }
1002
+
1003
+ get description() {
1004
+ return super.description || this.mgmt?.description;
1005
+ }
1006
+
1007
+ renew() {
1008
+ return this.cloudCredential?.renew();
1009
+ }
1010
+
1011
+ renewBulk(clusters = []) {
1012
+ // In theory we don't need to filter by cloudCred, but do so for safety
1013
+ const cloudCredentials = clusters.filter((c) => c.cloudCredential).map((c) => c.cloudCredential);
1014
+
1015
+ return this.cloudCredential?.renewBulk(cloudCredentials);
1016
+ }
1017
+
1018
+ get cloudCredential() {
1019
+ return this.$rootGetters['rancher/all'](NORMAN.CLOUD_CREDENTIAL).find((cc) => cc.id === this.spec.cloudCredentialSecretName);
1020
+ }
1021
+
1022
+ get cloudCredentialWarning() {
1023
+ const expireData = this.cloudCredential?.expireData;
1024
+
1025
+ return expireData?.expired || expireData?.expiring;
1026
+ }
989
1027
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
package/pages/404.vue ADDED
@@ -0,0 +1,15 @@
1
+ <script>
2
+ import Brand from '@shell/mixins/brand';
3
+
4
+ export default {
5
+ mixins: [Brand],
6
+ beforeMount() {
7
+ this.$store.commit('setError', { error: new Error('404: This page could not be found') });
8
+ this.$router.replace('/fail-whale');
9
+ }
10
+ };
11
+ </script>
12
+
13
+ <template>
14
+ <div class="dashboard-root" />
15
+ </template>
@@ -288,7 +288,10 @@ export default {
288
288
  </script>
289
289
 
290
290
  <template>
291
- <Loading v-if="$fetchState.pending" />
291
+ <Loading
292
+ v-if="$fetchState.pending"
293
+ mode="relative"
294
+ />
292
295
  <main
293
296
  v-else
294
297
  class="main-layout login"
@@ -276,7 +276,10 @@ export default {
276
276
  </script>
277
277
 
278
278
  <template>
279
- <Loading v-if="$fetchState.pending" />
279
+ <Loading
280
+ v-if="$fetchState.pending"
281
+ mode="relative"
282
+ />
280
283
  <form
281
284
  v-else
282
285
  class="setup"
@@ -302,8 +302,9 @@ export default {
302
302
  */
303
303
  userValues = diff(this.loadedVersionValues, this.chartValues);
304
304
  } else if ( this.existing ) {
305
+ await this.existing.fetchValues(); // In theory this has already been called, but do again to be safe
305
306
  /* For an already installed app, use the values from the previous install. */
306
- userValues = clone(this.existing.spec?.values || {});
307
+ userValues = clone(this.existing.values || {});
307
308
  } else {
308
309
  /* For an new app, start empty. */
309
310
  userValues = {};
@@ -163,7 +163,7 @@ describe('page: cluster dashboard', () => {
163
163
  ['created', 'glance.created', []],
164
164
  ['architecture', 'mixed', [{ labels: { [NODE_ARCHITECTURE]: 'amd64' } }, { labels: { [NODE_ARCHITECTURE]: 'intel' } }]],
165
165
  ['architecture', 'mixed', [{ labels: { [NODE_ARCHITECTURE]: 'amd64' } }, { labels: { } }]],
166
- ['architecture', 'Amd64', [{ labels: { [NODE_ARCHITECTURE]: 'amd64' } }]],
166
+ ['architecture', 'amd64', [{ labels: { [NODE_ARCHITECTURE]: 'amd64' } }]],
167
167
  ['architecture', 'unknown', [{ labels: { } }]],
168
168
  ['architecture', '—', [{ metadata: { state: { transitioning: true } } }]],
169
169
  ])('should show %p label %p', (label, text, nodes) => {
@@ -47,7 +47,7 @@ import Certificates from '@shell/components/Certificates';
47
47
  import { NAME as EXPLORER } from '@shell/config/product/explorer';
48
48
  import TabTitle from '@shell/components/TabTitle';
49
49
  import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
50
- import capitalize from 'lodash/capitalize';
50
+ import paginationUtils from '@shell/utils/pagination-utils';
51
51
 
52
52
  export const RESOURCES = [NAMESPACE, INGRESS, PV, WORKLOAD_TYPES.DEPLOYMENT, WORKLOAD_TYPES.STATEFUL_SET, WORKLOAD_TYPES.JOB, WORKLOAD_TYPES.DAEMON_SET, SERVICE];
53
53
 
@@ -122,6 +122,8 @@ export default {
122
122
  this.loadAgents();
123
123
  }
124
124
  }
125
+
126
+ this.showCertificates = !paginationUtils.isSteveCacheEnabled({ rootGetters: this.$store.getters });
125
127
  },
126
128
 
127
129
  data() {
@@ -156,6 +158,7 @@ export default {
156
158
  clusterCounts,
157
159
  selectedTab: 'cluster-events',
158
160
  extensionCards: getApplicableExtensionEnhancements(this, ExtensionPoint.CARD, CardLocation.CLUSTER_DASHBOARD_CARD, this.$route),
161
+ showCertificates: false,
159
162
  };
160
163
  },
161
164
 
@@ -212,7 +215,7 @@ export default {
212
215
  if (!node.metadata?.state?.transitioning) {
213
216
  const architecture = node.labels?.[NODE_ARCHITECTURE];
214
217
 
215
- const key = architecture ? capitalize(architecture) : this.t('cluster.architecture.label.unknown');
218
+ const key = architecture || this.t('cluster.architecture.label.unknown');
216
219
 
217
220
  obj[key] = (obj[key] || 0) + 1;
218
221
  }
@@ -739,6 +742,7 @@ export default {
739
742
  <AlertTable v-if="selectedTab === 'cluster-alerts'" />
740
743
  </Tab>
741
744
  <Tab
745
+ v-if="showCertificates"
742
746
  name="cluster-certs"
743
747
  :label="t('clusterIndexPage.sections.certs.label')"
744
748
  :weight="1"
@@ -12,6 +12,7 @@ import { WORKSPACE_ANNOTATION } from '@shell/config/labels-annotations';
12
12
  import { filterBy } from '@shell/utils/array';
13
13
  import FleetNoWorkspaces from '@shell/components/fleet/FleetNoWorkspaces.vue';
14
14
  import { NAME } from '@shell/config/product/fleet';
15
+ import { xOfy } from '@shell/utils/string';
15
16
 
16
17
  export default {
17
18
  name: 'FleetDashboard',
@@ -175,7 +176,12 @@ export default {
175
176
  }
176
177
 
177
178
  if (area === 'clusters') {
178
- group = row.targetClusters;
179
+ if (row.clusterInfo?.ready === row.clusterInfo?.total && row.clusterInfo?.ready) {
180
+ return {
181
+ badgeClass: STATES[STATES_ENUM.ACTIVE].color,
182
+ icon: STATES[STATES_ENUM.ACTIVE].compoundIcon
183
+ };
184
+ }
179
185
  } else if (area === 'bundles') {
180
186
  group = row.bundles;
181
187
  } else if (area === 'resources') {
@@ -223,7 +229,7 @@ export default {
223
229
  }
224
230
 
225
231
  if (area === 'clusters') {
226
- group = row.targetClusters;
232
+ group = '';
227
233
  } else if (area === 'bundles') {
228
234
  group = row.bundles;
229
235
  } else if (area === 'resources') {
@@ -262,11 +268,11 @@ export default {
262
268
  }
263
269
 
264
270
  if (area === 'clusters') {
265
- value = `${ row.targetClustersReady?.length || '0' }/${ row.targetClusters?.length || '?' }`;
271
+ return `${ row.clusterInfo.ready }/${ row.clusterInfo.total }`;
266
272
  } else if (area === 'bundles') {
267
- value = `${ row.bundlesReady?.length || '0' }/${ row.bundles?.length || '?' }`;
273
+ value = xOfy(row.bundlesReady?.length, row.bundles?.length);
268
274
  } else if (area === 'resources') {
269
- value = `${ row.status?.resourceCounts?.ready || '0' }/${ row.status?.resourceCounts?.desiredReady || '?' }`;
275
+ value = xOfy(row.status?.resourceCounts?.ready, row.status?.resourceCounts?.desiredReady);
270
276
  }
271
277
 
272
278
  return value;