@rancher/shell 3.0.8 → 3.0.9-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/apis/intf/modal.ts +38 -0
  2. package/apis/intf/slide-in.ts +3 -1
  3. package/apis/shell/__tests__/slide-in.test.ts +36 -0
  4. package/apis/shell/slide-in.ts +5 -1
  5. package/assets/styles/base/_color.scss +1 -0
  6. package/assets/styles/base/_typography.scss +14 -5
  7. package/assets/styles/themes/_light.scss +1 -1
  8. package/assets/styles/themes/_modern.scss +1 -1
  9. package/assets/translations/en-us.yaml +94 -33
  10. package/assets/translations/zh-hans.yaml +0 -2
  11. package/components/ActionMenuShell.vue +4 -4
  12. package/components/CodeMirror.vue +4 -3
  13. package/components/DetailText.vue +54 -7
  14. package/components/Drawer/Chrome.vue +11 -4
  15. package/components/Drawer/DrawerCard.vue +19 -0
  16. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +3 -11
  17. package/components/Drawer/ResourceDetailDrawer/__tests__/ConfigTab.test.ts +2 -2
  18. package/components/Drawer/ResourceDetailDrawer/index.vue +3 -20
  19. package/components/Drawer/types.ts +1 -0
  20. package/components/DynamicContent/DynamicContentCloseButton.vue +2 -2
  21. package/components/LocaleSelector.vue +1 -1
  22. package/components/Markdown.vue +1 -1
  23. package/components/PopoverCard.vue +3 -3
  24. package/components/Resource/Detail/Card/ExtrasCard.vue +39 -0
  25. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +142 -0
  26. package/components/Resource/Detail/Card/StateCard/composables.ts +41 -11
  27. package/components/Resource/Detail/Card/StateCard/index.vue +3 -9
  28. package/components/Resource/Detail/Card/StateCard/types.ts +6 -0
  29. package/components/Resource/Detail/Card/{PodsCard → StatusCard}/index.vue +11 -10
  30. package/components/Resource/Detail/Card/__tests__/PodsCard.test.ts +24 -25
  31. package/components/Resource/Detail/Cards.vue +27 -0
  32. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +70 -0
  33. package/components/Resource/Detail/Masthead/index.vue +5 -0
  34. package/components/Resource/Detail/Metadata/KeyValueRow.vue +4 -2
  35. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -2
  36. package/components/Resource/Detail/ResourceRow.types.ts +14 -0
  37. package/components/Resource/Detail/ResourceRow.vue +23 -35
  38. package/components/Resource/Detail/StatusRow.vue +5 -2
  39. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +38 -7
  40. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +106 -2
  41. package/components/Resource/Detail/TitleBar/composables.ts +2 -1
  42. package/components/Resource/Detail/TitleBar/index.vue +41 -6
  43. package/components/ResourceDetail/Masthead/__tests__/index.test.ts +49 -1
  44. package/components/ResourceDetail/Masthead/__tests__/latest.test.ts +85 -0
  45. package/components/ResourceDetail/Masthead/index.vue +1 -0
  46. package/components/ResourceDetail/Masthead/latest.vue +8 -1
  47. package/components/ResourceDetail/Masthead/legacy.vue +1 -1
  48. package/components/Setting.vue +1 -1
  49. package/components/SortableTable/index.vue +25 -0
  50. package/components/SortableTable/selection.js +25 -12
  51. package/components/SortableTable/sorting.js +1 -1
  52. package/components/Tabbed/Tab.vue +1 -0
  53. package/components/Tabbed/index.vue +29 -6
  54. package/components/Window/ContainerShell.vue +10 -13
  55. package/components/fleet/FleetClusterTargets/TargetsList.vue +47 -29
  56. package/components/fleet/FleetClusterTargets/index.vue +82 -29
  57. package/components/fleet/FleetClusters.vue +26 -12
  58. package/components/fleet/FleetGitRepoPaths.vue +2 -2
  59. package/components/fleet/FleetResources.vue +14 -0
  60. package/components/fleet/FleetValuesFrom.vue +2 -2
  61. package/components/fleet/__tests__/FleetClusterTargets.test.ts +531 -0
  62. package/components/fleet/__tests__/FleetClusters.test.ts +576 -0
  63. package/components/fleet/dashboard/ResourceDetails.vue +96 -123
  64. package/components/form/Conditions.vue +1 -15
  65. package/components/form/HookOption.vue +5 -0
  66. package/components/form/LabeledSelect.vue +1 -1
  67. package/components/form/LifecycleHooks.vue +2 -6
  68. package/components/form/ResourceLabeledSelect.vue +12 -1
  69. package/components/form/SeccompProfile.vue +113 -0
  70. package/components/form/Security.vue +244 -133
  71. package/components/form/__tests__/LabeledSelect.test.ts +1 -1
  72. package/components/form/__tests__/SeccompProfile.test.js +124 -0
  73. package/components/form/__tests__/Security.test.ts +125 -37
  74. package/components/formatter/Autoscaler.vue +2 -2
  75. package/components/formatter/FleetSummaryGraph.vue +4 -1
  76. package/components/nav/Group.vue +5 -0
  77. package/components/nav/Header.vue +3 -3
  78. package/components/nav/HeaderPageActionMenu.vue +1 -1
  79. package/components/nav/NamespaceFilter.vue +6 -6
  80. package/components/nav/NotificationCenter/index.vue +1 -1
  81. package/components/nav/TopLevelMenu.helper.ts +41 -16
  82. package/components/nav/TopLevelMenu.vue +45 -25
  83. package/components/nav/WorkspaceSwitcher.vue +1 -1
  84. package/components/nav/__tests__/TopLevelMenu.helper.test.ts +277 -0
  85. package/components/nav/__tests__/TopLevelMenu.test.ts +160 -4
  86. package/components/templates/default.vue +0 -3
  87. package/components/templates/home.vue +0 -3
  88. package/components/templates/plain.vue +0 -3
  89. package/composables/useClickOutside.ts +1 -1
  90. package/config/product/explorer.js +1 -2
  91. package/config/types.js +41 -8
  92. package/detail/__tests__/workload.test.ts +8 -16
  93. package/detail/catalog.cattle.io.app.vue +6 -0
  94. package/detail/fleet.cattle.io.cluster.vue +6 -0
  95. package/detail/workload/index.vue +7 -109
  96. package/edit/__tests__/projectsecret.test.ts +42 -0
  97. package/edit/auth/__tests__/oidc.test.ts +50 -0
  98. package/edit/auth/oidc.vue +68 -44
  99. package/edit/autoscaling.horizontalpodautoscaler/index.vue +140 -59
  100. package/edit/autoscaling.horizontalpodautoscaler/metrics-row.vue +41 -5
  101. package/edit/projectsecret.vue +29 -0
  102. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +89 -200
  103. package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +58 -17
  104. package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -0
  105. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +3 -63
  106. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +82 -14
  107. package/edit/workload/__tests__/index.test.ts +122 -85
  108. package/edit/workload/index.vue +48 -29
  109. package/edit/workload/mixins/workload.js +85 -32
  110. package/list/catalog.cattle.io.clusterrepo.vue +1 -1
  111. package/list/projectsecret.vue +2 -2
  112. package/machine-config/__tests__/vmwarevsphere.test.ts +64 -0
  113. package/machine-config/amazonec2.vue +2 -2
  114. package/machine-config/vmwarevsphere.vue +58 -4
  115. package/mixins/__tests__/brand.spec.ts +18 -13
  116. package/mixins/__tests__/chart.test.ts +63 -0
  117. package/mixins/chart.js +56 -51
  118. package/models/__tests__/catalog.cattle.io.app.test.ts +33 -0
  119. package/models/__tests__/workload.test.ts +333 -0
  120. package/models/catalog.cattle.io.app.js +8 -0
  121. package/models/pod.js +14 -0
  122. package/models/secret.js +1 -1
  123. package/models/workload.js +93 -27
  124. package/package.json +4 -4
  125. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +91 -0
  126. package/pages/c/_cluster/apps/charts/install.vue +4 -4
  127. package/pages/c/_cluster/explorer/EventsTable.vue +2 -2
  128. package/pages/c/_cluster/fleet/index.vue +18 -12
  129. package/pages/c/_cluster/manager/hostedprovider/index.vue +1 -19
  130. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
  131. package/pages/c/_cluster/uiplugins/index.vue +1 -1
  132. package/plugins/dashboard-store/__tests__/resource-class.test.ts +234 -0
  133. package/plugins/dashboard-store/actions.js +9 -8
  134. package/plugins/dashboard-store/resource-class.js +97 -1
  135. package/plugins/steve/__tests__/revision.test.ts +84 -0
  136. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +30 -0
  137. package/plugins/steve/__tests__/subscribe.spec.ts +134 -0
  138. package/plugins/steve/mutations.js +9 -0
  139. package/plugins/steve/revision.ts +26 -0
  140. package/plugins/steve/steve-pagination-utils.ts +6 -5
  141. package/plugins/steve/subscribe.js +211 -51
  142. package/plugins/subscribe-events.ts +2 -2
  143. package/rancher-components/Form/Checkbox/Checkbox.vue +13 -0
  144. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -1
  145. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +1 -1
  146. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +3 -1
  147. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -1
  148. package/rancher-components/Pill/RcTag/RcTag.vue +1 -1
  149. package/rancher-components/Pill/index.ts +4 -0
  150. package/rancher-components/RcButton/RcButton.test.ts +53 -9
  151. package/rancher-components/RcButton/RcButton.vue +217 -25
  152. package/rancher-components/RcButton/types.ts +27 -1
  153. package/rancher-components/RcDropdown/RcDropdownMenu.vue +4 -4
  154. package/rancher-components/RcDropdown/types.ts +3 -3
  155. package/rancher-components/RcIcon/RcIcon.test.ts +42 -0
  156. package/rancher-components/RcIcon/RcIcon.vue +9 -6
  157. package/rancher-components/RcIcon/types.ts +13 -9
  158. package/rancher-components/utils/status.test.ts +10 -15
  159. package/rancher-components/utils/status.ts +5 -6
  160. package/store/aws.js +18 -12
  161. package/store/index.js +4 -8
  162. package/store/type-map.utils.ts +1 -1
  163. package/types/kube/kube-api.ts +29 -3
  164. package/types/rancher/steve.api.ts +40 -0
  165. package/types/shell/index.d.ts +99 -0
  166. package/types/store/dashboard-store.types.ts +29 -7
  167. package/types/store/pagination.types.ts +1 -0
  168. package/types/store/subscribe-events.types.ts +1 -0
  169. package/utils/__tests__/azure.test.ts +56 -0
  170. package/utils/__tests__/back-off.test.ts +364 -245
  171. package/utils/__tests__/error.test.ts +44 -0
  172. package/utils/__tests__/fleet.test.ts +8 -1
  173. package/utils/__tests__/pagination-wrapper.test.ts +167 -0
  174. package/utils/__tests__/version.test.ts +55 -1
  175. package/utils/azure.js +12 -0
  176. package/utils/back-off.ts +302 -69
  177. package/utils/cspAdaptor.ts +32 -14
  178. package/utils/dynamic-content/__tests__/index.test.ts +1 -1
  179. package/utils/dynamic-content/__tests__/new-release.test.ts +48 -7
  180. package/utils/dynamic-content/__tests__/support-notice.test.ts +1 -4
  181. package/utils/dynamic-content/index.ts +1 -6
  182. package/utils/dynamic-content/new-release.ts +5 -3
  183. package/utils/dynamic-content/types.d.ts +0 -1
  184. package/utils/error.js +9 -0
  185. package/utils/fleet.ts +2 -2
  186. package/utils/inactivity.ts +2 -3
  187. package/utils/pagination-wrapper.ts +101 -17
  188. package/utils/validators/formRules/index.ts +3 -0
  189. package/utils/version.js +38 -0
  190. package/components/auth/AzureWarning.vue +0 -77
  191. /package/components/Resource/Detail/{Card/PodsCard/Bubble.vue → Bubble.vue} +0 -0
  192. /package/components/Resource/Detail/Card/{PodsCard → StatusCard}/composable.ts +0 -0
