@rancher/shell 3.0.6 → 3.0.7

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 (78) hide show
  1. package/assets/images/pl/dark/rancher-logo.svg +131 -44
  2. package/assets/images/pl/rancher-logo.svg +120 -44
  3. package/assets/styles/base/_basic.scss +2 -2
  4. package/assets/styles/base/_color-classic.scss +51 -0
  5. package/assets/styles/base/_color.scss +3 -3
  6. package/assets/styles/base/_mixins.scss +1 -1
  7. package/assets/styles/base/_variables-classic.scss +47 -0
  8. package/assets/styles/global/_button.scss +49 -17
  9. package/assets/styles/global/_form.scss +1 -1
  10. package/assets/styles/themes/_dark.scss +4 -0
  11. package/assets/styles/themes/_light.scss +3 -69
  12. package/assets/styles/themes/_modern.scss +194 -50
  13. package/assets/styles/vendor/vue-select.scss +1 -2
  14. package/assets/translations/en-us.yaml +33 -21
  15. package/components/ClusterIconMenu.vue +1 -1
  16. package/components/ClusterProviderIcon.vue +1 -1
  17. package/components/CodeMirror.vue +1 -1
  18. package/components/IconOrSvg.vue +40 -29
  19. package/components/ResourceDetail/index.vue +1 -0
  20. package/components/SortableTable/sorting.js +3 -1
  21. package/components/Tabbed/index.vue +5 -5
  22. package/components/form/ResourceTabs/index.vue +37 -18
  23. package/components/form/SecretSelector.vue +6 -2
  24. package/components/nav/Group.vue +29 -9
  25. package/components/nav/Header.vue +6 -8
  26. package/components/nav/NamespaceFilter.vue +1 -1
  27. package/components/nav/TopLevelMenu.helper.ts +47 -20
  28. package/components/nav/TopLevelMenu.vue +44 -14
  29. package/components/nav/Type.vue +0 -5
  30. package/components/nav/__tests__/TopLevelMenu.test.ts +2 -0
  31. package/config/pagination-table-headers.js +10 -2
  32. package/config/product/explorer.js +4 -3
  33. package/config/table-headers.js +9 -0
  34. package/core/plugin.ts +18 -6
  35. package/core/types.ts +8 -0
  36. package/detail/provisioning.cattle.io.cluster.vue +1 -0
  37. package/dialog/InstallExtensionDialog.vue +71 -45
  38. package/dialog/UninstallExtensionDialog.vue +2 -1
  39. package/dialog/__tests__/InstallExtensionDialog.test.ts +111 -0
  40. package/edit/auth/oidc.vue +86 -16
  41. package/mixins/__tests__/chart.test.ts +1 -1
  42. package/mixins/chart.js +1 -1
  43. package/models/event.js +7 -0
  44. package/models/provisioning.cattle.io.cluster.js +9 -0
  45. package/package.json +1 -1
  46. package/pages/c/_cluster/explorer/EventsTable.vue +3 -6
  47. package/pages/c/_cluster/settings/performance.vue +1 -1
  48. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +159 -62
  49. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +102 -0
  50. package/pages/c/_cluster/uiplugins/__tests__/{index.spec.ts → index.test.ts} +121 -55
  51. package/pages/c/_cluster/uiplugins/index.vue +110 -94
  52. package/plugins/__tests__/subscribe.events.test.ts +194 -0
  53. package/plugins/dashboard-store/actions.js +3 -0
  54. package/plugins/dashboard-store/getters.js +1 -1
  55. package/plugins/dashboard-store/resource-class.js +3 -3
  56. package/plugins/steve/__tests__/subscribe.spec.ts +27 -24
  57. package/plugins/steve/index.js +18 -10
  58. package/plugins/steve/mutations.js +2 -2
  59. package/plugins/steve/resourceWatcher.js +2 -2
  60. package/plugins/steve/steve-pagination-utils.ts +12 -9
  61. package/plugins/steve/subscribe.js +113 -85
  62. package/plugins/subscribe-events.ts +211 -0
  63. package/rancher-components/BadgeState/BadgeState.vue +8 -6
  64. package/rancher-components/Banner/Banner.vue +2 -1
  65. package/rancher-components/Form/Checkbox/Checkbox.vue +3 -3
  66. package/rancher-components/Form/Radio/RadioButton.vue +3 -3
  67. package/store/index.js +12 -22
  68. package/types/extension-manager.ts +8 -1
  69. package/types/resources/settings.d.ts +24 -17
  70. package/types/shell/index.d.ts +352 -335
  71. package/types/store/subscribe-events.types.ts +70 -0
  72. package/types/store/subscribe.types.ts +6 -22
  73. package/utils/pagination-utils.ts +87 -28
  74. package/utils/pagination-wrapper.ts +6 -8
  75. package/utils/sort.js +5 -0
  76. package/utils/unit-tests/pagination-utils.spec.ts +283 -0
  77. package/utils/validators/formRules/__tests__/index.test.ts +7 -0
  78. package/utils/validators/formRules/index.ts +2 -2
