@rancher/shell 3.0.9-rc.5 → 3.0.9

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 (172) hide show
  1. package/assets/images/providers/oci-open-containers.svg +22 -0
  2. package/assets/images/providers/traefik.png +0 -0
  3. package/assets/styles/themes/_dark.scss +2 -0
  4. package/assets/styles/themes/_light.scss +2 -0
  5. package/assets/styles/themes/_modern.scss +6 -0
  6. package/assets/translations/en-us.yaml +129 -25
  7. package/components/CruResource.vue +3 -1
  8. package/components/ExplorerProjectsNamespaces.vue +12 -12
  9. package/components/IconOrSvg.vue +61 -42
  10. package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +109 -0
  11. package/components/Resource/Detail/Card/StatusCard/index.vue +21 -4
  12. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +19 -2
  13. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +19 -11
  14. package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +12 -0
  15. package/components/Resource/Detail/ResourcePopover/index.vue +2 -0
  16. package/components/Resource/Detail/ResourceRow.vue +2 -2
  17. package/components/ResourceList/index.vue +7 -4
  18. package/components/SortableTable/index.vue +2 -2
  19. package/components/Window/ContainerLogs.vue +48 -37
  20. package/components/fleet/FleetClusterTargets/TargetsList.vue +2 -2
  21. package/components/fleet/FleetClusterTargets/index.vue +6 -1
  22. package/components/fleet/GitRepoAdvancedTab.vue +333 -0
  23. package/components/fleet/GitRepoMetadataTab.vue +43 -0
  24. package/components/fleet/GitRepoRepositoryTab.vue +101 -0
  25. package/components/fleet/GitRepoTargetTab.vue +77 -0
  26. package/components/fleet/HelmOpAdvancedTab.vue +247 -0
  27. package/components/fleet/HelmOpChartTab.vue +158 -0
  28. package/components/fleet/HelmOpMetadataTab.vue +46 -0
  29. package/components/fleet/HelmOpTargetTab.vue +84 -0
  30. package/components/fleet/HelmOpValuesTab.vue +147 -0
  31. package/components/fleet/__tests__/FleetClusterTargets.test.ts +119 -70
  32. package/components/form/BannerSettings.vue +2 -2
  33. package/components/form/NodeScheduling.vue +81 -7
  34. package/components/form/NotificationSettings.vue +2 -2
  35. package/components/form/PodAffinity.vue +1 -36
  36. package/components/form/ResourceLabeledSelect.vue +8 -4
  37. package/components/form/ResourceQuota/Namespace.vue +30 -9
  38. package/components/form/ResourceQuota/NamespaceRow.vue +25 -7
  39. package/components/form/ResourceQuota/Project.vue +140 -82
  40. package/components/form/ResourceQuota/ResourceQuotaEntry.vue +145 -0
  41. package/components/form/ResourceQuota/__tests__/Namespace.test.ts +307 -0
  42. package/components/form/ResourceQuota/__tests__/NamespaceRow.test.ts +281 -0
  43. package/components/form/ResourceQuota/__tests__/Project.test.ts +274 -27
  44. package/components/form/ResourceQuota/__tests__/ResourceQuotaEntry.test.ts +215 -0
  45. package/components/form/SchedulingCustomization.vue +14 -6
  46. package/components/form/SelectOrCreateAuthSecret.vue +107 -18
  47. package/components/form/__tests__/NodeScheduling.test.ts +12 -9
  48. package/components/form/__tests__/PodAffinity.test.ts +21 -2
  49. package/components/form/__tests__/SchedulingCustomization.test.ts +240 -0
  50. package/components/formatter/ClusterLink.vue +8 -0
  51. package/components/formatter/SecretOrigin.vue +79 -0
  52. package/config/labels-annotations.js +7 -6
  53. package/config/pagination-table-headers.js +6 -4
  54. package/config/product/explorer.js +1 -11
  55. package/config/product/manager.js +0 -1
  56. package/config/query-params.js +3 -0
  57. package/config/settings.ts +15 -2
  58. package/config/table-headers.js +21 -17
  59. package/config/types.js +23 -8
  60. package/detail/fleet.cattle.io.cluster.vue +1 -1
  61. package/detail/workload/index.vue +11 -16
  62. package/dialog/DeactivateDriverDialog.vue +1 -1
  63. package/dialog/FeatureFlagListDialog.vue +1 -1
  64. package/dialog/Ipv6NetworkingDialog.vue +156 -0
  65. package/dialog/ScalePoolDownDialog.vue +2 -2
  66. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -1
  67. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +1 -0
  68. package/edit/__tests__/management.cattle.io.project.test.js +56 -128
  69. package/edit/auth/oidc.vue +1 -1
  70. package/edit/catalog.cattle.io.clusterrepo.vue +155 -25
  71. package/edit/fleet.cattle.io.gitrepo.vue +153 -283
  72. package/edit/fleet.cattle.io.helmop.vue +190 -332
  73. package/edit/management.cattle.io.project.vue +5 -42
  74. package/edit/management.cattle.io.setting.vue +6 -0
  75. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/auth.spec.ts +145 -0
  76. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/index.test.ts +202 -0
  77. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/tls.spec.ts +226 -0
  78. package/edit/monitoring.coreos.com.alertmanagerconfig/auth.vue +24 -21
  79. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/opsgenie.spec.ts +157 -0
  80. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/pagerduty.spec.ts +132 -0
  81. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/slack.spec.ts +108 -0
  82. package/edit/monitoring.coreos.com.alertmanagerconfig/types/pagerduty.vue +2 -1
  83. package/edit/monitoring.coreos.com.receiver/__tests__/auth.spec.ts +165 -0
  84. package/edit/monitoring.coreos.com.receiver/__tests__/index.test.ts +153 -0
  85. package/edit/monitoring.coreos.com.receiver/__tests__/tls.spec.ts +115 -0
  86. package/edit/monitoring.coreos.com.receiver/types/__tests__/email.spec.ts +86 -0
  87. package/edit/monitoring.coreos.com.receiver/types/__tests__/opsgenie.spec.ts +209 -0
  88. package/edit/monitoring.coreos.com.receiver/types/__tests__/pagerduty.spec.ts +105 -0
  89. package/edit/monitoring.coreos.com.receiver/types/__tests__/slack.spec.ts +92 -0
  90. package/edit/monitoring.coreos.com.receiver/types/__tests__/webhook.spec.ts +131 -0
  91. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +55 -24
  92. package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +1 -103
  93. package/edit/provisioning.cattle.io.cluster/__tests__/index.test.ts +13 -1
  94. package/edit/provisioning.cattle.io.cluster/__tests__/rke2-fleet-cluster-agent.test.ts +283 -0
  95. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -49
  96. package/edit/provisioning.cattle.io.cluster/ingress/IngressCards.vue +114 -0
  97. package/edit/provisioning.cattle.io.cluster/ingress/IngressConfiguration.vue +158 -0
  98. package/edit/provisioning.cattle.io.cluster/rke2.vue +167 -69
  99. package/edit/provisioning.cattle.io.cluster/shared.ts +36 -1
  100. package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +2 -1
  101. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +70 -7
  102. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +343 -0
  103. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +2 -1
  104. package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +13 -1
  105. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +10 -44
  106. package/edit/secret/index.vue +1 -1
  107. package/edit/token.vue +68 -29
  108. package/edit/workload/__tests__/index.test.ts +2 -37
  109. package/edit/workload/index.vue +6 -2
  110. package/edit/workload/mixins/workload.js +0 -32
  111. package/list/__tests__/management.cattle.io.setting.test.ts +198 -0
  112. package/list/management.cattle.io.setting.vue +13 -0
  113. package/list/provisioning.cattle.io.cluster.vue +50 -1
  114. package/list/secret.vue +4 -9
  115. package/list/service.vue +6 -8
  116. package/machine-config/amazonec2.vue +11 -4
  117. package/machine-config/components/EC2Networking.vue +46 -30
  118. package/machine-config/components/__tests__/EC2Networking.test.ts +7 -7
  119. package/machine-config/components/__tests__/utils/vpcSubnetMockData.js +0 -9
  120. package/machine-config/digitalocean.vue +3 -3
  121. package/models/__tests__/chart.test.ts +2 -2
  122. package/models/__tests__/namespace.test.ts +11 -0
  123. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +96 -0
  124. package/models/__tests__/workload.test.ts +42 -1
  125. package/models/catalog.cattle.io.clusterrepo.js +30 -4
  126. package/models/chart.js +3 -3
  127. package/models/ext.cattle.io.token.js +48 -0
  128. package/models/kontainerdriver.js +2 -2
  129. package/models/namespace.js +7 -1
  130. package/models/nodedriver.js +2 -2
  131. package/models/provisioning.cattle.io.cluster.js +28 -7
  132. package/models/secret.js +0 -17
  133. package/models/service.js +44 -1
  134. package/models/token.js +4 -0
  135. package/models/workload.js +12 -6
  136. package/package.json +1 -1
  137. package/pages/account/index.vue +96 -67
  138. package/pages/auth/setup.vue +5 -14
  139. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +45 -18
  140. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +4 -1
  141. package/pages/c/_cluster/apps/charts/index.vue +82 -3
  142. package/pages/c/_cluster/apps/charts/install.vue +317 -42
  143. package/pages/c/_cluster/explorer/tools/index.vue +1 -1
  144. package/pages/c/_cluster/manager/cloudCredential/index.vue +1 -1
  145. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -4
  146. package/pages/c/_cluster/settings/index.vue +3 -1
  147. package/pages/c/_cluster/uiplugins/index.vue +1 -1
  148. package/plugins/dashboard-store/__tests__/getters.test.ts +108 -0
  149. package/plugins/dashboard-store/__tests__/resource-class.test.ts +27 -0
  150. package/plugins/dashboard-store/actions.js +3 -8
  151. package/plugins/dashboard-store/getters.js +7 -5
  152. package/plugins/dashboard-store/mutations.js +4 -1
  153. package/plugins/dashboard-store/resource-class.js +3 -3
  154. package/plugins/steve/__tests__/steve-class.test.ts +102 -141
  155. package/plugins/steve/steve-class.js +12 -3
  156. package/plugins/steve/steve-pagination-utils.ts +6 -2
  157. package/rancher-components/RcIcon/types.ts +2 -0
  158. package/rancher-components/RcItemCard/RcItemCard.vue +72 -20
  159. package/store/prefs.js +3 -0
  160. package/types/aws-sdk.d.ts +121 -0
  161. package/types/resources/node.ts +15 -0
  162. package/types/shell/index.d.ts +537 -506
  163. package/types/store/pagination.types.ts +5 -5
  164. package/utils/__tests__/array.test.ts +1 -29
  165. package/utils/__tests__/cluster-agent-configuration.test.ts +203 -0
  166. package/utils/array.ts +0 -11
  167. package/utils/aws.ts +21 -0
  168. package/utils/cluster.js +22 -2
  169. package/utils/selector-typed.ts +1 -1
  170. package/utils/svg-filter.js +4 -3
  171. package/components/__tests__/ProjectRow.test.ts +0 -206
  172. package/components/form/ResourceQuota/ProjectRow.vue +0 -277