package/mixins/chart.js CHANGED
@@ -303,57 +303,6 @@ export default {
303
303
 
304
304
  this.fetchStoreChart();
305
305
 
306
- if ( this.query.appNamespace && this.query.appName ) {
307
- // First check the URL query for an app name and namespace.
308
- // Use those values to check for a catalog app resource.
309
- // If found, set the form to edit mode. If not, set the
310
- // form to create mode.
311
-
312
- try {
313
- this.existing = await this.$store.dispatch('cluster/find', {
314
- type: CATALOG.APP,
315
- id: `${ this.query.appNamespace }/${ this.query.appName }`,
316
- });
317
-
318
- await this.existing?.fetchValues(true);
319
-
320
- this.mode = _EDIT;
321
- } catch (e) {
322
- this.mode = _CREATE;
323
- this.existing = null;
324
- }
325
- } else if ( this.chart?.targetNamespace && this.chart?.targetName ) {
326
- // If the app name and namespace values are not provided in the
327
- // query, fall back on target values defined in the Helm chart itself.
328
-
329
- // Ask to install a special chart with fixed namespace/name
330
- // or edit it if there's an existing install.
331
-
332
- try {
333
- this.existing = await this.$store.dispatch('cluster/find', {
334
- type: CATALOG.APP,
335
- id: `${ this.chart.targetNamespace }/${ this.chart.targetName }`,
336
- });
337
- this.mode = _EDIT;
338
- } catch (e) {
339
- this.mode = _CREATE;
340
- this.existing = null;
341
- }
342
- } else if (this.chart) {
343
- const matching = this.chart.matchingInstalledApps;
344
-
345
- if (matching.length === 1) {
346
- this.existing = matching[0];
347
- this.mode = _EDIT;
348
- } else {
349
- this.mode = _CREATE;
350
- }
351
- } else {
352
- // Regular create
353
-
354
- this.mode = _CREATE;
355
- }
356
-
357
306
  if ( !this.chart ) {
358
307
  return;
359
308
  }
@@ -418,6 +367,62 @@ export default {
418
367
 
419
368
  console.error('Unable to fetch VersionInfo: ', e); // eslint-disable-line no-console
420
369
  }
370
+
371
+ if ( this.query.appNamespace && this.query.appName ) {
372
+ // First check the URL query for an app name and namespace.
373
+ // Use those values to check for a catalog app resource.
374
+ // If found, set the form to edit mode. If not, set the
375
+ // form to create mode.
376
+
377
+ try {
378
+ this.existing = await this.$store.dispatch('cluster/find', {
379
+ type: CATALOG.APP,
380
+ id: `${ this.query.appNamespace }/${ this.query.appName }`,
381
+ });
382
+
383
+ await this.existing?.fetchValues(true);
384
+
385
+ this.mode = _EDIT;
386
+ } catch (e) {
387
+ this.mode = _CREATE;
388
+ this.existing = null;
389
+ }
390
+ } else {
391
+ const targetNamespace = this.version?.annotations?.[CATALOG_ANNOTATIONS.NAMESPACE];
392
+ const targetName = this.version?.annotations?.[CATALOG_ANNOTATIONS.RELEASE_NAME];
393
+
394
+ if ( targetNamespace && targetName ) {
395
+ // If the app name and namespace values are not provided in the
396
+ // query, fall back on target values defined in the Helm chart itself.
397
+
398
+ // Ask to install a special chart with fixed namespace/name
399
+ // or edit it if there's an existing install.
400
+
401
+ try {
402
+ this.existing = await this.$store.dispatch('cluster/find', {
403
+ type: CATALOG.APP,
404
+ id: `${ targetNamespace }/${ targetName }`,
405
+ });
406
+ this.mode = _EDIT;
407
+ } catch (e) {
408
+ this.mode = _CREATE;
409
+ this.existing = null;
410
+ }
411
+ } else if (this.chart) {
412
+ const matching = this.chart.matchingInstalledApps;
413
+
414
+ if (matching.length === 1) {
415
+ this.existing = matching[0];
416
+ this.mode = _EDIT;
417
+ } else {
418
+ this.mode = _CREATE;
419
+ }
420
+ } else {
421
+ // Regular create
422
+
423
+ this.mode = _CREATE;
424
+ }
425
+ }
421
426
  }, // End of fetchChart
