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

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 (142) 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/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +109 -0
  10. package/components/Resource/Detail/Card/StatusCard/index.vue +21 -4
  11. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +19 -2
  12. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +19 -11
  13. package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +12 -0
  14. package/components/Resource/Detail/ResourcePopover/index.vue +2 -0
  15. package/components/Resource/Detail/ResourceRow.vue +2 -2
  16. package/components/ResourceList/index.vue +7 -4
  17. package/components/Window/ContainerLogs.vue +48 -37
  18. package/components/fleet/FleetClusterTargets/TargetsList.vue +2 -2
  19. package/components/fleet/FleetClusterTargets/index.vue +6 -1
  20. package/components/fleet/GitRepoAdvancedTab.vue +333 -0
  21. package/components/fleet/GitRepoMetadataTab.vue +43 -0
  22. package/components/fleet/GitRepoRepositoryTab.vue +101 -0
  23. package/components/fleet/GitRepoTargetTab.vue +77 -0
  24. package/components/fleet/HelmOpAdvancedTab.vue +247 -0
  25. package/components/fleet/HelmOpChartTab.vue +158 -0
  26. package/components/fleet/HelmOpMetadataTab.vue +46 -0
  27. package/components/fleet/HelmOpTargetTab.vue +84 -0
  28. package/components/fleet/HelmOpValuesTab.vue +147 -0
  29. package/components/fleet/__tests__/FleetClusterTargets.test.ts +119 -70
  30. package/components/form/NodeScheduling.vue +81 -7
  31. package/components/form/PodAffinity.vue +1 -36
  32. package/components/form/ResourceLabeledSelect.vue +8 -4
  33. package/components/form/ResourceQuota/Namespace.vue +30 -9
  34. package/components/form/ResourceQuota/NamespaceRow.vue +25 -7
  35. package/components/form/ResourceQuota/Project.vue +140 -82
  36. package/components/form/ResourceQuota/ResourceQuotaEntry.vue +145 -0
  37. package/components/form/ResourceQuota/__tests__/Namespace.test.ts +307 -0
  38. package/components/form/ResourceQuota/__tests__/NamespaceRow.test.ts +281 -0
  39. package/components/form/ResourceQuota/__tests__/Project.test.ts +274 -27
  40. package/components/form/ResourceQuota/__tests__/ResourceQuotaEntry.test.ts +215 -0
  41. package/components/form/SchedulingCustomization.vue +14 -6
  42. package/components/form/SelectOrCreateAuthSecret.vue +107 -18
  43. package/components/form/__tests__/NodeScheduling.test.ts +12 -9
  44. package/components/form/__tests__/PodAffinity.test.ts +21 -2
  45. package/components/form/__tests__/SchedulingCustomization.test.ts +240 -0
  46. package/components/formatter/ClusterLink.vue +8 -0
  47. package/components/formatter/SecretOrigin.vue +79 -0
  48. package/config/labels-annotations.js +7 -6
  49. package/config/pagination-table-headers.js +6 -4
  50. package/config/product/explorer.js +1 -11
  51. package/config/query-params.js +3 -0
  52. package/config/settings.ts +15 -2
  53. package/config/table-headers.js +21 -17
  54. package/config/types.js +23 -8
  55. package/detail/workload/index.vue +11 -16
  56. package/dialog/DeactivateDriverDialog.vue +1 -1
  57. package/dialog/Ipv6NetworkingDialog.vue +156 -0
  58. package/dialog/ScalePoolDownDialog.vue +2 -2
  59. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -1
  60. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +1 -0
  61. package/edit/__tests__/management.cattle.io.project.test.js +56 -128
  62. package/edit/auth/oidc.vue +1 -1
  63. package/edit/catalog.cattle.io.clusterrepo.vue +155 -25
  64. package/edit/fleet.cattle.io.gitrepo.vue +153 -283
  65. package/edit/fleet.cattle.io.helmop.vue +190 -332
  66. package/edit/management.cattle.io.project.vue +5 -42
  67. package/edit/management.cattle.io.setting.vue +6 -0
  68. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +55 -24
  69. package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +1 -103
  70. package/edit/provisioning.cattle.io.cluster/__tests__/index.test.ts +13 -1
  71. package/edit/provisioning.cattle.io.cluster/__tests__/rke2-fleet-cluster-agent.test.ts +283 -0
  72. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -49
  73. package/edit/provisioning.cattle.io.cluster/ingress/IngressCards.vue +112 -0
  74. package/edit/provisioning.cattle.io.cluster/ingress/IngressConfiguration.vue +158 -0
  75. package/edit/provisioning.cattle.io.cluster/rke2.vue +171 -72
  76. package/edit/provisioning.cattle.io.cluster/shared.ts +36 -1
  77. package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +2 -1
  78. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +55 -7
  79. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +319 -0
  80. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +2 -1
  81. package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +13 -1
  82. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +10 -44
  83. package/edit/secret/index.vue +1 -1
  84. package/edit/token.vue +68 -29
  85. package/edit/workload/__tests__/index.test.ts +2 -37
  86. package/edit/workload/index.vue +6 -2
  87. package/edit/workload/mixins/workload.js +0 -32
  88. package/list/__tests__/management.cattle.io.setting.test.ts +198 -0
  89. package/list/management.cattle.io.setting.vue +13 -0
  90. package/list/provisioning.cattle.io.cluster.vue +50 -1
  91. package/list/secret.vue +4 -9
  92. package/list/service.vue +6 -8
  93. package/machine-config/amazonec2.vue +11 -4
  94. package/machine-config/components/EC2Networking.vue +46 -30
  95. package/machine-config/components/__tests__/EC2Networking.test.ts +7 -7
  96. package/machine-config/components/__tests__/utils/vpcSubnetMockData.js +0 -9
  97. package/machine-config/digitalocean.vue +3 -3
  98. package/models/__tests__/namespace.test.ts +11 -0
  99. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +96 -0
  100. package/models/__tests__/workload.test.ts +42 -1
  101. package/models/catalog.cattle.io.clusterrepo.js +30 -4
  102. package/models/ext.cattle.io.token.js +48 -0
  103. package/models/kontainerdriver.js +2 -2
  104. package/models/namespace.js +7 -1
  105. package/models/nodedriver.js +2 -2
  106. package/models/provisioning.cattle.io.cluster.js +28 -7
  107. package/models/secret.js +0 -17
  108. package/models/service.js +44 -1
  109. package/models/token.js +4 -0
  110. package/models/workload.js +12 -6
  111. package/package.json +1 -1
  112. package/pages/account/index.vue +96 -67
  113. package/pages/auth/setup.vue +5 -14
  114. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +4 -1
  115. package/pages/c/_cluster/apps/charts/index.vue +93 -4
  116. package/pages/c/_cluster/apps/charts/install.vue +317 -42
  117. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -4
  118. package/pages/c/_cluster/settings/index.vue +3 -1
  119. package/plugins/dashboard-store/__tests__/getters.test.ts +108 -0
  120. package/plugins/dashboard-store/__tests__/resource-class.test.ts +27 -0
  121. package/plugins/dashboard-store/actions.js +3 -8
  122. package/plugins/dashboard-store/getters.js +7 -5
  123. package/plugins/dashboard-store/mutations.js +4 -1
  124. package/plugins/dashboard-store/resource-class.js +3 -3
  125. package/plugins/steve/__tests__/steve-class.test.ts +102 -141
  126. package/plugins/steve/steve-class.js +12 -3
  127. package/plugins/steve/steve-pagination-utils.ts +6 -2
  128. package/rancher-components/RcIcon/types.ts +2 -0
  129. package/rancher-components/RcItemCard/RcItemCard.vue +64 -19
  130. package/store/prefs.js +3 -0
  131. package/types/aws-sdk.d.ts +121 -0
  132. package/types/resources/node.ts +15 -0
  133. package/types/shell/index.d.ts +536 -506
  134. package/types/store/pagination.types.ts +5 -5
  135. package/utils/__tests__/array.test.ts +1 -29
  136. package/utils/__tests__/cluster-agent-configuration.test.ts +203 -0
  137. package/utils/array.ts +0 -11
  138. package/utils/aws.ts +21 -0
  139. package/utils/cluster.js +22 -2
  140. package/utils/selector-typed.ts +1 -1
  141. package/components/__tests__/ProjectRow.test.ts +0 -206
  142. package/components/form/ResourceQuota/ProjectRow.vue +0 -277