@@ -428,6 +428,33 @@ describe('class: Resource', () => {
428
428
  });
429
429
  });
430
430
 
431
+ describe('getter: _glance', () => {
432
+ it('should not throw when currentCluster or currentProduct is undefined', () => {
433
+ const resource = new Resource({
434
+ type: 'test',
435
+ metadata: { creationTimestamp: '2024-01-01T00:00:00Z' }
436
+ }, {
437
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
438
+ dispatch: jest.fn(),
439
+ rootGetters: {
440
+ 'i18n/t': (key: string) => key,
441
+ currentCluster: undefined,
442
+ currentProduct: undefined,
443
+ 'type-map/labelFor': () => 'Test',
444
+ },
445
+ });
446
+
447
+ expect(() => resource._glance).not.toThrow();
448
+
449
+ const glance = resource._glance;
450
+ const namespaceItem = glance.find((item: any) => item.name === 'namespace');
451
+
452
+ expect(namespaceItem.formatter).toBeUndefined();
453
+ expect(namespaceItem.formatterOpts.to.cluster).toBeUndefined();
454
+ expect(namespaceItem.formatterOpts.to.product).toBeUndefined();
455
+ });
456
+ });
457
+
431
458
  describe('getter: detailPageAdditionalActions', () => {
432
459
  it('should return undefined by default', () => {
433
460
  const resource = new Resource({ type: 'test-type' }, {
@@ -471,7 +471,7 @@ export default {
471
471
  return findAllGetter(getters, type, opt);
472
472
  }
473
473
 
474
- console.log(`Find Page: [${ ctx.state.config.namespace }] ${ type }. Page: ${ opt.pagination.page }. Revision: ${ opt.revision || 'none' }. Size: ${ opt.pagination.pageSize }. Sort: ${ opt.pagination.sort.map((s) => s.field).join(', ') }`); // eslint-disable-line no-console
474
+ console.log(`Find Page: [${ ctx.state.config.namespace }] ${ type }. Page: ${ opt.pagination.page }. Revision: ${ opt.revision || 'none' }. Size: ${ opt.pagination.pageSize }. Sort: ${ opt.pagination.sort?.map((s) => s.field).join(', ') }`); // eslint-disable-line no-console
475
475
  opt = opt || {};
476
476
  opt.url = getters.urlFor(type, null, opt);
477
477
 
@@ -554,7 +554,6 @@ export default {
554
554
  */
555
555
  async findLabelSelector(ctx, {
556
556
  type,
557
- context,
558
557
  matching: {
559
558
  namespace,
560
559
  labelSelector
@@ -562,14 +561,10 @@ export default {
562
561
  opt
563
562
  }) {
564
563
  const { getters, dispatch } = ctx;
565
- const args = {
566
- id: type,
567
- context,
568
- };
569
564
 
570
565
  opt = opt || {};
571
566
 
572
- if (getters[`paginationEnabled`]?.(args)) {
567
+ if (getters[`paginationEnabled`]?.()) {
573
568
  if (isLabelSelectorEmpty(labelSelector)) {
574
569
  throw new Error(`labelSelector must not be empty when using findLabelSelector (avoid fetching all resources)`);
575
570
  }
@@ -581,7 +576,7 @@ export default {
581
576
  ...opt,
582
577
  namespaced: namespace,
583
578
  pagination: new FilterArgs({ labelSelector }),
584
- transient: opt?.transient !== undefined ? opt.transient : false // Call this out explicitly here, as by default findX methods ar eusually be cached AND watched
579
+ transient: opt?.transient !== undefined ? opt.transient : false // Call this out explicitly here, as by default findX methods are usually cached AND watched
585
580
  }
586
581
  });
587
582
  }
@@ -144,8 +144,7 @@ export default {
144
144
  page?.namespace === namespace &&
145
145
  page?.pagination?.filters?.length === 0 &&
146
146
  page?.pagination.labelSelector &&
147
- selector === labelSelectorToSelector(page?.pagination.labelSelector
148
- )
147
+ selector === labelSelectorToSelector(page?.pagination.labelSelector)
149
148
  ) {
150
149
  return getters.all(type);
151
150
  }
@@ -155,11 +154,14 @@ export default {
155
154
  return getters.all(type);
156
155
  }
157
156
 
157
+ // Does the store contain a page, assume it's a subset of all resources still covering applicable resources, and
158
+ // apply labelSelector to get actual result
158
159
  if (getters['havePage'](type)) {
159
- return getters.all(type);
160
+ return getters.matching( type, selector, namespace );
160
161
  }
161
162
 
162
- // Does the store have all and we can pretend like it contains a result of a labelSelector?
163
+ // Does the store contain all resources, if so
164
+ // apply labelSelector to get actual result
163
165
  if (getters['haveAll'](type)) {
164
166
  return getters.matching( type, selector, namespace );
165
167
  }
@@ -177,7 +179,7 @@ export default {
177
179
 
178
180
  // Filter first by namespace if one is provided, since this is efficient
179
181
  if (namespace && typeof namespace === 'string') {
180
- matching = type === POD ? getters['podsByNamespace'](namespace) : matching.filter((obj) => obj.namespace === namespace);
182
+ matching = type === POD && !selector ? getters['podsByNamespace'](namespace) : matching.filter((obj) => obj.namespace === namespace);
181
183
  }
182
184
 
183
185
  garbageCollect.gcUpdateLastAccessed({
@@ -440,7 +440,10 @@ export default {
440
440
  cache.map.set(proxies[i][keyField], proxies[i]);
441
441
  }
442
442
 
443
- cache.haveSelector[selector] = true;
443
+ cache.havePage = undefined;
444
+ cache.haveNamespace = undefined;
445
+ cache.haveSelector = { [selector]: true };
446
+ cache.haveAll = undefined;
444
447
  cache.revision = revision || 0;
445
448
  },
446
449
 
@@ -1956,12 +1956,12 @@ export default class Resource {
1956
1956
  {
1957
1957
  name: 'namespace',
1958
1958
  label: this.t('component.resource.detail.glance.namespace'),
1959
- formatter: 'Link',
1959
+ formatter: this.$rootGetters['currentProduct']?.id && this.$rootGetters['currentCluster']?.id ? 'Link' : undefined,
1960
1960
  formatterOpts: {
1961
1961
  to: {
1962
1962
  name: `c-cluster-product-resource-id`,
1963
- product: this.$rootGetters['currentProduct'].id,
1964
- cluster: this.$rootGetters['currentCluster'].id,
1963
+ product: this.$rootGetters['currentProduct']?.id,
1964
+ cluster: this.$rootGetters['currentCluster']?.id,
1965
1965
  resource: this.type
1966
1966
  },
1967
1967
  row: {},
@@ -77,150 +77,111 @@ describe('class: Steve', () => {
77
77
  expect(parentProcessSaveResponse).toHaveBeenCalledWith(response);
78
78
  });
79
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,
80
+ describe('growl notifications', () => {
81
+ describe('should show growl notification', () => {
82
+ it.each([
83
+ {
84
+ name: 'for autogenerated names on 201 status',
85
+ response: {
86
+ _status: 201,
87
+ metadata: { generateName: 'test-generated-' },
88
+ id: 'default/test-generated-abc123'
89
+ },
90
+ expectedResource: 'CustomResourceDefinition',
91
+ expectedId: 'test-generated-abc123'
92
+ },
93
+ {
94
+ name: 'for autogenerated names without namespace',
95
+ response: {
96
+ _status: 201,
97
+ metadata: { generateName: 'simple-' },
98
+ id: 'simple-id'
99
+ },
100
+ expectedResource: 'CustomResourceDefinition',
101
+ expectedId: 'simple-id'
102
+ }
103
+ ])('$name', ({ response, expectedResource, expectedId }) => {
104
+ const mockDispatch = jest.fn();
105
+ const mockT = jest.fn((key, params) => {
106
+ if (key === 'generic.autogeneratedCreated.title') {
107
+ return `${ params.resource } created`;
108
+ }
109
+ if (key === 'generic.autogeneratedCreated.message') {
110
+ return `Resource ${ params.id } created successfully`;
111
+ }
112
+
113
+ return key;
114
+ });
115
+ const mockRootGetters = { 'i18n/t': mockT };
116
+ const steve = new Steve(customResource, {
117
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
118
+ dispatch: mockDispatch,
119
+ rootGetters: mockRootGetters,
120
+ });
121
+
122
+ // Mock typeDisplay to return the expected resource label
123
+ Object.defineProperty(steve, 'typeDisplay', { get: jest.fn(() => expectedResource) });
124
+
125
+ steve.processSaveResponse(response);
126
+
127
+ expect(mockT).toHaveBeenCalledWith('generic.autogeneratedCreated.title', { resource: expectedResource });
128
+ expect(mockT).toHaveBeenCalledWith('generic.autogeneratedCreated.message', { id: expectedId, resource: expectedResource });
129
+
130
+ expect(mockDispatch).toHaveBeenCalledWith(
131
+ 'growl/success',
132
+ {
133
+ title: `${ expectedResource } created`,
134
+ message: `Resource ${ expectedId } created successfully`,
135
+ timeout: 3000
136
+ },
137
+ { root: true }
138
+ );
139
+ });
158
140
  });
159
141
 
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,
142
+ describe('should not show growl notification', () => {
143
+ it.each([
144
+ {
145
+ name: 'for non-201 status',
146
+ response: {
147
+ _status: 200,
148
+ metadata: { generateName: 'test-generated-' },
149
+ id: 'default/test-generated-abc123'
150
+ }
151
+ },
152
+ {
153
+ name: 'without generateName',
154
+ response: {
155
+ _status: 201,
156
+ id: 'default/test-regular-name'
157
+ }
158
+ },
159
+ {
160
+ name: 'without id',
161
+ response: {
162
+ _status: 201,
163
+ metadata: { generateName: 'test-generated-' }
164
+ }
165
+ }
166
+ ])('$name', ({ response }) => {
167
+ const mockDispatch = jest.fn();
168
+ const mockT = jest.fn();
169
+ const mockRootGetters = { 'i18n/t': mockT };
170
+ const steve = new Steve(customResource, {
171
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
172
+ dispatch: mockDispatch,
173
+ rootGetters: mockRootGetters,
174
+ });
175
+
176
+ steve.processSaveResponse(response);
177
+
178
+ expect(mockDispatch).not.toHaveBeenCalledWith(
179
+ 'growl/success',
180
+ expect.any(Object),
181
+ { root: true }
182
+ );
183
+ });
184
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
185
  });
225
186
  });
226
187
  });
@@ -64,19 +64,28 @@ export default class SteveModel extends HybridModel {
64
64
  return this.$getters['paginationEnabled'](this.type);
65
65
  }
66
66
 
67
+ /**
68
+ * Override the default save response processing to conditionally show a growl when a resource is created with an autogenerated name.
69
+ * The growl will display the resource type and the generated name, providing immediate feedback to the user about the creation of the resource.
70
+ *
71
+ * @param {*} res
72
+ */
67
73
  processSaveResponse(res) {
68
74
  super.processSaveResponse(res);
69
75
 
70
76
  // Conditionally show the growl for autogenerated names
71
77
  if (res && res._status === 201 && res.metadata?.generateName && res.id) {
72
- // Split to remove the namespace if present (default/generated-xxx)
78
+ // Split to remove the namespace if present (default/generated-xxx)
73
79
  const nameOnly = res.id.split('/').pop();
74
80
 
75
81
  // Avoid showing the growl without the ID.
76
82
  if (nameOnly.length > 0) {
83
+ const resource = this.typeDisplay;
84
+
85
+ // Show the growl with the resource type and generated name
77
86
  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 }),
87
+ title: this.t('generic.autogeneratedCreated.title', { resource }),
88
+ message: this.t('generic.autogeneratedCreated.message', { id: nameOnly, resource }),
80
89
  timeout: 3000
81
90
  }, { root: true });
82
91
  }
@@ -15,7 +15,8 @@ import {
15
15
  INGRESS,
16
16
  WORKLOAD_TYPES,
17
17
  HPA,
18
- SECRET
18
+ SECRET,
19
+ EXT
19
20
  } from '@shell/config/types';
20
21
  import { CAPI as CAPI_LAB_AND_ANO, CATTLE_PUBLIC_ENDPOINTS, STORAGE, UI_PROJECT_SECRET_COPY } from '@shell/config/labels-annotations';
21
22
  import { Schema } from '@shell/plugins/steve/schema';
@@ -234,6 +235,8 @@ class StevePaginationUtils extends NamespaceProjectFilters {
234
235
  [NODE]: [
235
236
  { field: 'status.nodeInfo.kubeletVersion' },
236
237
  { field: 'status.nodeInfo.operatingSystem' },
238
+ { field: 'spec.taints.key' },
239
+ { field: 'status.addresses.type' },
237
240
  ],
238
241
  [POD]: [
239
242
  { field: 'spec.containers.image' },
@@ -765,7 +768,8 @@ export const PAGINATION_SETTINGS_STORE_DEFAULTS: PaginationSettingsStores = {
765
768
  { resource: MANAGEMENT.CLUSTER, context: ['side-bar'] },
766
769
  { resource: CATALOG.APP, context: ['branding'] },
767
770
  SECRET,
768
- CAPI.MACHINE_SET
771
+ CAPI.MACHINE_SET,
772
+ EXT.TOKEN
769
773
  ],
770
774
  generic: false,
771
775
  }
@@ -146,6 +146,8 @@ export const RcIconTypeToClass = {
146
146
  };
147
147
 
148
148
  export const RcIconSizeToCSS = {
149
+ xxlarge: '40px',
150
+ xlarge: '32px',
149
151
  large: '25px',
150
152
  medium: '18px',
151
153
  small: '14px',
@@ -6,6 +6,8 @@ import LazyImage from '@shell/components/LazyImage.vue';
6
6
  import { DropdownOption } from '@components/RcDropdown/types';
7
7
  import ActionMenu from '@shell/components/ActionMenuShell.vue';
8
8
  import RcItemCardAction from './RcItemCardAction';
9
+ import RcIcon from '@components/RcIcon/RcIcon.vue';
10
+ import { RcIconType } from '@components/RcIcon/types';
9
11
 
10
12
  const store = useStore();
11
13
  const { t } = useI18n(store);
@@ -25,11 +27,11 @@ type Label = {
25
27
 
26
28
  /**
27
29
  * Represents an image used in the card.
30
+ * Can be either a traditional image (src) or an icon (icon).
28
31
  */
29
- type Image = {
30
- src: string;
31
- alt?: Label;
32
- };
32
+ type Image =
33
+ | { src?: never, icon: RcIconType; alt: Label; }
34
+ | { src: string; icon?: never, alt: Label; };
33
35
 
34
36
  /**
35
37
  * Optional pill badge, typically used to highlight a tag or state.
@@ -113,6 +115,12 @@ interface RcItemCardProps {
113
115
  /** Makes the card clickable and emits 'card-click' on click/enter/space */
114
116
  clickable?: boolean;
115
117
 
118
+ /** The card will have same style as hover clickable with the blue border when selected */
119
+ selected?: boolean;
120
+
121
+ /** Disables the card, preventing clicks and keyboard interaction */
122
+ disabled?: boolean;
123
+
116
124
  role?: 'link' | 'button' | undefined;
117
125
  }
118
126
 
@@ -131,6 +139,10 @@ const emit = defineEmits<{(e: 'card-click', value?: ItemValue): void; (e: 'actio
131
139
  * which then gets used to ignore 'card-click'
132
140
  */
133
141
  function _handleCardClick(e: MouseEvent | KeyboardEvent) {
142
+ if (props.disabled) {
143
+ return;
144
+ }
145
+
134
146
  const interactiveSelector = '[item-card-action]';
135
147
 
136
148
  // Prevent card click if the user clicks on an inner actionable element like repo, category, or tag
@@ -162,8 +174,8 @@ const statusTooltips = computed(() => props.header.statuses?.map((status) => lab
162
174
 
163
175
  const cardMeta = computed(() => ({
164
176
  ariaLabel: props.clickable ? t('itemCard.ariaLabel.clickable', { cardTitle: labelText(props.header.title) }) : undefined,
165
- tabIndex: props.clickable ? '0' : undefined,
166
- role: props.role ?? (props.clickable ? 'button' : undefined),
177
+ tabIndex: props.clickable && !props.disabled ? '0' : undefined,
178
+ role: props.role ?? (props.clickable && !props.disabled ? 'button' : undefined),
167
179
  actionMenuLabel: props.actions && t('itemCard.actionMenu.label', { cardTitle: labelText(props.header.title) }),
168
180
  }));
169
181
 
@@ -177,7 +189,7 @@ const cursorValue = computed(() => props.clickable ? 'pointer' : 'auto');
177
189
  :data-testid="`item-card-${id}`"
178
190
  :class="{
179
191
  'clickable':
180
- clickable
192
+ clickable && !disabled,'selected': selected, 'disabled': disabled
181
193
  }"
182
194
  @click="_handleCardClick"
183
195
  >
@@ -190,11 +202,19 @@ const cursorValue = computed(() => props.clickable ? 'pointer' : 'auto');
190
202
  :class="['item-card-image', variant]"
191
203
  data-testid="item-card-image"
192
204
  >
193
- <LazyImage
194
- :src="image.src"
195
- :alt="imageAlt"
196
- :style="{'width': '40px', 'height': '40px', 'object-fit': 'contain'}"
197
- />
205
+ <template v-if="image.icon">
206
+ <RcIcon
207
+ :type="image.icon"
208
+ size="xxlarge"
209
+ />
210
+ </template>
211
+ <template v-else-if="image.src">
212
+ <LazyImage
213
+ :src="image.src"
214
+ :alt="imageAlt"
215
+ :style="{'width': '40px', 'height': '40px', 'object-fit': 'contain'}"
216
+ />
217
+ </template>
198
218
  </div>
199
219
  </slot>
200
220
  <slot name="item-card-pill">
@@ -229,11 +249,19 @@ const cursorValue = computed(() => props.clickable ? 'pointer' : 'auto');
229
249
  :class="['item-card-image', variant]"
230
250
  data-testid="item-card-image"
231
251
  >
232
- <LazyImage
233
- :src="image.src"
234
- :alt="imageAlt"
235
- :style="{'width': '24px', 'height': '24px', 'object-fit': 'contain'}"
236
- />
252
+ <template v-if="image.icon">
253
+ <RcIcon
254
+ :type="image.icon"
255
+ size="xlarge"
256
+ />
257
+ </template>
258
+ <template v-else-if="image.src">
259
+ <LazyImage
260
+ :src="image.src"
261
+ :alt="imageAlt"
262
+ :style="{'width': '32px', 'height': '32px', 'object-fit': 'contain'}"
263
+ />
264
+ </template>
237
265
  </div>
238
266
  </slot>
239
267
  </template>
@@ -286,7 +314,10 @@ const cursorValue = computed(() => props.clickable ? 'pointer' : 'auto');
286
314
  </div>
287
315
  </div>
288
316
 
289
- <slot name="item-card-sub-header" />
317
+ <slot name="item-card-sub-header">
318
+ <!-- DIV added to add the gap if the sub-header is not provided -->
319
+ <div />
320
+ </slot>
290
321
 
291
322
  <template v-if="$slots['item-card-content']">
292
323
  <slot name="item-card-content">
@@ -324,10 +355,26 @@ $image-medium-box-width: 48px;
324
355
  background: var(--body-bg);
325
356
  cursor: v-bind(cursorValue);
326
357
 
327
- &.clickable:hover {
358
+ &.clickable:hover,
359
+ &.selected {
328
360
  border-color: var(--primary);
329
361
  }
330
362
 
363
+ &.disabled {
364
+ cursor: not-allowed;
365
+ background-color: var(--disabled-bg);
366
+ color: var(--disabled-text);
367
+
368
+ .item-card-image {
369
+ background-color: var(--disabled-bg);
370
+ }
371
+
372
+ .item-card-header-title,
373
+ .item-card-content {
374
+ color: var(--disabled-text);
375
+ }
376
+ }
377
+
331
378
  &:has(.item-card-header-left:focus-visible) {
332
379
  border-color: var(--primary);
333
380
  @include focus-outline;
@@ -344,8 +391,9 @@ $image-medium-box-width: 48px;
344
391
  display: flex;
345
392
  align-items: center;
346
393
  justify-content: center;
347
- background: #fff;
394
+ background: var(--rc-image-bg);
348
395
  border-radius: var(--border-radius);
396
+ color: var(--rc-image-color);
349
397
 
350
398
  &.small {
351
399
  width: 32px;
@@ -362,6 +410,10 @@ $image-medium-box-width: 48px;
362
410
  height: 24px;
363
411
  color: var(--body-text);
364
412
 
413
+ &.small {
414
+ height: 32px;
415
+ }
416
+
365
417
  &-left,
366
418
  &-right {
367
419
  display: flex;
package/store/prefs.js CHANGED
@@ -122,6 +122,9 @@ export const READ_SUPPORT_NOTICE = create('read-support-notice', '', { parseJSON
122
122
  export const READ_UPCOMING_SUPPORT_NOTICE = create('read-upcoming-support-notice', '', { parseJSON });
123
123
  export const READ_ANNOUNCEMENTS = create('read-announcements', '', { parseJSON });
124
124
 
125
+ // Hidden banners
126
+ export const HIDE_SUSE_APP_COLLECTION_REPO_BANNER = create('hide-suse-app-collection-repo-banner', false);
127
+
125
128
  // --------------------
126
129
 
127
130
  const cookiePrefix = 'R_';