422
427
 
423
428
  // Charts have an annotation that specifies any additional charts that should be installed at the same time eg CRDs
@@ -145,4 +145,37 @@ describe('class CatalogApp', () => {
145
145
  expect(catalogApp.upgradeAvailable).toBe(expected);
146
146
  });
147
147
  });
148
+
149
+ describe('valuesLoaded', () => {
150
+ it('should be false if data is missing (e.g. secret)', () => {
151
+ const catalogApp = new CatalogApp({});
152
+
153
+ jest.spyOn(catalogApp, '_secret', 'get').mockReturnValue(null);
154
+
155
+ expect(catalogApp.valuesLoaded).toBe(false);
156
+ });
157
+
158
+ it('should be false if part of the data is missing', () => {
159
+ const catalogApp = new CatalogApp({});
160
+
161
+ jest.spyOn(catalogApp, '_secret', 'get').mockReturnValue({ data: {} });
162
+
163
+ expect(catalogApp.valuesLoaded).toBe(false);
164
+ });
165
+
166
+ it('should be true if all required data is present', () => {
167
+ const catalogApp = new CatalogApp({});
168
+
169
+ jest.spyOn(catalogApp, '_secret', 'get').mockReturnValue({
170
+ data: {
171
+ release: {
172
+ config: { foo: 'bar' },
173
+ chart: { values: { baz: 'qux' } }
174
+ }
175
+ }
176
+ });
177
+
178
+ expect(catalogApp.valuesLoaded).toBe(true);
179
+ });
180
+ });
148
181
  });