@@ -1,9 +1,10 @@
1
1
  <script>
2
2
  import { mapGetters } from 'vuex';
3
+ import day from 'dayjs';
3
4
  import { mapPref, PLUGIN_DEVELOPER } from '@shell/store/prefs';
4
5
  import { sortBy } from '@shell/utils/sort';
5
6
  import { allHash } from '@shell/utils/promise';
6
- import { CATALOG, UI_PLUGIN, MANAGEMENT } from '@shell/config/types';
7
+ import { CATALOG, UI_PLUGIN, MANAGEMENT, ZERO_TIME } from '@shell/config/types';
7
8
  import { SETTING } from '@shell/config/settings';
8
9
  import { fetchOrCreateSetting } from '@shell/utils/settings';
9
10
  import { getVersionData, isRancherPrime } from '@shell/config/version';
@@ -40,10 +41,8 @@ const MAX_DESCRIPTION_LENGTH = 200;
40
41
 
41
42
  const TABS_VALUES = {
42
43
  INSTALLED: 'installed',
43
- UPDATES: 'updates',
44
44
  AVAILABLE: 'available',
45
45
  BUILTIN: 'builtin',
46
- ALL: 'all',
47
46
  };
48
47
 
49
48
  export default {
@@ -67,7 +66,7 @@ export default {
67
66
  return {
68
67
  TABS_VALUES,
69
68
  kubeVersion: null,
70
- view: '',
69
+ activeTab: '',
71
70
  installing: {},
72
71
  errors: {},
73
72
  plugins: [], // The installed plugins
@@ -122,11 +121,6 @@ export default {
122
121
 
123
122
  this.addExtensionReposBannerSetting = await fetchOrCreateSetting(this.$store, SETTING.ADD_EXTENSION_REPOS_BANNER_DISPLAY, 'true', true) || {};
124
123
 
125
- // If there are no plugins installed, default to the catalog view
126
- if (this.plugins.length === 0) {
127
- this.$refs.tabs?.select(TABS_VALUES.AVAILABLE);
128
- }
129
-
130
124
  this.loading = false;
131
125
  },
132
126
 
@@ -207,12 +201,10 @@ export default {
207
201
  // If not an extension developer, then don't include built-in extensions
208
202
  const all = this.pluginDeveloper ? this.available : this.available.filter((p) => !p.builtin);
209
203
 
210
- switch (this.view) {
204
+ switch (this.activeTab) {
211
205
  case TABS_VALUES.INSTALLED:
212
206
  // We never show built-in extensions as installed - installed are just the ones the user has installed
213
- return all.filter((p) => !p.builtin && (!!p.installed || !!p.installing));
214
- case TABS_VALUES.UPDATES:
215
- return this.updates;
207
+ return all.filter((p) => !p.builtin && (!!p.installed || !!p.installing) && p.installableVersions?.length > 0);
216
208
  case TABS_VALUES.AVAILABLE:
217
209
  return all.filter((p) => !p.installed);
218
210
  case TABS_VALUES.BUILTIN:
@@ -226,14 +218,14 @@ export default {
226
218
  return this.menuActions?.length > 0;
227
219
  },
228
220
 
229
- // Message to display when the tab view is empty (depends on the tab)
221
+ // Message to display when the active tab is empty (depends on the tab)
230
222
  emptyMessage() {
231
223
  // i18n-uses plugins.empty.*
232
- return this.t(`plugins.empty.${ this.view }`);
224
+ return this.t(`plugins.empty.${ this.activeTab }`);
233
225
  },
234
226
 
235
- updates() {
236
- return this.available.filter((plugin) => !!plugin.upgrade);
227
+ installed() {
228
+ return this.available.filter((p) => !p.builtin && (!!p.installed || !!p.installing));
237
229
  },
238
230
 
239
231
  pluginCards() {
@@ -298,6 +290,7 @@ export default {
298
290
  item.displayVersion = latestCompatible.version;
299
291
  item.displayVersionLabel = getPluginChartVersionLabel(latestCompatible);
300
292
  item.icon = latestCompatible.icon;
293
+ item.created = latestCompatible.created;
301
294
  } else {
302
295
  item.experimental = uiPluginHasAnnotation(chart, CATALOG_ANNOTATIONS.EXPERIMENTAL, 'true');
303
296
  item.certified = uiPluginHasAnnotation(chart, CATALOG_ANNOTATIONS.CERTIFIED, CATALOG_ANNOTATIONS._RANCHER);
@@ -305,6 +298,7 @@ export default {
305
298
  item.displayVersion = item.versions?.[0]?.version;
306
299
  item.displayVersionLabel = getPluginChartVersionLabel(item.versions?.[0]);
307
300
  item.icon = chart.icon || latestCompatible?.annotations?.['catalog.cattle.io/ui-icon'];
301
+ item.created = item.versions?.[0]?.created;
308
302
  }
309
303
 
310
304
  // add message of extension card if there's a newer version of the extension, but it's not available to be installed
@@ -353,6 +347,7 @@ export default {
353
347
  displayVersion: p.metadata?.version,
354
348
  displayVersionLabel: p.metadata?.version || '-',
355
349
  installed: true,
350
+ installedVersion: p.metadata?.version,
356
351
  builtin: !!p.builtin,
357
352
  experimental: rancher?.annotations?.[CATALOG_ANNOTATIONS.EXPERIMENTAL] === 'true',
358
353
  certified: rancher?.annotations?.[CATALOG_ANNOTATIONS.CERTIFIED] === CATALOG_ANNOTATIONS._RANCHER
@@ -373,8 +368,7 @@ export default {
373
368
  if (chart) {
374
369
  chart.installed = true;
375
370
  chart.uiplugin = p;
376
- chart.displayVersion = p.version;
377
- let displayVersionLabel = p.version;
371
+ chart.installedVersion = p.version;
378
372
 
379
373
  // Can't do this here
380
374
  chart.installing = this.installing[chart.name];
@@ -389,13 +383,10 @@ export default {
389
383
  if (installedVersion) {
390
384
  chart.experimental = installedVersion?.annotations?.[CATALOG_ANNOTATIONS.EXPERIMENTAL] === 'true';
391
385
  chart.certified = installedVersion?.annotations?.[CATALOG_ANNOTATIONS.CERTIFIED] === CATALOG_ANNOTATIONS._RANCHER;
392
- displayVersionLabel = getPluginChartVersionLabel(installedVersion);
393
386
  }
394
387
 
395
388
  chart.upgrade = getPluginChartVersionLabel(latestInstallableVersion);
396
389
  }
397
-
398
- chart.displayVersionLabel = displayVersionLabel;
399
390
  } else {
400
391
  // No chart, so add a card for the plugin based on its Custom resource being present
401
392
  const item = {
@@ -474,10 +465,10 @@ export default {
474
465
 
475
466
  if (active) {
476
467
  // Can use the status directly, apart from upgrade, which maps to update
477
- const status = op.status.action === 'upgrade' ? 'update' : op.status.action;
468
+ const status = op.status.action;
478
469
 
479
- if (status === 'update' && this.installing[plugin.name] === 'rollback') {
480
- // Helm op is an upgrade, but we initiated a rollback, so keep the 'rollback' status
470
+ if (status === 'upgrade' && this.installing[plugin.name] === 'downgrade') {
471
+ // Helm op is an upgrade, but we initiated a downgrade, so keep the 'downgrade' status
481
472
  } else {
482
473
  this.updatePluginInstallStatus(plugin.name, status);
483
474
  }
@@ -535,6 +526,16 @@ export default {
535
526
  },
536
527
 
537
528
  methods: {
529
+ handlePanelAction(button) {
530
+ const { action, plugin, version } = button;
531
+
532
+ if (action === 'uninstall') {
533
+ this.showUninstallDialog(plugin, {});
534
+ } else {
535
+ this.showInstallDialog(plugin, action, {}, version);
536
+ }
537
+ },
538
+
538
539
  async refreshCharts(forceChartsUpdate = false) {
539
540
  // we might need to force the request, so that we know at all times if what's the status of the offical and partners repos (installed or not)
540
541
  // tied to the SetupUIPlugins, AddExtensionRepos checkboxes
@@ -560,8 +561,8 @@ export default {
560
561
  return hasFeatureFlag;
561
562
  },
562
563
 
563
- filterChanged(f) {
564
- this.view = f.selectedName;
564
+ tabChanged(f) {
565
+ this.activeTab = f.selectedName;
565
566
  },
566
567
 
567
568
  // Developer Load is in the action menu
@@ -623,10 +624,10 @@ export default {
623
624
  });
624
625
  },
625
626
 
626
- showInstallDialog(plugin, action, ev) {
627
- ev.target?.blur();
628
- ev.preventDefault();
629
- ev.stopPropagation();
627
+ showInstallDialog(plugin, action, ev, initialVersion = null) {
628
+ ev?.target?.blur();
629
+ ev?.preventDefault?.();
630
+ ev?.stopPropagation?.();
630
631
 
631
632
  this.$store.dispatch('management/promptModal', {
632
633
  component: 'InstallExtensionDialog',
@@ -636,6 +637,7 @@ export default {
636
637
  componentProps: {
637
638
  plugin,
638
639
  action,
640
+ initialVersion,
639
641
  updateStatus: (pluginName, type) => {
640
642
  this.updatePluginInstallStatus(pluginName, type);
641
643
  },
@@ -647,9 +649,9 @@ export default {
647
649
  },
648
650
 
649
651
  showUninstallDialog(plugin, ev) {
650
- ev.target?.blur();
651
- ev.preventDefault();
652
- ev.stopPropagation();
652
+ ev?.target?.blur();
653
+ ev?.preventDefault?.();
654
+ ev?.stopPropagation?.();
653
655
 
654
656
  this.$store.dispatch('management/promptModal', {
655
657
  component: 'UninstallExtensionDialog',
@@ -683,7 +685,7 @@ export default {
683
685
 
684
686
  didInstall(plugin) {
685
687
  if (plugin) {
686
- // Change the view to installed if we started installing a plugin
688
+ // Change the activeTab to installed if we started installing a plugin
687
689
  this.$refs.tabs?.select(TABS_VALUES.INSTALLED);
688
690
 
689
691
  // Clear the load error, if there was one previously
@@ -692,7 +694,9 @@ export default {
692
694
  },
693
695
 
694
696
  showPluginDetail(plugin) {
695
- this.$refs.infoPanel.show(plugin);
697
+ const tags = this.getFooterItems(plugin);
698
+
699
+ this.$refs.infoPanel.show({ ...plugin, tags });
696
700
  },
697
701
 
698
702
  updatePluginInstallStatus(name, status) {
@@ -741,42 +745,46 @@ export default {
741
745
  getPluginActions(plugin) {
742
746
  const actions = [];
743
747
 
744
- const canInstall = !plugin.installed && plugin.installableVersions?.length;
745
- const canUninstall = plugin.installed && !plugin.builtin;
746
- const canUpdate = plugin.installed && plugin.upgrade;
747
- const canRollback = plugin.installed && !plugin.upgrade && plugin.installableVersions?.length > 1;
748
-
749
- if (canUninstall) {
750
- actions.push({
751
- label: this.t('plugins.uninstall.label'),
752
- action: 'uninstall',
753
- icon: 'icon-delete',
754
- });
755
- }
756
-
757
- if (canUpdate) {
758
- actions.push({
759
- label: this.t('plugins.update.label'),
760
- action: 'update',
761
- icon: 'icon-edit',
762
- });
763
- }
748
+ if (plugin.installed) {
749
+ const canUpdate = !!plugin.upgrade;
750
+ const canDowngrade = plugin.installableVersions?.some((v) => isChartVersionHigher(plugin.installedVersion, v.version));
751
+ const canUninstall = !plugin.builtin;
752
+
753
+ if (canUpdate) {
754
+ actions.push({
755
+ label: this.t('plugins.upgrade.label'),
756
+ action: 'upgrade',
757
+ icon: 'icon-upgrade-alt',
758
+ });
759
+ }
764
760
 
765
- if (canRollback) {
766
- actions.push({
767
- label: this.t('plugins.rollback.label'),
768
- action: 'rollback',
769
- icon: 'icon-history',
770
- });
771
- }
761
+ if (canDowngrade) {
762
+ actions.push({
763
+ label: this.t('plugins.downgrade.label'),
764
+ action: 'downgrade',
765
+ icon: 'icon-downgrade-alt',
766
+ });
767
+ }
772
768
 
773
- if (canInstall) {
774
- actions.push({
775
- label: this.t('plugins.install.label'),
776
- action: 'install',
777
- icon: 'icon-plus',
778
- enabled: true,
779
- });
769
+ if (canUninstall) {
770
+ if (actions.length > 0) {
771
+ actions.push({ divider: true });
772
+ }
773
+ actions.push({
774
+ label: this.t('plugins.uninstall.label'),
775
+ action: 'uninstall',
776
+ icon: 'icon-delete',
777
+ });
778
+ }
779
+ } else {
780
+ if (plugin.installableVersions?.length) {
781
+ actions.push({
782
+ label: this.t('plugins.install.label'),
783
+ action: 'install',
784
+ icon: 'icon-plus',
785
+ enabled: true,
786
+ });
787
+ }
780
788
  }
781
789
 
782
790
  return actions;
@@ -784,12 +792,25 @@ export default {
784
792
 
785
793
  getSubHeaderItems(plugin) {
786
794
  const items = [{
787
- icon: 'icon-version-alt',
788
- iconTooltip: { key: 'tableHeaders.version' },
789
- label: plugin.displayVersionLabel,
790
- labelTooltip: plugin.upgrade ? this.t('plugins.upgradeAvailableTooltip', { version: plugin.upgrade }) : ''
795
+ icon: 'icon-version-alt',
796
+ iconTooltip: { key: 'tableHeaders.version' },
797
+ label: plugin.displayVersionLabel,
791
798
  }];
792
799
 
800
+ if (plugin.created) {
801
+ const hasZeroTime = plugin.created === ZERO_TIME;
802
+ const lastUpdatedItem = {
803
+ icon: 'icon-refresh-alt',
804
+ iconTooltip: { key: 'tableHeaders.lastUpdated' },
805
+ label: hasZeroTime ? this.t('generic.na') : day(plugin.created).format('MMM D, YYYY')
806
+ };
807
+
808
+ if (hasZeroTime) {
809
+ lastUpdatedItem.labelTooltip = this.t('catalog.charts.appChartCard.subHeaderItem.missingVersionDate');
810
+ }
811
+ items.push(lastUpdatedItem);
812
+ }
813
+
793
814
  if (plugin.installing) {
794
815
  let label;
795
816
 
@@ -800,11 +821,11 @@ export default {
800
821
  case 'uninstall':
801
822
  label = this.t('plugins.labels.uninstalling');
802
823
  break;
803
- case 'update':
804
- label = this.t('plugins.labels.updating');
824
+ case 'upgrade':
825
+ label = this.t('plugins.labels.upgrading');
805
826
  break;
806
- case 'rollback':
807
- label = this.t('plugins.labels.rollingBack');
827
+ case 'downgrade':
828
+ label = this.t('plugins.labels.downgrading');
808
829
  break;
809
830
  default:
810
831
  label = this.t('plugins.labels.installing');
@@ -874,7 +895,7 @@ export default {
874
895
 
875
896
  if (plugin.installed && !plugin.builtin && !plugin.installing) {
876
897
  statuses.push({
877
- icon: 'icon-confirmation-alt', color: 'success', tooltip: { key: 'generic.installed' }
898
+ icon: 'icon-confirmation-alt', color: 'success', tooltip: { text: `${ this.t('generic.installed') } (${ plugin.installedVersion })` }
878
899
  });
879
900
  }
880
901
 
@@ -958,7 +979,10 @@ export default {
958
979
  </div>
959
980
 
960
981
  <!-- extensions slide-in panel -->
961
- <PluginInfoPanel ref="infoPanel" />
982
+ <PluginInfoPanel
983
+ ref="infoPanel"
984
+ @action="handlePanelAction"
985
+ />
962
986
 
963
987
  <!-- extensions not enabled by feature flag -->
964
988
  <div v-if="!hasFeatureFlag">
@@ -1008,15 +1032,18 @@ export default {
1008
1032
  </Banner>
1009
1033
 
1010
1034
  <Tabbed
1035
+ v-if="!loading"
1011
1036
  ref="tabs"
1012
1037
  :tabs-only="true"
1013
1038
  data-testid="extension-tabs"
1014
- @changed="filterChanged"
1039
+ @changed="tabChanged"
1015
1040
  >
1016
1041
  <Tab
1042
+ v-if="installed.length"
1017
1043
  :name="TABS_VALUES.INSTALLED"
1018
1044
  data-testid="extension-tab-installed"
1019
1045
  label-key="plugins.tabs.installed"
1046
+ :badge="installed.length"
1020
1047
  :weight="20"
1021
1048
  />
1022
1049
  <Tab
@@ -1025,23 +1052,12 @@ export default {
1025
1052
  label-key="plugins.tabs.available"
1026
1053
  :weight="19"
1027
1054
  />
1028
- <Tab
1029
- :name="TABS_VALUES.UPDATES"
1030
- label-key="plugins.tabs.updates"
1031
- :weight="18"
1032
- :badge="updates.length"
1033
- />
1034
1055
  <Tab
1035
1056
  v-if="pluginDeveloper"
1036
1057
  :name="TABS_VALUES.BUILTIN"
1037
1058
  label-key="plugins.tabs.builtin"
1038
1059
  :weight="17"
1039
1060
  />
1040
- <Tab
1041
- :name="TABS_VALUES.ALL"
1042
- label-key="plugins.tabs.all"
1043
- :weight="16"
1044
- />
1045
1061
  </Tabbed>
1046
1062
  <div
1047
1063
  v-if="loading"
@@ -1079,8 +1095,8 @@ export default {
1079
1095
  :clickable="true"
1080
1096
  @card-click="showPluginDetail(card.plugin)"
1081
1097
  @uninstall="({event}) => showUninstallDialog(card.plugin, event)"
1082
- @update="({event}) => showInstallDialog(card.plugin, 'update', event)"
1083
- @rollback="({event}) => showInstallDialog(card.plugin, 'rollback', event)"
1098
+ @upgrade="({event}) => showInstallDialog(card.plugin, 'upgrade', event)"
1099
+ @downgrade="({event}) => showInstallDialog(card.plugin, 'downgrade', event)"
1084
1100
  @install="({event}) => showInstallDialog(card.plugin, 'install', event)"
1085
1101
  >
1086
1102
  <template #item-card-sub-header>
@@ -0,0 +1,194 @@
1
+ import { SteveWatchEventListenerManager } from '@shell/plugins/subscribe-events';
2
+ import { STEVE_WATCH_EVENT_TYPES } from '@shell/types/store/subscribe.types';
3
+
4
+ // Mock function to create a consistent key for testing
5
+ const mockKeyForSubscribe = jest.fn(({
6
+ params: {
7
+ type, name, id, selector, mode
8
+ }
9
+ }) => {
10
+ return `${ type }-${ name }-${ id }-${ selector }-${ mode }`;
11
+ });
12
+
13
+ // Mock parameters and callbacks
14
+ const mockParams1 = {
15
+ type: 'pods', name: 'my-pod', id: 'abc-123', selector: 'app=test'
16
+ };
17
+ const mockCallback1 = jest.fn();
18
+ const mockCallback2 = jest.fn();
19
+
20
+ // The class under test
21
+ let manager: SteveWatchEventListenerManager;
22
+
23
+ describe('steveWatchEventListenerManager', () => {
24
+ beforeEach(() => {
25
+ // Reset the manager and mocks before each test
26
+ manager = new SteveWatchEventListenerManager();
27
+ jest.clearAllMocks();
28
+ // Replace the internal keyForSubscribe with our mock
29
+ (manager as any).keyForSubscribe = mockKeyForSubscribe;
30
+ });
31
+
32
+ describe('initialization and Properties', () => {
33
+ it('should be created successfully', () => {
34
+ expect(manager).toBeInstanceOf(SteveWatchEventListenerManager);
35
+ });
36
+
37
+ it('should have a supportedEventTypes array with STEVE_WATCH_EVENT_TYPES.CHANGES', () => {
38
+ expect(manager.supportedEventTypes).toStrictEqual([STEVE_WATCH_EVENT_TYPES.CHANGES]);
39
+ });
40
+
41
+ it('should correctly identify a supported event type', () => {
42
+ const isSupported = manager.isSupportedEventType(STEVE_WATCH_EVENT_TYPES.CHANGES);
43
+
44
+ expect(isSupported).toBe(true);
45
+ });
46
+
47
+ it('should correctly identify an unsupported event type', () => {
48
+ const isSupported = manager.isSupportedEventType('some.other.event' as STEVE_WATCH_EVENT_TYPES);
49
+
50
+ expect(isSupported).toBe(false);
51
+ });
52
+ });
53
+
54
+ describe('watch Management', () => {
55
+ it('should return undefined when getting a non-existent watch', () => {
56
+ const watch = manager.getWatch({ params: mockParams1 });
57
+
58
+ expect(watch).toBeUndefined();
59
+ });
60
+
61
+ it('should create a watch when setStandardWatch is called with standardWatch true and no watch exists', () => {
62
+ manager.setStandardWatch({ standardWatch: true, args: { params: mockParams1 } });
63
+ const watch = (manager as any).watches[mockKeyForSubscribe({ params: mockParams1 })];
64
+
65
+ expect(watch).toBeDefined();
66
+ expect(watch.hasStandardWatch).toBe(true);
67
+ expect(watch.listeners).toStrictEqual([]);
68
+ });
69
+
70
+ it('should not create a watch when setStandardWatch is called with standardWatch false and no watch exists', () => {
71
+ manager.setStandardWatch({ standardWatch: false, args: { params: mockParams1 } });
72
+ const watch = (manager as any).watches[mockKeyForSubscribe({ params: mockParams1 })];
73
+
74
+ expect(watch).toBeUndefined();
75
+ });
76
+
77
+ it('should delete a watch when hasStandardWatch becomes false and there are no listeners', () => {
78
+ manager.setStandardWatch({ standardWatch: true, args: { params: mockParams1 } });
79
+ manager.setStandardWatch({ standardWatch: false, args: { params: mockParams1 } });
80
+ const watch = (manager as any).watches[mockKeyForSubscribe({ params: mockParams1 })];
81
+
82
+ expect(watch).toBeUndefined();
83
+ });
84
+ });
85
+
86
+ describe('listener and Callback Management', () => {
87
+ it('should add a new listener and a callback', () => {
88
+ const listener = manager.addEventListenerCallback({
89
+ callback: mockCallback1,
90
+ args: {
91
+ event: STEVE_WATCH_EVENT_TYPES.CHANGES,
92
+ params: mockParams1,
93
+ id: 'cb-1'
94
+ }
95
+ });
96
+
97
+ expect(listener).toBeDefined();
98
+ expect(listener.event).toBe(STEVE_WATCH_EVENT_TYPES.CHANGES);
99
+ expect(listener.callbacks['cb-1']).toBe(mockCallback1);
100
+ const watch = manager.getWatch({ params: mockParams1 });
101
+
102
+ expect(watch?.listeners.length).toBe(1);
103
+ });
104
+
105
+ it('should add a second callback to an existing listener', () => {
106
+ manager.addEventListenerCallback({
107
+ callback: mockCallback1,
108
+ args: {
109
+ event: STEVE_WATCH_EVENT_TYPES.CHANGES,
110
+ params: mockParams1,
111
+ id: 'cb-1'
112
+ }
113
+ });
114
+ manager.addEventListenerCallback({
115
+ callback: mockCallback2,
116
+ args: {
117
+ event: STEVE_WATCH_EVENT_TYPES.CHANGES,
118
+ params: mockParams1,
119
+ id: 'cb-2'
120
+ }
121
+ });
122
+
123
+ const listener = manager.getEventListener({ args: { event: STEVE_WATCH_EVENT_TYPES.CHANGES, params: mockParams1 } });
124
+
125
+ expect(Object.keys(listener?.callbacks || {})).toHaveLength(2);
126
+ expect(listener?.callbacks['cb-2']).toBe(mockCallback2);
127
+ });
128
+
129
+ it('should trigger a specific event listener and its callbacks', () => {
130
+ manager.addEventListenerCallback({
131
+ callback: mockCallback1,
132
+ args: {
133
+ event: STEVE_WATCH_EVENT_TYPES.CHANGES,
134
+ params: mockParams1,
135
+ id: 'cb-1'
136
+ }
137
+ });
138
+ manager.addEventListenerCallback({
139
+ callback: mockCallback2,
140
+ args: {
141
+ event: STEVE_WATCH_EVENT_TYPES.CHANGES,
142
+ params: mockParams1,
143
+ id: 'cb-2'
144
+ }
145
+ });
146
+
147
+ manager.triggerEventListener({ event: STEVE_WATCH_EVENT_TYPES.CHANGES, params: mockParams1 });
148
+ expect(mockCallback1).toHaveBeenCalledTimes(1);
149
+ expect(mockCallback2).toHaveBeenCalledTimes(1);
150
+ });
151
+
152
+ it('should remove a specific callback from a listener', () => {
153
+ manager.addEventListenerCallback({
154
+ callback: mockCallback1,
155
+ args: {
156
+ event: STEVE_WATCH_EVENT_TYPES.CHANGES,
157
+ params: mockParams1,
158
+ id: 'cb-1'
159
+ }
160
+ });
161
+ manager.removeEventListenerCallback({
162
+ event: STEVE_WATCH_EVENT_TYPES.CHANGES,
163
+ params: mockParams1,
164
+ id: 'cb-1'
165
+ });
166
+ const listener = manager.getEventListener({ args: { event: STEVE_WATCH_EVENT_TYPES.CHANGES, params: mockParams1 } });
167
+
168
+ expect(listener?.callbacks['cb-1']).toBeUndefined();
169
+ });
170
+
171
+ it('should trigger all callbacks for a given watch', () => {
172
+ manager.addEventListenerCallback({
173
+ callback: mockCallback1,
174
+ args: {
175
+ event: STEVE_WATCH_EVENT_TYPES.CHANGES,
176
+ params: mockParams1,
177
+ id: 'cb-1'
178
+ }
179
+ });
180
+ manager.addEventListenerCallback({
181
+ callback: mockCallback2,
182
+ args: {
183
+ event: 'another.event' as STEVE_WATCH_EVENT_TYPES,
184
+ params: mockParams1,
185
+ id: 'cb-2'
186
+ }
187
+ });
188
+
189
+ manager.triggerAllEventListeners({ params: mockParams1 });
190
+ expect(mockCallback1).toHaveBeenCalledTimes(1);
191
+ expect(mockCallback2).toHaveBeenCalledTimes(1);
192
+ });
193
+ });
194
+ });
@@ -220,6 +220,9 @@ export default {
220
220
  )
221
221
  ) {
222
222
  if (opt.watch !== false ) {
223
+ // Note - Empty revision here seems broken
224
+ // - list page (watch all) --> detail page (stop watch all, watch one) --> list page (watch all - no revision)
225
+ // - the missing revision means watch start from now... instead of the point the clusters were last monitored (cache contains stale data)
223
226
  const args = {
224
227
  type,
225
228
  revision: '',
@@ -526,7 +526,7 @@ export default {
526
526
  const store = state.config.namespace;
527
527
  const resource = id || context ? { id, context } : null;
528
528
 
529
- return paginationUtils.isEnabled({ rootGetters }, { store, resource });
529
+ return paginationUtils.isEnabled({ rootGetters, $plugin: rootState.$plugin }, { store, resource });
530
530
  },
531
531
 
532
532
  /**
@@ -62,7 +62,7 @@ const DEFAULT_COLOR = 'warning';
62
62
  const DEFAULT_ICON = 'x';
63
63
 
64
64
  const DEFAULT_WAIT_INTERVAL = 1000;
65
- const DEFAULT_WAIT_TMIMEOUT = 30000;
65
+ const DEFAULT_WAIT_TIMEOUT = 30000;
66
66
 
67
67
  export const STATES_ENUM = {
68
68
  IN_USE: 'in-use',
@@ -806,7 +806,7 @@ export default class Resource {
806
806
  // ------------------------------------------------------------------
807
807
 
808
808
  waitForTestFn(fn, msg, timeoutMs, intervalMs) {
809
- return waitFor(() => fn.apply(this), msg, timeoutMs || DEFAULT_WAIT_TMIMEOUT, intervalMs || DEFAULT_WAIT_INTERVAL, true);
809
+ return waitFor(() => fn.apply(this), msg, timeoutMs || DEFAULT_WAIT_TIMEOUT, intervalMs || DEFAULT_WAIT_INTERVAL, true);
810
810
  }
811
811
 
812
812
  waitForState(state, timeout, interval) {
@@ -855,7 +855,7 @@ export default class Resource {
855
855
  return (entry.status || '').toLowerCase() === `${ withStatus }`.toLowerCase();
856
856
  }
857
857
 
858
- waitForCondition(name, withStatus = 'True', timeoutMs = DEFAULT_WAIT_TMIMEOUT, intervalMs = DEFAULT_WAIT_INTERVAL) {
858
+ waitForCondition(name, withStatus = 'True', timeoutMs = DEFAULT_WAIT_TIMEOUT, intervalMs = DEFAULT_WAIT_INTERVAL) {
859
859
  return this.waitForTestFn(() => {
860
860
  return this.isCondition(name, withStatus);
861
861
  }, `condition ${ name }=${ withStatus }`, timeoutMs, intervalMs);