@rancher/shell 3.0.9-rc.3 → 3.0.9-rc.5

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 (128) hide show
  1. package/assets/brand/suse/metadata.json +2 -1
  2. package/assets/translations/en-us.yaml +105 -5
  3. package/components/ActionMenuShell.vue +1 -1
  4. package/components/Inactivity.vue +2 -2
  5. package/components/Resource/Detail/Card/ExtrasCard.vue +49 -15
  6. package/components/Resource/Detail/Card/__tests__/ExtrasCard.test.ts +111 -0
  7. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +0 -17
  8. package/components/Resource/Detail/Masthead/index.vue +11 -4
  9. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +3 -1
  10. package/components/Resource/Detail/Metadata/index.vue +1 -1
  11. package/components/Resource/Detail/ResourceRow.vue +1 -1
  12. package/components/ResourceDetail/Masthead/latest.vue +12 -2
  13. package/components/ResourceList/index.vue +9 -0
  14. package/components/ResourceTable.vue +38 -4
  15. package/components/Tabbed/Tab.vue +4 -0
  16. package/components/Tabbed/index.vue +4 -1
  17. package/components/__tests__/ProjectRow.test.ts +60 -0
  18. package/components/form/ChangePassword.vue +41 -35
  19. package/components/form/ResourceQuota/Project.vue +42 -1
  20. package/components/form/ResourceQuota/ProjectRow.vue +71 -4
  21. package/components/form/ResourceQuota/__tests__/Project.test.ts +63 -0
  22. package/components/form/SelectOrCreateAuthSecret.vue +6 -1
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +35 -0
  24. package/components/formatter/KubeconfigClusters.vue +74 -0
  25. package/components/formatter/MachineSummaryGraph.vue +10 -2
  26. package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
  27. package/components/nav/TopLevelMenu.helper.ts +50 -2
  28. package/components/nav/TopLevelMenu.vue +14 -0
  29. package/components/nav/Type.vue +5 -0
  30. package/components/nav/__tests__/TopLevelMenu.test.ts +3 -3
  31. package/components/nav/__tests__/Type.test.ts +6 -4
  32. package/config/product/explorer.js +4 -3
  33. package/config/product/manager.js +47 -3
  34. package/config/router/navigation-guards/authentication.js +8 -9
  35. package/config/router/routes.js +4 -1
  36. package/config/types.js +10 -2
  37. package/detail/auditlog.cattle.io.auditpolicy.vue +19 -0
  38. package/detail/management.cattle.io.user.vue +1 -2
  39. package/detail/node.vue +0 -1
  40. package/detail/provisioning.cattle.io.cluster.vue +2 -1
  41. package/dialog/ChangePasswordDialog.vue +8 -0
  42. package/dialog/GenericPrompt.vue +20 -3
  43. package/dialog/ScaleMachineDownDialog.vue +65 -15
  44. package/dialog/SearchDialog.vue +10 -2
  45. package/dialog/__tests__/ScaleMachineDownDialog.test.ts +184 -0
  46. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +89 -0
  47. package/edit/__tests__/management.cattle.io.project.test.js +56 -1
  48. package/edit/auditlog.cattle.io.auditpolicy/AdditionalRedactions.vue +114 -0
  49. package/edit/auditlog.cattle.io.auditpolicy/Filters.vue +119 -0
  50. package/edit/auditlog.cattle.io.auditpolicy/General.vue +180 -0
  51. package/edit/auditlog.cattle.io.auditpolicy/__tests__/AdditionalRedactions.test.ts +327 -0
  52. package/edit/auditlog.cattle.io.auditpolicy/__tests__/Filters.test.ts +449 -0
  53. package/edit/auditlog.cattle.io.auditpolicy/__tests__/General.test.ts +472 -0
  54. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/AdditionalRedactions.test.ts.snap +27 -0
  55. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/Filters.test.ts.snap +39 -0
  56. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +174 -0
  57. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +29 -0
  58. package/edit/auditlog.cattle.io.auditpolicy/__tests__/index.test.ts +215 -0
  59. package/edit/auditlog.cattle.io.auditpolicy/index.vue +104 -0
  60. package/edit/auditlog.cattle.io.auditpolicy/types.ts +28 -0
  61. package/edit/fleet.cattle.io.gitrepo.vue +16 -1
  62. package/edit/management.cattle.io.project.vue +8 -2
  63. package/edit/management.cattle.io.user.vue +29 -34
  64. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +178 -0
  65. package/edit/provisioning.cattle.io.cluster/rke2.vue +22 -2
  66. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -0
  67. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  68. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
  69. package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
  70. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
  71. package/list/auditlog.cattle.io.auditpolicy.vue +63 -0
  72. package/list/ext.cattle.io.kubeconfig.vue +118 -0
  73. package/list/group.principal.vue +11 -15
  74. package/list/management.cattle.io.user.vue +11 -21
  75. package/machine-config/azure.vue +14 -0
  76. package/mixins/__tests__/chart.test.ts +147 -0
  77. package/mixins/browser-tab-visibility.js +5 -4
  78. package/mixins/chart.js +10 -8
  79. package/mixins/fetch.client.js +6 -0
  80. package/models/__tests__/auditlog.cattle.io.auditpolicy.test.ts +117 -0
  81. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
  82. package/models/__tests__/secret.test.ts +55 -0
  83. package/models/__tests__/workload.test.ts +49 -6
  84. package/models/auditlog.cattle.io.auditpolicy.js +46 -0
  85. package/models/cluster.x-k8s.io.machine.js +1 -1
  86. package/models/cluster.x-k8s.io.machinedeployment.js +5 -5
  87. package/models/event.js +5 -0
  88. package/models/ext.cattle.io.groupmembershiprefreshrequest.js +15 -0
  89. package/models/ext.cattle.io.kubeconfig.ts +97 -0
  90. package/models/ext.cattle.io.passwordchangerequest.js +15 -0
  91. package/models/ext.cattle.io.selfuser.js +15 -0
  92. package/models/fleet-application.js +17 -7
  93. package/models/management.cattle.io.user.js +28 -31
  94. package/models/schema.js +18 -0
  95. package/models/secret.js +28 -25
  96. package/models/steve-schema.ts +39 -2
  97. package/models/workload.js +3 -2
  98. package/package.json +2 -2
  99. package/pages/about.vue +3 -2
  100. package/pages/account/index.vue +23 -16
  101. package/pages/auth/login.vue +15 -8
  102. package/pages/auth/setup.vue +52 -15
  103. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
  104. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  105. package/pages/home.vue +9 -3
  106. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -3
  107. package/plugins/dashboard-store/actions.js +7 -0
  108. package/plugins/dashboard-store/getters.js +23 -1
  109. package/plugins/dashboard-store/index.js +3 -2
  110. package/plugins/dashboard-store/mutations.js +4 -0
  111. package/plugins/dashboard-store/resource-class.js +12 -5
  112. package/plugins/steve/__tests__/steve-class.test.ts +167 -0
  113. package/plugins/steve/schema.d.ts +5 -0
  114. package/plugins/steve/steve-class.js +19 -0
  115. package/plugins/steve/steve-pagination-utils.ts +2 -1
  116. package/rancher-components/RcItemCard/RcItemCard.test.ts +4 -2
  117. package/rancher-components/RcItemCard/RcItemCard.vue +27 -10
  118. package/store/auth.js +57 -19
  119. package/store/notifications.ts +1 -1
  120. package/store/type-map.js +12 -1
  121. package/types/shell/index.d.ts +24 -15
  122. package/types/store/dashboard-store.types.ts +7 -0
  123. package/utils/__tests__/chart.test.ts +96 -0
  124. package/utils/__tests__/version.test.ts +1 -19
  125. package/utils/chart.js +64 -0
  126. package/utils/pagination-wrapper.ts +11 -3
  127. package/utils/version.js +5 -17
  128. package/vue.config.js +26 -13
