@rancher/shell 3.0.8-rc.7 → 3.0.8-rc.8

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 (34) hide show
  1. package/components/Drawer/Chrome.vue +2 -6
  2. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +3 -9
  3. package/components/Drawer/ResourceDetailDrawer/YamlTab.vue +3 -8
  4. package/components/Drawer/ResourceDetailDrawer/composables.ts +3 -4
  5. package/components/Drawer/ResourceDetailDrawer/index.vue +3 -9
  6. package/components/Drawer/ResourceDetailDrawer/types.ts +16 -0
  7. package/components/Drawer/types.ts +3 -0
  8. package/components/PaginatedResourceTable.vue +2 -6
  9. package/components/Resource/Detail/Metadata/composables.ts +9 -9
  10. package/components/Resource/Detail/TitleBar/composables.ts +2 -1
  11. package/components/Resource/Detail/composables.ts +12 -0
  12. package/components/nav/Header.vue +1 -2
  13. package/components/nav/TopLevelMenu.helper.ts +16 -6
  14. package/machine-config/components/EC2Networking.vue +5 -2
  15. package/machine-config/components/__tests__/EC2Networking.test.ts +24 -0
  16. package/mixins/__tests__/chart.test.ts +21 -0
  17. package/mixins/chart.js +7 -1
  18. package/package.json +1 -1
  19. package/pages/home.vue +5 -2
  20. package/pkg/dynamic-importer.lib.js +4 -0
  21. package/plugins/dashboard-store/resource-class.js +1 -2
  22. package/plugins/steve/subscribe.js +17 -9
  23. package/plugins/subscribe-events.ts +4 -2
  24. package/store/index.js +32 -13
  25. package/store/type-map.js +3 -3
  26. package/types/shell/index.d.ts +1 -0
  27. package/types/store/subscribe-events.types.ts +8 -1
  28. package/types/store/subscribe.types.ts +1 -0
  29. package/utils/__tests__/version.test.ts +19 -1
  30. package/utils/back-off.ts +3 -3
  31. package/utils/dynamic-content/__tests__/info.test.ts +15 -9
  32. package/utils/dynamic-content/info.ts +1 -2
  33. package/utils/pagination-wrapper.ts +12 -8
  34. package/utils/version.js +15 -0
@@ -1,13 +1,9 @@
1
- <script lang="ts">
1
+ <script setup lang="ts">
2
2
  import { useI18n } from '@shell/composables/useI18n';
3
3
  import { useStore } from 'vuex';
4
4
  import { computed } from 'vue';
5
- export interface Props {
6
- ariaTarget: string;
7
- }
8
- </script>
5
+ import { Props } from './types';
9
6
 
10
- <script setup lang="ts">
11
7
  const props = defineProps<Props>();
12
8
  const emit = defineEmits(['close']);
13
9
 
@@ -1,17 +1,11 @@
1
- <script lang="ts">
1
+ <script setup lang="ts">
2
2
  import { useI18n } from '@shell/composables/useI18n';
3
3
  import { _VIEW } from '@shell/config/query-params';
4
4
  import { useStore } from 'vuex';
5
5
  import Tab from '@shell/components/Tabbed/Tab.vue';
6
+ import { ConfigProps } from '@shell/components/Drawer/ResourceDetailDrawer/types';
6
7
 
7
- export interface Props {
8
- resource: any;
9
- component: any;
10
- resourceType: string;
11
- }
12
- </script>
13
- <script setup lang="ts">
14
- const props = defineProps<Props>();
8
+ const props = defineProps<ConfigProps>();
15
9
  const store = useStore();
16
10
  const i18n = useI18n(store);
17
11
  </script>
@@ -1,18 +1,13 @@
1
- <script lang="ts">
1
+ <script setup lang="ts">
2
2
  import { useI18n } from '@shell/composables/useI18n';
3
3
  import { _VIEW } from '@shell/config/query-params';
4
4
  import { useStore } from 'vuex';
5
5
  import Tab from '@shell/components/Tabbed/Tab.vue';
6
6
  import { useTemplateRef } from 'vue';
7
7
  import ResourceYaml from '@shell/components/ResourceYaml.vue';
8
+ import { YamlProps } from '@shell/components/Drawer/ResourceDetailDrawer/types';
8
9
 
9
- export interface Props {
10
- resource: any;
11
- yaml: string;
12
- }
13
- </script>
14
- <script setup lang="ts">
15
- const props = defineProps<Props>();
10
+ const props = defineProps<YamlProps>();
16
11
  const store = useStore();
17
12
  const i18n = useI18n(store);
18
13
  const yamlComponent: any = useTemplateRef('yaml');
@@ -1,9 +1,8 @@
1
- import { Props as YamlTabProps } from '@shell/components/Drawer/ResourceDetailDrawer/YamlTab.vue';
2
- import { Props as ConfigTabProps } from '@shell/components/Drawer/ResourceDetailDrawer/ConfigTab.vue';
3
1
  import { useStore } from 'vuex';
4
2
  import { getYaml } from '@shell/components/Drawer/ResourceDetailDrawer/helpers';
3
+ import { ConfigProps, YamlProps } from '@shell/components/Drawer/ResourceDetailDrawer/types';
5
4
 
