@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
@@ -33,12 +33,14 @@ import forIn from 'lodash/forIn';
33
33
  import isEmpty from 'lodash/isEmpty';
34
34
  import isFunction from 'lodash/isFunction';
35
35
  import isString from 'lodash/isString';
36
- import { markRaw } from 'vue';
36
+ import { defineAsyncComponent, markRaw } from 'vue';
37
37
 
38
38
  import { handleConflict } from '@shell/plugins/dashboard-store/normalize';
39
39
  import { ExtensionPoint, ActionLocation } from '@shell/core/types';
40
40
  import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
41
41
  import { parse } from '@shell/utils/selector';
42
+ import { EVENT } from '@shell/config/types';
43
+ import { useResourceCardRow } from '@shell/components/Resource/Detail/Card/StateCard/composables';
42
44
 
43
45
  export const DNS_LIKE_TYPES = ['dnsLabel', 'dnsLabelRestricted', 'hostname'];
44
46
 
@@ -1383,6 +1385,49 @@ export default class Resource {
1383
1385
  return this._detailLocation;
1384
1386
  }
1385
1387
 
1388
+ /**
1389
+ * Override this getter to provide additional action buttons or a custom component
1390
+ * for the detail page title bar.
1391
+ *
1392
+ * @returns {undefined|object|Array} A Vue component definition, an array of RcButton props, or undefined
1393
+ *
1394
+ * @example
1395
+ * // Using an array of button props with the new variant/size props
1396
+ * get detailPageAdditionalActions() {
1397
+ * return [
1398
+ * { label: 'Action 1', variant: 'secondary', onClick: () => this.doAction1() },
1399
+ * { label: 'Action 2', variant: 'primary', size: 'large', onClick: () => this.doAction2() }
1400
+ * ];
1401
+ * }
1402
+ *
1403
+ * @example
1404
+ * // Using defineComponent with h() render function for custom rendering
1405
+ * import { defineComponent, h } from 'vue';
1406
+ * import RcButton from '@components/RcButton/RcButton.vue';
1407
+ *
1408
+ * get detailPageAdditionalActions() {
1409
+ * return defineComponent({
1410
+ * render() {
1411
+ * return h(RcButton, {
1412
+ * variant: 'primary',
1413
+ * onClick: () => console.log('clicked')
1414
+ * }, () => 'Click Me');
1415
+ * }
1416
+ * });
1417
+ * }
1418
+ *
1419
+ * @example
1420
+ * // Using dynamic import for a custom component
1421
+ * import { defineAsyncComponent } from 'vue';
1422
+ *
1423
+ * get detailPageAdditionalActions() {
1424
+ * return defineAsyncComponent(() => import('@shell/components/MyCustomActions.vue'));
1425
+ * }
1426
+ */
1427
+ get detailPageAdditionalActions() {
1428
+ return undefined;
1429
+ }
1430
+
1386
1431
  goToDetail() {
1387
1432
  this.currentRouter().push(this.detailLocation);
1388
1433
  }
