@rancher/shell 3.0.5-rc.3 → 3.0.5-rc.5

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 (200) hide show
  1. package/assets/images/icons/document.svg +3 -0
  2. package/assets/images/vendor/cognito.svg +1 -0
  3. package/assets/styles/app.scss +1 -0
  4. package/assets/styles/base/_basic.scss +10 -0
  5. package/assets/styles/base/_spacing.scss +29 -0
  6. package/assets/styles/global/_layout.scss +1 -1
  7. package/assets/styles/themes/_dark.scss +25 -0
  8. package/assets/styles/themes/_light.scss +65 -0
  9. package/assets/translations/en-us.yaml +322 -24
  10. package/assets/translations/zh-hans.yaml +8 -5
  11. package/components/Certificates.vue +5 -0
  12. package/components/FilterPanel.vue +156 -0
  13. package/components/{fleet/ForceDirectedTreeChart/index.vue → ForceDirectedTreeChart.vue} +47 -41
  14. package/components/IconOrSvg.vue +14 -35
  15. package/components/PromptRemove.vue +5 -1
  16. package/components/Resource/Detail/Card/PodsCard/Bubble.vue +13 -0
  17. package/components/Resource/Detail/Card/PodsCard/composable.ts +30 -0
  18. package/components/Resource/Detail/Card/PodsCard/index.vue +118 -0
  19. package/components/Resource/Detail/Card/ResourceUsageCard/composable.ts +51 -0
  20. package/components/Resource/Detail/Card/ResourceUsageCard/index.vue +79 -0
  21. package/components/Resource/Detail/Card/Scaler.vue +89 -0
  22. package/components/Resource/Detail/Card/StateCard/composables.ts +112 -0
  23. package/components/Resource/Detail/Card/StateCard/index.vue +39 -0
  24. package/components/Resource/Detail/Card/VerticalGap.vue +11 -0
  25. package/components/Resource/Detail/Card/__tests__/Card.test.ts +36 -0
  26. package/components/Resource/Detail/Card/__tests__/PodsCard.test.ts +84 -0
  27. package/components/Resource/Detail/Card/__tests__/ResourceUsageCard.test.ts +72 -0
  28. package/components/Resource/Detail/Card/__tests__/Scaler.test.ts +87 -0
  29. package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +53 -0
  30. package/components/Resource/Detail/Card/__tests__/VerticalGap.test.ts +14 -0
  31. package/components/Resource/Detail/Card/__tests__/index.test.ts +36 -0
  32. package/components/Resource/Detail/Card/index.vue +56 -0
  33. package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +19 -0
  34. package/components/Resource/Detail/Metadata/Annotations/composable.ts +12 -0
  35. package/components/Resource/Detail/Metadata/Annotations/index.vue +26 -0
  36. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +103 -0
  37. package/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts +281 -0
  38. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +111 -0
  39. package/components/Resource/Detail/Metadata/KeyValue.vue +130 -0
  40. package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +18 -0
  41. package/components/Resource/Detail/Metadata/Labels/composable.ts +12 -0
  42. package/components/Resource/Detail/Metadata/Labels/index.vue +27 -0
  43. package/components/Resource/Detail/Metadata/Rectangle.vue +32 -0
  44. package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +107 -0
  45. package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +24 -0
  46. package/components/Resource/Detail/Metadata/__tests__/index.test.ts +91 -0
  47. package/components/Resource/Detail/Metadata/composables.ts +29 -0
  48. package/components/Resource/Detail/Metadata/index.vue +66 -0
  49. package/components/Resource/Detail/Page.vue +22 -0
  50. package/components/Resource/Detail/PercentageBar.vue +40 -0
  51. package/components/Resource/Detail/ResourceRow.vue +119 -0
  52. package/components/Resource/Detail/SpacedRow.vue +14 -0
  53. package/components/Resource/Detail/StatusBar.vue +59 -0
  54. package/components/Resource/Detail/StatusRow.vue +61 -0
  55. package/components/Resource/Detail/TitleBar/Title.vue +13 -0
  56. package/components/Resource/Detail/TitleBar/Top.vue +14 -0
  57. package/components/Resource/Detail/TitleBar/__tests__/Title.test.ts +17 -0
  58. package/components/Resource/Detail/TitleBar/__tests__/Top.test.ts +17 -0
  59. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +142 -0
  60. package/components/Resource/Detail/TitleBar/composable.ts +31 -0
  61. package/components/Resource/Detail/TitleBar/index.vue +124 -0
  62. package/components/Resource/Detail/Top/index.vue +34 -0
  63. package/components/Resource/Detail/__tests__/Page.test.ts +32 -0
  64. package/components/ResourceDetail/__tests__/index.test.ts +114 -0
  65. package/components/ResourceDetail/index.vue +64 -562
  66. package/components/ResourceDetail/legacy.vue +545 -0
  67. package/components/ResourceTable.vue +41 -7
  68. package/components/SlideInPanelManager.vue +76 -8
  69. package/components/SortableTable/index.vue +13 -2
  70. package/components/SortableTable/selection.js +21 -8
  71. package/components/StatusBadge.vue +6 -4
  72. package/components/SubtleLink.vue +25 -0
  73. package/components/Wizard.vue +12 -1
  74. package/components/YamlEditor.vue +1 -1
  75. package/components/__tests__/FilterPanel.test.ts +81 -0
  76. package/components/auth/AuthBanner.vue +2 -3
  77. package/components/auth/RoleDetailEdit.vue +45 -3
  78. package/components/auth/login/oidc.vue +6 -1
  79. package/components/fleet/FleetApplications.vue +181 -0
  80. package/components/fleet/FleetHelmOps.vue +115 -0
  81. package/components/fleet/FleetIntro.vue +58 -28
  82. package/components/fleet/FleetNoWorkspaces.vue +5 -1
  83. package/components/fleet/FleetOCIStorageSecret.vue +171 -0
  84. package/components/fleet/FleetRepos.vue +38 -76
  85. package/components/fleet/FleetResources.vue +50 -22
  86. package/components/fleet/FleetSummary.vue +26 -51
  87. package/components/fleet/__tests__/FleetOCIStorageSecret.test.ts +213 -0
  88. package/components/fleet/__tests__/FleetSummary.test.ts +39 -39
  89. package/components/fleet/dashboard/Empty.vue +73 -0
  90. package/components/fleet/dashboard/ResourceCard.vue +183 -0
  91. package/components/fleet/dashboard/ResourceCardSummary.vue +199 -0
  92. package/components/fleet/dashboard/ResourceDetails.vue +196 -0
  93. package/components/fleet/dashboard/ResourcePanel.vue +376 -0
  94. package/components/form/ArrayList.vue +6 -0
  95. package/components/form/SimpleSecretSelector.vue +8 -2
  96. package/components/form/ValueFromResource.vue +31 -19
  97. package/components/formatter/FleetApplicationClustersReady.vue +77 -0
  98. package/components/formatter/FleetApplicationSource.vue +71 -0
  99. package/components/formatter/FleetSummaryGraph.vue +7 -0
  100. package/components/nav/Header.vue +8 -7
  101. package/components/nav/TopLevelMenu.helper.ts +55 -34
  102. package/components/nav/TopLevelMenu.vue +11 -0
  103. package/components/nav/Type.vue +4 -1
  104. package/composables/useI18n.ts +12 -11
  105. package/config/labels-annotations.js +14 -11
  106. package/config/product/auth.js +1 -0
  107. package/config/product/fleet.js +70 -17
  108. package/config/query-params.js +3 -1
  109. package/config/roles.ts +1 -0
  110. package/config/router/routes.js +20 -2
  111. package/config/secret.ts +15 -0
  112. package/config/settings.ts +3 -2
  113. package/config/table-headers.js +52 -22
  114. package/config/types.js +2 -0
  115. package/core/plugin-helpers.ts +3 -2
  116. package/detail/fleet.cattle.io.cluster.vue +28 -15
  117. package/detail/fleet.cattle.io.gitrepo.vue +10 -1
  118. package/detail/fleet.cattle.io.helmop.vue +157 -0
  119. package/dialog/HelmOpForceUpdateDialog.vue +132 -0
  120. package/dialog/RedeployWorkloadDialog.vue +164 -0
  121. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +56 -67
  122. package/edit/auth/oidc.vue +159 -93
  123. package/edit/fleet.cattle.io.gitrepo.vue +26 -33
  124. package/edit/fleet.cattle.io.helmop.vue +997 -0
  125. package/edit/management.cattle.io.fleetworkspace.vue +43 -10
  126. package/list/fleet.cattle.io.gitrepo.vue +1 -1
  127. package/list/fleet.cattle.io.helmop.vue +108 -0
  128. package/list/namespace.vue +5 -2
  129. package/mixins/auth-config.js +8 -1
  130. package/mixins/preset.js +100 -0
  131. package/mixins/resource-fetch-api-pagination.js +2 -0
  132. package/mixins/resource-fetch.js +1 -1
  133. package/mixins/resource-table-watch.js +45 -0
  134. package/models/__tests__/chart.test.ts +273 -0
  135. package/models/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -1
  136. package/models/chart.js +144 -2
  137. package/models/fleet-application.js +385 -0
  138. package/models/fleet.cattle.io.bundle.js +9 -8
  139. package/models/fleet.cattle.io.gitrepo.js +41 -365
  140. package/models/fleet.cattle.io.helmop.js +228 -0
  141. package/models/management.cattle.io.authconfig.js +1 -0
  142. package/models/management.cattle.io.fleetworkspace.js +12 -0
  143. package/models/workload.js +14 -18
  144. package/package.json +2 -1
  145. package/pages/auth/verify.vue +13 -1
  146. package/pages/c/_cluster/apps/charts/AddRepoLink.vue +37 -0
  147. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +80 -0
  148. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +54 -0
  149. package/pages/c/_cluster/apps/charts/StatusLabel.vue +33 -0
  150. package/pages/c/_cluster/apps/charts/index.vue +302 -484
  151. package/pages/c/_cluster/explorer/EventsTable.vue +1 -1
  152. package/pages/c/_cluster/fleet/__tests__/index.test.ts +426 -0
  153. package/pages/c/_cluster/fleet/application/_resource/_id.vue +14 -0
  154. package/pages/c/_cluster/fleet/application/_resource/create.vue +14 -0
  155. package/pages/c/_cluster/fleet/application/create.vue +340 -0
  156. package/pages/c/_cluster/fleet/application/index.vue +139 -0
  157. package/pages/c/_cluster/fleet/graph/config.js +277 -0
  158. package/pages/c/_cluster/fleet/index.vue +772 -330
  159. package/pages/explorer/resource/detail/configmap.vue +19 -0
  160. package/plugins/dashboard-store/actions.js +31 -9
  161. package/plugins/dashboard-store/getters.js +34 -21
  162. package/plugins/dashboard-store/mutations.js +51 -7
  163. package/plugins/dashboard-store/resource-class.js +14 -2
  164. package/plugins/steve/__tests__/subscribe.spec.ts +66 -1
  165. package/plugins/steve/actions.js +3 -0
  166. package/plugins/steve/steve-pagination-utils.ts +14 -13
  167. package/plugins/steve/subscribe.js +229 -42
  168. package/rancher-components/BadgeState/BadgeState.vue +3 -1
  169. package/rancher-components/Form/Checkbox/Checkbox.vue +2 -2
  170. package/rancher-components/RcItemCard/RcItemCard.test.ts +189 -0
  171. package/rancher-components/RcItemCard/RcItemCard.vue +425 -0
  172. package/rancher-components/RcItemCard/RcItemCardAction.vue +24 -0
  173. package/rancher-components/RcItemCard/index.ts +2 -0
  174. package/store/auth.js +1 -0
  175. package/store/catalog.js +62 -24
  176. package/store/index.js +33 -14
  177. package/store/slideInPanel.ts +6 -0
  178. package/store/type-map.js +1 -0
  179. package/types/fleet.d.ts +35 -0
  180. package/types/resources/settings.d.ts +19 -1
  181. package/types/shell/index.d.ts +339 -272
  182. package/types/store/dashboard-store.types.ts +17 -3
  183. package/types/store/pagination.types.ts +6 -1
  184. package/types/store/subscribe.types.ts +50 -0
  185. package/utils/auth.js +32 -3
  186. package/utils/fleet-types.ts +0 -0
  187. package/utils/fleet.ts +200 -1
  188. package/utils/pagination-utils.ts +26 -1
  189. package/utils/pagination-wrapper.ts +132 -50
  190. package/utils/settings.ts +4 -1
  191. package/utils/style.ts +39 -0
  192. package/utils/validators/formRules/__tests__/index.test.ts +36 -3
  193. package/utils/validators/formRules/index.ts +10 -3
  194. package/utils/window.js +11 -7
  195. package/components/__tests__/ApplicationCard.test.ts +0 -27
  196. package/components/cards/ApplicationCard.vue +0 -145
  197. package/components/fleet/ForceDirectedTreeChart/chartIcons.js +0 -17
  198. package/config/secret.js +0 -14
  199. package/pages/c/_cluster/fleet/GitRepoGraphConfig.js +0 -249
  200. /package/{components/form/SSHKnownHosts → dialog}/__tests__/KnownHostsEditDialog.test.ts +0 -0