@@ -1,5 +1,6 @@
1
1
  import Workload from '@shell/models/workload.js';
2
2
  import { steveClassJunkObject } from '@shell/plugins/steve/__tests__/utils/steve-mocks';
3
+ import { WORKLOAD_TYPES, SERVICE } from '@shell/config/types';
3
4
 
4
5
  describe('class: Workload', () => {
5
6
  describe('given custom workload keys', () => {
@@ -89,4 +90,336 @@ describe('class: Workload', () => {
89
90
  });
90
91
  });
91
92
  });
93
+
94
+ describe('method: scale', () => {
95
+ it('should call scaleUp when isUp is true', async() => {
96
+ const scaleUpMock = jest.fn().mockResolvedValue(undefined);
97
+ const workload = new Workload({
98
+ type: WORKLOAD_TYPES.DEPLOYMENT,
99
+ metadata: { name: 'test', namespace: 'default' },
100
+ spec: { replicas: 1 }
101
+ }, {
102
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
103
+ dispatch: jest.fn(),
104
+ rootGetters: { 'i18n/t': jest.fn() },
105
+ });
106
+
107
+ workload.scaleUp = scaleUpMock;
108
+
109
+ await workload.scale(true);
110
+
111
+ expect(scaleUpMock).toHaveBeenCalledWith();
112
+ });
113
+
114
+ it('should call scaleDown when isUp is false', async() => {
115
+ const scaleDownMock = jest.fn().mockResolvedValue(undefined);
116
+ const workload = new Workload({
117
+ type: WORKLOAD_TYPES.DEPLOYMENT,
118
+ metadata: { name: 'test', namespace: 'default' },
119
+ spec: { replicas: 2 }
120
+ }, {
121
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
122
+ dispatch: jest.fn(),
123
+ rootGetters: { 'i18n/t': jest.fn() },
124
+ });
125
+
126
+ workload.scaleDown = scaleDownMock;
127
+
128
+ await workload.scale(false);
129
+
130
+ expect(scaleDownMock).toHaveBeenCalledWith();
131
+ });
132
+
133
+ it('should dispatch growl error on failure', async() => {
134
+ const dispatchMock = jest.fn();
135
+ const scaleUpMock = jest.fn().mockRejectedValue(new Error('Scale failed'));
136
+ const workload = new Workload({
137
+ type: WORKLOAD_TYPES.DEPLOYMENT,
138
+ metadata: { name: 'test-workload', namespace: 'default' },
139
+ spec: { replicas: 1 }
140
+ }, {
141
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
142
+ dispatch: dispatchMock,
143
+ rootGetters: { 'i18n/t': (key: string) => key },
144
+ });
145
+
146
+ workload.scaleUp = scaleUpMock;
147
+ workload.$store = { dispatch: dispatchMock };
148
+
149
+ await workload.scale(true);
150
+
151
+ expect(dispatchMock).toHaveBeenCalledWith(
152
+ 'growl/fromError',
153
+ expect.objectContaining({
154
+ title: expect.stringContaining('workload.list.errorCannotScale'),
155
+ err: expect.any(Error)
156
+ }),
157
+ { root: true }
158
+ );
159
+ });
160
+ });
161
+
162
+ describe('getter: relatedServices', () => {
163
+ it('should return services that match workload pods', () => {
164
+ const mockPod = {
165
+ metadata: {
166
+ name: 'pod-1', namespace: 'default', labels: { app: 'my-app' }
167
+ }
168
+ };
169
+ const mockService = {
170
+ metadata: { name: 'my-service', namespace: 'default' },
171
+ spec: { selector: { app: 'my-app' } }
172
+ };
173
+ const workload = new Workload({
174
+ type: WORKLOAD_TYPES.DEPLOYMENT,
175
+ metadata: { name: 'test', namespace: 'default' },
176
+ spec: {}
177
+ }, {
178
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
179
+ dispatch: jest.fn(),
180
+ rootGetters: {
181
+ 'i18n/t': jest.fn(),
182
+ 'cluster/all': (type: string) => (type === SERVICE ? [mockService] : [])
183
+ },
184
+ });
185
+
186
+ // Mock pods getter
187
+ Object.defineProperty(workload, 'pods', { get: () => [mockPod] });
188
+
189
+ const related = workload.relatedServices;
190
+
191
+ expect(related).toContain(mockService);
192
+ });
193
+
194
+ it('should not return services from different namespace', () => {
195
+ const mockPod = {
196
+ metadata: {
197
+ name: 'pod-1', namespace: 'default', labels: { app: 'my-app' }
198
+ }
199
+ };
200
+ const mockService = {
201
+ metadata: { name: 'my-service', namespace: 'other-namespace' },
202
+ spec: { selector: { app: 'my-app' } }
203
+ };
204
+ const workload = new Workload({
205
+ type: WORKLOAD_TYPES.DEPLOYMENT,
206
+ metadata: { name: 'test', namespace: 'default' },
207
+ spec: {}
208
+ }, {
209
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
210
+ dispatch: jest.fn(),
211
+ rootGetters: {
212
+ 'i18n/t': jest.fn(),
213
+ 'cluster/all': (type: string) => (type === SERVICE ? [mockService] : [])
214
+ },
215
+ });
216
+
217
+ Object.defineProperty(workload, 'pods', { get: () => [mockPod] });
218
+
219
+ const related = workload.relatedServices;
220
+
221
+ expect(related).toHaveLength(0);
222
+ });
223
+
224
+ it('should not return services with non-matching selectors', () => {
225
+ const mockPod = {
226
+ metadata: {
227
+ name: 'pod-1', namespace: 'default', labels: { app: 'my-app' }
228
+ }
229
+ };
230
+ const mockService = {
231
+ metadata: { name: 'my-service', namespace: 'default' },
232
+ spec: { selector: { app: 'different-app' } }
233
+ };
234
+ const workload = new Workload({
235
+ type: WORKLOAD_TYPES.DEPLOYMENT,
236
+ metadata: { name: 'test', namespace: 'default' },
237
+ spec: {}
238
+ }, {
239
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
240
+ dispatch: jest.fn(),
241
+ rootGetters: {
242
+ 'i18n/t': jest.fn(),
243
+ 'cluster/all': (type: string) => (type === SERVICE ? [mockService] : [])
244
+ },
245
+ });
246
+
247
+ Object.defineProperty(workload, 'pods', { get: () => [mockPod] });
248
+
249
+ const related = workload.relatedServices;
250
+
251
+ expect(related).toHaveLength(0);
252
+ });
253
+ });
254
+
255
+ describe('getter: podsCard', () => {
256
+ it('should return card for Deployment type', () => {
257
+ const workload = new Workload({
258
+ type: WORKLOAD_TYPES.DEPLOYMENT,
259
+ metadata: { name: 'test', namespace: 'default' },
260
+ spec: {}
261
+ }, {
262
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
263
+ dispatch: jest.fn(),
264
+ rootGetters: { 'i18n/t': (key: string) => key },
265
+ });
266
+
267
+ Object.defineProperty(workload, 'pods', { get: () => [] });
268
+ Object.defineProperty(workload, 'canUpdate', { get: () => true });
269
+
270
+ const card = workload.podsCard;
271
+
272
+ expect(card).not.toBeNull();
273
+ expect(card.props.title).toBe('component.resource.detail.card.podsCard.title');
274
+ expect(card.props.showScaling).toBe(true);
275
+ });
276
+
277
+ it('should return card for DaemonSet type without scaling', () => {
278
+ const workload = new Workload({
279
+ type: WORKLOAD_TYPES.DAEMON_SET,
280
+ metadata: { name: 'test', namespace: 'default' },
281
+ spec: {}
282
+ }, {
283
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
284
+ dispatch: jest.fn(),
285
+ rootGetters: { 'i18n/t': (key: string) => key },
286
+ });
287
+
288
+ Object.defineProperty(workload, 'pods', { get: () => [] });
289
+ Object.defineProperty(workload, 'canUpdate', { get: () => true });
290
+
291
+ const card = workload.podsCard;
292
+
293
+ expect(card).not.toBeNull();
294
+ expect(card.props.showScaling).toBe(false);
295
+ });
296
+
297
+ it('should return null for unsupported types like CronJob', () => {
298
+ const workload = new Workload({
299
+ type: WORKLOAD_TYPES.CRON_JOB,
300
+ metadata: { name: 'test', namespace: 'default' },
301
+ spec: {}
302
+ }, {
303
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
304
+ dispatch: jest.fn(),
305
+ rootGetters: { 'i18n/t': (key: string) => key },
306
+ });
307
+
308
+ const card = workload.podsCard;
309
+
310
+ expect(card).toBeNull();
311
+ });
312
+
313
+ it('should hide scaling when canUpdate is false', () => {
314
+ const workload = new Workload({
315
+ type: WORKLOAD_TYPES.DEPLOYMENT,
316
+ metadata: { name: 'test', namespace: 'default' },
317
+ spec: {}
318
+ }, {
319
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
320
+ dispatch: jest.fn(),
321
+ rootGetters: { 'i18n/t': (key: string) => key },
322
+ });
323
+
324
+ Object.defineProperty(workload, 'pods', { get: () => [] });
325
+ Object.defineProperty(workload, 'canUpdate', { get: () => false });
326
+
327
+ const card = workload.podsCard;
328
+
329
+ expect(card.props.showScaling).toBe(false);
330
+ });
331
+ });
332
+
333
+ describe('getter: jobsCard', () => {
334
+ it('should return card for CronJob type', () => {
335
+ const workload = new Workload({
336
+ type: WORKLOAD_TYPES.CRON_JOB,
337
+ metadata: { name: 'test', namespace: 'default' },
338
+ spec: {}
339
+ }, {
340
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
341
+ dispatch: jest.fn(),
342
+ rootGetters: { 'i18n/t': (key: string) => key },
343
+ });
344
+
345
+ Object.defineProperty(workload, 'jobs', { get: () => [] });
346
+
347
+ const card = workload.jobsCard;
348
+
349
+ expect(card).not.toBeNull();
350
+ expect(card.props.title).toBe('component.resource.detail.card.jobsCard.title');
351
+ expect(card.props.showScaling).toBe(false);
352
+ });
353
+
354
+ it('should return null for non-CronJob types', () => {
355
+ const workload = new Workload({
356
+ type: WORKLOAD_TYPES.DEPLOYMENT,
357
+ metadata: { name: 'test', namespace: 'default' },
358
+ spec: {}
359
+ }, {
360
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
361
+ dispatch: jest.fn(),
362
+ rootGetters: { 'i18n/t': (key: string) => key },
363
+ });
364
+
365
+ const card = workload.jobsCard;
366
+
367
+ expect(card).toBeNull();
368
+ });
369
+ });
370
+
371
+ describe('getter: cards', () => {
372
+ it('should include podsCard for Deployment', () => {
373
+ const workload = new Workload({
374
+ type: WORKLOAD_TYPES.DEPLOYMENT,
375
+ metadata: { name: 'test', namespace: 'default' },
376
+ spec: {},
377
+ status: {}
378
+ }, {
379
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
380
+ dispatch: jest.fn(),
381
+ rootGetters: {
382
+ 'i18n/t': (key: string) => key,
383
+ 'cluster/all': () => []
384
+ },
385
+ });
386
+
387
+ Object.defineProperty(workload, 'pods', { get: () => [] });
388
+ Object.defineProperty(workload, 'canUpdate', { get: () => true });
389
+
390
+ const cards = workload.cards;
391
+
392
+ // Cards should include podsCard (not null), jobsCard (null for deployment), and _cards from parent
393
+ const nonNullCards = cards.filter((c: any) => c !== null);
394
+
395
+ expect(nonNullCards.length).toBeGreaterThanOrEqual(1);
396
+ expect(nonNullCards[0].props.title).toBe('component.resource.detail.card.podsCard.title');
397
+ });
398
+
399
+ it('should include jobsCard for CronJob', () => {
400
+ const workload = new Workload({
401
+ type: WORKLOAD_TYPES.CRON_JOB,
402
+ metadata: { name: 'test', namespace: 'default' },
403
+ spec: {},
404
+ status: {}
405
+ }, {
406
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
407
+ dispatch: jest.fn(),
408
+ rootGetters: {
409
+ 'i18n/t': (key: string) => key,
410
+ 'cluster/all': () => []
411
+ },
412
+ });
413
+
414
+ Object.defineProperty(workload, 'jobs', { get: () => [] });
415
+
416
+ const cards = workload.cards;
417
+ const nonNullCards = cards.filter((c: any) => c !== null);
418
+
419
+ // Should have jobsCard and insight card from parent
420
+ const jobsCard = nonNullCards.find((c: any) => c.props.title === 'component.resource.detail.card.jobsCard.title');
421
+
422
+ expect(jobsCard).toBeDefined();
423
+ });
424
+ });
92
425
  });