@@ -4,7 +4,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
4
4
  import CopyToClipboard from '@shell/components/CopyToClipboard';
5
5
  import AsyncButton from '@shell/components/AsyncButton';
6
6
  import { LOGGED_OUT, SETUP } from '@shell/config/query-params';
7
- import { NORMAN, MANAGEMENT } from '@shell/config/types';
7
+ import { NORMAN, MANAGEMENT, EXT } from '@shell/config/types';
8
8
  import { findBy } from '@shell/utils/array';
9
9
  import { Checkbox } from '@components/Form/Checkbox';
10
10
  import { getVendor, getProduct, setVendor } from '@shell/config/private-label';
@@ -21,6 +21,7 @@ import FormValidation from '@shell/mixins/form-validation';
21
21
  import isUrl from 'is-url';
22
22
  import { isLocalhost } from '@shell/utils/validators/setting';
23
23
  import Loading from '@shell/components/Loading';
24
+ import { getBrandMeta } from '@shell/utils/brand';
24
25
 
25
26
  const calcIsFirstLogin = (store) => {
26
27
  const firstLoginSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.FIRST_LOGIN);
@@ -31,7 +32,7 @@ const calcIsFirstLogin = (store) => {
31
32
  const calcMustChangePassword = async(store) => {
32
33
  await store.dispatch('auth/getUser');
33
34
 
34
- const out = store.getters['auth/v3User']?.mustChangePassword;
35
+ const out = store.getters['auth/user']?.mustChangePassword;
35
36
 
36
37
  return out;
37
38
  };
@@ -62,7 +63,7 @@ export default {
62
63
  current: null,
63
64
  password: randomStr(),
64
65
  confirm: null,
65
- v3User: null,
66
+ user: null,
66
67
  serverUrl: null,
67
68
  mcmEnabled: null,
68
69
  eula: false,
@@ -124,7 +125,7 @@ export default {
124
125
  const me = findBy(principals, 'me', true);
125
126
 
126
127
  const current = this.$route.query[SETUP] || this.$store.getters['auth/initialPass'];
127
- const v3User = this.$store.getters['auth/v3User'] ?? {};
128
+ const user = this.$store.getters['auth/user'] ?? {};
128
129
 
129
130
  const mcmFeature = await this.$store.dispatch('management/find', {
130
131
  type: MANAGEMENT.FEATURE, id: 'multi-cluster-management', opt: { url: `/v1/${ MANAGEMENT.FEATURE }/multi-cluster-management` }
@@ -149,7 +150,7 @@ export default {
149
150
  this['isFirstLogin'] = isFirstLogin;
150
151
  this['mustChangePassword'] = mustChangePassword;
151
152
  this['current'] = current;
152
- this['v3User'] = v3User;
153
+ this['user'] = user;
153
154
  this['serverUrl'] = serverUrl;
154
155
  this['mcmEnabled'] = mcmEnabled;
155
156
  this['principals'] = principals;
@@ -188,6 +189,21 @@ export default {
188
189
 
189
190
  showLocalhostWarning() {
190
191
  return isLocalhost(this.serverUrl);
192
+ },
193
+
194
+ customizations() {
195
+ const brandMeta = getBrandMeta(this.$store.getters['management/brand']);
196
+ const login = brandMeta?.login || {};
197
+
198
+ return {
199
+ setupLabelKey: 'setup.welcome',
200
+ logoClass: 'login-logo',
201
+ ...login,
202
+ };
203
+ },
204
+
205
+ brandLogo() {
206
+ return this.customizations.logo;
191
207
  }
192
208
  },
193
209
 
@@ -213,19 +229,25 @@ export default {
213
229
  await this.$store.dispatch('loadManagement');
214
230
 
215
231
  if ( this.mustChangePassword ) {
216
- await this.$store.dispatch('rancher/request', {
217
- url: '/v3/users?action=changepassword',
218
- method: 'post',
219
- data: {
220
- currentPassword: this.current,
221
- newPassword: this.password
222
- },
223
- });
232
+ const passwordChangeRequest = await this.$store.dispatch('management/create', { type: EXT.PASSWORD_CHANGE_REQUESTS });
233
+
234
+ if (!passwordChangeRequest?.canChangePassword) {
235
+ this.errors = exceptionToErrorsArray(this.t('changePassword.errors.cannotChange'));
236
+ throw new Error(this.t('changePassword.errors.cannotChange'));
237
+ }
238
+
239
+ passwordChangeRequest.spec = {
240
+ currentPassword: this.current,
241
+ newPassword: this.password,
242
+ userID: this.user?.id
243
+ };
244
+
245
+ await passwordChangeRequest.save();
224
246
  } else {
225
247
  promises.push(setSetting(this.$store, SETTING.FIRST_LOGIN, 'false'));
226
248
  }
227
249
 
228
- const user = this.v3User;
250
+ const user = this.user;
229
251
 
230
252
  user.mustChangePassword = false;
231
253
  this.$store.dispatch('auth/gotUser', user);
@@ -278,8 +300,18 @@ export default {
278
300
   
279
301
  </div>
280
302
  <div>
303
+ <div
304
+ v-if="brandLogo"
305
+ class="brand-logo"
306
+ >
307
+ <BrandImage
308
+ :class="{[customizations.logoClass]: !!customizations.logoClass}"
309
+ :file-name="brandLogo"
310
+ :alt="t('setup.setup')"
311
+ />
312
+ </div>
281
313
  <h1 class="text-center">
282
- {{ t('setup.welcome', {product}) }}
314
+ {{ t(customizations.setupLabelKey, {product}) }}
283
315
  </h1>
284
316
 
285
317
  <template v-if="mustChangePassword">
@@ -467,6 +499,11 @@ export default {
467
499
  .setup {
468
500
  overflow: hidden;
469
501
 
502
+ .brand-logo {
503
+ display: flex;
504
+ justify-content: center;
505
+ }
506
+
470
507
  .row {
471
508
  & .checkbox {
472
509
  margin: auto
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { RcItemCardAction } from '@components/RcItemCard';
3
+ import { RcButton } from '@components/RcButton';
3
4
 
4
5
  interface FooterItem {
5
6
  icon?: string;
@@ -30,26 +31,34 @@ function onClickItem(type: string, label: string) {
30
31
  class="app-chart-card-footer-item"
31
32
  data-testid="app-chart-card-footer-item"
32
33
  >
33
- <i
34
- v-if="footerItem.icon"
35
- v-clean-tooltip="t(footerItem.iconTooltip?.key)"
36
- :class="['icon', 'app-chart-card-footer-item-icon', footerItem.icon]"
37
- />
38
34
  <template
39
35
  v-for="(label, j) in footerItem.labels"
40
36
  :key="j"
41
37
  >
42
38
  <rc-item-card-action
43
39
  v-if="clickable && footerItem.type"
44
- v-clean-tooltip="footerItem.labelTooltip"
45
- class="app-chart-card-footer-item-text secondary-text-link"
46
- data-testid="app-chart-card-footer-item-text"
47
- tabindex="0"
48
- :aria-label="t('catalog.charts.appChartCard.footerItem.ariaLabel')"
49
- @click="onClickItem(footerItem.type, label)"
40
+ class="app-chart-card-footer-item-text"
50
41
  >
51
- {{ label }}
52
- <span v-if="footerItem.labels.length > 1 && j !== footerItem.labels.length - 1">, </span>
42
+ <rc-button
43
+ v-clean-tooltip="footerItem.labelTooltip"
44
+ variant="ghost"
45
+ class="app-chart-card-footer-button secondary-text-link"
46
+ data-testid="app-chart-card-footer-item-text"
47
+ :aria-label="t('catalog.charts.appChartCard.footerItem.ariaLabel', { filter: label })"
48
+ @click="onClickItem(footerItem.type, label)"
49
+ >
50
+ <template
51
+ v-if="footerItem.icon"
52
+ #before
53
+ >
54
+ <i
55
+ v-clean-tooltip="t(footerItem.iconTooltip?.key)"
56
+ :class="['icon', 'app-chart-card-footer-item-icon', footerItem.icon]"
57
+ />
58
+ </template>
59
+ {{ label }}
60
+ <span v-if="footerItem.labels.length > 1 && j !== footerItem.labels.length - 1">, </span>
61
+ </rc-button>
53
62
  </rc-item-card-action>
54
63
  <span
55
64
  v-else
@@ -78,7 +87,6 @@ function onClickItem(type: string, label: string) {
78
87
  margin-right: 8px;
79
88
 
80
89
  &-text {
81
- text-transform: capitalize;
82
90
  margin-right: 8px;
83
91
  display: -webkit-box;
84
92
  -webkit-line-clamp: 1;
@@ -98,5 +106,21 @@ function onClickItem(type: string, label: string) {
98
106
  margin-right: 8px;
99
107
  }
100
108
  }
109
+
110
+ &-button {
111
+ text-transform: capitalize;
112
+ }
113
+ }
114
+
115
+ button.variant-ghost.app-chart-card-footer-button {
116
+ padding: 0;
117
+ gap: 0;
118
+ min-height: 20px;
119
+
120
+ &:focus-visible {
121
+ border-color: var(--primary);
122
+ @include focus-outline;
123
+ outline-offset: -2px;
124
+ }
101
125
  }
102
126
  </style>
@@ -696,6 +696,7 @@ export default {
696
696
  :content="card.content"
697
697
  :value="card.rawChart"
698
698
  variant="medium"
699
+ role="link"
699
700
  :class="{ 'single-card': appChartCards.length === 1 }"
700
701
  :clickable="true"
701
702
  @card-click="selectChart"
package/pages/home.vue CHANGED
@@ -10,7 +10,7 @@ import SingleClusterInfo from '@shell/components/SingleClusterInfo.vue';
10
10
  import DynamicContentBanner from '@shell/components/DynamicContent/DynamicContentBanner.vue';
11
11
  import DynamicContentPanel from '@shell/components/DynamicContent/DynamicContentPanel.vue';
12
12
  import { mapGetters, mapState } from 'vuex';
13
- import { MANAGEMENT, CAPI, COUNT } from '@shell/config/types';
13
+ import { MANAGEMENT, CAPI, COUNT, SAVED_COUNTS } from '@shell/config/types';
14
14
  import { NAME as MANAGER } from '@shell/config/product/manager';
15
15
  import { AGE, STATE } from '@shell/config/table-headers';
16
16
  import { MODE, _IMPORT } from '@shell/config/query-params';
@@ -256,8 +256,14 @@ export default defineComponent({
256
256
  */
257
257
  altClusterList() {
258
258
  return this.tooManyClusters && !this.altClusterListDisabled;
259
- }
259
+ },
260
260
 
261
+ clusterCountDisplay() {
262
+ // If we have the cluster count from the store, use that instead
263
+ const savedCount = this.$store.getters['management/getSavedCount'](SAVED_COUNTS.K8S_CLUSTERS);
264
+
265
+ return typeof savedCount !== 'undefined' ? savedCount : this.clusterCount;
266
+ }
261
267
  },
262
268
 
263
269
  watch: {
@@ -806,7 +812,7 @@ export default defineComponent({
806
812
  </h1>
807
813
  <BadgeState
808
814
  v-if="clusterCount && !tooManyClusters"
809
- :label="clusterCount.toString()"
815
+ :label="clusterCountDisplay.toString()"
810
816
  color="bg-info ml-20 mr-20"
811
817
  />
812
818
  </div>
@@ -397,9 +397,7 @@ describe('class: Resource', () => {
397
397
 
398
398
  const cards = resource.cards;
399
399
 
400
- expect(cards).toHaveLength(1);
401
- expect(cards[0]).toHaveProperty('component');
402
- expect(cards[0]).toHaveProperty('props');
400
+ expect(cards).toHaveLength(0);
403
401
  });
404
402
  });
405
403
 
@@ -515,6 +515,13 @@ export default {
515
515
  });
516
516
  }
517
517
 
518
+ if (opt.saveCountAs) {
519
+ commit('setSavedCount', {
520
+ name: opt.saveCountAs,
521
+ count: out.count,
522
+ });
523
+ }
524
+
518
525
  if ( !opt.transient && opt.watch !== false ) {
519
526
  dispatch('watch', watchArgs);
520
527
  }
@@ -351,10 +351,22 @@ export default {
351
351
  return out;
352
352
  },
353
353
 
354
+ /**
355
+ * Can the user GET a resource of the given type
356
+ */
357
+ canGet: (state, getters) => (type) => {
358
+ const schema = getters.schemaFor(type);
359
+
360
+ return schema?.canGet;
361
+ },
362
+
363
+ /**
364
+ * Can the user LIST a resource of the given type
365
+ */
354
366
  canList: (state, getters) => (type) => {
355
367
  const schema = getters.schemaFor(type);
356
368
 
357
- return schema && schema.hasLink('collection');
369
+ return schema?.canList;
358
370
  },
359
371
 
360
372
  typeRegistered: (state, getters) => (type) => {
@@ -559,4 +571,14 @@ export default {
559
571
  * Can be used to change behaviour given steve cache api functionality
560
572
  */
561
573
  isSteveCacheUrl: (state) => () => false,
574
+
575
+ /**
576
+ * Get the saved count for the given name
577
+ *
578
+ * @param {string} name Name of the saved count
579
+ * @returns {number|undefined} The saved count or undefined if not found
580
+ */
581
+ getSavedCount: (state) => (name) => {
582
+ return state.savedCounts[name];
583
+ }
562
584
  };
@@ -23,8 +23,9 @@ export const coreStoreState = (namespace, baseUrl, isClusterStore) => ({
23
23
  namespace,
24
24
  isClusterStore
25
25
  },
26
- types: {},
27
- $ctx: markRaw({}),
26
+ types: {},
27
+ savedCounts: {}, // Saved counts for resource types (from paginated API called where marked)
28
+ $ctx: markRaw({}),
28
29
  });
29
30
 
30
31
  export default (vuexModule, config, init) => {
@@ -588,6 +588,10 @@ export default {
588
588
  cache.haveNamespace = namespace;
589
589
  },
590
590
 
591
+ setSavedCount(state, { name, count }) {
592
+ state.savedCounts[name] = count;
593
+ },
594
+
591
595
  loadedAll(state, { type }) {
592
596
  const cache = registerType(state, type);
593
597
 
@@ -1284,9 +1284,15 @@ export default class Resource {
1284
1284
 
1285
1285
  // Steve sometimes returns Table responses instead of the resource you just saved.. ignore
1286
1286
  if ( res && res.kind !== 'Table') {
1287
- await this.$dispatch('load', {
1288
- data: res, existing: (forNew ? this : undefined ), invalidatePageCache
1289
- });
1287
+ const keyField = this.$getters.keyFieldForType(this.type);
1288
+ const id = res[keyField];
1289
+
1290
+ // only items with ID will be added to the store, this prevents "new" resources that return an empty body OR no ID from being added to the store with an ID of "undefined"
1291
+ if (id) {
1292
+ await this.$dispatch('load', {
1293
+ data: res, existing: (forNew ? this : undefined ), invalidatePageCache
1294
+ });
1295
+ }
1290
1296
  }
1291
1297
  } catch (e) {
1292
1298
  if ( this.type && this.id && e?._status === 409) {
@@ -2164,7 +2170,7 @@ export default class Resource {
2164
2170
  get insightCardProps() {
2165
2171
  const rows = [
2166
2172
  useResourceCardRow(this.t('component.resource.detail.card.insightsCard.rows.conditions'), this.resourceConditions, undefined, 'condition', '#conditions'),
2167
- useResourceCardRow(this.t('component.resource.detail.card.insightsCard.rows.events'), this.resourceEvents, undefined, undefined, '#events'),
2173
+ useResourceCardRow(this.t('component.resource.detail.card.insightsCard.rows.events'), this.resourceEvents, 'insightsColor', 'eventType', '#events'),
2168
2174
  ];
2169
2175
 
2170
2176
  return {
@@ -2181,7 +2187,8 @@ export default class Resource {
2181
2187
  }
2182
2188
 
2183
2189
  get _cards() {
2184
- return [this.insightCard];
2190
+ // All cards are opt in, we're leaving the insights card as part of the base resource since it should proliferate to most resources
2191
+ return [];
2185
2192
  }
2186
2193
 
2187
2194
  get cards() {
@@ -56,5 +56,172 @@ describe('class: Steve', () => {
56
56
  expect({ ...steve }).toStrictEqual(customResource);
57
57
  });
58
58
  });
59
+
60
+ describe('method: processSaveResponse', () => {
61
+ it('should call parent processSaveResponse', () => {
62
+ const mockDispatch = jest.fn();
63
+ const mockRootGetters = { 'i18n/t': jest.fn().mockReturnValue('Resource created: test-id') };
64
+ const steve = new Steve(customResource, {
65
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
66
+ dispatch: mockDispatch,
67
+ rootGetters: mockRootGetters,
68
+ });
69
+
70
+ // Mock the parent processSaveResponse method
71
+ const parentProcessSaveResponse = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(steve)), 'processSaveResponse');
72
+
73
+ const response = { _status: 200 };
74
+
75
+ steve.processSaveResponse(response);
76
+
77
+ expect(parentProcessSaveResponse).toHaveBeenCalledWith(response);
78
+ });
79
+
80
+ it('should show growl notification for autogenerated names on 201 status', () => {
81
+ const mockDispatch = jest.fn();
82
+ const mockT = jest.fn()
83
+ .mockReturnValueOnce('CustomResourceDefinition')
84
+ .mockReturnValueOnce('CustomResourceDefinition created')
85
+ .mockReturnValueOnce('Resource test-generated-abc123 created successfully');
86
+ const mockRootGetters = { 'i18n/t': mockT };
87
+ const steve = new Steve(customResource, {
88
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
89
+ dispatch: mockDispatch,
90
+ rootGetters: mockRootGetters,
91
+ });
92
+
93
+ const response = {
94
+ _status: 201,
95
+ metadata: { generateName: 'test-generated-' },
96
+ id: 'default/test-generated-abc123'
97
+ };
98
+
99
+ steve.processSaveResponse(response);
100
+
101
+ expect(mockT).toHaveBeenCalledWith(`typeLabel."${ customResource.type }"`, { count: 1 });
102
+ expect(mockT).toHaveBeenCalledWith('generic.autogeneratedCreated.title', { resource: 'CustomResourceDefinition' });
103
+ expect(mockT).toHaveBeenCalledWith('generic.autogeneratedCreated.message', { id: 'test-generated-abc123' });
104
+ expect(mockDispatch).toHaveBeenCalledWith(
105
+ 'growl/success',
106
+ {
107
+ title: 'CustomResourceDefinition created',
108
+ message: 'Resource test-generated-abc123 created successfully',
109
+ timeout: 3000
110
+ },
111
+ { root: true }
112
+ );
113
+ });
114
+
115
+ it('should show growl notification for autogenerated names without namespace', () => {
116
+ const mockDispatch = jest.fn();
117
+ const mockT = jest.fn()
118
+ .mockReturnValueOnce('CustomResourceDefinition')
119
+ .mockReturnValueOnce('CustomResourceDefinition created')
120
+ .mockReturnValueOnce('Resource simple-id created successfully');
121
+ const mockRootGetters = { 'i18n/t': mockT };
122
+ const steve = new Steve(customResource, {
123
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
124
+ dispatch: mockDispatch,
125
+ rootGetters: mockRootGetters,
126
+ });
127
+
128
+ const response = {
129
+ _status: 201,
130
+ metadata: { generateName: 'simple-' },
131
+ id: 'simple-id'
132
+ };
133
+
134
+ steve.processSaveResponse(response);
135
+
136
+ expect(mockT).toHaveBeenCalledWith(`typeLabel."${ customResource.type }"`, { count: 1 });
137
+ expect(mockT).toHaveBeenCalledWith('generic.autogeneratedCreated.title', { resource: 'CustomResourceDefinition' });
138
+ expect(mockT).toHaveBeenCalledWith('generic.autogeneratedCreated.message', { id: 'simple-id' });
139
+ expect(mockDispatch).toHaveBeenCalledWith(
140
+ 'growl/success',
141
+ {
142
+ title: 'CustomResourceDefinition created',
143
+ message: 'Resource simple-id created successfully',
144
+ timeout: 3000
145
+ },
146
+ { root: true }
147
+ );
148
+ });
149
+
150
+ it('should not show growl notification for non-201 status', () => {
151
+ const mockDispatch = jest.fn();
152
+ const mockT = jest.fn();
153
+ const mockRootGetters = { 'i18n/t': mockT };
154
+ const steve = new Steve(customResource, {
155
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
156
+ dispatch: mockDispatch,
157
+ rootGetters: mockRootGetters,
158
+ });
159
+
160
+ const response = {
161
+ _status: 200,
162
+ metadata: { generateName: 'test-generated-' },
163
+ id: 'default/test-generated-abc123'
164
+ };
165
+
166
+ steve.processSaveResponse(response);
167
+
168
+ expect(mockT).not.toHaveBeenCalled();
169
+ expect(mockDispatch).not.toHaveBeenCalledWith(
170
+ 'growl/success',
171
+ expect.any(Object),
172
+ { root: true }
173
+ );
174
+ });
175
+
176
+ it('should not show growl notification without generateName', () => {
177
+ const mockDispatch = jest.fn();
178
+ const mockT = jest.fn();
179
+ const mockRootGetters = { 'i18n/t': mockT };
180
+ const steve = new Steve(customResource, {
181
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
182
+ dispatch: mockDispatch,
183
+ rootGetters: mockRootGetters,
184
+ });
185
+
186
+ const response = {
187
+ _status: 201,
188
+ id: 'default/test-regular-name'
189
+ };
190
+
191
+ steve.processSaveResponse(response);
192
+
193
+ expect(mockT).not.toHaveBeenCalled();
194
+ expect(mockDispatch).not.toHaveBeenCalledWith(
195
+ 'growl/success',
196
+ expect.any(Object),
197
+ { root: true }
198
+ );
199
+ });
200
+
201
+ it('should not show growl notification without id', () => {
202
+ const mockDispatch = jest.fn();
203
+ const mockT = jest.fn();
204
+ const mockRootGetters = { 'i18n/t': mockT };
205
+ const steve = new Steve(customResource, {
206
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
207
+ dispatch: mockDispatch,
208
+ rootGetters: mockRootGetters,
209
+ });
210
+
211
+ const response = {
212
+ _status: 201,
213
+ metadata: { generateName: 'test-generated-' }
214
+ };
215
+
216
+ steve.processSaveResponse(response);
217
+
218
+ expect(mockT).not.toHaveBeenCalled();
219
+ expect(mockDispatch).not.toHaveBeenCalledWith(
220
+ 'growl/success',
221
+ expect.any(Object),
222
+ { root: true }
223
+ );
224
+ });
225
+ });
59
226
  });
60
227
  });
@@ -8,12 +8,17 @@ export interface SchemaAttributeColumn {
8
8
  type: string,
9
9
  }
10
10
 
11
+ export type SchemaAttributeVerbs = 'get' | 'patch' | 'list' | 'update'
12
+
11
13
  export interface SchemaAttribute {
12
14
  columns: SchemaAttributeColumn[],
13
15
  namespaced: boolean
16
+ verbs: SchemaAttributeVerbs[]
14
17
  }
15
18
 
16
19
  /**
20
+ * Interface for a steve schema, represents raw json
21
+ *
17
22
  * At some point this will be properly typed, until then...
18
23
  */
19
24
  export interface Schema {
@@ -63,4 +63,23 @@ export default class SteveModel extends HybridModel {
63
63
  paginationEnabled() {
64
64
  return this.$getters['paginationEnabled'](this.type);
65
65
  }
66
+
67
+ processSaveResponse(res) {
68
+ super.processSaveResponse(res);
69
+
70
+ // Conditionally show the growl for autogenerated names
71
+ if (res && res._status === 201 && res.metadata?.generateName && res.id) {
72
+ // Split to remove the namespace if present (default/generated-xxx)
73
+ const nameOnly = res.id.split('/').pop();
74
+
75
+ // Avoid showing the growl without the ID.
76
+ if (nameOnly.length > 0) {
77
+ this.$dispatch('growl/success', {
78
+ title: this.t('generic.autogeneratedCreated.title', { resource: this.t(`typeLabel."${ this.type }"`, { count: 1 }) }),
79
+ message: this.t('generic.autogeneratedCreated.message', { id: nameOnly }),
80
+ timeout: 3000
81
+ }, { root: true });
82
+ }
83
+ }
84
+ }
66
85
  }
@@ -764,7 +764,8 @@ export const PAGINATION_SETTINGS_STORE_DEFAULTS: PaginationSettingsStores = {
764
764
  { resource: CAPI.RANCHER_CLUSTER, context: ['side-bar'] },
765
765
  { resource: MANAGEMENT.CLUSTER, context: ['side-bar'] },
766
766
  { resource: CATALOG.APP, context: ['branding'] },
767
- SECRET
767
+ SECRET,
768
+ CAPI.MACHINE_SET
768
769
  ],
769
770
  generic: false,
770
771
  }
@@ -124,7 +124,7 @@ describe('rcItemCard', () => {
124
124
  }
125
125
  });
126
126
 
127
- const root = wrapper.get(`[data-testid="item-card-${ id }"]`);
127
+ const root = wrapper.get(`[data-testid="card-header-left"]`);
128
128
 
129
129
  expect(root.attributes('role')).toBe('button');
130
130
  expect(root.attributes('tabindex')).toBe('0');
@@ -152,7 +152,9 @@ describe('rcItemCard', () => {
152
152
  }
153
153
  });
154
154
 
155
- await wrapper.trigger('keydown.enter');
155
+ const clickTarget = wrapper.find('.item-card-header-left');
156
+
157
+ await clickTarget.trigger('keydown.enter');
156
158
  expect(wrapper.emitted('card-click')).toBeTruthy();
157
159
  });
158
160