@@ -47,6 +47,7 @@ describe('dashboard-store: getters', () => {
47
47
  expect(urlForGetter('typeFoo', 'idBar')).toBe('protocol/urlFoo/idBar');
48
48
  });
49
49
  });
50
+
50
51
  describe('dashboard-store > getters > urlOptions', () => {
51
52
  // we're not testing function output based off of state or getter inputs here since they are dependencies
52
53
  const state = { config: { baseUrl: 'protocol' } };
@@ -97,4 +98,111 @@ describe('dashboard-store: getters', () => {
97
98
  expect(urlOptionsGetter('foo', { sortBy: 'bar', sortOrder: 'baz' })).toBe('foo');
98
99
  });
99
100
  });
101
+
102
+ describe('dashboard-store > getters > matchingLabelSelector', () => {
103
+ const { matchingLabelSelector } = getters;
104
+ const labelSelector = { matchLabels: { foo: 'bar' } };
105
+ const selectorString = 'foo=bar';
106
+ const type = 'pod';
107
+ const namespace = 'default';
108
+ const allResources = [{ id: '1' }];
109
+ const matchingResources = [{ id: '1' }];
110
+
111
+ it('returns all resources if store has a VAI page matching the selector', () => {
112
+ const state = {};
113
+ const rootState = {};
114
+ const gettersMock = {
115
+ normalizeType: jest.fn((t) => t),
116
+ havePage: jest.fn().mockReturnValue({
117
+ request: {
118
+ namespace,
119
+ pagination: {
120
+ filters: [],
121
+ labelSelector
122
+ }
123
+ }
124
+ }),
125
+ all: jest.fn().mockReturnValue(allResources),
126
+ haveSelector: jest.fn(),
127
+ haveAll: jest.fn(),
128
+ matching: jest.fn(),
129
+ };
130
+
131
+ const result = matchingLabelSelector(state, gettersMock, rootState)(type, labelSelector, namespace);
132
+
133
+ expect(result).toStrictEqual(allResources);
134
+ expect(gettersMock.all).toHaveBeenCalledWith(type);
135
+ });
136
+
137
+ it('returns all resources if store has the specific selector cached', () => {
138
+ const state = {};
139
+ const rootState = {};
140
+ const gettersMock = {
141
+ normalizeType: jest.fn((t) => t),
142
+ havePage: jest.fn().mockReturnValue(null),
143
+ all: jest.fn().mockReturnValue(allResources),
144
+ haveSelector: jest.fn().mockReturnValue(true),
145
+ haveAll: jest.fn(),
146
+ matching: jest.fn(),
147
+ };
148
+
149
+ const result = matchingLabelSelector(state, gettersMock, rootState)(type, labelSelector, namespace);
150
+
151
+ expect(result).toStrictEqual(allResources);
152
+ expect(gettersMock.haveSelector).toHaveBeenCalledWith(type, selectorString);
153
+ });
154
+
155
+ it('returns matching resources if store has a page (subset)', () => {
156
+ const state = {};
157
+ const rootState = {};
158
+ const gettersMock = {
159
+ normalizeType: jest.fn((t) => t),
160
+ havePage: jest.fn().mockReturnValue({ request: {} }), // Truthy, but doesn't match first if
161
+ all: jest.fn(),
162
+ haveSelector: jest.fn().mockReturnValue(false),
163
+ haveAll: jest.fn(),
164
+ matching: jest.fn().mockReturnValue(matchingResources),
165
+ };
166
+
167
+ const result = matchingLabelSelector(state, gettersMock, rootState)(type, labelSelector, namespace);
168
+
169
+ expect(result).toStrictEqual(matchingResources);
170
+ expect(gettersMock.matching).toHaveBeenCalledWith(type, selectorString, namespace);
171
+ });
172
+
173
+ it('returns matching resources if store has all resources', () => {
174
+ const state = {};
175
+ const rootState = {};
176
+ const gettersMock = {
177
+ normalizeType: jest.fn((t) => t),
178
+ havePage: jest.fn().mockReturnValue(null),
179
+ all: jest.fn(),
180
+ haveSelector: jest.fn().mockReturnValue(false),
181
+ haveAll: jest.fn().mockReturnValue(true),
182
+ matching: jest.fn().mockReturnValue(matchingResources),
183
+ };
184
+
185
+ const result = matchingLabelSelector(state, gettersMock, rootState)(type, labelSelector, namespace);
186
+
187
+ expect(result).toStrictEqual(matchingResources);
188
+ expect(gettersMock.matching).toHaveBeenCalledWith(type, selectorString, namespace);
189
+ });
190
+
191
+ it('returns empty array if no conditions met', () => {
192
+ const state = {};
193
+ const rootState = {};
194
+ const gettersMock = {
195
+ normalizeType: jest.fn((t) => t),
196
+ havePage: jest.fn().mockReturnValue(null),
197
+ all: jest.fn(),
198
+ haveSelector: jest.fn().mockReturnValue(false),
199
+ haveAll: jest.fn().mockReturnValue(false),
200
+ matching: jest.fn(),
201
+ };
202
+
203
+ const result = matchingLabelSelector(state, gettersMock, rootState)(type, labelSelector, namespace);
204
+
205
+ expect(result).toStrictEqual([]);
206
+ });
207
+ });
100
208
  });
@@ -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',