@@ -2091,4 +2136,55 @@ export default class Resource {
2091
2136
  get yamlFolding() {
2092
2137
  return [];
2093
2138
  }
2139
+
2140
+ get resourceConditions() {
2141
+ return (this.status?.conditions || []).map((cond) => {
2142
+ let message = cond.message || '';
2143
+
2144
+ if ( cond.reason ) {
2145
+ message = `[${ cond.reason }] ${ message }`.trim();
2146
+ }
2147
+
2148
+ return {
2149
+ condition: cond.type || 'Unknown',
2150
+ status: cond.status || 'Unknown',
2151
+ stateSimpleColor: cond.error ? 'error' : 'disabled',
2152
+ error: cond.error,
2153
+ time: cond.lastProbeTime || cond.lastUpdateTime || cond.lastTransitionTime,
2154
+ message,
2155
+ };
2156
+ });
2157
+ }
2158
+
2159
+ get resourceEvents() {
2160
+ return this.$rootGetters['cluster/all'](EVENT)
2161
+ .filter((e) => e.involvedObject?.uid === this.metadata?.uid);
2162
+ }
2163
+
2164
+ get insightCardProps() {
2165
+ const rows = [
2166
+ useResourceCardRow(this.t('component.resource.detail.card.insightsCard.rows.conditions'), this.resourceConditions, undefined, 'condition', '#conditions'),
2167
+ useResourceCardRow(this.t('component.resource.detail.card.insightsCard.rows.events'), this.resourceEvents, undefined, undefined, '#events'),
2168
+ ];
2169
+
2170
+ return {
2171
+ title: this.t('component.resource.detail.card.insightsCard.title'),
2172
+ rows
2173
+ };
2174
+ }
2175
+
2176
+ get insightCard() {
2177
+ return {
2178
+ component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/Card/StateCard/index.vue'))),
2179
+ props: this.insightCardProps
2180
+ };
2181
+ }
2182
+
2183
+ get _cards() {
2184
+ return [this.insightCard];
2185
+ }
2186
+
2187
+ get cards() {
2188
+ return this._cards;
2189
+ }
2094
2190
  }
@@ -0,0 +1,84 @@
1
+ import { SteveRevision } from '../revision';
2
+
3
+ describe('class: SteveRevision', () => {
4
+ describe('constructor', () => {
5
+ it('should correctly parse a numeric string', () => {
6
+ const rev = new SteveRevision('123');
7
+
8
+ expect(rev.asNumber).toBe(123);
9
+ expect(rev.isNumber).toBe(true);
10
+ });
11
+
12
+ it('should correctly parse a number', () => {
13
+ const rev = new SteveRevision(456);
14
+
15
+ expect(rev.asNumber).toBe(456);
16
+ expect(rev.isNumber).toBe(true);
17
+ });
18
+
19
+ it('should handle non-numeric strings', () => {
20
+ const rev = new SteveRevision('abc');
21
+
22
+ expect(rev.asNumber).toBeNaN();
23
+ expect(rev.isNumber).toBe(false);
24
+ });
25
+
26
+ it('should handle undefined', () => {
27
+ const rev = new SteveRevision(undefined);
28
+
29
+ expect(rev.asNumber).toBeNaN();
30
+ expect(rev.isNumber).toBe(false);
31
+ });
32
+ });
33
+
34
+ describe('method: isNewerThan', () => {
35
+ it('should return true if current revision is greater than provided revision', () => {
36
+ const r1 = new SteveRevision('10');
37
+ const r2 = new SteveRevision('5');
38
+
39
+ expect(r1.isNewerThan(r2)).toBe(true);
40
+ });
41
+
42
+ it('should return false if current revision is less than provided revision', () => {
43
+ const r1 = new SteveRevision('5');
44
+ const r2 = new SteveRevision('10');
45
+
46
+ expect(r1.isNewerThan(r2)).toBe(false);
47
+ });
48
+
49
+ it('should return false if revisions are equal', () => {
50
+ const r1 = new SteveRevision('10');
51
+ const r2 = new SteveRevision('10');
52
+
53
+ expect(r1.isNewerThan(r2)).toBe(false);
54
+ });
55
+
56
+ it('should return false if current revision is not a number', () => {
57
+ const r1 = new SteveRevision('abc');
58
+ const r2 = new SteveRevision('10');
59
+
60
+ expect(r1.isNewerThan(r2)).toBe(false);
61
+ });
62
+
63
+ it('should return false if provided revision is not a number', () => {
64
+ const r1 = new SteveRevision('10');
65
+ const r2 = new SteveRevision('abc');
66
+
67
+ expect(r1.isNewerThan(r2)).toBe(false);
68
+ });
69
+
70
+ it('should return false if current revision is undefined', () => {
71
+ const r1 = new SteveRevision(undefined);
72
+ const r2 = new SteveRevision('10');
73
+
74
+ expect(r1.isNewerThan(r2)).toBe(false);
75
+ });
76
+
77
+ it('should return false if provided revision is undefined', () => {
78
+ const r1 = new SteveRevision('10');
79
+ const r2 = new SteveRevision(undefined);
80
+
81
+ expect(r1.isNewerThan(r2)).toBe(false);
82
+ });
83
+ });
84
+ });
@@ -477,6 +477,20 @@ describe('class StevePaginationUtils', () => {
477
477
  expect(result).toBe('filter=spec.containers.image~nginx&filter=metadata.name!=test');
478
478
  });
