@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
@@ -20,40 +20,67 @@
20
20
  *
21
21
  * Below are some VERY brief steps for common flows. Some will link together
22
22
  *
23
- * Successfully flow - watch
23
+ * # Successfully flow
24
+ * ## watch - standard mode
24
25
  * 1. UI --> Rancher: _watch_ request
25
26
  * 2. Rancher --> UI: `resource.start`. UI sets watch as started
26
27
  * ...
27
28
  * 3. Rancher --> UI: `resource.change` (contains data). UI caches data
28
29
  *
29
- * Successful flow - watch - new mode
30
+ * ## watch - new resource.changes mode
30
31
  * 1. UI --> Rancher: _watch_ request
31
32
  * 2. Rancher --> UI: `resource.start`. UI sets watch as started
32
33
  * ...
33
34
  * 3. Rancher --> UI: `resource.changes` (contains no data). UI makes a HTTP request to fetch data
34
35
  *
35
- * Successful flow - unwatch
36
+ * ## watch - unwatch
36
37
  * 1. UI --> Rancher: _unwatch_ request
37
38
  * 2. Rancher --> UI: `resource.stop`. UI sets watch as stopped
38
39
  *
39
- * Successful flow - resource.stop received
40
+ * ## watch - resource.stop received
40
41
  * 1. Rancher --> UI: `resource.stop`. UI sets watch as stopped
41
42
  * 2. UI --> Rancher: _watch_ request
42
43
  *
43
- * Successful flow - socket disconnected
44
+ * ## watch - socket disconnected
44
45
  * 1. Socket closes|disconnects (not sure which)
45
46
  * 2. UI: reopens socket
46
47
  * 3. UI --> Rancher: _watch_ request (for every started watch)
47
48
  *
48
- * Error Flow
49
+ * # Error Flow
50
+ * ## resource.error
49
51
  * 1. UI --> Rancher: _watch_ request
50
52
  * 2. Rancher --> UI: `resource.start`. UI sets watch as started
51
53
  * 3. Rancher --> UI: `resource.error`. UI sets watch as errored.
52
54
  * a) UI: in the event of 'too old' the UI will make a http request to fetch a new revision and re-watch with it. This process is delayed on each call
53
55
  * 4. Rancher --> UI: `resource.stop`. UI sets watch as stop (note the resource.stop flow above is avoided given error state)
54
56
  *
57
+ * # HA Support for Stale Replicates - https://github.com/rancher/dashboard/issues/14974
58
+ *
59
+ * ## Scenario 1 - handle case where watch request is handled by a stale replica
60
+ * 1. UI --> Rancher: _watch_ request (contains latest revision)
61
+ * 2. Rancher --> UI: `resource.error` (stale replica does not know new revision)
62
+ * 3. Rancher --> UI: `resource.stop` (stale replica cannot provide updates for unknown revision)
63
+ * 4. UI --> Rancher : UI makes a HTTP request to fetch data
64
+ * 5. Loop back to step 1 (if stale again, backoff retry)
65
+ *
66
+ * ## Scenario 2 - handle case where http request is handled by a stale replica (don't fetch stale data)
67
+ * 1. UI --> Rancher: _watch_ request
68
+ * 2. Rancher --> UI: `resource.start`. UI sets watch as started
69
+ * ...
70
+ * 3. Rancher --> UI: `resource.changes` (sent by good replica containing good revision)
71
+ * 4. UI --> Rancher : UI makes a HTTP request to fetch data. Stale Replica handles request, does not know revision, returns error
72
+ * 5. Loop back to step 4 (if errors with stale again, backoff retry)
73
+ *
74
+ * ## Scenario 3 - handle case where update request was sent by stale replica (don't overwrite good data with stale)
75
+ * 1. UI --> Rancher: _watch_ request
76
+ * 2. Rancher --> UI: `resource.start`. UI sets watch as started
77
+ * ...
78
+ * 3. Rancher --> UI: `resource.changes` (sent by stale replica containing stale revision)
79
+ * 4. UI compares stale revision with newer store revision
80
+ * 5. UI does not make new http request, which could be handled by stale replica --> overwrites newer local values
81
+ *
55
82
  * Additionally