6
- export async function useDefaultYamlTabProps(resource: any): Promise<YamlTabProps> {
5
+ export async function useDefaultYamlTabProps(resource: any): Promise<YamlProps> {
7
6
  const yaml = await getYaml(resource);
8
7
 
9
8
  return {
@@ -12,7 +11,7 @@ export async function useDefaultYamlTabProps(resource: any): Promise<YamlTabProp
12
11
  };
13
12
  }
14
13
 
15
- export function useDefaultConfigTabProps(resource: any): ConfigTabProps | undefined {
14
+ export function useDefaultConfigTabProps(resource: any): ConfigProps | undefined {
16
15
  const store = useStore();
17
16
 
18
17
  // You don't want to show the Config tab if there isn't a an edit page to show and you don't want to show it if there isn't
@@ -1,4 +1,4 @@
1
- <script lang="ts">
1
+ <script setup lang="ts">
2
2
  import Drawer from '@shell/components/Drawer/Chrome.vue';
3
3
  import { useI18n } from '@shell/composables/useI18n';
4
4
  import { useStore } from 'vuex';
@@ -9,17 +9,11 @@ import ConfigTab from '@shell/components/Drawer/ResourceDetailDrawer/ConfigTab.v
9
9
  import { computed, ref } from 'vue';
10
10
  import RcButton from '@components/RcButton/RcButton.vue';
11
11
  import StateDot from '@shell/components/StateDot/index.vue';
12
+ import { ResourceDetailDrawerProps } from '@shell/components/Drawer/ResourceDetailDrawer/types';
12
13
 
13
- export interface Props {
14
- resource: any;
15
-
16
- onClose?: () => void;
17
- }
18
- </script>
19
- <script setup lang="ts">
20
14
  const editBttnDataTestId = 'save-configuration-bttn';
21
15
  const componentTestid = 'configuration-drawer-tabbed';
22
- const props = defineProps<Props>();
16
+ const props = defineProps<ResourceDetailDrawerProps>();
23
17
  const emit = defineEmits(['close']);
24
18
  const store = useStore();
25
19
  const i18n = useI18n(store);
@@ -0,0 +1,16 @@
1
+ export interface YamlProps {
2
+ resource: any;
3
+ yaml: string;
4
+ }
5
+
6
+ export interface ConfigProps {
7
+ resource: any;
8
+ component: any;
9
+ resourceType: string;
10
+ }
11
+
12
+ export interface ResourceDetailDrawerProps {
13
+ resource: any;
14
+
15
+ onClose?: () => void;
16
+ }
@@ -0,0 +1,3 @@
1
+ export interface Props {
2
+ ariaTarget: string;
3
+ }
@@ -116,15 +116,11 @@ export default defineComponent({
116
116
  },
117
117
 
118
118
  async fetch() {
119
- const promises = [
120
- this.$fetchType(this.resource, [], this.overrideInStore || this.inStore),
121
- ];
122
-
123
119
  if (this.fetchSecondaryResources) {
124
- promises.push(this.fetchSecondaryResources({ canPaginate: this.canPaginate }));
120
+ await this.fetchSecondaryResources({ canPaginate: this.canPaginate });
125
121
  }
126
122
 
127
- await Promise.all(promises);
123
+ await this.$fetchType(this.resource, [], this.overrideInStore || this.inStore);
128
124
  },
129
125
 
130
126
  computed: {
@@ -6,18 +6,19 @@ import { computed, toValue, Ref } from 'vue';
6
6
  import {
7
7
  useLiveDate, useNamespace, useProject, useResourceDetails, useWorkspace
8
8
  } from '@shell/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields';
9
+ import { useOnShowConfiguration } from '@shell/components/Resource/Detail/composables';
9
10
 
10
11
  export const useBasicMetadata = (resource: any) => {
11
12
  const labels = useDefaultLabels(resource);
12
13
  const annotations = useDefaultAnnotations(resource);
13
- const resourceValue = toValue(resource);
14
+ const onShowConfiguration = useOnShowConfiguration(resource);
14
15
 
15
16
  return computed(() => {
16
17
  return {
17
- resource: toValue(resource),
18
- labels: labels.value,
19
- annotations: annotations.value,
20
- onShowConfiguration: () => resourceValue.showConfiguration()
18
+ resource: toValue(resource),
19
+ labels: labels.value,
20
+ annotations: annotations.value,
21
+ onShowConfiguration
21
22
  };
22
23
  });
23
24
  };
@@ -28,7 +29,7 @@ export const useDefaultMetadataProps = (resource: any, additionalIdentifyingInfo
28
29
 
29
30
  const identifyingInformation = computed(() => [...defaultIdentifyingInformation.value, ...(additionalIdentifyingInformationValue || [])]);
30
31
  const basicMetaData = useBasicMetadata(resource);
31
- const resourceValue = toValue(resource);
32
+ const onShowConfiguration = useOnShowConfiguration(resource);
32
33
 
33
34
  return computed(() => {
34
35
  return {
@@ -36,7 +37,7 @@ export const useDefaultMetadataProps = (resource: any, additionalIdentifyingInfo
36
37
  identifyingInformation: identifyingInformation.value,
37
38
  labels: basicMetaData.value.labels,
38
39
  annotations: basicMetaData.value.annotations,
39
- onShowConfiguration: (returnFocusSelector: string) => resourceValue.showConfiguration(returnFocusSelector)
40
+ onShowConfiguration
40
41
  };
41
42
  });
42
43
  };
@@ -47,7 +48,6 @@ export const useDefaultMetadataForLegacyPagesProps = (resource: any) => {
47
48
  const workspace = useWorkspace(resource);
48
49
  const namespace = useNamespace(resource);
49
50
  const liveDate = useLiveDate(resource);
50
- const resourceValue = toValue(resource);
51
51
 
52
52
  const identifyingInformation = computed((): IdentifyingInformationRow[] => {
53
53
  const defaultInfo = [
@@ -71,7 +71,7 @@ export const useDefaultMetadataForLegacyPagesProps = (resource: any) => {
71
71
  identifyingInformation: identifyingInformation.value,
72
72
  labels: basicMetaData.value.labels,
73
73
  annotations: basicMetaData.value.annotations,
74
- onShowConfiguration: (returnFocusSelector?: string) => resourceValue.showConfiguration(returnFocusSelector)
74
+ onShowConfiguration: basicMetaData.value.onShowConfiguration
75
75
  };
76
76
  });
77
77
  };
@@ -1,3 +1,4 @@
1
+ import { useOnShowConfiguration } from '@shell/components/Resource/Detail/composables';
1
2
  import { TitleBarProps } from '@shell/components/Resource/Detail/TitleBar/index.vue';
2
3
  import { computed, Ref, toValue } from 'vue';
3
4
  import { useRoute } from 'vue-router';
@@ -23,7 +24,7 @@ export const useDefaultTitleBarProps = (resource: any, resourceSubtype?: Ref<str
23
24
  resource: resourceValue.type
24
25
  }
25
26
  };
26
- const onShowConfiguration = resourceValue.disableResourceDetailDrawer ? undefined : (returnFocusSelector: string) => resourceValue.showConfiguration(returnFocusSelector);
27
+ const onShowConfiguration = resourceValue.disableResourceDetailDrawer ? undefined : useOnShowConfiguration(resource);
27
28
 
28
29
  return {
29
30
  resource: resourceValue,
@@ -2,6 +2,7 @@ import { computed, Ref, toValue } from 'vue';
2
2
  import { useStore } from 'vuex';
3
3
  import { Props as BannerProps } from '@components/Banner/Banner.vue';
4
4
  import { useI18n } from '@shell/composables/useI18n';
5
+ import ResourceClass from '@shell/plugins/dashboard-store/resource-class';
5
6
 
6
7
  export const useResourceDetailBannerProps = (resource: any): Ref<BannerProps | undefined> => {
7
8
  const store = useStore();
@@ -43,3 +44,14 @@ export const useResourceDetailBannerProps = (resource: any): Ref<BannerProps | u
43
44
  return undefined;
44
45
  });
45
46
  };
47
+
48
+ export const useOnShowConfiguration = (resource: any) => {
49
+ return (returnFocusSelector?: string) => {
50
+ const resourceValue = toValue(resource);
51
+ // Because extensions can make a copy of the resource-class it's possible that an extension will have a resource-class which predates the inclusion of showConfiguration
52
+ // to still the rest of shell to consume
53
+ const showConfiguration = resourceValue.showConfiguration ? resourceValue.showConfiguration.bind(resourceValue) : ResourceClass.prototype.showConfiguration.bind(resourceValue);
54
+
55
+ showConfiguration(returnFocusSelector);
56
+ };
57
+ };
@@ -97,7 +97,6 @@ export default {
97
97
  'isSingleProduct',
98
98
  'isRancherInHarvester',
99
99
  'showTopLevelMenu',
100
- 'isMultiCluster',
101
100
  'showWorkspaceSwitcher'
102
101
  ]),
103
102
 
@@ -424,7 +423,7 @@ export default {
424
423
  data-testid="header"
425
424
  >
426
425
  <div>
427
- <TopLevelMenu v-if="isRancherInHarvester || isMultiCluster || !isSingleProduct" />
426
+ <TopLevelMenu v-if="showTopLevelMenu" />
428
427
  </div>
429
428
 
430
429
  <div
@@ -28,6 +28,7 @@ interface UpdateArgs {
28
28
  searchTerm: string,
29
29
  pinnedIds: string[],
30
30
  unPinnedMax?: number,
31
+ forceWatch?: boolean
31
32
  }
32
33
 
33
34
  type MgmtCluster = {
@@ -192,9 +193,12 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
192
193
  this.clustersOthersWrapper = new PaginationWrapper({
193
194
  $store,
194
195
  id: 'tlm-unpinned-clusters',
195
- onChange: async() => {
196
+ onChange: async({ forceWatch }) => {
196
197
  if (this.args) {
197
- await this.update(this.args);
198
+ await this.update({
199
+ ...this.args,
200
+ forceWatch
201
+ });
198
202
  }
199
203
  },
200
204
  enabledFor: {
@@ -210,9 +214,12 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
210
214
  this.provClusterWrapper = new PaginationWrapper({
211
215
  $store,
212
216
  id: 'tlm-prov-clusters',
213
- onChange: async() => {
217
+ onChange: async({ forceWatch }) => {
214
218
  if (this.args) {
215
- await this.update(this.args);
219
+ await this.update({
220
+ ...this.args,
221
+ forceWatch
222
+ });
216
223
  }
217
224
  },
218
225
  enabledFor: {
@@ -244,7 +251,7 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
244
251
  pinned: MgmtCluster[],
245
252
  notPinned: MgmtCluster[]
246
253
  } = await allHash(promises) as any;
247
- const provClusters = await this.updateProvCluster(res.notPinned, res.pinned);
254
+ const provClusters = await this.updateProvCluster(res.notPinned, res.pinned, args.forceWatch || false);
248
255
  const provClustersByMgmtId = provClusters.reduce((res: { [mgmtId: string]: ProvCluster}, provCluster: ProvCluster) => {
249
256
  if (provCluster.mgmtClusterId) {
250
257
  res[provCluster.mgmtClusterId] = provCluster;
@@ -340,6 +347,7 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
340
347
  }
341
348
 
342
349
  return this.clustersPinnedWrapper.request({
350
+ forceWatch: args.forceWatch,
343
351
  pagination: {
344
352
  filters: this.constructParams({
345
353
  pinnedIds: args.pinnedIds,
@@ -357,6 +365,7 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
357
365
  */
358
366
  private async updateOthers(args: UpdateArgs): Promise<MgmtCluster[]> {
359
367
  return this.clustersOthersWrapper.request({
368
+ forceWatch: args.forceWatch,
360
369
  pagination: {
361
370
  filters: this.constructParams({
362
371
  searchTerm: args.searchTerm,
@@ -375,8 +384,9 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
375
384
  /**
376
385
  * Find all provisioning clusters associated with the displayed mgmt clusters
377
386
  */
378
- private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[]): Promise<ProvCluster[]> {
387
+ private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[], forceWatch: boolean): Promise<ProvCluster[]> {
379
388
  return this.provClusterWrapper.request({
389
+ forceWatch,
380
390
  pagination: {
381
391
  filters: [
382
392
  PaginationParamFilter.createMultipleFields(
@@ -157,8 +157,11 @@ export default {
157
157
  this.$emit('update:hasIpv6', neu);
158
158
  },
159
159
 
160
- allValid(neu) {
161
- this.$emit('validationChanged', neu);
160
+ allValid: {
161
+ handler(neu) {
162
+ this.$emit('validationChanged', neu);
163
+ },
164
+ immediate: true
162
165
  }
163
166
  },
164
167
 
@@ -121,4 +121,28 @@ describe('component: EC2Networking', () => {
121
121
  expect(wrapper.vm.enableIpv6).toBe(false);
122
122
  expect(ipv6AddressCountInput.exists()).toBe(false);
123
123
  });
124
+
125
+ it('should emit a validationChanged: false event when created with ipv6 enabled while some other pools have ipv6 disabled', async() => {
126
+ const wrapper = shallowMount(EC2Networking, {
127
+ ...defaultCreateSetup,
128
+ propsData: {
129
+ ...defaultCreateSetup.propsData,
130
+ machinePools: [{ hasIpv6: true }, { hasIpv6: false }],
131
+ },
132
+ });
133
+
134
+ expect(wrapper.emitted('validationChanged')?.[0][0]).toBe(false);
135
+ });
136
+
137
+ it('should emit a validationChanged: true event when created with ipv6 enabled while all other pools also have ipv6 enabled', async() => {
138
+ const wrapper = shallowMount(EC2Networking, {
139
+ ...defaultCreateSetup,
140
+ propsData: {
141
+ ...defaultCreateSetup.propsData,
142
+ machinePools: [{ hasIpv6: true }, { hasIpv6: true }],
143
+ },
144
+ });
145
+
146
+ expect(wrapper.emitted('validationChanged')?.[0][0]).toBe(true);
147
+ });
124
148
  });
@@ -238,5 +238,26 @@ describe('chartMixin', () => {
238
238
  icon: 'icon-downgrade-alt',
239
239
  });
240
240
  });
241
+
242
+ it('should return "upgrade" action when upgrading from a pre-release to a stable version', () => {
243
+ const wrapper = mount(DummyComponent, {
244
+ data: () => ({
245
+ existing: { spec: { chart: { metadata: { version: '1.0.0-rc1' } } } },
246
+ version: { version: '1.0.0' }
247
+ }),
248
+ global: {
249
+ mocks: {
250
+ $store: mockStore,
251
+ $route: { query: {} }
252
+ }
253
+ }
254
+ });
255
+
256
+ expect(wrapper.vm.action).toStrictEqual({
257
+ name: 'upgrade',
258
+ tKey: 'upgrade',
259
+ icon: 'icon-upgrade-alt',
260
+ });
261
+ });
241
262
  });
242
263
  });
package/mixins/chart.js CHANGED
@@ -10,7 +10,7 @@ import { NAME as MANAGER } from '@shell/config/product/manager';
10
10
  import { OPA_GATE_KEEPER_ID } from '@shell/pages/c/_cluster/gatekeeper/index.vue';
11
11
  import { formatSi, parseSi } from '@shell/utils/units';
12
12
  import { CAPI, CATALOG } from '@shell/config/types';
13
- import { isPrerelease, compare } from '@shell/utils/version';
13
+ import { isPrerelease, compare, isUpgradeFromPreToStable } from '@shell/utils/version';
14
14
  import difference from 'lodash/difference';
15
15
  import { LINUX, APP_UPGRADE_STATUS } from '@shell/store/catalog';
16
16
  import { clone } from '@shell/utils/object';
@@ -240,6 +240,12 @@ export default {
240
240
  };
241
241
  }
242
242
 
243
+ if (isUpgradeFromPreToStable(this.currentVersion, this.targetVersion)) {
244
+ return {
245
+ name: 'upgrade', tKey: 'upgrade', icon: 'icon-upgrade-alt'
246
+ };
247
+ }
248
+
243
249
  if (compare(this.currentVersion, this.targetVersion) < 0) {
244
250
  return {
245
251
  name: 'upgrade', tKey: 'upgrade', icon: 'icon-upgrade-alt'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "3.0.8-rc.7",
3
+ "version": "3.0.8-rc.8",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
package/pages/home.vue CHANGED
@@ -302,8 +302,11 @@ export default defineComponent({
302
302
  return Promise.resolve({});
303
303
  }
304
304
 
305
+ const promises = [];
306
+
305
307
  if ( this.canViewMgmtClusters ) {
306
- this.$store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER });
308
+ // This is the only one we need to block on (needed for the initial sort on mgmt name)
309
+ promises.push(this.$store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }));
307
310
  }
308
311
 
309
312
  if ( this.canViewMachine ) {
@@ -323,7 +326,7 @@ export default defineComponent({
323
326
  this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE_TEMPLATE });
324
327
  }
325
328
 
326
- return Promise.resolve({});
329
+ return Promise.all(promises);
327
330
  },
328
331
 
329
332
  async fetchPageSecondaryResources({
@@ -25,6 +25,10 @@ export function importDetail(name) {
25
25
  return () => undefined;
26
26
  }
27
27
 
28
+ export function importDrawer(name) {
29
+ return () => undefined;
30
+ }
31
+
28
32
  export function importEdit(name) {
29
33
  return () => undefined;
30
34
  }
@@ -39,7 +39,6 @@ import { handleConflict } from '@shell/plugins/dashboard-store/normalize';
39
39
  import { ExtensionPoint, ActionLocation } from '@shell/core/types';
40
40
  import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
41
41
  import { parse } from '@shell/utils/selector';
42
- import { importDrawer } from '@shell/utils/dynamic-importer';
43
42
 
44
43
  export const DNS_LIKE_TYPES = ['dnsLabel', 'dnsLabelRestricted', 'hostname'];
45
44
 
@@ -910,7 +909,7 @@ export default class Resource {
910
909
  const onClose = () => this.$ctx.commit('slideInPanel/close', undefined, { root: true });
911
910
 
912
911
  this.$ctx.commit('slideInPanel/open', {
913
- component: importDrawer('ResourceDetailDrawer'),
912
+ component: require(`@shell/components/Drawer/ResourceDetailDrawer/index.vue`).default,
914
913
  componentProps: {
915
914
  resource: this,
916
915
  onClose,
@@ -888,16 +888,20 @@ const defaultActions = {
888
888
  }
889
889
  });
890
890
  }
891
-
892
891
  // Should any listeners be notified of this request for them to kick off their own event handling?
893
- getters.listenerManager.triggerEventListener({ event: STEVE_WATCH_MODE.RESOURCE_CHANGES, params });
892
+ getters.listenerManager.triggerEventListener({
893
+ event: STEVE_WATCH_MODE.RESOURCE_CHANGES,
894
+ params: {
895
+ ...params,
896
+ forceWatch: opt.forceWatch
897
+ }
898
+ });
894
899
  } else {
895
900
  have = getters['all'](resourceType).slice();
896
901
 
897
902
  if ( namespace ) {
898
903
  have = have.filter((x) => x.metadata?.namespace === namespace);
899
904
  }
900
-
901
905
  want = await dispatch('findAll', {
902
906
  type: resourceType,
903
907
  watchNamespace: namespace,
@@ -1181,12 +1185,16 @@ const defaultActions = {
1181
1185
  });
1182
1186
 
1183
1187
  if (hasEventListeners) {
1184
- // If there's event listeners always kick them off
1185
- // - The re-watch associated with normal watches will watch from a revision from it's own cache
1186
- // - The revision in that cache might be ahead of the state the listeners have, so the watch won't ping something for the listeners to trigger on
1187
- // - so to work around this whenever we start the watches again trigger off the changes for it
1188
- // Improvement - we only do one event here (currently the only one supported), could expand to others
1189
- getters.listenerManager.triggerEventListener({ event: STEVE_WATCH_EVENT_TYPES.CHANGES, params: obj });
1188
+ const inError = getters.inError(obj); // We don't want to force listeners to resync if the socket is in error (handled by resource.error mechanism)
1189
+
1190
+ if (!inError) {
1191
+ // If there's event listeners kick them off
1192
+ // - The re-watch associated with normal watches will watch from a revision from it's own cache
1193
+ // - The revision in that cache might be ahead of the state the listeners have, so the watch won't ping something for the listeners to trigger on
1194
+ // - so to work around this whenever we start the watches again trigger off the changes for it
1195
+ // Improvement - we only do one event here (currently the only one supported), could expand to others
1196
+ getters.listenerManager.triggerEventListener({ event: STEVE_WATCH_EVENT_TYPES.CHANGES, params: obj });
1197
+ }
1190
1198
  }
1191
1199
  }
1192
1200
  },
@@ -167,7 +167,7 @@ export class SteveWatchEventListenerManager {
167
167
 
168
168
  if (eventWatcher) {
169
169
  Object.values(eventWatcher.callbacks).forEach((cb) => {
170
- cb();
170
+ cb({ forceWatch: params.forceWatch }); // eslint-disable-line node/no-callback-literal
171
171
  });
172
172
  }
173
173
  }
@@ -176,7 +176,9 @@ export class SteveWatchEventListenerManager {
176
176
  const watch = this.getWatch({ params });
177
177
 
178
178
  watch.listeners.forEach((l) => {
179
- Object.values(l.callbacks || {}).forEach((cb) => cb());
179
+ Object.values(l.callbacks || {}).forEach((cb) => {
180
+ cb({ forceWatch: params.forceWatch });// eslint-disable-line node/no-callback-literal
181
+ });
180
182
  });
181
183
  }
182
184
 
package/store/index.js CHANGED
@@ -263,7 +263,7 @@ export const state = () => {
263
263
  $route: markRaw({}),
264
264
  $plugin: markRaw({}),
265
265
  showWorkspaceSwitcher: true,
266
-
266
+ localCluster: null,
267
267
  };
268
268
  };
269
269
 
@@ -272,10 +272,20 @@ export const getters = {
272
272
  return state.clusterReady === true;
273
273
  },
274
274
 
275
+ /**
276
+ * Cache of the mgmt cluster fetched at start up
277
+ *
278
+ * We cannot rely on the store to cache this as the store may contain a page without the local cluster
279
+ */
280
+ localCluster(state) {
281
+ return state.localCluster;
282
+ },
283
+
275
284
  isMultiCluster(state, getters) {
276
- const clusters = getters['management/all'](MANAGEMENT.CLUSTER);
285
+ const clusterCount = getters['management/all'](COUNT)?.[0]?.counts?.[MANAGEMENT.CLUSTER]?.summary?.count || 0;
286
+ const localCluster = getters['localCluster'];
277
287
 
278
- if (clusters.length === 1 && clusters[0].metadata?.name === 'local') {
288
+ if (clusterCount === 1 && !!localCluster) {
279
289
  return false;
280
290
  } else {
281
291
  return true;
@@ -592,10 +602,9 @@ export const getters = {
592
602
  },
593
603
 
594
604
  isStandaloneHarvester(state, getters) {
595
- const clusters = getters['management/all'](MANAGEMENT.CLUSTER);
596
- const cluster = clusters.find((c) => c.id === 'local') || {};
605
+ const localCluster = getters['localCluster'];
597
606
 
598
- return getters['isSingleProduct'] && cluster.isHarvester && !getters['isRancherInHarvester'];
607
+ return getters['isSingleProduct'] && localCluster?.isHarvester && !getters['isRancherInHarvester'];
599
608
  },
600
609
 
601
610
  showTopLevelMenu(getters) {
@@ -640,9 +649,10 @@ export const mutations = {
640
649
  clearPageActionHandler(state) {
641
650
  state.pageActionHandler = null;
642
651
  },
643
- managementChanged(state, { ready, isRancher }) {
652
+ managementChanged(state, { ready, isRancher, localCluster }) {
644
653
  state.managementReady = ready;
645
654
  state.isRancher = isRancher;
655
+ state.localCluster = localCluster;
646
656
  },
647
657
  clusterReady(state, ready) {
648
658
  state.clusterReady = ready;
@@ -846,11 +856,21 @@ export const actions = {
846
856
 
847
857
  res = await allHash(promises);
848
858
 
859
+ let localCluster = null;
860
+
849
861
  if (!res[MANAGEMENT.SETTING] || !paginateClusters({ rootGetters, state })) {
850
862
  // This introduces a synchronous request, however we need settings to determine if SSP is enabled
851
- // Eventually it will be removed when SSP is always on
852
- res[MANAGEMENT.CLUSTER] = await dispatch('management/findAll', { type: MANAGEMENT.CLUSTER, opt: { watch: false } });
863
+ await dispatch('management/findAll', { type: MANAGEMENT.CLUSTER, opt: { watch: false } });
853
864
  toWatch.push(MANAGEMENT.CLUSTER);
865
+
866
+ localCluster = getters['management/byId'](MANAGEMENT.CLUSTER, 'local');
867
+ } else {
868
+ try {
869
+ localCluster = await dispatch('management/find', {
870
+ type: MANAGEMENT.CLUSTER, id: 'local', opt: { watch: false }
871
+ });
872
+ } catch (e) { // we don't care about errors, specifically 404s
873
+ }
854
874
  }
855
875
 
856
876
  // See comment above. Now that we have feature flags we can watch resources
@@ -858,11 +878,7 @@ export const actions = {
858
878
  dispatch('management/watch', { type });
859
879
  });
860
880
 
861
- const isMultiCluster = getters['isMultiCluster'];
862
-
863
881
  // If the local cluster is a Harvester cluster and 'rancher-manager-support' is true, it means that the embedded Rancher is being used.
864
- const localCluster = res[MANAGEMENT.CLUSTER]?.find((c) => c.id === 'local');
865
-
866
882
  if (localCluster?.isHarvester) {
867
883
  const harvesterSetting = await dispatch('cluster/findAll', { type: HCI.SETTING, opt: { url: `/v1/harvester/${ HCI.SETTING }s` } });
868
884
  const rancherManagerSupport = harvesterSetting.find((setting) => setting.id === 'rancher-manager-support');
@@ -904,6 +920,7 @@ export const actions = {
904
920
  commit('managementChanged', {
905
921
  ready: true,
906
922
  isRancher,
923
+ localCluster
907
924
  });
908
925
 
909
926
  if ( res[FLEET.WORKSPACE] ) {
@@ -914,6 +931,8 @@ export const actions = {
914
931
  });
915
932
  }
916
933
 
934
+ const isMultiCluster = getters['isMultiCluster'];
935
+
917
936
  console.log(`Done loading management; isRancher=${ isRancher }; isMultiCluster=${ isMultiCluster }`); // eslint-disable-line no-console
918
937
  },
919
938
 
package/store/type-map.js CHANGED
@@ -1895,7 +1895,7 @@ function ifHave(getters, option) {
1895
1895
  case IF_HAVE.NOT_V1_ISTIO: {
1896
1896
  return !isV1Istio(getters);
1897
1897
  }
1898
- case IF_HAVE.MULTI_CLUSTER: {
1898
+ case IF_HAVE.MULTI_CLUSTER: { // Used by harvester extension
1899
1899
  return getters.isMultiCluster;
1900
1900
  }
1901
1901
  case IF_HAVE.NEUVECTOR_NAMESPACE: {
@@ -1904,10 +1904,10 @@ function ifHave(getters, option) {
1904
1904
  case IF_HAVE.ADMIN: {
1905
1905
  return isAdminUser(getters);
1906
1906
  }
1907
- case IF_HAVE.MCM_DISABLED: {
1907
+ case IF_HAVE.MCM_DISABLED: { // There's a general MCM ff, this is conflating it with a harvester concept
1908
1908
  return !getters['isRancherInHarvester'];
1909
1909
  }
1910
- case IF_HAVE.NOT_STANDALONE_HARVESTER: {
1910
+ case IF_HAVE.NOT_STANDALONE_HARVESTER: { // Not used by harvester extension...
1911
1911
  return !getters['isStandaloneHarvester'];
1912
1912
  }
1913
1913
  default:
@@ -5257,6 +5257,7 @@ export function parse(str: any): any;
5257
5257
  export function sortable(str: any): any;
5258
5258
  export function compare(in1: any, in2: any): any;
5259
5259
  export function isPrerelease(version?: string): boolean;
5260
+ export function isUpgradeFromPreToStable(currentVersion: any, targetVersion: any): any;
5260
5261
  export function isDevBuild(version: any): boolean;
5261
5262
  export function getVersionInfo(store: any): {
5262
5263
  displayVersion: any;
@@ -12,10 +12,17 @@ export interface STEVE_WATCH_EVENT_PARAMS_COMMON {
12
12
  params: STEVE_WATCH_PARAMS,
13
13
  }
14
14
 
15
+ /**
16
+ * Args for @STEVE_WATCH_EVENT_LISTENER_CALLBACK
17
+ */
18
+ export type STEVE_WATCH_EVENT_LISTENER_CALLBACK_PARAMS = {
19
+ forceWatch?: boolean,
20
+ }
21
+
15
22
  /**
16
23
  * Executes when a watch event has a listener and it's triggered
17
24
  */
18
- export type STEVE_WATCH_EVENT_LISTENER_CALLBACK = () => void
25
+ export type STEVE_WATCH_EVENT_LISTENER_CALLBACK = (params: STEVE_WATCH_EVENT_LISTENER_CALLBACK_PARAMS) => void
19
26
 
20
27
  /**
21
28
  * Common params used when a watcher adds a listener to a watch
@@ -30,5 +30,6 @@ export interface STEVE_WATCH_PARAMS {
30
30
  namespace?: string,
31
31
  stop?: boolean,
32
32
  force?: boolean,
33
+ forceWatch?: boolean,
33
34
  mode?: STEVE_WATCH_MODE
34
35
  }
@@ -1,4 +1,4 @@
1
- import { isDevBuild } from '@shell/utils/version';
1
+ import { isDevBuild, isUpgradeFromPreToStable } from '@shell/utils/version';
2
2
 
3
3
  describe('fx: isDevBuild', () => {
4
4
  it.each([
@@ -16,3 +16,21 @@ describe('fx: isDevBuild', () => {
16
16
  }
17
17
  );
18
18
  });
19
+
20
+ describe('fx: isUpgradeFromPreToStable', () => {
21
+ it('should be true when going from pre-release to stable of same version', () => {
22
+ expect(isUpgradeFromPreToStable('1.0.0-rc1', '1.0.0')).toBe(true);
23
+ });
24
+
25
+ it('should be false when going from stable to pre-release', () => {
26
+ expect(isUpgradeFromPreToStable('1.0.0', '1.0.0-rc1')).toBe(false );
27
+ });
28
+
29
+ it('should be false for stable to stable', () => {
30
+ expect(isUpgradeFromPreToStable('1.0.0', '1.1.0')).toBe(false);
31
+ });
32
+
33
+ it('should be false for pre-release to pre-release', () => {
34
+ expect(isUpgradeFromPreToStable('1.0.0-rc1', '1.0.0-rc2')).toBe(false);
35
+ });
36
+ });
package/utils/back-off.ts CHANGED
@@ -137,9 +137,9 @@ class BackOff {
137
137
 
138
138
  // First step is immediate (0.001s)
139
139
  // Second and others are exponential
140
- // 1, 2, 3, 4, 5, 6, 7, 8, 9
141
- // 1, 4, 9, 16, 25, 36, 49, 64, 81
142
- // 0.25s, 1s, 2.25s, 4s, 6.25s, 9s, 12.25s, 16s, 20.25s
140
+ // Try: 1, 2, 3, 4, 5, 6, 7, 8, 9
141
+ // Multiple: 1, 4, 9, 16, 25, 36, 49, 64, 81
142
+ // Actual Time: 0.25s, 1s, 2.25s, 4s, 6.25s, 9s, 12.25s, 16s, 20.25s
143
143
  const delay = backOffTry === 0 ? 1 : Math.pow(backOffTry, 2) * 250;
144
144
 
145
145
  this.log('info', id, `Delaying call (attempt ${ backOffTry + 1 }, delayed by ${ delay }ms)`, description);
@@ -96,6 +96,7 @@ describe('systemInfoProvider', () => {
96
96
  return undefined;
97
97
  }),
98
98
  'management/schemaFor': jest.fn(),
99
+ localCluster: mockClusters.find((c) => c.id === 'local') || null,
99
100
  };
100
101
 
101
102
  (version.getVersionData as jest.Mock).mockReturnValue({
@@ -159,6 +160,7 @@ describe('systemInfoProvider', () => {
159
160
 
160
161
  mockGetters['uiplugins/plugins'] = null; // No plugins
161
162
  mockGetters['auth/principalId'] = null; // No user
163
+ mockGetters['localCluster'] = null; // No clusters
162
164
 
163
165
  const infoProvider = new SystemInfoProvider(mockGetters, {});
164
166
  const qs = infoProvider.buildQueryString();
@@ -191,6 +193,7 @@ describe('systemInfoProvider', () => {
191
193
 
192
194
  mockGetters['auth/principalId'] = 'user-456';
193
195
  mockGetters['uiplugins/plugins'] = []; // No plugins
196
+ mockGetters['localCluster'] = null; // No clusters
194
197
 
195
198
  const infoProvider = new SystemInfoProvider(mockGetters, {});
196
199
  const qs = infoProvider.buildQueryString();
@@ -199,7 +202,6 @@ describe('systemInfoProvider', () => {
199
202
  expect(mockGetters['management/byId']).toHaveBeenCalledWith(MANAGEMENT.SETTING, 'install-uuid');
200
203
  expect(mockGetters['management/byId']).toHaveBeenCalledWith(MANAGEMENT.SETTING, 'server-version-type');
201
204
  expect(mockGetters['management/typeRegistered']).toHaveBeenCalledWith(COUNT);
202
- expect(mockGetters['management/typeRegistered']).toHaveBeenCalledWith(MANAGEMENT.CLUSTER);
203
205
  expect(mockGetters['management/all']).not.toHaveBeenCalled();
204
206
 
205
207
  // Verify the query string is built with fallback or empty values
@@ -225,6 +227,16 @@ describe('systemInfoProvider', () => {
225
227
  return { id, value: '' }; // Empty values for all settings
226
228
  }
227
229
  });
230
+
231
+ // local cluster with missing properties
232
+ const localCluster = {
233
+ id: 'local',
234
+ isLocal: true,
235
+ status: { nodeCount: 1 },
236
+ // kubernetesVersionBase is missing
237
+ // provisioner is missing
238
+ };
239
+
228
240
  mockGetters['management/all'].mockImplementation((type: string) => {
229
241
  if (type === MANAGEMENT.SETTING) {
230
242
  // Return settings, but with empty values
@@ -237,20 +249,14 @@ describe('systemInfoProvider', () => {
237
249
  return [{ counts: { [MANAGEMENT.CLUSTER]: { summary: { count: 1 } } } }];
238
250
  }
239
251
  if (type === MANAGEMENT.CLUSTER) {
240
- // local cluster with missing properties
241
- return [{
242
- id: 'local',
243
- isLocal: true,
244
- status: { nodeCount: 1 },
245
- // kubernetesVersionBase is missing
246
- // provisioner is missing
247
- }];
252
+ return [localCluster];
248
253
  }
249
254
 
250
255
  return [];
251
256
  });
252
257
 
253
258
  mockGetters['auth/principalId'] = null; // No user
259
+ mockGetters['localCluster'] = localCluster;
254
260
 
255
261
  const infoProvider = new SystemInfoProvider(mockGetters, {});
256
262
  const qs = infoProvider.buildQueryString();
@@ -107,8 +107,7 @@ export class SystemInfoProvider {
107
107
  // High-level information from clusters
108
108
  const counts = this.getAll(getters, COUNT)?.[0]?.counts || {};
109
109
  const clusterCount = counts[MANAGEMENT.CLUSTER] || {};
110
- const all = this.getAll(getters, MANAGEMENT.CLUSTER);
111
- const localCluster = all ? all.find((c: any) => c.isLocal) : undefined;
110
+ const localCluster = getters['localCluster'];
112
111
 
113
112
  // Stats for installed extensions
114
113
  const uiExtensionList = getters['uiplugins/plugins'];
@@ -19,7 +19,7 @@ interface Args {
19
19
  /**
20
20
  * Callback called when the resource is changed (notified by socket)
21
21
  */
22
- onChange?: () => void,
22
+ onChange?: STEVE_WATCH_EVENT_LISTENER_CALLBACK,
23
23
 
24
24
  formatResponse?: {
25
25
  /**
@@ -72,13 +72,13 @@ class PaginationWrapper<T extends object> {
72
72
  this.isEnabled = paginationUtils.isEnabled({ rootGetters: $store.getters, $plugin: this.$store.$plugin }, enabledFor);
73
73
  }
74
74
 
75
- async request(args: {
76
- pagination: PaginationArgs,
75
+ async request({ pagination, forceWatch }: {
76
+ forceWatch?: boolean,
77
+ pagination: PaginationArgs,
77
78
  }): Promise<Result<T>> {
78
79
  if (!this.isEnabled) {
79
80
  throw new Error(`Wrapper for type '${ this.enabledFor.store }/${ this.enabledFor.resource?.id }' in context '${ this.enabledFor.resource?.context }' not supported`);
80
81
  }
81
- const { pagination } = args;
82
82
  const opt: ActionFindPageArgs = {
83
83
  watch: false,
84
84
  pagination,
@@ -89,14 +89,18 @@ class PaginationWrapper<T extends object> {
89
89
  const out: ActionFindPageTransientResult<T> = await this.$store.dispatch(`${ this.enabledFor.store }/findPage`, { opt, type: this.enabledFor.resource?.id });
90
90
 
91
91
  // Watch
92
- if (this.onChange && !this.steveWatchParams) {
92
+ const firstTime = !this.steveWatchParams;
93
+
94
+ if (this.onChange && (firstTime || forceWatch) ) { // && !this.steveWatchParams
93
95
  this.steveWatchParams = {
94
96
  event: STEVE_WATCH_EVENT_TYPES.CHANGES,
95
97
  id: this.id,
96
98
  params: {
97
- type: this.enabledFor.resource?.id as string,
98
- mode: STEVE_WATCH_MODE.RESOURCE_CHANGES,
99
- }
99
+ type: this.enabledFor.resource?.id as string,
100
+ mode: STEVE_WATCH_MODE.RESOURCE_CHANGES,
101
+ force: forceWatch,
102
+ },
103
+
100
104
  };
101
105
 
102
106
  this.watch();
package/utils/version.js CHANGED
@@ -74,6 +74,21 @@ export function isPrerelease(version = '') {
74
74
  return !!semver.prerelease(version);
75
75
  }
76
76
 
77
+ export function isUpgradeFromPreToStable(currentVersion, targetVersion) {
78
+ if (!isPrerelease(currentVersion) || isPrerelease(targetVersion)) {
79
+ return false;
80
+ }
81
+
82
+ const cVersion = semver.clean(currentVersion, { loose: true });
83
+ const tVersion = semver.clean(targetVersion, { loose: true });
84
+
85
+ if (cVersion && tVersion && semver.valid(cVersion) && semver.valid(tVersion)) {
86
+ return semver.lt(cVersion, tVersion);
87
+ }
88
+
89
+ return false;
90
+ }
91
+
77
92
  export function isDevBuild(version) {
78
93
  if ( ['dev', 'master', 'head'].includes(version) || version.endsWith('-head') || version.match(/-rc\d+$/) || version.match(/-alpha\d+$/) ) {
79
94
  return true;