@@ -34,6 +34,8 @@ import { waitFor } from '@shell/utils/async';
34
34
  import { WORKER_MODES } from './worker';
35
35
  import acceptOrRejectSocketMessage from './accept-or-reject-socket-message';
36
36
  import { BLANK_CLUSTER, STORE } from '@shell/store/store-types.js';
37
+ import { _MERGE } from '@shell/plugins/dashboard-store/actions';
38
+ import { STEVE_WATCH_EVENT, STEVE_WATCH_MODE } from '@shell/types/store/subscribe.types';
37
39
  import paginationUtils from '@shell/utils/pagination-utils';
38
40
 
39
41
  // minimum length of time a disconnect notification is shown
@@ -185,10 +187,14 @@ export async function createWorker(store, ctx) {
185
187
  }
186
188
 
187
189
  export function equivalentWatch(a, b) {
188
- const aresourceType = a.resourceType || a.type;
189
- const bresourceType = b.resourceType || b.type;
190
+ const aResourceType = a.resourceType || a.type;
191
+ const bResourceType = b.resourceType || b.type;
190
192
 
191
- if ( aresourceType !== bresourceType ) {
193
+ if ( aResourceType !== bResourceType ) {
194
+ return false;
195
+ }
196
+
197
+ if (a.mode !== b.mode && (a.mode || b.mode)) {
192
198
  return false;
193
199
  }
194
200
 
@@ -256,6 +262,13 @@ function growlsDisabled(rootGetters) {
256
262
  return getPerformanceSetting(rootGetters)?.disableWebsocketNotification;
257
263
  }
258
264
 
265
+ /**
266
+ * Supported events are listed
267
+ *
268
+ * of type { [key: STEVE_WATCH_EVENT]: STEVE_WATCH_EVENT_LISTENER[]}
269
+ */
270
+ const listeners = { [STEVE_WATCH_EVENT.CHANGES]: [] };
271
+
259
272
  /**
260
273
  * Actions that cover all cases (see file description)
261
274
  */
@@ -354,14 +367,80 @@ const sharedActions = {
354
367
  return Promise.all(cleanupTasks);
355
368
  },
356
369
 
370
+ /**
371
+ * Create a trigger for a specific type of watch event
372
+ *
373
+ * For example if a watch on mgmt clusters exists and a page wants to know when any changes occur
374
+ * @param {} ctx
375
+ * @param {STEVE_WATCH_EVENT_PARAMS} event
376
+ */
377
+ watchEvent(ctx, {
378
+ event = STEVE_WATCH_EVENT.CHANGES,
379
+ id,
380
+ callback,
381
+ /**
382
+ * of type @STEVE_WATCH_PARAMS
383
+ */
384
+ params
385
+ }) {
386
+ if (!listeners[event]) {
387
+ console.error(`Unknown event type "${ event }", only ${ Object.keys(listeners).join(',') } are supported`); // eslint-disable-line no-console
388
+
389
+ return;
390
+ }
391
+
392
+ // STEVE_WATCH_EVENT_LISTENER | undefined
393
+ let listener = listeners[event].find((l) => equivalentWatch(l.params, params));
394
+
395
+ if (!listener) {
396
+ listener = {
397
+ params,
398
+ callbacks: { }
399
+ };
400
+ listeners[event].push(listener);
401
+ }
402
+
403
+ if (!listener.callbacks[id]) {
404
+ listener.callbacks[id] = callback;
405
+ ctx.dispatch('watch', params);
406
+ }
407
+ },
408
+
409
+ /**
410
+ * @param {} ctx
411
+ * @param {STEVE_UNWATCH_EVENT_PARAMS} event
412
+ */
413
+ unwatchEvent(ctx, {
414
+ event = STEVE_WATCH_EVENT.CHANGES,
415
+ id,
416
+ /**
417
+ * of type @STEVE_WATCH_PARAMS
418
+ */
419
+ params
420
+ }) {
421
+ if (!listeners[event]) {
422
+ console.info(`Attempted to unwatch for an event "${ event }" but it had no watchers`); // eslint-disable-line no-console
423
+
424
+ return;
425
+ }
426
+
427
+ const existing = listeners[event].find((l) => equivalentWatch(l.params, params));
428
+
429
+ if (existing) {
430
+ delete existing.callbacks[id];
431
+ }
432
+ },
433
+
434
+ /**
435
+ * @param {STEVE_WATCH_PARAMS} params
436
+ */
357
437
  watch({
358
438
  state, dispatch, getters, rootGetters
359
439
  }, params) {
360
440
  state.debugSocket && console.info(`Watch Request [${ getters.storeName }]`, JSON.stringify(params)); // eslint-disable-line no-console
361
-
362
441
  let {
363
442
  // eslint-disable-next-line prefer-const
364
- type, selector, id, revision, namespace, stop, force
443
+ type, selector, id, revision, namespace, stop, force, mode
365
444
  } = params;
366
445
 
367
446
  namespace = acceptOrRejectSocketMessage.subscribeNamespace(namespace);
@@ -393,29 +472,43 @@ const sharedActions = {
393
472
  return;
394
473
  }
395
474
 
396
- if ( !stop && getters.watchStarted({
397
- type, id, selector, namespace
398
- }) ) {
475
+ const messageMeta = {
476
+ type, id, selector, namespace, mode
477
+ };
478
+
479
+ if (!stop && getters.watchStarted(messageMeta)) {
399
480
  // eslint-disable-next-line no-console
400
481
  state.debugSocket && console.debug(`Already Watching [${ getters.storeName }]`, {
401
- type, id, selector, namespace
482
+ type, id, selector, namespace, mode
402
483
  });
403
484
 
404
485
  return;
405
486
  }
406
487
 
407
- // isSteveCacheEnabled check is temporary and will be removed once Part 3 of https://github.com/rancher/dashboard/pull/10349 is resolved by backend
408
- // Steve cache backed api does not return a revision, so `revision` here is always undefined
409
- // Which means we find a revision within a resource itself and use it in the watch
410
- // That revision is probably too old and results in a watch error
488
+ if (!stop) {
489
+ dispatch('unwatchIncompatible', messageMeta);
490
+ }
491
+
411
492
  // Watch errors mean we make a http request to get latest revision (which is still missing) and try to re-watch with it...
412
493
  // etc
413
- if (typeof revision === 'undefined' && !paginationUtils.isSteveCacheEnabled({ rootGetters })) {
494
+ if (typeof revision === 'undefined') {
414
495
  revision = getters.nextResourceVersion(type, id);
415
496
  }
416
497
 
417
498
  const msg = { resourceType: type };
418
499
 
500
+ if (mode) {
501
+ msg.mode = mode;
502
+
503
+ if (mode === STEVE_WATCH_MODE.RESOURCE_CHANGES) {
504
+ const debounceMs = paginationUtils.resourceChangesDebounceMs({ rootGetters });
505
+
506
+ if (debounceMs) {
507
+ msg.debounceMs = debounceMs;
508
+ }
509
+ }
510
+ }
511
+
419
512
  if ( revision ) {
420
513
  msg.resourceVersion = `${ revision }`;
421
514
  }
@@ -452,7 +545,7 @@ const sharedActions = {
452
545
  },
453
546
 
454
547
  unwatch(ctx, {
455
- type, id, namespace, selector, all
548
+ type, id, namespace, selector, all, mode
456
549
  }) {
457
550
  const { commit, getters, dispatch } = ctx;
458
551
 
@@ -464,6 +557,7 @@ const sharedActions = {
464
557
  id,
465
558
  namespace,
466
559
  selector,
560
+ mode,
467
561
  stop: true, // Stops the watch on a type
468
562
  };
469
563
 
@@ -491,6 +585,24 @@ const sharedActions = {
491
585
  }
492
586
  },
493
587
 
588
+ /**
589
+ * Unwatch watches that are incompatible with the new type
590
+ */
591
+ unwatchIncompatible({ state, dispatch, getters }, messageMeta) {
592
+ const watchesOfType = getters.watchesOfType(messageMeta.type);
593
+ let unwatch = [];
594
+
595
+ if (messageMeta.mode === STEVE_WATCH_EVENT.CHANGES) {
596
+ // resource.changes should not be running when other types are, so unwatch
597
+ unwatch = watchesOfType.filter((entry) => entry.mode !== STEVE_WATCH_EVENT.CHANGES);
598
+ } else {
599
+ // all other modes of watches should not be running when resource.changes is, so unwatch
600
+ unwatch = watchesOfType.filter((entry) => entry.mode === STEVE_WATCH_EVENT.CHANGES);
601
+ }
602
+
603
+ unwatch.forEach((entry) => dispatch('unwatch', entry));
604
+ },
605
+
494
606
  'ws.ping'({ getters, dispatch }, msg) {
495
607
  if ( getters.storeName === 'management' ) {
496
608
  const version = msg?.data?.version || null;
@@ -587,16 +699,30 @@ const defaultActions = {
587
699
  return Promise.all(promises);
588
700
  },
589
701
 
590
- async resyncWatch({
702
+ /**
703
+ * Socket has been closed, restart afresh (make http request, ensure we re-watch)
704
+ */
705
+ async resyncWatch({ getters, dispatch }, params) {
706
+ console.info(`Resync [${ getters.storeName }]`, params); // eslint-disable-line no-console
707
+
708
+ await dispatch('fetchResources', {
709
+ ...params,
710
+ opt: { force: true, forceWatch: true }
711
+ });
712
+ },
713
+
714
+ async fetchResources({
591
715
  state, getters, dispatch, commit
592
- }, params) {
716
+ }, { opt, ...params }) {
593
717
  const {
594
- resourceType, namespace, id, selector
718
+ resourceType, namespace, id, selector, mode
595
719
  } = params;
596
720
 
597
- console.info(`Resync [${ getters.storeName }]`, params); // eslint-disable-line no-console
721
+ if (!resourceType) {
722
+ console.error(`A socket message has prompted a request to fetch a resource but no resource type was supplied`); // eslint-disable-line no-console
598
723
 
599
- const opt = { force: true, forceWatch: true };
724
+ return;
725
+ }
600
726
 
601
727
  if ( id ) {
602
728
  await dispatch('find', {
@@ -613,7 +739,7 @@ const defaultActions = {
613
739
 
614
740
  return;
615
741
  }
616
- let have, want;
742
+ let have = []; let want = [];
617
743
 
618
744
  if ( selector ) {
619
745
  have = getters['matching'](resourceType, selector).slice();
@@ -623,17 +749,48 @@ const defaultActions = {
623
749
  opt,
624
750
  });
625
751
  } else {
626
- have = getters['all'](resourceType).slice();
752
+ if (mode === STEVE_WATCH_MODE.RESOURCE_CHANGES) {
753
+ // Other findX use options (id/ns/selector) from the messages received over socket.
754
+ // However paginated requests have more complex params so grab them from store from the store.
755
+ const storePagination = getters['havePage'](resourceType);
756
+
757
+ if (!!storePagination) {
758
+ have = []; // findPage removes stale entries, so we don't need to rely on below process to remove them
759
+
760
+ // This could have been kicked off given a resource.changes message
761
+ // If the messages come in quicker than findPage completes (resource.changes debounce time >= http request time),
762
+ // and the request is the same, only the first request will be processed. all others until it finishes will be ignored
763
+ // (see deferred process - `waiting.push(later);` - in request action).
764
+ // If this becomes an issue we need to debounce and work around the deferred issue within request
765
+ want = await dispatch('findPage', {
766
+ type: resourceType,
767
+ opt: {
768
+ ...opt,
769
+ // This brings in page, page size, filter, etc
770
+ ...storePagination.request
771
+ }
772
+ });
773
+ }
627
774
 
628
- if ( namespace ) {
629
- have = have.filter((x) => x.metadata?.namespace === namespace);
630
- }
775
+ // Should any listeners be notified of this request for them to kick off their own event handling?
776
+ const listener = listeners[STEVE_WATCH_MODE.RESOURCE_CHANGES].find((sl) => equivalentWatch(sl.params, params));
631
777
 
632
- want = await dispatch('findAll', {
633
- type: resourceType,
634
- watchNamespace: namespace,
635
- opt
636
- });
778
+ if (listener) {
779
+ Object.values(listener.callbacks).forEach((cb) => cb());
780
+ }
781
+ } else {
782
+ have = getters['all'](resourceType).slice();
783
+
784
+ if ( namespace ) {
785
+ have = have.filter((x) => x.metadata?.namespace === namespace);
786
+ }
787
+
788
+ want = await dispatch('findAll', {
789
+ type: resourceType,
790
+ watchNamespace: namespace,
791
+ opt
792
+ });
793
+ }
637
794
  }
638
795
 
639
796
  const wantMap = {};
@@ -792,7 +949,8 @@ const defaultActions = {
792
949
  type: msg.resourceType,
793
950
  namespace: msg.namespace,
794
951
  id: msg.id,
795
- selector: msg.selector
952
+ selector: msg.selector,
953
+ mode: msg.mode,
796
954
  };
797
955
 
798
956
  state.started.filter((entry) => {
@@ -846,7 +1004,8 @@ const defaultActions = {
846
1004
  type,
847
1005
  id: msg.id,
848
1006
  namespace: msg.namespace,
849
- selector: msg.selector
1007
+ selector: msg.selector,
1008
+ mode: msg.mode
850
1009
  };
851
1010
 
852
1011
  state.debugSocket && console.info(`Resource Stop [${ getters.storeName }]`, type, msg); // eslint-disable-line no-console
@@ -922,6 +1081,13 @@ const defaultActions = {
922
1081
  }
923
1082
  },
924
1083
 
1084
+ 'ws.resource.changes'({ dispatch }, msg) {
1085
+ dispatch('fetchResources', {
1086
+ ...msg,
1087
+ opt: { force: true, load: _MERGE }
1088
+ } );
1089
+ },
1090
+
925
1091
  'ws.resource.remove'(ctx, msg) {
926
1092
  const data = msg.data;
927
1093
  const type = data.type;
@@ -1040,9 +1206,24 @@ const defaultGetters = {
1040
1206
  },
1041
1207
 
1042
1208
  watchStarted: (state) => (obj) => {
1043
- return !!state.started.find((entry) => equivalentWatch(obj, entry));
1209
+ const existing = state.started.find((entry) => equivalentWatch(obj, entry));
1210
+
1211
+ return !!existing;
1044
1212
  },
1045
1213
 
1214
+ /**
1215
+ * Try to determine the latest revision to use in a watch request.
1216
+ *
1217
+ * It does some dodgy revision comparisons (revisions are not guaranteed to be numerical or equate higher to newer)
1218
+ *
1219
+ * If we have an id - and that resource has a revision - use it
1220
+ * If we have a list - and the store has a revision - and it's a string - use it straight away
1221
+ * If we have a list - and the store has a revision - and it's a number - compare it to the revisions in the list and use overall highest
1222
+ *
1223
+ * Note - This used to use parseInt which does stuff like `abc-123` --> NaN, `123-abc` --> 123
1224
+ *
1225
+ * Returns string, non-zero number or null
1226
+ */
1046
1227
  nextResourceVersion: (state, getters) => (type, id) => {
1047
1228
  type = normalizeType(type);
1048
1229
  let revision = 0;
@@ -1050,32 +1231,38 @@ const defaultGetters = {
1050
1231
  if ( id ) {
1051
1232
  const existing = getters['byId'](type, id);
1052
1233
 
1053
- revision = parseInt(existing?.metadata?.resourceVersion, 10);
1234
+ revision = existing?.metadata?.resourceVersion;
1054
1235
  }
1055
1236
 
1056
1237
  if ( !revision ) {
1057
1238
  const cache = state.types[type];
1058
1239
 
1240
+ // No Cache, nothing to compare to, return early
1059
1241
  if ( !cache ) {
1060
1242
  return null;
1061
1243
  }
1062
1244
 
1063
- revision = cache.revision; // This is always zero.....
1245
+ revision = Number(cache.revision);
1246
+
1247
+ // Cached LIST revision isn't a number, cannot compare to, return early
1248
+ if (Number.isNaN(revision)) {
1249
+ return cache.revision || null;
1250
+ }
1064
1251
 
1065
- for ( const obj of cache.list ) {
1252
+ for ( const obj of cache.list || [] ) {
1066
1253
  if ( obj && obj.metadata ) {
1067
- const neu = parseInt(obj.metadata.resourceVersion, 10);
1254
+ const neu = Number(obj.metadata.resourceVersion);
1255
+
1256
+ if (Number.isNaN(neu)) {
1257
+ continue;
1258
+ }
1068
1259
 
1069
1260
  revision = Math.max(revision, neu);
1070
1261
  }
1071
1262
  }
1072
1263
  }
1073
1264
 
1074
- if ( revision ) {
1075
- return revision;
1076
- }
1077
-
1078
- return null;
1265
+ return revision || null;
1079
1266
  },
1080
1267
  };
1081
1268
 
@@ -64,7 +64,9 @@ export default defineComponent({
64
64
  v-if="icon"
65
65
  class="icon"
66
66
  :class="{[icon]: true, 'mr-5': !!msg}"
67
- />{{ msg }}
67
+ />
68
+ <span class="msg">{{ msg }}</span>
69
+ <slot name="content-right" />
68
70
  </span>
69
71
  </template>
70
72
 
@@ -13,7 +13,7 @@ export default defineComponent({
13
13
  * The checkbox value.
14
14
  */
15
15
  value: {
16
- type: [Boolean, Array, String] as PropType<boolean | boolean[] | string>,
16
+ type: [Boolean, Array, String] as PropType<boolean | boolean[] | string | string[]>,
17
17
  default: false
18
18
  },
19
19
 
@@ -245,7 +245,7 @@ export default defineComponent({
245
245
  /**
246
246
  * Determines if there are multiple values for the checkbox.
247
247
  */
248
- isMulti(value: boolean | boolean[] | string): value is boolean[] {
248
+ isMulti(value: boolean | boolean[] | string | string[]): value is boolean[] {
249
249
  return Array.isArray(value);
250
250
  },
251
251
 
@@ -0,0 +1,189 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import RcItemCard from './RcItemCard.vue';
3
+ import RcItemCardAction from './RcItemCardAction.vue';
4
+
5
+ class ResizeObserverMock {
6
+ observe = jest.fn();
7
+ unobserve = jest.fn();
8
+ disconnect = jest.fn();
9
+ }
10
+
11
+ global.ResizeObserver = ResizeObserverMock;
12
+
13
+ const id = 'test';
14
+
15
+ const baseProps = {
16
+ id,
17
+ value: { someProperty: 'some-value' },
18
+ image: { src: 'logo.png', alt: { text: 'Logo' } },
19
+ header: {
20
+ title: { text: 'Card Title' },
21
+ statuses: [
22
+ { icon: 'icon-one', tooltip: { text: 'Status One' } },
23
+ { icon: 'icon-two' }
24
+ ]
25
+ },
26
+ content: { text: 'Card description here' }
27
+ };
28
+
29
+ describe('rcItemCard', () => {
30
+ it('renders title, image, and content', () => {
31
+ const wrapper = mount(RcItemCard, { props: baseProps });
32
+
33
+ expect(wrapper.get('[data-testid="item-card-header-title"]').text()).toBe('Card Title');
34
+ expect(wrapper.get('[data-testid="item-card-content"]').text()).toContain('Card description here');
35
+ expect(wrapper.get('[data-testid="item-card-image"]')).toBeTruthy();
36
+ expect(wrapper.findAll(`[data-testid="item-card-header-statuses-status"]`)).toHaveLength(2);
37
+ });
38
+
39
+ it('renders pill only in medium variant', () => {
40
+ const wrapper = mount(RcItemCard, {
41
+ props: {
42
+ ...baseProps,
43
+ variant: 'medium',
44
+ pill: { label: { text: 'Installed' } }
45
+ }
46
+ });
47
+
48
+ expect(wrapper.get('[data-testid="item-card-pill"]').text()).toBe('Installed');
49
+
50
+ // now test that it's not rendered when variant is small
51
+ const wrapperSmall = mount(RcItemCard, {
52
+ props: {
53
+ ...baseProps,
54
+ variant: 'small',
55
+ pill: { label: { text: 'Installed' } }
56
+ }
57
+ });
58
+
59
+ expect(wrapperSmall.find('[data-testid="item-card-pill"]').exists()).toBe(false);
60
+ });
61
+
62
+ it('renders action-menu if slot content is provided for it', () => {
63
+ const wrapper = mount(RcItemCard, {
64
+ props: { ...baseProps },
65
+ slots: { 'item-card-actions': '<div class="test-slot-for-actions">test</div>' }
66
+ });
67
+
68
+ expect(wrapper.find('.test-slot-for-actions').exists()).toBe(true);
69
+ });
70
+
71
+ it('renders action-menu when actions are passed as a prop', () => {
72
+ const wrapper = mount(RcItemCard, {
73
+ props: {
74
+ ...baseProps,
75
+ actions: [{ action: 'test', label: 'test' }]
76
+ }
77
+ });
78
+
79
+ expect(wrapper.findComponent('[data-testid="item-card-header-action-menu"]').exists()).toBe(true);
80
+ });
81
+
82
+ it('does not render action-menu if no slot and no actions', () => {
83
+ const wrapper = mount(RcItemCard, { props: { ...baseProps } });
84
+
85
+ expect(wrapper.findComponent('[data-testid="item-card-header-action-menu"]').exists()).toBe(false);
86
+ });
87
+
88
+ it('emits card-click when clicked and clickable', async() => {
89
+ const wrapper = mount(RcItemCard, {
90
+ props: {
91
+ ...baseProps,
92
+ clickable: true
93
+ }
94
+ });
95
+
96
+ await wrapper.trigger('click');
97
+
98
+ const emitted = wrapper.emitted('card-click');
99
+
100
+ expect(emitted).toBeTruthy();
101
+ expect(emitted?.[0]).toStrictEqual([{ someProperty: 'some-value' }]);
102
+ });
103
+
104
+ it('does not emit card-click when clicking on rc-item-card-action content', async() => {
105
+ const wrapper = mount(RcItemCard, {
106
+ props: {
107
+ ...baseProps,
108
+ clickable: true
109
+ },
110
+ global: { components: { RcItemCardAction } },
111
+ slots: { 'item-card-actions': '<rc-item-card-action>Click me</rc-item-card-action>' }
112
+ });
113
+
114
+ await wrapper.get('[data-testid="rc-item-card-action"]').trigger('click');
115
+
116
+ expect(wrapper.emitted('card-click')).toBeFalsy();
117
+ });
118
+
119
+ it('sets role and tabindex when clickable', () => {
120
+ const wrapper = mount(RcItemCard, {
121
+ props: {
122
+ ...baseProps,
123
+ clickable: true
124
+ }
125
+ });
126
+
127
+ const root = wrapper.get(`[data-testid="item-card-${ id }"]`);
128
+
129
+ expect(root.attributes('role')).toBe('button');
130
+ expect(root.attributes('tabindex')).toBe('0');
131
+ });
132
+
133
+ it('does not set role or tabindex when not clickable', () => {
134
+ const wrapper = mount(RcItemCard, {
135
+ props: {
136
+ ...baseProps,
137
+ clickable: false
138
+ }
139
+ });
140
+
141
+ const root = wrapper.get(`[data-testid="item-card-${ id }"]`);
142
+
143
+ expect(root.attributes('role')).toBeUndefined();
144
+ expect(root.attributes('tabindex')).toBeUndefined();
145
+ });
146
+
147
+ it('supports keyboard enter to trigger click', async() => {
148
+ const wrapper = mount(RcItemCard, {
149
+ props: {
150
+ ...baseProps,
151
+ clickable: true
152
+ }
153
+ });
154
+
155
+ await wrapper.trigger('keydown.enter');
156
+ expect(wrapper.emitted('card-click')).toBeTruthy();
157
+ });
158
+
159
+ it('supports slot for footer and sub-header', () => {
160
+ const wrapper = mount(RcItemCard, {
161
+ props: baseProps,
162
+ slots: {
163
+ 'item-card-footer': '<div>FooterContent</div>',
164
+ 'item-card-sub-header': '<div>SubHeaderContent</div>'
165
+ }
166
+ });
167
+
168
+ expect(wrapper.text()).toContain('FooterContent');
169
+ expect(wrapper.text()).toContain('SubHeaderContent');
170
+ });
171
+
172
+ it('renders icon with custom color', () => {
173
+ const wrapper = mount(RcItemCard, {
174
+ props: {
175
+ ...baseProps,
176
+ header: {
177
+ ...baseProps.header,
178
+ statuses: [
179
+ { icon: 'icon-custom', customColor: 'red' }
180
+ ]
181
+ }
182
+ }
183
+ });
184
+
185
+ const icon = wrapper.get('[data-testid="item-card-header-status-0"]');
186
+
187
+ expect(icon.attributes('style')).toContain('color: red');
188
+ });
189
+ });