56
- * - if we receive resource.stop, unless the watch is in error, we immediately send back a watch event
83
+ * - if we receive resource.stop, unless the watch is in error, we immediately send back a watch request to re-start the watch
57
84
  * - if the web socket is disconnected (for steve based sockets it happens every 30 mins, or when there are permission changes)
58
85
  * the ui will re-connect it and re-watch all previous watches using a best effort revision
59
86
  */
@@ -68,7 +95,6 @@ import Socket, {
68
95
  EVENT_CONNECTED,
69
96
  EVENT_DISCONNECTED,
70
97
  EVENT_MESSAGE,
71
- // EVENT_FRAME_TIMEOUT,
72
98
  EVENT_CONNECT_ERROR,
73
99
  EVENT_DISCONNECT_ERROR,
74
100
  NO_WATCH,
@@ -90,6 +116,8 @@ import { STEVE_WATCH_EVENT_TYPES, STEVE_WATCH_MODE } from '@shell/types/store/su
90
116
  import paginationUtils from '@shell/utils/pagination-utils';
91
117
  import backOff from '@shell/utils/back-off';
92
118
  import { SteveWatchEventListenerManager } from '@shell/plugins/subscribe-events';
119
+ import { SteveRevision } from '@shell/plugins/steve/revision';
120
+ import { STEVE_RESPONSE_CODE } from '@shell/types/rancher/steve.api';
93
121
 
94
122
  // minimum length of time a disconnect notification is shown
95
123
  const MINIMUM_TIME_NOTIFIED = 3000;
@@ -683,7 +711,7 @@ const sharedActions = {
683
711
  /**
684
712
  * Ensure there's no back-off process waiting to run for
685
713
  * - resource.changes fetchResources
686
- * - resource.error resyncWatches
714
+ * - resource.error resyncWatch
687
715
  */
688
716
  resetWatchBackOff({ state, getters, commit }, {
689
717
  type, compareWatches, resetInError = true, resetStarted = true
@@ -820,17 +848,117 @@ const defaultActions = {
820
848
  async resyncWatch({ getters, dispatch }, params) {
821
849
  console.info(`Resync [${ getters.storeName }]`, params); // eslint-disable-line no-console
822
850
 
851
+ const { backOffId, ...others } = params;
852
+
823
853
  await dispatch('fetchResources', {
824
- ...params,
825
- opt: { force: true, forceWatch: true }
854
+ params: others,
855
+ backOffId,
856
+ opt: { force: true, forceWatch: true }
826
857
  });
827
858
  },
828
859
 
860
+ /**
861
+ * Helper function used by fetchResources
862
+ *
863
+ * Integrates the concept of 'back-off' to reduce spam, overwrite stale old requests, etc
864
+ */
865
+ async fetchPageResources({ getters, dispatch }, {
866
+ opt, storePagination, params, backOffId
867
+ }) {
868
+ const { resourceType, namespace, revision } = params;
869
+ const type = resourceType || params.type;
870
+
871
+ const safeBackOffId = backOffId || getters.backOffId(params, `fetchPageResources`);
872
+
873
+ const activeRevisionSt = backOff.getBackOff(safeBackOffId)?.metadata?.revision;
874
+ const cachedRevisionSt = getters['typeEntry'](resourceType || type)?.revision;
875
+
876
+ const targetRevision = new SteveRevision(revision);
877
+ const activeRevision = new SteveRevision(activeRevisionSt);
878
+ const cachedRevision = new SteveRevision(cachedRevisionSt);
879
+ const currentRevision = new SteveRevision(activeRevisionSt || cachedRevisionSt);
880
+
881
+ // Three cases to support HA scenarios 2 + 3
882
+ // 1. current version is newer than target revision - abort/ignore (don't overwrite new with old)
883
+ // 2. current version is older than target revision - reset previous (drop older requests with older revision, use new revision)
884
+ // 3. current version is same as target revision - we're retrying
885
+
886
+ // There are two places we do this to cover the two cases we make http request following socket changes
887
+ // shell/utils/pagination-wrapper.ts - request
888
+ // shell/plugins/steve/subscribe.js - fetchPageResources
889
+
890
+ if (currentRevision.isNewerThan(targetRevision)) {
891
+ // Case 1 - abort/ignore (don't overwrite new with old)
892
+
893
+ // eslint-disable-next-line no-console
894
+ console.warn(`Ignoring subscribe request to update '${ type }' with revision '${ targetRevision.revision }' (active revision '${ currentRevision.revision } & cached revision '${ cachedRevision.revision }''). ` +
895
+ `This probably means the replica that provided the web socket message has not yet correctly synced it's cache with other fresher replicas.`);
896
+
897
+ return;
898
+ }
899
+
900
+ if (targetRevision.isNewerThan(activeRevision)) {
901
+ // Case 2 - reset previous (drop older requests with older revision, use new revision)
902
+
903
+ console.info(`Dropping previous subscribe request to update '${ type }' with revision '${ currentRevision.revision }' (new target revision '${ targetRevision.revision }'). `); // eslint-disable-line no-console
904
+
905
+ backOff.reset(safeBackOffId);
906
+ }
907
+
908
+ try {
909
+ // Keep making requests until we make one that succeeds, fails with unknown revision or we run out of retries
910
+ await backOff.recurse({
911
+ id: safeBackOffId,
912
+ metadata: { revision },
913
+ description: `Fetching resources for ${ type }. Triggered by web socket`,
914
+ canFn: () => {
915
+ if (!getters.canBackoff(this.$socket)) {
916
+ console.info(`Aborting subscribe request to update '${ type }' with revision '${ currentRevision.revision }' (socket closed). `); // eslint-disable-line no-console
917
+
918
+ return false;
919
+ }
920
+
921
+ if (!getters['watchStarted'](params)) {
922
+ // No watch has started... but are we in initial state where the watch failed due to a bad revision?
923
+ const inError = getters.inError(params);
924
+
925
+ if (inError !== REVISION_TOO_OLD) {
926
+ console.info(`Aborting subscribe request to update '${ type }' with revision '${ currentRevision.revision }' (resource not watched). `); // eslint-disable-line no-console
927
+
928
+ return false;
929
+ }
930
+ }
931
+
932
+ return true;
933
+ },
934
+ continueOnError: async(err) => {
935
+ // Have we made a request to a stale replica that does not know about the required revision? If so continue to try until we hit a ripe replica
936
+ return err?.status === 400 && err?.code === STEVE_RESPONSE_CODE.UNKNOWN_REVISION;
937
+ },
938
+ delayedFn: async() => {
939
+ return await dispatch('findPage', {
940
+ type,
941
+ opt: {
942
+ ...opt,
943
+ namespaced: namespace,
944
+ revision,
945
+ // This brings in page, page size, filter, etc
946
+ ...storePagination.request,
947
+ }
948
+ });
949
+ },
950
+ });
951
+ } catch (err) {
952
+ // Nothing depends on the error higher in the call stack, so prevent dev full screen errors by catching it
953
+ console.info(`Failed subscribe request to update '${ type }' with revision '${ currentRevision.revision }' (error). `, err); // eslint-disable-line no-console
954
+ }
955
+ },
956
+
829
957
  async fetchResources({
830
958
  state, getters, dispatch, commit
831
- }, { opt, ...params }) {
959
+ }, { opt, params, backOffId }) {
832
960
  const {
833
- resourceType, namespace, id, selector, mode
961
+ resourceType, namespace, id, selector, mode, revision
834
962
  } = params;
835
963
 
836
964
  if (!resourceType) {
@@ -840,6 +968,7 @@ const defaultActions = {
840
968
  }
841
969
 
842
970
  if ( id ) {
971
+ // Fetch an individual resource
843
972
  await dispatch('find', {
844
973
  type: resourceType,
845
974
  id,
@@ -857,6 +986,7 @@ const defaultActions = {
857
986
  let have = []; let want = [];
858
987
 
859
988
  if ( selector ) {
989
+ // Fetch a selection of resources
860
990
  have = getters['matching'](resourceType, selector).slice();
861
991
  want = await dispatch('findMatching', {
862
992
  type: resourceType,
@@ -864,39 +994,40 @@ const defaultActions = {
864
994
  opt,
865
995
  });
866
996
  } else {
997
+ // Fetch all or a page of resources
867
998
  if (mode === STEVE_WATCH_MODE.RESOURCE_CHANGES) {
999
+ // Fetch a page of resources
1000
+
868
1001
  // Other findX use options (id/ns/selector) from the messages received over socket.
869
- // However paginated requests have more complex params so grab them from store from the store.
1002
+ // However paginated requests have more complex params so grab them from the store.
1003
+
870
1004
  // of type @StorePagination
871
1005
  const storePagination = getters['havePage'](resourceType);
872
1006
 
873
1007
  if (!!storePagination) {
874
- have = []; // findPage removes stale entries, so we don't need to rely on below process to remove them
875
-
876
- // This could have been kicked off given a resource.changes message
877
- // If the messages come in quicker than findPage completes (resource.changes debounce time >= http request time),
878
- // and the request is the same, only the first request will be processed. all others until it finishes will be ignored
879
- // (see deferred process - `waiting.push(later);` - in request action).
880
- // If this becomes an issue we need to debounce and work around the deferred issue within request
881
- want = await dispatch('findPage', {
882
- type: resourceType,
883
- opt: {
884
- ...opt,
885
- namespaced: namespace,
886
- // This brings in page, page size, filter, etc
887
- ...storePagination.request
888
- }
1008
+ await dispatch('fetchPageResources', {
1009
+ params,
1010
+ storePagination,
1011
+ opt,
1012
+ backOffId
889
1013
  });
1014
+
1015
+ // findPage removes stale entries, so we don't need to rely on below process to remove them
1016
+ have = [];
1017
+ want = [];
890
1018
  }
1019
+
891
1020
  // Should any listeners be notified of this request for them to kick off their own event handling?
892
1021
  getters.listenerManager.triggerEventListener({
893
1022
  event: STEVE_WATCH_MODE.RESOURCE_CHANGES,
894
1023
  params: {
895
1024
  ...params,
896
- forceWatch: opt.forceWatch
1025
+ revision,
1026
+ forceWatch: opt.forceWatch,
897
1027
  }
898
1028
  });
899
1029
  } else {
1030
+ // Fetch all of a resource
900
1031
  have = getters['all'](resourceType).slice();
901
1032
 
902
1033
  if ( namespace ) {
@@ -1110,7 +1241,7 @@ const defaultActions = {
1110
1241
  // 2) will be cleared when resyncWatch --> watch (with force) --> resource.start completes
1111
1242
  commit('setInError', { msg, reason: REVISION_TOO_OLD });
1112
1243
 
1113
- // See Scenario 1 from https://github.com/rancher/dashboard/issues/14974
1244
+ // HA scenario 1 - handle case where stale replica processes watch request
1114
1245
  // The watch that results from resyncWatch will fail and end up here if the revision isn't (yet) known
1115
1246
  // So re-retry resyncWatch until it does OR
1116
1247
  // - we're already re-retrying
@@ -1120,11 +1251,17 @@ const defaultActions = {
1120
1251
  // - we need to stop (socket is disconnected or closed, type is 'forgotten', watch is unwatched)
1121
1252
  // - `reset` called asynchronously
1122
1253
  // - Note - we won't need to clear the id outside of the above scenarios because `too old` only occurs on fresh watches (covered by above scenarios)
1254
+
1255
+ const backOffId = getters.backOffId(msg, REVISION_TOO_OLD);
1256
+
1123
1257
  backOff.execute({
1124
- id: getters.backOffId(msg, REVISION_TOO_OLD),
1258
+ id: backOffId,
1125
1259
  description: `Invalid watch revision, re-syncing`,
1126
1260
  canFn: () => getters.canBackoff(this.$socket),
1127
- delayedFn: () => dispatch('resyncWatch', msg),
1261
+ delayedFn: () => dispatch('resyncWatch', {
1262
+ ...msg,
1263
+ backOffId: undefined,
1264
+ }),
1128
1265
  });
1129
1266
  } else if ( err.includes('the server does not allow this method on the requested resource')) {
1130
1267
  commit('setInError', { msg, reason: NO_PERMS });
@@ -1200,6 +1337,18 @@ const defaultActions = {
1200
1337
  },
1201
1338
 
1202
1339
  'ws.resource.create'(ctx, msg) {
1340
+ const data = msg.data;
1341
+ const type = data?.type;
1342
+
1343
+ const havePage = ctx.getters['havePage'](type);
1344
+
1345
+ if (havePage) {
1346
+ console.warn(`Prevented watch \`resource.create\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, msg); // eslint-disable-line no-console
1347
+ ctx.dispatch('unwatch', { ...msg, type });
1348
+
1349
+ return;
1350
+ }
1351
+
1203
1352
  ctx.state.debugSocket && console.info(`Resource Create [${ ctx.getters.storeName }]`, msg.resourceType, msg); // eslint-disable-line no-console
1204
1353
  queueChange(ctx, msg, true, 'Create');
1205
1354
  },
@@ -1230,8 +1379,8 @@ const defaultActions = {
1230
1379
  const havePage = ctx.getters['havePage'](type);
1231
1380
 
1232
1381
  if (havePage) {
1233
- console.warn(`Prevented watch \`resource.change\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, data); // eslint-disable-line no-console
1234
- ctx.dispatch('unwatch', data);
1382
+ console.warn(`Prevented watch \`resource.change\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, msg); // eslint-disable-line no-console
1383
+ ctx.dispatch('unwatch', { ...msg, type });
1235
1384
 
1236
1385
  return;
1237
1386
  }
@@ -1256,10 +1405,10 @@ const defaultActions = {
1256
1405
  }
1257
1406
  },
1258
1407
 
1259
- 'ws.resource.changes'({ dispatch }, msg) {
1260
- dispatch('fetchResources', {
1261
- ...msg,
1262
- opt: { force: true, load: _MERGE }
1408
+ async 'ws.resource.changes'({ dispatch }, msg) {
1409
+ await dispatch('fetchResources', {
1410
+ params: msg,
1411
+ opt: { force: true, load: _MERGE }
1263
1412
  } );
1264
1413
  },
1265
1414
 
@@ -1277,6 +1426,15 @@ const defaultActions = {
1277
1426
  }
1278
1427
  }
1279
1428
 
1429
+ const havePage = ctx.getters['havePage'](type);
1430
+
1431
+ if (havePage) {
1432
+ console.warn(`Prevented watch \`resource.remove\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, msg); // eslint-disable-line no-console
1433
+ ctx.dispatch('unwatch', { ...msg, type });
1434
+
1435
+ return;
1436
+ }
1437
+
1280
1438
  queueChange(ctx, msg, false, 'Remove');
1281
1439
 
1282
1440
  const typeOption = ctx.rootGetters['type-map/optionsFor'](type);
@@ -1392,7 +1550,7 @@ const defaultGetters = {
1392
1550
  * @param postFix - something else to uniquely id this back-off
1393
1551
  */
1394
1552
  backOffId: () => (obj, postFix) => {
1395
- return `${ keyForSubscribe(obj) }${ postFix ? `:${ postFix }` : '' }`;
1553
+ return `${ keyForSubscribe(obj) }${ postFix ? `:detail=${ postFix }` : '' }`;
1396
1554
  },
1397
1555
 
1398
1556
  /**
@@ -1433,15 +1591,15 @@ const defaultGetters = {
1433
1591
  */
1434
1592
  nextResourceVersion: (state, getters) => (type, id) => {
1435
1593
  type = normalizeType(type);
1436
- let revision = 0;
1594
+ let nextRevision = 0;
1437
1595
 
1438
1596
  if ( id ) {
1439
1597
  const existing = getters['byId'](type, id);
1440
1598
 
1441
- revision = existing?.metadata?.resourceVersion;
1599
+ nextRevision = existing?.metadata?.resourceVersion;
1442
1600
  }
1443
1601
 
1444
- if ( !revision ) {
1602
+ if ( !nextRevision ) {
1445
1603
  const cache = state.types[type];
1446
1604
 
1447
1605
  // No Cache, nothing to compare to, return early
@@ -1449,27 +1607,29 @@ const defaultGetters = {
1449
1607
  return null;
1450
1608
  }
1451
1609
 
1452
- revision = Number(cache.revision);
1610
+ const cacheRevision = new SteveRevision(cache.revision);
1453
1611
 
1454
1612
  // Cached LIST revision isn't a number, cannot compare to, return early
1455
- if (Number.isNaN(revision)) {
1613
+ if (!cacheRevision.isNumber) {
1456
1614
  return cache.revision || null;
1457
1615
  }
1458
1616
 
1617
+ nextRevision = cacheRevision;
1618
+
1459
1619
  for ( const obj of cache.list || [] ) {
1460
1620
  if ( obj && obj.metadata ) {
1461
- const neu = Number(obj.metadata.resourceVersion);
1621
+ const candidateRevision = new SteveRevision(obj.metadata.resourceVersion);
1462
1622
 
1463
- if (Number.isNaN(neu)) {
1464
- continue;
1623
+ if (candidateRevision.isNewerThan(nextRevision)) {
1624
+ nextRevision = candidateRevision;
1465
1625
  }
1466
-
1467
- revision = Math.max(revision, neu);
1468
1626
  }
1469
1627
  }
1628
+
1629
+ nextRevision = nextRevision.asNumber;
1470
1630
  }
1471
1631
 
1472
- return revision || null;
1632
+ return nextRevision || null;
1473
1633
  },
1474
1634
 
1475
1635
  /**
@@ -167,7 +167,7 @@ export class SteveWatchEventListenerManager {
167
167
 
168
168
  if (eventWatcher) {
169
169
  Object.values(eventWatcher.callbacks).forEach((cb) => {
170
- cb({ forceWatch: params.forceWatch }); // eslint-disable-line node/no-callback-literal
170
+ cb({ forceWatch: params.forceWatch, revision: params.revision }); // eslint-disable-line node/no-callback-literal
171
171
  });
172
172
  }
173
173
  }
@@ -177,7 +177,7 @@ export class SteveWatchEventListenerManager {
177
177
 
178
178
  watch.listeners.forEach((l) => {
179
179
  Object.values(l.callbacks || {}).forEach((cb) => {
180
- cb({ forceWatch: params.forceWatch });// eslint-disable-line node/no-callback-literal
180
+ cb({ forceWatch: params.forceWatch, revision: params.revision });// eslint-disable-line node/no-callback-literal
181
181
  });
182
182
  });
183
183
  }
@@ -259,6 +259,12 @@ export default defineComponent({
259
259
  */
260
260
  findTrueValues(value: boolean[]): boolean {
261
261
  return value.find((v) => v === this.valueWhenTrue) || false;
262
+ },
263
+
264
+ focus() {
265
+ if (!this.isDisabled) {
266
+ (this.$refs.checkbox as HTMLElement)?.focus();
267
+ }
262
268
  }
263
269
  }
264
270
  });
@@ -285,10 +291,12 @@ export default defineComponent({
285
291
  :value="valueWhenTrue"
286
292
  type="checkbox"
287
293
  tabindex="-1"
294
+ aria-hidden="true"
288
295
  @click.stop.prevent
289
296
  @keyup.enter.stop.prevent
290
297
  >
291
298
  <span
299
+ ref="checkbox"
292
300
  class="checkbox-custom"
293
301
  :class="{indeterminate: indeterminate}"
294
302
  :tabindex="isDisabled ? -1 : 0"
@@ -424,6 +432,11 @@ $fontColor: var(--input-label);
424
432
  outline-offset: 2px;
425
433
  border-radius: 0;
426
434
  }
435
+ &:focus {
436
+ @include focus-outline;
437
+ outline-offset: 2px;
438
+ border-radius: 0;
439
+ }
427
440
  }
428
441
 
429
442
  input {
@@ -68,7 +68,7 @@ export default defineComponent({
68
68
  <template v-if="hover">
69
69
  <i
70
70
  v-clean-tooltip="tooltipContent"
71
- v-stripped-aria-label="isObject(value) ? value.content : value"
71
+ v-stripped-aria-label="`${t('generic.tooltip')} - ${(isObject(value) ? value.content : value)}`"
72
72
  :class="{'hover':!value, [iconClass]: true}"
73
73
  class="icon status-icon"
74
74
  tabindex="0"
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { RcCounterBadgeProps } from '@components/Pill/RcCounterBadge/types';
2
+ import { RcCounterBadgeProps } from './types';
3
3
  import { computed } from 'vue';
4
4
  const props = withDefaults(defineProps<RcCounterBadgeProps>(), { disabled: false });
5
5
  const displayCount = computed(() => props.count < 1000 ? props.count : '999+');
@@ -1,10 +1,12 @@
1
1
  <script setup lang="ts">
2
+ import { toRef } from 'vue';
2
3
  import { RcStatusBadgeProps } from './types';
3
4
  import { useStatusColors } from '@components/utils/status';
4
5
 
5
6
  const props = defineProps<RcStatusBadgeProps>();
6
7
 
7
- const { backgroundColor, borderColor, textColor } = useStatusColors(props, 'outlined');
8
+ const status = toRef(props, 'status');
9
+ const { backgroundColor, borderColor, textColor } = useStatusColors(status, 'outlined');
8
10
  </script>
9
11
 
10
12
  <template>
@@ -1,10 +1,12 @@
1
1
  <script setup lang="ts">
2
+ import { toRef } from 'vue';
2
3
  import { RcStatusIndicatorProps } from './types';
3
4
  import { useStatusColors } from '@components/utils/status';
4
5
 
5
6
  const props = defineProps<RcStatusIndicatorProps>();
6
7
 
7
- const { backgroundColor, borderColor } = useStatusColors(props, 'solid');
8
+ const status = toRef(props, 'status');
9
+ const { backgroundColor, borderColor } = useStatusColors(status, 'solid');
8
10
  </script>
9
11
 
10
12
  <template>
@@ -16,7 +16,7 @@ const emit = defineEmits(['close']);
16
16
  <slot name="default" />
17
17
  <RcButton
18
18
  v-if="props.showClose"
19
- ghost
19
+ variant="ghost"
20
20
  :aria-label="props.closeAriaLabel"
21
21
  @click="emit('close')"
22
22
  >
@@ -0,0 +1,4 @@
1
+ export { default as RcCounterBadge } from './RcCounterBadge';
2
+ export { default as RcStatusBadge } from './RcStatusBadge';
3
+ export { default as RcStatusIndicator } from './RcStatusIndicator';
4
+ export { default as RcTag } from './RcTag';
@@ -2,22 +2,22 @@ import { mount } from '@vue/test-utils';
2
2
  import RcButton from './RcButton.vue';
3
3
 
4
4
  describe('rcButton.vue', () => {
5
- it('renders with default role', () => {
5
+ it('renders with default variant', () => {
6
6
  const wrapper = mount(RcButton);
7
7
  const button = wrapper.find('button');
8
8
 
9
9
  expect(button.classes()).toContain('btn');
10
- expect(button.classes()).toContain('role-primary');
10
+ expect(button.classes()).toContain('variant-primary');
11
11
  });
12
12
 
13
- it('applies correct role', () => {
13
+ it('applies correct variant', () => {
14
14
  const wrapper = mount(RcButton, { props: { primary: true } });
15
15
  const button = wrapper.find('button');
16
16
 
17
- expect(button.classes()).toContain('role-primary');
17
+ expect(button.classes()).toContain('variant-primary');
18
18
  });
19
19
 
20
- it('defaults to primary role if multiple roles are provided', () => {
20
+ it('defaults to primary variant if multiple variants are provided', () => {
21
21
  const wrapper = mount(
22
22
  RcButton,
23
23
  {
@@ -30,10 +30,10 @@ describe('rcButton.vue', () => {
30
30
  );
31
31
  const button = wrapper.find('button');
32
32
 
33
- expect(button.classes()).toContain('role-primary');
33
+ expect(button.classes()).toContain('variant-primary');
34
34
  });
35
35
 
36
- it('defaults to secondary role if both secondary and tertiary roles are provided', () => {
36
+ it('defaults to secondary variant if both secondary and tertiary variants are provided', () => {
37
37
  const wrapper = mount(
38
38
  RcButton,
39
39
  {
@@ -45,7 +45,7 @@ describe('rcButton.vue', () => {
45
45
  );
46
46
  const button = wrapper.find('button');
47
47
 
48
- expect(button.classes()).toContain('role-secondary');
48
+ expect(button.classes()).toContain('variant-secondary');
49
49
  });
50
50
 
51
51
  it('applies correct size class', () => {
@@ -92,6 +92,50 @@ describe('rcButton.vue', () => {
92
92
  const wrapper = mount(RcButton, { props: { ghost: true } });
93
93
  const button = wrapper.find('button');
94
94
 
95
- expect(button.classes()).toContain('role-ghost');
95
+ expect(button.classes()).toContain('variant-ghost');
96
+ });
97
+
98
+ describe('variant prop', () => {
99
+ it('applies variant-primary class when variant="primary"', () => {
100
+ const wrapper = mount(RcButton, { props: { variant: 'primary' } });
101
+ const button = wrapper.find('button');
102
+
103
+ expect(button.classes()).toContain('variant-primary');
104
+ });
105
+
106
+ it('applies variant-secondary class when variant="secondary"', () => {
107
+ const wrapper = mount(RcButton, { props: { variant: 'secondary' } });
108
+ const button = wrapper.find('button');
109
+
110
+ expect(button.classes()).toContain('variant-secondary');
111
+ });
112
+
113
+ it('applies variant-tertiary class when variant="tertiary"', () => {
114
+ const wrapper = mount(RcButton, { props: { variant: 'tertiary' } });
115
+ const button = wrapper.find('button');
116
+
117
+ expect(button.classes()).toContain('variant-tertiary');
118
+ });
119
+
120
+ it('applies variant-link class when variant="link"', () => {
121
+ const wrapper = mount(RcButton, { props: { variant: 'link' } });
122
+ const button = wrapper.find('button');
123
+
124
+ expect(button.classes()).toContain('variant-link');
125
+ });
126
+
127
+ it('applies variant-multi-action class when variant="multiAction"', () => {
128
+ const wrapper = mount(RcButton, { props: { variant: 'multiAction' } });
129
+ const button = wrapper.find('button');
130
+
131
+ expect(button.classes()).toContain('variant-multi-action');
132
+ });
133
+
134
+ it('applies variant-ghost class when variant="ghost"', () => {
135
+ const wrapper = mount(RcButton, { props: { variant: 'ghost' } });
136
+ const button = wrapper.find('button');
137
+
138
+ expect(button.classes()).toContain('variant-ghost');
139
+ });
96
140
  });
97
141
  });