@@ -442,6 +442,14 @@ export default class CatalogApp extends SteveModel {
442
442
  }
443
443
  }
444
444
 
445
+ /**
446
+ * Safely checks if the required data is loaded.
447
+ * This avoids the exceptions thrown by `values` and `chartValues` when the data (e.g. secret) is missing.
448
+ */
449
+ get valuesLoaded() {
450
+ return !!this._values && !!this._chartValues;
451
+ }
452
+
445
453
  /**
446
454
  * The user's helm values
447
455
  */
package/models/pod.js CHANGED
@@ -46,6 +46,20 @@ export default class Pod extends WorkloadService {
46
46
  return this.$getters['byId'](NODE, this.spec.nodeName);
47
47
  }
48
48
 
49
+ get customValidationRules() {
50
+ const out = [
51
+ {
52
+ nullable: false,
53
+ path: 'metadata.name',
54
+ required: true,
55
+ translationKey: 'generic.name',
56
+ type: 'subDomain',
57
+ },
58
+ ];
59
+
60
+ return out;
61
+ }
62
+
49
63
  get _availableActions() {
50
64
  const out = super._availableActions;
51
65
 
package/models/secret.js CHANGED
@@ -573,7 +573,7 @@ export default class Secret extends SteveModel {
573
573
  const id = this.id?.replace(/.*\//, '');
574
574
 
575
575
  return {
576
- name: `c-cluster-product-${ VIRTUAL_TYPES.PROJECT_SECRETS }-namespace-id`,
576
+ name: `c-cluster-product-resource-namespace-id`,
577
577
  params: {
578
578
  product: this.$rootGetters['productId'],
579
579
  cluster: this.$rootGetters['clusterId'],
@@ -3,10 +3,12 @@ import { CATTLE_PUBLIC_ENDPOINTS } from '@shell/config/labels-annotations';
3
3
  import { WORKLOAD_TYPES, SERVICE, POD } from '@shell/config/types';
4
4
  import { set } from '@shell/utils/object';
5
5
  import day from 'dayjs';
6
- import { convertSelectorObj, parse } from '@shell/utils/selector';
6
+ import { convertSelectorObj, parse, matches } from '@shell/utils/selector';
7
7
  import { SEPARATOR } from '@shell/config/workload';
8
8
  import WorkloadService from '@shell/models/workload.service';
9
9
  import { matching } from '@shell/utils/selector-typed';
10
+ import { defineAsyncComponent, markRaw } from 'vue';
11
+ import { useResourceCardRow } from '@shell/components/Resource/Detail/Card/StateCard/composables';
10
12
 
11
13
  export const defaultContainer = {
12
14
  imagePullPolicy: 'Always',
@@ -179,6 +181,22 @@ export default class Workload extends WorkloadService {
179
181
  await this.save();
180
182
  }
181
183
 
184
+ async scale(isUp) {
185
+ try {
186
+ if (isUp) {
187
+ await this.scaleUp();
188
+ } else {
189
+ await this.scaleDown();
190
+ }
191
+ } catch (err) {
192
+ this.$store.dispatch('growl/fromError', {
193
+ title: this.t('workload.list.errorCannotScale', { direction: isUp ? 'up' : 'down', workloadName: this.name }),
194
+ err
195
+ },
196
+ { root: true });
197
+ }
198
+ }
199
+
182
200
  get state() {
183
201
  if ( this.spec?.paused === true ) {
184
202
  return 'paused';
@@ -665,32 +683,6 @@ export default class Workload extends WorkloadService {
665
683
  }).filter((x) => !!x);
666
684
  }
667
685
 
668
- get jobGauges() {
669
- const out = {
670
- succeeded: { color: 'success', count: 0 }, running: { color: 'info', count: 0 }, failed: { color: 'error', count: 0 }
671
- };
672
-
673
- if (this.type === WORKLOAD_TYPES.CRON_JOB) {
674
- this.jobs.forEach((job) => {
675
- const { status = {} } = job;
676
-
677
- out.running.count += status.active || 0;
678
- out.succeeded.count += status.succeeded || 0;
679
- out.failed.count += status.failed || 0;
680
- });
681
- } else if (this.type === WORKLOAD_TYPES.JOB) {
682
- const { status = {} } = this;
683
-
684
- out.running.count = status.active || 0;
685
- out.succeeded.count = status.succeeded || 0;
686
- out.failed.count = status.failed || 0;
687
- } else {
688
- return null;
689
- }
690
-
691
- return out;
692
- }
693
-
694
686
  get currentRevisionNumber() {
695
687
  if (this.ownedByWorkload || this.kind === 'Job' || this.kind === 'CronJob') {
696
688
  return undefined;
@@ -731,4 +723,78 @@ export default class Workload extends WorkloadService {
731
723
 
732
724
  return val;
733
725
  }
726
+
727
+ get servicesInNamespace() {
728
+ return this.$rootGetters['cluster/all'](SERVICE).filter((s) => s.metadata.namespace === this.metadata.namespace);
729
+ }
730
+
731
+ get relatedServices() {
732
+ // Find Services that have selectors that match this workload's Pod(s).
733
+ return this.servicesInNamespace.filter((service) => {
734
+ const selector = service.spec.selector;
735
+
736
+ for (let i = 0; i < this.pods.length; i++) {
737
+ const pod = this.pods[i];
738
+
739
+ if (service.metadata?.namespace === this.metadata?.namespace && matches(pod, selector)) {
740
+ return true;
741
+ }
742
+ }
743
+
744
+ return false;
745
+ });
746
+ }
747
+
748
+ get resourcesCardRows() {
749
+ return [
750
+ useResourceCardRow(this.t('component.resource.detail.card.resourcesCard.rows.services'), this.relatedServices, undefined, undefined, '#services'),
751
+ ...this._resourcesCardRows,
752
+ ];
753
+ }
754
+
755
+ get podsCard() {
756
+ const supportedTypes = [WORKLOAD_TYPES.DEPLOYMENT, WORKLOAD_TYPES.DAEMON_SET, WORKLOAD_TYPES.JOB, WORKLOAD_TYPES.STATEFUL_SET];
757
+
758
+ if (!supportedTypes.includes(this.type)) {
759
+ return null;
760
+ }
761
+
762
+ const scalingTypes = [WORKLOAD_TYPES.DEPLOYMENT, WORKLOAD_TYPES.STATEFUL_SET];
763
+
764
+ return {
765
+ component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/Card/StatusCard/index.vue'))),
766
+ props: {
767
+ title: this.t('component.resource.detail.card.podsCard.title'),
768
+ resources: this.pods,
769
+ showScaling: this.canUpdate && scalingTypes.includes(this.type),
770
+ onIncrease: () => this.scale(true),
771
+ onDecrease: () => this.scale(false)
772
+ }
773
+ };
774
+ }
775
+
776
+ get jobsCard() {
777
+ const supportedTypes = [WORKLOAD_TYPES.CRON_JOB];
778
+
779
+ if (!supportedTypes.includes(this.type)) {
780
+ return null;
781
+ }
782
+
783
+ return {
784
+ component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/Card/StatusCard/index.vue'))),
785
+ props: {
786
+ title: this.t('component.resource.detail.card.jobsCard.title'),
787
+ resources: this.jobs,
788
+ showScaling: false,
789
+ }
790
+ };
791
+ }
792
+
793
+ get cards() {
794
+ return [
795
+ this.podsCard,
796
+ this.jobsCard,
797
+ ...this._cards
798
+ ];
799
+ }
734
800
  }