@rancher/shell 3.0.0-rc.7 → 3.0.0-rc.8

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 (37) hide show
  1. package/assets/styles/global/_tooltip.scss +1 -12
  2. package/assets/translations/en-us.yaml +13 -1
  3. package/components/CodeMirror.vue +18 -15
  4. package/components/PromptRemove.vue +2 -2
  5. package/components/Questions/index.vue +2 -2
  6. package/components/ResourceDetail/Masthead.vue +1 -0
  7. package/components/ResourceDetail/index.vue +6 -7
  8. package/components/auth/RoleDetailEdit.vue +1 -6
  9. package/components/auth/__tests__/RoleDetailEdit.test.ts +53 -16
  10. package/components/form/ArrayList.vue +7 -3
  11. package/components/formatter/CloudCredExpired.vue +69 -0
  12. package/components/formatter/Date.vue +1 -1
  13. package/components/nav/TopLevelMenu.vue +115 -51
  14. package/components/nav/__tests__/TopLevelMenu.test.ts +49 -23
  15. package/config/labels-annotations.js +9 -5
  16. package/core/types.ts +1 -1
  17. package/detail/provisioning.cattle.io.cluster.vue +0 -4
  18. package/edit/management.cattle.io.project.vue +4 -1
  19. package/edit/provisioning.cattle.io.cluster/index.vue +7 -2
  20. package/edit/provisioning.cattle.io.cluster/rke2.vue +28 -5
  21. package/list/provisioning.cattle.io.cluster.vue +57 -10
  22. package/machine-config/vmwarevsphere.vue +133 -95
  23. package/mixins/vue-select-overrides.js +0 -1
  24. package/models/catalog.cattle.io.app.js +4 -3
  25. package/models/cloudcredential.js +158 -2
  26. package/models/management.cattle.io.globalrole.js +6 -0
  27. package/models/management.cattle.io.roletemplate.js +6 -0
  28. package/models/nodedriver.js +5 -0
  29. package/models/provisioning.cattle.io.cluster.js +35 -1
  30. package/package.json +1 -1
  31. package/pages/c/_cluster/apps/charts/index.vue +0 -6
  32. package/pages/c/_cluster/manager/cloudCredential/index.vue +68 -4
  33. package/pages/home.vue +1 -0
  34. package/plugins/dashboard-store/mutations.js +24 -3
  35. package/plugins/dashboard-store/resource-class.js +6 -0
  36. package/store/type-map.js +4 -2
  37. package/types/shell/index.d.ts +12 -1
@@ -5,7 +5,7 @@ import { mount, Wrapper } from '@vue/test-utils';
5
5
  // DISCLAIMER: This should not be added here, although we have several store requests which are irrelevant