479
479
 
480
+ it.each([
481
+ ['test', 'test'],
482
+ ['-test', '"-test"'],
483
+ ['te-st', '"te-st"'],
484
+ ['test-', '"test-"'],
485
+ ])('should handle filter value %s with hyphens', (x, y) => {
486
+ const filters = [
487
+ new PaginationParamFilter({ fields: [new PaginationFilterField({ field: 'metadata.name', value: x })] }),
488
+ ];
489
+ const result = testStevePaginationUtils.convertPaginationParams({ schema, filters });
490
+
491
+ expect(result).toBe(`filter=metadata.name=${ y }`);
492
+ });
493
+
480
494
  it('should handle IN and NOT_IN operators', () => {
481
495
  const filters = [
482
496
  new PaginationParamFilter({
@@ -502,5 +516,21 @@ describe('class StevePaginationUtils', () => {
502
516
 
503
517
  expect(result).toBe('filter=metadata.name IN (test1,test2)&filter=metadata.namespace NOTIN (ns1,ns2)');
504
518
  });
519
+
520
+ it.each([
521
+ [null, '""'],
522
+ [undefined, '""'],
523
+ [false, 'false'],
524
+ [true, 'true'],
525
+ [0, '0'],
526
+ [1, '1'],
527
+ ])('should handle falsy filter value %s', (x: any, y) => {
528
+ const filters = [
529
+ new PaginationParamFilter({ fields: [new PaginationFilterField({ field: 'metadata.name', value: x })] }),
530
+ ];
531
+ const result = testStevePaginationUtils.convertPaginationParams({ schema, filters });
532
+
533
+ expect(result).toBe(`filter=metadata.name=${ y }`);
534
+ });
505
535
  });
506
536
  });
@@ -3,6 +3,7 @@ import { REVISION_TOO_OLD } from '../../../utils/socket';
3
3
  import { STEVE_WATCH_MODE } from '../../../types/store/subscribe.types';
4
4
  import backOff from '../../../utils/back-off';
5
5
  import { SteveWatchEventListenerManager } from '../../subscribe-events';
6
+ import { STEVE_RESPONSE_CODE } from '../../../types/rancher/steve.api';
6
7
 
7
8
  describe('steve: subscribe', () => {
8
9
  describe('actions', () => {
@@ -110,6 +111,139 @@ describe('steve: subscribe', () => {
110
111
  expect(commit).toHaveBeenCalledWith('setInError', { msg, reason: 'NO_PERMS' });
111
112
  });
112
113
  });
114
+
115
+ describe('fetchPageResources', () => {
116
+ const dispatch = jest.fn();
117
+ const getters = {
118
+ backOffId: jest.fn(),
119
+ typeEntry: jest.fn(),
120
+ canBackoff: jest.fn(),
121
+ watchStarted: jest.fn(),
122
+ inError: jest.fn(),
123
+ };
124
+ let backOffSpy: any;
125
+ const context = { $socket: {} };
126
+
127
+ beforeEach(() => {
128
+ jest.clearAllMocks();
129
+ backOffSpy = {
130
+ getBackOff: jest.spyOn(backOff, 'getBackOff'),
131
+ reset: jest.spyOn(backOff, 'reset'),
132
+ recurse: jest.spyOn(backOff, 'recurse'),
133
+ };
134
+ getters.backOffId.mockReturnValue('backoff-id');
135
+ getters.typeEntry.mockReturnValue({ revision: '10' });
136
+ backOffSpy.getBackOff.mockReturnValue({ metadata: { revision: '10' } } as any);
137
+ backOffSpy.recurse.mockResolvedValue(undefined);
138
+ });
139
+
140
+ afterEach(() => {
141
+ jest.restoreAllMocks();
142
+ });
143
+
144
+ it('should abort if current revision is newer than target revision', async() => {
145
+ // Current (10) > Target (5)
146
+ const params = {
147
+ resourceType: 'type', namespace: 'ns', revision: '5'
148
+ };
149
+
150
+ await actions.fetchPageResources.call(context, { getters, dispatch }, {
151
+ opt: {}, storePagination: {}, params
152
+ });
153
+
154
+ expect(backOffSpy.recurse).not.toHaveBeenCalled();
155
+ });
156
+
157
+ it('should reset backoff if target revision is newer than active revision', async() => {
158
+ // Active (10) < Target (15)
159
+ const params = {
160
+ resourceType: 'type', namespace: 'ns', revision: '15'
161
+ };
162
+
163
+ await actions.fetchPageResources.call(context, { getters, dispatch }, {
164
+ opt: {}, storePagination: {}, params
165
+ });
166
+
167
+ expect(backOffSpy.reset).toHaveBeenCalledWith('backoff-id');
168
+ expect(backOffSpy.recurse).toHaveBeenCalledWith(expect.objectContaining({
169
+ id: 'backoff-id',
170
+ metadata: { revision: '15' }
171
+ }));
172
+ });
173
+
174
+ it('should recurse if revisions are valid', async() => {
175
+ const params = {
176
+ resourceType: 'type', namespace: 'ns', revision: '10'
177
+ };
178
+
179
+ await actions.fetchPageResources.call(context, { getters, dispatch }, {
180
+ opt: {}, storePagination: {}, params
181
+ });
182
+
183
+ expect(backOffSpy.recurse).toHaveBeenCalledWith(expect.objectContaining({
184
+ id: 'backoff-id',
185
+ metadata: { revision: '10' }
186
+ }));
187
+ });
188
+
189
+ describe('recurse options', () => {
190
+ const params = {
191
+ resourceType: 'type', namespace: 'ns', revision: '10'
192
+ };
193
+ let recurseArgs: any;
194
+
195
+ beforeEach(async() => {
196
+ await actions.fetchPageResources.call(context, { getters, dispatch }, {
197
+ opt: {}, storePagination: { request: { filter: 'foo' } }, params
198
+ });
199
+ recurseArgs = backOffSpy.recurse.mock.calls[0][0];
200
+ });
201
+
202
+ it('canFn should return false if socket closed', () => {
203
+ getters.canBackoff.mockReturnValue(false);
204
+ expect(recurseArgs.canFn()).toBe(false);
205
+ expect(getters.canBackoff).toHaveBeenCalledWith(context.$socket);
206
+ });
207
+
208
+ it('canFn should return false if watch not started and not in REVISION_TOO_OLD error', () => {
209
+ getters.canBackoff.mockReturnValue(true);
210
+ getters.watchStarted.mockReturnValue(false);
211
+ getters.inError.mockReturnValue('some-other-error');
212
+ expect(recurseArgs.canFn()).toBe(false);
213
+ });
214
+
215
+ it('canFn should return true if watch started', () => {
216
+ getters.canBackoff.mockReturnValue(true);
217
+ getters.watchStarted.mockReturnValue(true);
218
+ expect(recurseArgs.canFn()).toBe(true);
219
+ });
220
+
221
+ it('canFn should return true if watch not started but in REVISION_TOO_OLD error', () => {
222
+ getters.canBackoff.mockReturnValue(true);
223
+ getters.watchStarted.mockReturnValue(false);
224
+ getters.inError.mockReturnValue(REVISION_TOO_OLD);
225
+ expect(recurseArgs.canFn()).toBe(true);
226
+ });
227
+
228
+ it('continueOnError should return true for UNKNOWN_REVISION error', async() => {
229
+ const err = { status: 400, code: STEVE_RESPONSE_CODE.UNKNOWN_REVISION };
230
+
231
+ expect(await recurseArgs.continueOnError(err)).toBe(true);
232
+ });
233
+
234
+ it('delayedFn should dispatch findPage', async() => {
235
+ await recurseArgs.delayedFn();
236
+ expect(dispatch).toHaveBeenCalledWith('findPage', {
237
+ type: 'type',
238
+ opt: {
239
+ namespaced: 'ns',
240
+ revision: '10',
241
+ filter: 'foo'
242
+ }
243
+ });
244
+ });
245
+ });
246
+ });
113
247
  });
114
248
 
115
249
  describe('getters', () => {
@@ -180,6 +180,15 @@ export default {
180
180
  * Load multiple different types of resources
181
181
  */
182
182
  loadMulti(state, { data, ctx }) {
183
+ const type = data[0]?.type;
184
+ const cache = state.types[type];
185
+
186
+ if (cache?.havePage) {
187
+ console.warn(`Prevented \`loadMulti\` mutation from polluting the cache for type "${ type }" (currently represents a page).`); // eslint-disable-line no-console
188
+
189
+ return;
190
+ }
191
+
183
192
  for (const entry of data) {
184
193
  const resource = load(state, { data: entry, ctx });
185
194
 
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Helper class to handle Steve API revisions comparisons
3
+ */
4
+ export class SteveRevision {
5
+ public asNumber: number;
6
+ public isNumber: boolean;
7
+
8
+ constructor(public revision: any) {
9
+ this.asNumber = Number(revision);
10
+ this.isNumber = !Number.isNaN(this.asNumber);
11
+ }
12
+
13
+ /**
14
+ * Is this provided revision newer than this revision?
15
+ *
16
+ * @param revision
17
+ * @returns
18
+ */
19
+ isNewerThan(revision: SteveRevision): boolean {
20
+ return SteveRevision.areAllNumbers([this, revision]) && this.asNumber > revision.asNumber;
21
+ }
22
+
23
+ private static areAllNumbers(revisions: SteveRevision[]): boolean {
24
+ return revisions.every((r) => r.isNumber);
25
+ }
26
+ }
@@ -213,9 +213,9 @@ class StevePaginationUtils extends NamespaceProjectFilters {
213
213
  * Match
214
214
  * - a-z (case insensitive)
215
215
  * - 0-9
216
- * - `-`, `_`, `.`
216
+ * - `_`, `.`
217
217
  */
218
- static VALID_FIELD_VALUE_REGEX = /^[\w\-.]+$/;
218
+ static VALID_FIELD_VALUE_REGEX = /^[\w.]+$/;
219
219
 
220
220
  /**
221
221
  * Filtering with the vai cache supports specific fields
@@ -596,10 +596,11 @@ class StevePaginationUtils extends NamespaceProjectFilters {
596
596
  if ([PaginationFilterEquality.IN, PaginationFilterEquality.NOT_IN].includes(equality)) {
597
597
  safeValue = `(${ field.value })`;
598
598
  } else {
599
- const encodedValue = encodeURIComponent(field.value || '');
599
+ const booleanSafeValue = typeof field.value === 'undefined' || field.value === null ? '' : field.value;
600
+ const encodedValue = encodeURIComponent(booleanSafeValue);
600
601
 
601
- if (StevePaginationUtils.VALID_FIELD_VALUE_REGEX.test(field.value || '')) {
602
- // Does not contain any protected characters, send as is
602
+ if (StevePaginationUtils.VALID_FIELD_VALUE_REGEX.test(booleanSafeValue)) {
603
+ // All characters safe, send as is
603
604
  safeValue = encodedValue;
604
605
  } else {
605
606
  // Contains protected characters, wrap in quotes to ensure backend doesn't fail