6
6
  const defaultStore = {
7
7
  'management/byId': jest.fn(),
8
- 'management/schemaFor': jest.fn(),
8
+ 'management/schemaFor': () => ({}),
9
9
  'i18n/t': jest.fn(),
10
10
  'features/get': jest.fn(),
11
11
  'prefs/theme': jest.fn(),
@@ -21,7 +21,11 @@ describe('topLevelMenu', () => {
21
21
  mocks: {
22
22
  $store: {
23
23
  getters: {
24
- 'management/all': () => [{ name: 'whatever' }],
24
+ 'management/all': () => [{
25
+ name: 'whatever',
26
+ id: 'an-id1',
27
+ mgmt: { id: 'an-id1' },
28
+ }],
25
29
  ...defaultStore
26
30
  },
27
31
  },
@@ -53,21 +57,21 @@ describe('topLevelMenu', () => {
53
57
  'management/all': () => [
54
58
  {
55
59
  name: 'x32-cwf5-name',
56
- id: 'x32-cwf5-id',
60
+ id: 'an-id1',
57
61
  mgmt: { id: 'an-id1' },
58
62
  nameDisplay: 'c-cluster',
59
63
  isReady: true
60
64
  },
61
65
  {
62
66
  name: 'x33-cwf5-name',
63
- id: 'x33-cwf5-id',
67
+ id: 'an-id2',
64
68
  mgmt: { id: 'an-id2' },
65
69
  nameDisplay: 'a-cluster',
66
70
  isReady: true
67
71
  },
68
72
  {
69
73
  name: 'x34-cwf5-name',
70
- id: 'x34-cwf5-id',
74
+ id: 'an-id3',
71
75
  mgmt: { id: 'an-id3' },
72
76
  nameDisplay: 'b-cluster',
73
77
  isReady: true
@@ -75,7 +79,7 @@ describe('topLevelMenu', () => {
75
79
  {
76
80
  name: 'local-name',
77
81
  id: 'local',
78
- mgmt: { id: 'an-id4' },
82
+ mgmt: { id: 'local' },
79
83
  nameDisplay: 'local',
80
84
  isReady: true
81
85
  },
@@ -112,21 +116,21 @@ describe('topLevelMenu', () => {
112
116
  'management/all': () => [
113
117
  {
114
118
  name: 'x32-cwf5-name',
115
- id: 'x32-cwf5-id',
119
+ id: 'an-id1',
116
120
  mgmt: { id: 'an-id1' },
117
121
  nameDisplay: 'c-cluster',
118
122
  isReady: true
119
123
  },
120
124
  {
121
125
  name: 'x33-cwf5-name',
122
- id: 'x33-cwf5-id',
126
+ id: 'an-id2',
123
127
  mgmt: { id: 'an-id2' },
124
128
  nameDisplay: 'a-cluster',
125
129
  isReady: false
126
130
  },
127
131
  {
128
132
  name: 'x34-cwf5-name',
129
- id: 'x34-cwf5-id',
133
+ id: 'an-id3',
130
134
  mgmt: { id: 'an-id3' },
131
135
  nameDisplay: 'b-cluster',
132
136
  isReady: true
@@ -134,7 +138,7 @@ describe('topLevelMenu', () => {
134
138
  {
135
139
  name: 'local-name',
136
140
  id: 'local',
137
- mgmt: { id: 'an-id4' },
141
+ mgmt: { id: 'local' },
138
142
  nameDisplay: 'local',
139
143
  isReady: true
140
144
  },
@@ -171,7 +175,7 @@ describe('topLevelMenu', () => {
171
175
  'management/all': () => [
172
176
  {
173
177
  name: 'x32-cwf5-name',
174
- id: 'x32-cwf5-id',
178
+ id: 'an-id1',
175
179
  mgmt: { id: 'an-id1' },
176
180
  nameDisplay: 'c-cluster',
177
181
  isReady: true,
@@ -179,7 +183,7 @@ describe('topLevelMenu', () => {
179
183
  },
180
184
  {
181
185
  name: 'x33-cwf5-name',
182
- id: 'x33-cwf5-id',
186
+ id: 'an-id2',
183
187
  mgmt: { id: 'an-id2' },
184
188
  nameDisplay: 'a-cluster',
185
189
  isReady: true,
@@ -187,7 +191,7 @@ describe('topLevelMenu', () => {
187
191
  },
188
192
  {
189
193
  name: 'x34-cwf5-name',
190
- id: 'x34-cwf5-id',
194
+ id: 'an-id3',
191
195
  mgmt: { id: 'an-id3' },
192
196
  nameDisplay: 'b-cluster',
193
197
  isReady: true,
@@ -196,7 +200,7 @@ describe('topLevelMenu', () => {
196
200
  {
197
201
  name: 'local-name',
198
202
  id: 'local',
199
- mgmt: { id: 'an-id4' },
203
+ mgmt: { id: 'local' },
200
204
  nameDisplay: 'local',
201
205
  isReady: true,
202
206
  pinned: true
@@ -234,7 +238,7 @@ describe('topLevelMenu', () => {
234
238
  'management/all': () => [
235
239
  {
236
240
  name: 'x32-cwf5-name',
237
- id: 'x32-cwf5-id',
241
+ id: 'an-id1',
238
242
  mgmt: { id: 'an-id1' },
239
243
  nameDisplay: 'c-cluster',
240
244
  isReady: true,
@@ -242,7 +246,7 @@ describe('topLevelMenu', () => {
242
246
  },
243
247
  {
244
248
  name: 'x33-cwf5-name',
245
- id: 'x33-cwf5-id',
249
+ id: 'an-id2',
246
250
  mgmt: { id: 'an-id2' },
247
251
  nameDisplay: 'a-cluster',
248
252
  isReady: true,
@@ -250,7 +254,7 @@ describe('topLevelMenu', () => {
250
254
  },
251
255
  {
252
256
  name: 'x34-cwf5-name',
253
- id: 'x34-cwf5-id',
257
+ id: 'an-id3',
254
258
  mgmt: { id: 'an-id3' },
255
259
  nameDisplay: 'b-cluster',
256
260
  isReady: false,
@@ -259,7 +263,7 @@ describe('topLevelMenu', () => {
259
263
  {
260
264
  name: 'local-name',
261
265
  id: 'local',
262
- mgmt: { id: 'an-id4' },
266
+ mgmt: { id: 'local' },
263
267
  nameDisplay: 'local',
264
268
  isReady: true,
265
269
  pinned: true
@@ -356,7 +360,7 @@ describe('topLevelMenu', () => {
356
360
  it('should show description if it is available on the mgmt cluster (relevant for RKE1/ember world)', async() => {
357
361
  const wrapper: Wrapper<InstanceType<typeof TopLevelMenu>> = mount(TopLevelMenu, {
358
362
  data: () => {
359
- return { hasProvCluster: false, showPinClusters: true };
363
+ return { hasProvCluster: true, showPinClusters: true };
360
364
  },
361
365
 
362
366
  global: {
@@ -372,6 +376,7 @@ describe('topLevelMenu', () => {
372
376
  {
373
377
  name: 'whatever',
374
378
  id: 'an-id1',
379
+ mgmt: { id: 'an-id1' },
375
380
  description: 'some-description1',
376
381
  nameDisplay: 'some-label',
377
382
  isReady: true,
@@ -381,6 +386,7 @@ describe('topLevelMenu', () => {
381
386
  {
382
387
  name: 'whatever',
383
388
  id: 'an-id2',
389
+ mgmt: { id: 'an-id2' },
384
390
  description: 'some-description2',
385
391
  nameDisplay: 'some-label',
386
392
  pinned: true
@@ -389,6 +395,7 @@ describe('topLevelMenu', () => {
389
395
  {
390
396
  name: 'whatever',
391
397
  id: 'an-id3',
398
+ mgmt: { id: 'an-id3' },
392
399
  description: 'some-description3',
393
400
  nameDisplay: 'some-label',
394
401
  isReady: true
@@ -397,6 +404,7 @@ describe('topLevelMenu', () => {
397
404
  {
398
405
  name: 'whatever',
399
406
  id: 'an-id4',
407
+ mgmt: { id: 'an-id4' },
400
408
  description: 'some-description4',
401
409
  nameDisplay: 'some-label'
402
410
  },
@@ -466,7 +474,11 @@ describe('topLevelMenu', () => {
466
474
  mocks: {
467
475
  $store: {
468
476
  getters: {
469
- 'management/all': () => [{ nameDisplay: 'something else' }],
477
+ 'management/all': () => [{
478
+ id: 'an-id1',
479
+ mgmt: { id: 'an-id1' },
480
+ nameDisplay: 'something else'
481
+ }],
470
482
  ...defaultStore
471
483
  },
472
484
  },
@@ -489,7 +501,12 @@ describe('topLevelMenu', () => {
489
501
  mocks: {
490
502
  $store: {
491
503
  getters: {
492
- 'management/all': () => [{ nameDisplay: 'something else', pinned: true }],
504
+ 'management/all': () => [{
505
+ id: 'an-id1',
506
+ mgmt: { id: 'an-id1' },
507
+ nameDisplay: 'something else',
508
+ pinned: true
509
+ }],
493
510
  ...defaultStore
494
511
  },
495
512
  },
@@ -515,7 +532,11 @@ describe('topLevelMenu', () => {
515
532
  mocks: {
516
533
  $store: {
517
534
  getters: {
518
- 'management/all': () => [{ nameDisplay: search }],
535
+ 'management/all': () => [{
536
+ id: 'an-id1',
537
+ mgmt: { id: 'an-id1' },
538
+ nameDisplay: search
539
+ }],
519
540
  ...defaultStore
520
541
  },
521
542
  },
@@ -540,7 +561,12 @@ describe('topLevelMenu', () => {
540
561
  mocks: {
541
562
  $store: {
542
563
  getters: {
543
- 'management/all': () => [{ nameDisplay: search, pinned: true }],
564
+ 'management/all': () => [{
565
+ nameDisplay: search,
566
+ pinned: true,
567
+ id: 'an-id1',
568
+ mgmt: { id: 'an-id1' },
569
+ }],
544
570
  ...defaultStore
545
571
  },
546
572
  },
@@ -11,6 +11,7 @@ export const CATTLE_PUBLIC_ENDPOINTS = 'field.cattle.io/publicEndpoints';
11
11
  export const TARGET_WORKLOADS = 'field.cattle.io/targetWorkloadIds';
12
12
  export const UI_MANAGED = 'management.cattle.io/ui-managed';
13
13
  export const CREATOR_ID = 'field.cattle.io/creatorId';
14
+ export const CREATOR_PRINCIPAL_ID = 'field.cattle.io/creator-principal-name';
14
15
  export const RESOURCE_QUOTA = 'field.cattle.io/resourceQuota';
15
16
  export const AZURE_MIGRATED = 'auth.cattle.io/azuread-endpoint-migrated';
16
17
  export const WORKSPACE_ANNOTATION = 'objectset.rio.cattle.io/id';
@@ -90,11 +91,12 @@ export const CATALOG = {
90
91
  _CLUSTER_TPL: 'cluster-template',
91
92
  _CLUSTER_TOOL: 'cluster-tool',
92
93
 
93
- COMPONENT: 'catalog.cattle.io/ui-component',
94
- SOURCE_REPO_TYPE: 'catalog.cattle.io/ui-source-repo-type',
95
- SOURCE_REPO_NAME: 'catalog.cattle.io/ui-source-repo',
96
- COLOR: 'catalog.cattle.io/ui-color',
97
- DISPLAY_NAME: 'catalog.cattle.io/display-name',
94
+ COMPONENT: 'catalog.cattle.io/ui-component',
95
+ SOURCE_REPO_TYPE: 'catalog.cattle.io/ui-source-repo-type',
96
+ SOURCE_REPO_NAME: 'catalog.cattle.io/ui-source-repo',
97
+ COLOR: 'catalog.cattle.io/ui-color',
98
+ DISPLAY_NAME: 'catalog.cattle.io/display-name',
99
+ CLUSTER_REPO_NAME: 'catalog.cattle.io/cluster-repo-name',
98
100
 
99
101
  SUPPORTED_OS: 'catalog.cattle.io/os',
100
102
  PERMITTED_OS: 'catalog.cattle.io/permits-os',
@@ -172,3 +174,5 @@ export const SYSTEM_LABELS = [
172
174
  'node.kubernetes.io',
173
175
  'egress.rke2.io'
174
176
  ];
177
+
178
+ export const CLOUD_CREDENTIALS = { EXPIRATION: 'rancher.io/expiration-timestamp' };
package/core/types.ts CHANGED
@@ -235,7 +235,7 @@ export interface ProductOptions {
235
235
  /**
236
236
  * The route that the product will lead to if click on in navigation.
237
237
  */
238
- to?: Location;
238
+ to?: PluginRouteRecordRaw;
239
239
 
240
240
  /**
241
241
  * Leaving these here for completeness but I don't think these should be advertised as useable to plugin creators.
@@ -573,10 +573,6 @@ export default {
573
573
  }
574
574
  },
575
575
 
576
- mounted() {
577
- window.c = this;
578
- },
579
-
580
576
  methods: {
581
577
  toggleScaleDownModal( event, resources ) {
582
578
  // Check if the user held alt key when an action is clicked.
@@ -14,7 +14,7 @@ import { MANAGEMENT } from '@shell/config/types';
14
14
  import { NAME } from '@shell/config/product/explorer';
15
15
  import { PROJECT_ID, _VIEW, _CREATE, _EDIT } from '@shell/config/query-params';
16
16
  import ProjectMembershipEditor, { canViewProjectMembershipEditor } from '@shell/components/form/Members/ProjectMembershipEditor';
17
-
17
+ import { CREATOR_PRINCIPAL_ID } from '@shell/config/labels-annotations';
18
18
  import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
19
19
  import { Banner } from '@components/Banner';
20
20
 
@@ -103,6 +103,9 @@ export default {
103
103
  this.value.metadata['namespace'] = this.$store.getters['currentCluster'].id;
104
104
  this.value['spec'] = this.value.spec || {};
105
105
  this.value.spec['containerDefaultResourceLimit'] = this.value.spec.containerDefaultResourceLimit || {};
106
+ if (!this.$store.getters['auth/principalId'].includes('local://')) {
107
+ this.value.metadata.annotations[CREATOR_PRINCIPAL_ID] = this.$store.getters['auth/principalId'];
108
+ }
106
109
  },
107
110
  methods: {
108
111
  async save(saveCb) {
@@ -86,10 +86,15 @@ export default {
86
86
  // These aren't explicitly used, but need to be listening for change events
87
87
  mgmtClusters: this.$store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }),
88
88
  provClusters: this.$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER }),
89
-
90
- catalog: this.$store.dispatch('catalog/load'),
91
89
  };
92
90
 
91
+ // No need to fetch charts when editing an RKE1 cluster
92
+ // The computed property `isRke1` in this file is based on the RKE1/RKE2 toggle, which is not applicable in this case
93
+ // Instead, we should rely on the value from the model: `this.value.isRke1`
94
+ if (!this.value.isRke1 || (this.value.isRke1 && this.mode !== 'edit')) {
95
+ hash['catalog'] = this.$store.dispatch('catalog/load');
96
+ }
97
+
93
98
  if (this.$store.getters[`management/canList`](MANAGEMENT.NODE_DRIVER)) {
94
99
  hash.nodeDrivers = this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE_DRIVER });
95
100
  }
@@ -42,6 +42,7 @@ import Tabbed from '@shell/components/Tabbed';
42
42
  import { canViewClusterMembershipEditor } from '@shell/components/form/Members/ClusterMembershipEditor';
43
43
  import semver from 'semver';
44
44
 
45
+ import { CLOUD_CREDENTIAL_OVERRIDE } from '@shell/models/nodedriver';
45
46
  import { SETTING } from '@shell/config/settings';
46
47
  import { base64Encode } from '@shell/utils/crypto';
47
48
  import { CAPI as CAPI_ANNOTATIONS, CLUSTER_BADGE } from '@shell/config/labels-annotations';
@@ -413,11 +414,22 @@ export default {
413
414
  },
414
415
 
415
416
  needCredential() {
416
- if (this.provider === 'custom' || this.provider === 'import' || this.isElementalCluster || this.mode === _VIEW || (this.providerConfig?.spec?.builtin === false && this.providerConfig?.spec?.addCloudCredential === false)) {
417
+ // Check non-provider specific config
418
+ if (
419
+ this.provider === 'custom' ||
420
+ this.provider === 'import' ||
421
+ this.isElementalCluster || // Elemental cluster can make use of `cloud-credential`: false
422
+ this.mode === _VIEW
423
+ ) {
417
424
  return false;
418
425
  }
419
426
 
420
- if (this.customCredentialComponentRequired === false) {
427
+ // Check provider specific config
428
+ if (this.cloudCredentialsOverride === true || this.cloudCredentialsOverride === false) {
429
+ return this.cloudCredentialsOverride;
430
+ }
431
+
432
+ if (this.providerConfig?.spec?.builtin === false && this.providerConfig?.spec?.addCloudCredential === false) {
421
433
  return false;
422
434
  }
423
435
 
@@ -425,10 +437,21 @@ export default {
425
437
  },
426
438
 
427
439
  /**
428
- * Only for extensions - extension can register a 'false' cloud credential to indicate that a cloud credential is not needed
440
+ * Override the native way of determining if cloud credentials are required (builtin ++ node driver spec.addCloudCredentials)
441
+ *
442
+ * 1) Override via extensions
443
+ * - `true` or actual component - return true
444
+ * - `false` - return false
445
+ * 2) Override via hardcoded setting
429
446
  */
430
- customCredentialComponentRequired() {
431
- return this.$plugin.getDynamic('cloud-credential', this.provider);
447
+ cloudCredentialsOverride() {
448
+ const cloudCredential = this.$plugin.getDynamic('cloud-credential', this.provider);
449
+
450
+ if (cloudCredential === undefined) {
451
+ return CLOUD_CREDENTIAL_OVERRIDE[this.provider];
452
+ }
453
+
454
+ return !!cloudCredential;
432
455
  },
433
456
 
434
457
  hasMachinePools() {
@@ -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,17 +144,36 @@ 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
 
147
173
  $loadingResources() {
148
174
  // results are filtered so we wouldn't get the correct count on indicator...
149
175
  return { loadIndeterminate: true };
150
- },
151
-
152
- mounted() {
153
- window.c = this;
154
- },
176
+ }
155
177
  };
156
178
  </script>
157
179
 
@@ -186,6 +208,17 @@ export default {
186
208
  </template>
187
209
  </Masthead>
188
210
 
211
+ <Banner
212
+ v-if="tokenExpiredData.expiring"
213
+ color="warning"
214
+ :label="tokenExpiredData.expiring"
215
+ />
216
+ <Banner
217
+ v-if="tokenExpiredData.expired"
218
+ color="error"
219
+ :label="tokenExpiredData.expired"
220
+ />
221
+
189
222
  <ResourceTable
190
223
  :schema="schema"
191
224
  :rows="filteredRows"
@@ -194,6 +227,7 @@ export default {
194
227
  :use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
195
228
  :data-testid="'cluster-list'"
196
229
  :force-update-live-and-delayed="forceUpdateLiveAndDelayed"
230
+ :sub-rows="true"
197
231
  >
198
232
  <!-- Why are state column and subrow overwritten here? -->
199
233
  <!-- for rke1 clusters, where they try to use the mgmt cluster stateObj instead of prov cluster stateObj, -->
@@ -207,19 +241,32 @@ export default {
207
241
  </template>
208
242
  <template #sub-row="{fullColspan, row, keyField, componentTestid, i, onRowMouseEnter, onRowMouseLeave}">
209
243
  <tr
210
- v-if="row.stateDescription"
211
244
  :key="row[keyField] + '-description'"
212
245
  :data-testid="componentTestid + '-' + i + '-row-description'"
213
246
  class="state-description sub-row"
214
247
  @mouseenter="onRowMouseEnter"
215
248
  @mouseleave="onRowMouseLeave"
216
249
  >
217
- <td>&nbsp;</td>
250
+ <td v-if="row.cloudCredentialWarning || row.stateDescription">
251
+ &nbsp;
252
+ </td>
218
253
  <td
254
+ v-if="row.cloudCredentialWarning || row.stateDescription"
219
255
  :colspan="fullColspan - 1"
220
- :class="{ 'text-error' : row.stateObj.error }"
221
256
  >
222
- {{ row.stateDescription }}
257
+ <CloudCredExpired
258
+ v-if="row.cloudCredentialWarning"
259
+ :value="row.cloudCredential.expires"
260
+ :row="row.cloudCredential"
261
+ :verbose="true"
262
+ :class="{'mb-10': row.stateDescription}"
263
+ />
264
+ <div
265
+ v-if="row.stateDescription"
266
+ :class="{ 'text-error' : row.stateObj.error }"
267
+ >
268
+ {{ row.stateDescription }}
269
+ </div>
223
270
  </td>
224
271
  </tr>
225
272
  </template>