@rancher/shell 3.0.9 → 3.0.11

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 (104) hide show
  1. package/assets/styles/base/_color.scss +4 -0
  2. package/assets/styles/themes/_light.scss +6 -6
  3. package/assets/styles/themes/_modern.scss +14 -6
  4. package/assets/translations/en-us.yaml +9 -10
  5. package/chart/__tests__/rancher-backup-index.test.ts +248 -0
  6. package/chart/rancher-backup/index.vue +41 -2
  7. package/components/BrandImage.vue +6 -5
  8. package/components/ConsumptionGauge.vue +12 -4
  9. package/components/CopyToClipboard.vue +28 -0
  10. package/components/CopyToClipboardText.vue +4 -0
  11. package/components/CruResource.vue +1 -0
  12. package/components/DynamicContent/DynamicContentIcon.vue +3 -2
  13. package/components/ExplorerProjectsNamespaces.vue +1 -4
  14. package/components/GlobalRoleBindings.vue +1 -5
  15. package/components/LazyImage.vue +2 -1
  16. package/components/Resource/Detail/Card/Scaler.vue +4 -4
  17. package/components/ResourceDetail/index.vue +0 -21
  18. package/components/Tabbed/index.vue +6 -0
  19. package/components/__tests__/ConsumptionGauge.test.ts +31 -0
  20. package/components/__tests__/CruResource.test.ts +35 -1
  21. package/components/form/ProjectMemberEditor.vue +0 -10
  22. package/components/nav/TopLevelMenu.helper.ts +7 -79
  23. package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
  24. package/composables/useIsNewDetailPageEnabled.test.ts +98 -0
  25. package/composables/useIsNewDetailPageEnabled.ts +12 -0
  26. package/config/private-label.js +2 -1
  27. package/config/product/apps.js +1 -0
  28. package/config/product/explorer.js +11 -1
  29. package/config/table-headers.js +0 -9
  30. package/config/types.js +0 -1
  31. package/core/__tests__/extension-manager-impl.test.js +187 -2
  32. package/core/extension-manager-impl.js +4 -2
  33. package/core/plugin-helpers.ts +31 -0
  34. package/detail/__tests__/node.test.ts +83 -0
  35. package/detail/management.cattle.io.oidcclient.vue +2 -1
  36. package/detail/node.vue +1 -0
  37. package/edit/auth/github-app-steps.vue +2 -0
  38. package/edit/auth/github-steps.vue +2 -0
  39. package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
  40. package/edit/cloudcredential.vue +2 -1
  41. package/edit/management.cattle.io.user.vue +60 -35
  42. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
  43. package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
  44. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
  45. package/edit/secret/generic.vue +1 -0
  46. package/edit/secret/index.vue +2 -1
  47. package/edit/service.vue +2 -14
  48. package/edit/token.vue +29 -68
  49. package/list/management.cattle.io.feature.vue +7 -1
  50. package/list/provisioning.cattle.io.cluster.vue +0 -49
  51. package/mixins/brand.js +2 -1
  52. package/models/catalog.cattle.io.clusterrepo.js +9 -0
  53. package/models/cluster.x-k8s.io.machinedeployment.js +8 -3
  54. package/models/management.cattle.io.authconfig.js +2 -1
  55. package/models/management.cattle.io.cluster.js +4 -3
  56. package/models/monitoring.coreos.com.receiver.js +11 -6
  57. package/models/provisioning.cattle.io.cluster.js +2 -2
  58. package/models/token.js +0 -4
  59. package/package.json +12 -12
  60. package/pages/account/index.vue +67 -96
  61. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +66 -9
  62. package/pages/c/_cluster/apps/charts/index.vue +3 -8
  63. package/pages/c/_cluster/apps/charts/install.vue +8 -9
  64. package/pages/c/_cluster/explorer/index.vue +2 -19
  65. package/pages/c/_cluster/istio/index.vue +4 -2
  66. package/pages/c/_cluster/longhorn/index.vue +2 -1
  67. package/pages/c/_cluster/monitoring/index.vue +2 -2
  68. package/pages/c/_cluster/neuvector/index.vue +2 -1
  69. package/pages/c/_cluster/settings/performance.vue +0 -5
  70. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
  71. package/pages/c/_cluster/uiplugins/index.vue +2 -1
  72. package/pkg/auto-import.js +41 -0
  73. package/plugins/dashboard-store/resource-class.js +2 -2
  74. package/plugins/steve/__tests__/steve-class.test.ts +1 -1
  75. package/plugins/steve/steve-class.js +3 -3
  76. package/plugins/steve/steve-pagination-utils.ts +2 -5
  77. package/plugins/steve/subscribe.js +29 -4
  78. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +7 -7
  79. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +5 -2
  80. package/rancher-components/RcButton/RcButton.vue +3 -3
  81. package/rancher-components/RcButtonSplit/RcButtonSplit.test.ts +253 -0
  82. package/rancher-components/RcButtonSplit/RcButtonSplit.vue +158 -0
  83. package/rancher-components/RcButtonSplit/index.ts +1 -0
  84. package/rancher-components/RcIcon/types.ts +2 -2
  85. package/rancher-components/RcSection/RcSection.test.ts +323 -0
  86. package/rancher-components/RcSection/RcSection.vue +252 -0
  87. package/rancher-components/RcSection/RcSectionActions.test.ts +212 -0
  88. package/rancher-components/RcSection/RcSectionActions.vue +85 -0
  89. package/rancher-components/RcSection/RcSectionBadges.test.ts +149 -0
  90. package/rancher-components/RcSection/RcSectionBadges.vue +29 -0
  91. package/rancher-components/RcSection/index.ts +12 -0
  92. package/rancher-components/RcSection/types.ts +86 -0
  93. package/scripts/test-plugins-build.sh +9 -8
  94. package/types/shell/index.d.ts +93 -108
  95. package/utils/__tests__/require-asset.test.ts +98 -0
  96. package/utils/async.ts +1 -5
  97. package/utils/brand.ts +3 -1
  98. package/utils/favicon.js +4 -3
  99. package/utils/require-asset.ts +95 -0
  100. package/utils/style.ts +17 -0
  101. package/utils/units.js +14 -5
  102. package/vue.config.js +4 -3
  103. package/components/HarvesterServiceAddOnConfig.vue +0 -207
  104. package/models/ext.cattle.io.token.js +0 -48
@@ -49,10 +49,6 @@ export default {
49
49
  userId: {
50
50
  type: String,
51
51
  default: ''
52
- },
53
- watchOverride: {
54
- type: Boolean,
55
- default: true,
56
52
  }
57
53
  },
58
54
  async fetch() {
@@ -142,7 +138,7 @@ export default {
142
138
  this.update();
143
139
  },
144
140
  userId(userId, oldUserId) {
145
- if (userId === oldUserId || this.watchOverride === true) {
141
+ if (userId === oldUserId) {
146
142
  return;
147
143
  }
148
144
  this.update();
@@ -1,5 +1,6 @@
1
1
  <script>
2
2
  import { BLANK_IMAGE } from '@shell/utils/style';
3
+ import genericCatalogSvg from '@shell/assets/images/generic-catalog.svg';
3
4
 
4
5
  export default {
5
6
  props: {
@@ -10,7 +11,7 @@ export default {
10
11
 
11
12
  errorSrc: {
12
13
  type: String,
13
- default: require('@shell/assets/images/generic-catalog.svg'),
14
+ default: genericCatalogSvg,
14
15
  },
15
16
 
16
17
  src: {
@@ -54,9 +54,9 @@ const i18n = useI18n(store);
54
54
  .scaler {
55
55
  display: inline-flex;
56
56
  align-items: center;
57
- background-color: hsl(from var(--primary) h s calc(l + 30));
57
+ background-color: var(--accent-btn);
58
58
  border-radius: var(--border-radius-md);
59
- border: 1px solid var(--primary);
59
+ border: solid thin var(--primary);
60
60
  overflow: hidden;
61
61
 
62
62
  button {
@@ -77,7 +77,7 @@ const i18n = useI18n(store);
77
77
  }
78
78
 
79
79
  &:hover {
80
- background-color: hsl(from var(--primary) h s calc(l + 20));
80
+ background-color: var(--accent-btn);
81
81
  }
82
82
 
83
83
  &[disabled] {
@@ -88,7 +88,7 @@ const i18n = useI18n(store);
88
88
  }
89
89
 
90
90
  .value {
91
- color: initial;
91
+ color: var(--body-text);
92
92
  cursor: default;
93
93
  padding: 4px;
94
94
  padding-top: 5px;
@@ -403,25 +403,6 @@ export default {
403
403
  // Remove id? How does subtype get in (cluster/node)
404
404
  this.detailComponent = this.$store.getters['type-map/importDetail'](detailResource, id);
405
405
  this.editComponent = this.$store.getters['type-map/importEdit'](editResource, id);
406
- },
407
- /**
408
- * Sets the mode and initializes the resource components.
409
- *
410
- * This method sets the mode of the component and configures the resource
411
- * components based on the provided user and resource.
412
- *
413
- * @param {Object} payload - An object containing the mode, user, and
414
- * resource properties.
415
- * @param {string} payload.mode - The mode to set.
416
- * @param {Object} payload.user - The user object containing user-specific
417
- * information.
418
- * @param {string} payload.resource - The resource string to use for
419
- * initialization.
420
- */
421
- setMode({ mode, userId, resource }) {
422
- this.mode = mode;
423
- this.value.id = userId;
424
- this.configureResource(userId, resource);
425
406
  }
426
407
  }
427
408
  };
@@ -444,7 +425,6 @@ export default {
444
425
  :class="{'flex-content': flexContent}"
445
426
  :resource-errors="errors"
446
427
  @update:value="$emit('input', $event)"
447
- @update:mode="setMode"
448
428
  @set-subtype="setSubtype"
449
429
  />
450
430
  <div v-else>
@@ -514,7 +494,6 @@ export default {
514
494
  :real-mode="realMode"
515
495
  :class="{'flex-content': flexContent}"
516
496
  @update:value="$emit('input', $event)"
517
- @update:mode="setMode"
518
497
  @set-subtype="setSubtype"
519
498
  />
520
499
 
@@ -76,6 +76,12 @@ export default {
76
76
  type: Boolean,
77
77
  default: true,
78
78
  },
79
+
80
+ extensionParams: {
81
+ type: Object,
82
+ default: null,
83
+ },
84
+
79
85
  /**
80
86
  * Inherited global identifier prefix for tests
81
87
  * Define a term based on the parent component to avoid conflicts on multiple components
@@ -64,6 +64,37 @@ describe('component: ConsumptionGauge', () => {
64
64
  expect(slotTitle.text()).toBe('some-resource-name');
65
65
  });
66
66
 
67
+ it('should display the default "Used" label when usedLabel is not provided', () => {
68
+ const wrapper = mount(ConsumptionGauge, {
69
+ props: {
70
+ resourceName: 'some-resource-name',
71
+ capacity: 100,
72
+ used: 50,
73
+ }
74
+ });
75
+
76
+ const usedSpan = wrapper.find('.consumption-gauge .numbers span:nth-child(1)');
77
+
78
+ expect(usedSpan.exists()).toBe(true);
79
+ expect(usedSpan.text()).toBe('%node.detail.glance.consumptionGauge.used%');
80
+ });
81
+
82
+ it('usedLabel should override the default "Used" label text', () => {
83
+ const wrapper = mount(ConsumptionGauge, {
84
+ props: {
85
+ resourceName: 'some-resource-name',
86
+ capacity: 100,
87
+ used: 50,
88
+ usedLabel: 'Running'
89
+ }
90
+ });
91
+
92
+ const usedSpan = wrapper.find('.consumption-gauge .numbers span:nth-child(1)');
93
+
94
+ expect(usedSpan.exists()).toBe(true);
95
+ expect(usedSpan.text()).toBe('Running');
96
+ });
97
+
67
98
  it('passing slot TITLE should render correctly', () => {
68
99
  const colorStops = {
69
100
  0: '--success', 30: '--warning', 70: '--error'
@@ -1,6 +1,6 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import CruResource from '@shell/components/CruResource.vue';
3
- import { _EDIT, _YAML } from '@shell/config/query-params';
3
+ import { _CREATE, _EDIT, _VIEW, _YAML } from '@shell/config/query-params';
4
4
  import TextAreaAutoGrow from '@components/Form/TextArea/TextAreaAutoGrow.vue';
5
5
 
6
6
  describe('component: CruResource', () => {
@@ -171,6 +171,40 @@ describe('component: CruResource', () => {
171
171
  expect(event.preventDefault).toHaveBeenCalledWith();
172
172
  });
173
173
 
174
+ it.each([
175
+ [_EDIT, true],
176
+ [_CREATE, true],
177
+ [_VIEW, false],
178
+ ])('should render CruResourceFooter when mode is %s: %s', (mode: string, shouldRender: boolean) => {
179
+ const wrapper = mount(CruResource, {
180
+ props: {
181
+ canYaml: false,
182
+ mode,
183
+ resource: {}
184
+ },
185
+ global: {
186
+ mocks: {
187
+ $store: {
188
+ getters: {
189
+ currentStore: () => 'current_store',
190
+ 'current_store/schemaFor': jest.fn(),
191
+ 'current_store/all': jest.fn(),
192
+ 'i18n/t': jest.fn(),
193
+ 'i18n/exists': jest.fn(),
194
+ },
195
+ dispatch: jest.fn(),
196
+ },
197
+ $route: { query: { AS: _YAML } },
198
+ $router: { applyQuery: jest.fn() },
199
+ },
200
+ }
201
+ });
202
+
203
+ const footer = wrapper.find('.cru-resource-footer');
204
+
205
+ expect(footer.exists()).toBe(shouldRender);
206
+ });
207
+
174
208
  it('should not prevent default events on keypress Enter', async() => {
175
209
  const event = { preventDefault: jest.fn() };
176
210
  const wrapper = mount(CruResource, {
@@ -61,11 +61,6 @@ export default {
61
61
  label: this.t('projectMembers.projectPermissions.ingressManage'),
62
62
  value: false,
63
63
  },
64
- {
65
- key: 'projectcatalogs-manage',
66
- label: this.t('projectMembers.projectPermissions.projectcatalogsManage'),
67
- value: false,
68
- },
69
64
  {
70
65
  key: 'projectroletemplatebindings-manage',
71
66
  label: this.t('projectMembers.projectPermissions.projectroletemplatebindingsManage'),
@@ -111,11 +106,6 @@ export default {
111
106
  label: this.t('projectMembers.projectPermissions.monitoringUiView'),
112
107
  value: false,
113
108
  },
114
- {
115
- key: 'projectcatalogs-view',
116
- label: this.t('projectMembers.projectPermissions.projectcatalogsView'),
117
- value: false,
118
- },
119
109
  {
120
110
  key: 'projectroletemplatebindings-view',
121
111
  label: this.t('projectMembers.projectPermissions.projectroletemplatebindingsView'),
@@ -107,7 +107,6 @@ export interface TopLevelMenuHelper {
107
107
 
108
108
  export abstract class BaseTopLevelMenuHelper {
109
109
  protected $store: VuexStore;
110
- protected hasProvCluster: boolean;
111
110
 
112
111
  /**
113
112
  * Filter mgmt clusters by
@@ -143,11 +142,9 @@ export abstract class BaseTopLevelMenuHelper {
143
142
  $store: VuexStore,
144
143
  }) {
145
144
  this.$store = $store;
146
-
147
- this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
148
145
  }
149
146
 
150
- protected convertToCluster(mgmtCluster: MgmtCluster, provCluster: ProvCluster): TopLevelMenuCluster {
147
+ protected convertToCluster(mgmtCluster: MgmtCluster, provCluster?: ProvCluster): TopLevelMenuCluster {
151
148
  return {
152
149
  id: mgmtCluster.id,
153
150
  label: mgmtCluster.nameDisplay,
@@ -173,7 +170,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
173
170
 
174
171
  private clustersPinnedWrapper: PaginationWrapper<any>;
175
172
  private clustersOthersWrapper: PaginationWrapper<any>;
176
- private provClusterWrapper: PaginationWrapper<any>;
177
173
 
178
174
  private clusterCount = 0;
179
175
 
@@ -223,43 +219,10 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
223
219
  },
224
220
  formatResponse: { classify: true },
225
221
  });
226
- // Fetch all prov clusters for the mgmt clusters we have
227
- this.provClusterWrapper = new PaginationWrapper({
228
- $store,
229
- id: 'top-level-menu-prov-clusters',
230
- onChange: async({ forceWatch, revision }) => {
231
- if (!this.args) {
232
- return;
233
- }
234
- try {
235
- await this.update({
236
- ...this.args,
237
- forceWatch,
238
- provClusterRevision: revision,
239
- });
240
- } catch {
241
- // Failures should be logged lower down, not much we can do here except catch to prevent whole ui page warnings in dev mode
242
- }
243
- },
244
- enabledFor: {
245
- store: STORE.MANAGEMENT,
246
- resource: {
247
- id: CAPI.RANCHER_CLUSTER,
248
- context: 'side-bar',
249
- }
250
- },
251
- formatResponse: { classify: true }
252
- });
253
222
  }
254
223
 
255
224
  // ---------- requests ----------
256
225
  async update(args: UpdateArgs) {
257
- if (!this.hasProvCluster) {
258
- // We're filtering out mgmt clusters without prov clusters, so if the user can't see any prov clusters at all
259
- // exit early
260
- return;
261
- }
262
-
263
226
  this.args = args;
264
227
  const promises = {
265
228
  pinned: this.updatePinned(args),
@@ -271,22 +234,11 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
271
234
  notPinned: MgmtCluster[]
272
235
  } = await allHash(promises) as any;
273
236
 
274
- const provClusters = await this.updateProvCluster(res.notPinned, res.pinned, args);
275
- const provClustersByMgmtId = provClusters.reduce((res: { [mgmtId: string]: ProvCluster}, provCluster: ProvCluster) => {
276
- if (provCluster.mgmtClusterId) {
277
- res[provCluster.mgmtClusterId] = provCluster;
278
- }
279
-
280
- return res;
281
- }, {} as { [mgmtId: string]: ProvCluster});
282
-
283
237
  // Filter out mgmt clusters that don't have matching prov cluster and convert remaining to required format
284
238
  const _clustersNotPinned = res.notPinned
285
- .filter((mgmtCluster) => !!provClustersByMgmtId[mgmtCluster.id])
286
- .map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
239
+ .map((mgmtCluster) => this.convertToCluster(mgmtCluster));
287
240
  const _clustersPinned = res.pinned
288
- .filter((mgmtCluster) => !!provClustersByMgmtId[mgmtCluster.id])
289
- .map((mgmtCluster) => this.convertToCluster(mgmtCluster, provClustersByMgmtId[mgmtCluster.id]));
241
+ .map((mgmtCluster) => this.convertToCluster(mgmtCluster));
290
242
 
291
243
  this.clustersPinned.length = 0;
292
244
  this.clustersOthers.length = 0;
@@ -298,7 +250,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
298
250
  async destroy() {
299
251
  this.clustersPinnedWrapper.onDestroy();
300
252
  this.clustersOthersWrapper.onDestroy();
301
- this.provClusterWrapper.onDestroy();
302
253
  }
303
254
 
304
255
  /**
@@ -445,41 +396,21 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
445
396
  console.warn('Unable to set saved count for clusters', err); // eslint-disable-line no-console
446
397
  }
447
398
  }
448
-
449
- /**
450
- * Find all provisioning clusters associated with the displayed mgmt clusters
451
- */
452
- private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[], args: UpdateArgs): Promise<ProvCluster[]> {
453
- return this.provClusterWrapper.request({
454
- forceWatch: args.forceWatch,
455
- pagination: {
456
- filters: [
457
- PaginationParamFilter.createMultipleFields(
458
- [...notPinned, ...pinned]
459
- .map((mgmtCluster) => ({
460
- field: 'status.clusterName', value: mgmtCluster.id, equals: true, exact: true
461
- }))
462
- )
463
- ],
464
- page: 1,
465
- sort: [],
466
- projectsOrNamespaces: []
467
- },
468
- revision: args.provClusterRevision
469
- })
470
- .then((r) => r.data);
471
- }
472
399
  }
473
400
 
474
401
  /**
475
402
  * Helper designed to supply non-paginated results for the top level menu cluster resources
476
403
  */
477
404
  export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements TopLevelMenuHelper {
405
+ protected hasProvCluster: boolean;
406
+
478
407
  constructor({ $store }: {
479
408
  $store: VuexStore,
480
409
  }) {
481
410
  super({ $store });
482
411
 
412
+ this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
413
+
483
414
  if (this.hasProvCluster) {
484
415
  $store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
485
416
  }
@@ -664,9 +595,6 @@ class TopLevelMenuHelperService {
664
595
  const canPagination = $store.getters[`management/paginationEnabled`]({
665
596
  id: MANAGEMENT.CLUSTER,
666
597
  context: 'side-bar',
667
- }) && $store.getters[`management/paginationEnabled`]({
668
- id: CAPI.RANCHER_CLUSTER,
669
- context: 'side-bar',
670
598
  });
671
599
 
672
600
  this._helper = canPagination ? new TopLevelMenuHelperPagination({ $store }) : new TopLevelMenuHelperLegacy({ $store });
@@ -102,7 +102,7 @@ describe('topLevelMenu.helper', () => {
102
102
  it('should initialize PaginationWrappers', () => {
103
103
  mockStore.getters['management/schemaFor'].mockReturnValue(true);
104
104
  new TopLevelMenuHelperPagination({ $store: mockStore });
105
- expect(PaginationWrapper).toHaveBeenCalledTimes(3);
105
+ expect(PaginationWrapper).toHaveBeenCalledTimes(2);
106
106
  });
107
107
 
108
108
  it('should update clusters correctly', async() => {
@@ -113,19 +113,13 @@ describe('topLevelMenu.helper', () => {
113
113
  const mgmtOthers = [{
114
114
  id: 'c2', nameDisplay: 'Other', isReady: true, pinned: false, pin: jest.fn(), unpin: jest.fn()
115
115
  }];
116
- const provClusters = [
117
- { mgmtClusterId: 'c1' },
118
- { mgmtClusterId: 'c2' }
119
- ];
120
116
 
121
117
  const mockRequestPinned = jest.fn().mockResolvedValue({ data: mgmtPinned });
122
118
  const mockRequestOthers = jest.fn().mockResolvedValue({ data: mgmtOthers });
123
- const mockRequestProv = jest.fn().mockResolvedValue({ data: provClusters });
124
119
 
125
120
  (PaginationWrapper as unknown as jest.Mock)
126
121
  .mockImplementationOnce(() => ({ request: mockRequestPinned, onDestroy: jest.fn() }))
127
- .mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }))
128
- .mockImplementationOnce(() => ({ request: mockRequestProv, onDestroy: jest.fn() }));
122
+ .mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }));
129
123
 
130
124
  const helper = new TopLevelMenuHelperPagination({ $store: mockStore });
131
125
 
@@ -169,57 +163,12 @@ describe('topLevelMenu.helper', () => {
169
163
  },
170
164
  revision: undefined
171
165
  });
172
- expect(mockRequestProv).toHaveBeenCalledWith({
173
- forceWatch: undefined,
174
- pagination: {
175
- filters: [{
176
- equals: true,
177
- fields: [{
178
- equals: true, exact: true, field: 'status.clusterName', value: mgmtOthers[0].id
179
- }, {
180
- equals: true, exact: true, field: 'status.clusterName', value: mgmtPinned[0].id
181
- }],
182
- param: 'filter'
183
- }],
184
- page: 1,
185
- projectsOrNamespaces: [],
186
- sort: []
187
- },
188
- revision: undefined
189
- });
190
166
 
191
167
  expect(helper.clustersPinned).toHaveLength(1);
192
168
  expect(helper.clustersPinned[0].id).toBe('c1');
193
169
  expect(helper.clustersOthers).toHaveLength(1);
194
170
  expect(helper.clustersOthers[0].id).toBe('c2');
195
171
  });
196
-
197
- it('should filter out mgmt clusters without matching prov clusters', async() => {
198
- mockStore.getters['management/schemaFor'].mockReturnValue(true);
199
- const mgmtOthers = [{
200
- id: 'c2', nameDisplay: 'Other', isReady: true, pinned: false, pin: jest.fn(), unpin: jest.fn()
201
- }];
202
- // No prov cluster for c2
203
- const provClusters: any[] = [];
204
-
205
- const mockRequestPinned = jest.fn().mockResolvedValue({ data: [] });
206
- const mockRequestOthers = jest.fn().mockResolvedValue({ data: mgmtOthers });
207
- const mockRequestProv = jest.fn().mockResolvedValue({ data: provClusters });
208
-
209
- (PaginationWrapper as unknown as jest.Mock)
210
- .mockImplementationOnce(() => ({ request: mockRequestPinned, onDestroy: jest.fn() }))
211
- .mockImplementationOnce(() => ({ request: mockRequestOthers, onDestroy: jest.fn() }))
212
- .mockImplementationOnce(() => ({ request: mockRequestProv, onDestroy: jest.fn() }));
213
-
214
- const helper = new TopLevelMenuHelperPagination({ $store: mockStore });
215
-
216
- await helper.update({
217
- searchTerm: '',
218
- pinnedIds: [],
219
- });
220
-
221
- expect(helper.clustersOthers).toHaveLength(0);
222
- });
223
172
  });
224
173
 
225
174
  describe('class: TopLevelMenuHelperService', () => {
@@ -0,0 +1,98 @@
1
+ import { useIsNewDetailPageEnabled } from '@shell/composables/useIsNewDetailPageEnabled';
2
+
3
+ const mockStore: any = { getters: {} };
4
+ const mockRoute: any = { query: {} };
5
+
6
+ jest.mock('vuex', () => ({ useStore: () => mockStore }));
7
+ jest.mock('vue-router', () => ({ useRoute: () => mockRoute }));
8
+
9
+ const mockGetVersionInfo = jest.fn(() => ({ fullVersion: '2.12.0' }));
10
+
11
+ jest.mock('@shell/utils/version', () => ({ getVersionInfo: (...args: any[]) => mockGetVersionInfo(...args) }));
12
+
13
+ describe('useIsNewDetailPageEnabled', () => {
14
+ beforeEach(() => {
15
+ mockRoute.query = {};
16
+ mockGetVersionInfo.mockReturnValue({ fullVersion: '2.12.0' });
17
+ });
18
+
19
+ describe('version gating', () => {
20
+ it('should return false when version is below 2.12.0', () => {
21
+ mockGetVersionInfo.mockReturnValue({ fullVersion: '2.11.9' });
22
+ const result = useIsNewDetailPageEnabled();
23
+
24
+ expect(result.value).toBe(false);
25
+ });
26
+
27
+ it('should return false when version is 2.10.0', () => {
28
+ mockGetVersionInfo.mockReturnValue({ fullVersion: '2.10.0' });
29
+ const result = useIsNewDetailPageEnabled();
30
+
31
+ expect(result.value).toBe(false);
32
+ });
33
+
34
+ it('should return true when version is exactly 2.12.0 and no legacy query', () => {
35
+ mockGetVersionInfo.mockReturnValue({ fullVersion: '2.12.0' });
36
+ const result = useIsNewDetailPageEnabled();
37
+
38
+ expect(result.value).toBe(true);
39
+ });
40
+
41
+ it('should return true when version is above 2.12.0', () => {
42
+ mockGetVersionInfo.mockReturnValue({ fullVersion: '2.13.0' });
43
+ const result = useIsNewDetailPageEnabled();
44
+
45
+ expect(result.value).toBe(true);
46
+ });
47
+
48
+ it('should return false when version is undefined', () => {
49
+ mockGetVersionInfo.mockReturnValue({ fullVersion: undefined });
50
+ const result = useIsNewDetailPageEnabled();
51
+
52
+ expect(result.value).toBe(false);
53
+ });
54
+
55
+ it('should return false when version is null', () => {
56
+ mockGetVersionInfo.mockReturnValue({ fullVersion: null });
57
+ const result = useIsNewDetailPageEnabled();
58
+
59
+ expect(result.value).toBe(false);
60
+ });
61
+
62
+ it('should handle pre-release version strings', () => {
63
+ mockGetVersionInfo.mockReturnValue({ fullVersion: 'v2.12.1-rc1' });
64
+ const result = useIsNewDetailPageEnabled();
65
+
66
+ expect(result.value).toBe(true);
67
+ });
68
+ });
69
+
70
+ describe('legacy query param (with version >= 2.12.0)', () => {
71
+ it('should return true when no legacy query param is present', () => {
72
+ const result = useIsNewDetailPageEnabled();
73
+
74
+ expect(result.value).toBe(true);
75
+ });
76
+
77
+ it('should return false when legacy query param is "true"', () => {
78
+ mockRoute.query = { legacy: 'true' };
79
+ const result = useIsNewDetailPageEnabled();
80
+
81
+ expect(result.value).toBe(false);
82
+ });
83
+
84
+ it('should return true when legacy query param is "false"', () => {
85
+ mockRoute.query = { legacy: 'false' };
86
+ const result = useIsNewDetailPageEnabled();
87
+
88
+ expect(result.value).toBe(true);
89
+ });
90
+
91
+ it('should return true when legacy query param has an unexpected value', () => {
92
+ mockRoute.query = { legacy: 'something' };
93
+ const result = useIsNewDetailPageEnabled();
94
+
95
+ expect(result.value).toBe(true);
96
+ });
97
+ });
98
+ });
@@ -1,6 +1,9 @@
1
1
  import { useRoute } from 'vue-router';
2
2
  import { LEGACY } from '@shell/config/query-params';
3
3
  import { computed } from 'vue';
4
+ import { getVersionInfo } from '@shell/utils/version';
5
+ import semver from 'semver';
6
+ import { useStore } from 'vuex';
4
7
 
5
8
  const enabledByDefault = true;
6
9
 
@@ -8,6 +11,15 @@ export const useIsNewDetailPageEnabled = () => {
8
11
  const route = useRoute();
9
12
 
10
13
  return computed(() => {
14
+ const store = useStore();
15
+ const { fullVersion } = getVersionInfo(store);
16
+
17
+ const coerced = semver.coerce(fullVersion) || { version: '0.0.0' };
18
+
19
+ if (!semver.gte(coerced.version, '2.12.0')) {
20
+ return false;
21
+ }
22
+
11
23
  if (enabledByDefault) {
12
24
  return route?.query?.[LEGACY] !== 'true';
13
25
  }
@@ -1,5 +1,6 @@
1
1
  import { SETTING } from './settings';
2
2
  import { CURRENT_RANCHER_VERSION } from './version';
3
+ import { requireAsset } from '@shell/utils/require-asset';
3
4
 
4
5
  export const ANY = 0;
5
6
  export const STANDARD = 1;
@@ -78,7 +79,7 @@ export function setTitle() {
78
79
  const v = getVendor();
79
80
 
80
81
  if (v === 'Harvester') {
81
- const ico = require(`~shell/assets/images/pl/harvester.png`);
82
+ const ico = requireAsset(`~shell/assets/images/pl/harvester.png`);
82
83
 
83
84
  document.title = 'Harvester';
84
85
  const link = document.createElement('link');
@@ -53,6 +53,7 @@ export function init(store) {
53
53
 
54
54
  configureType(CATALOG.APP, { isCreatable: false, isEditable: false });
55
55
  configureType(CATALOG.OPERATION, { isCreatable: false, isEditable: false });
56
+ configureType(CATALOG.CLUSTER_REPO, { listCreateButtonLabelKey: 'catalog.repo.add' });
56
57
 
57
58
  const repoType = {
58
59
  name: 'type',
@@ -19,7 +19,7 @@ import {
19
19
  USER_ID, USERNAME, USER_DISPLAY_NAME, USER_PROVIDER, USER_LAST_LOGIN, USER_DISABLED_IN, USER_DELETED_IN, WORKLOAD_ENDPOINTS, STORAGE_CLASS_DEFAULT,
20
20
  STORAGE_CLASS_PROVISIONER, PERSISTENT_VOLUME_SOURCE,
21
21
  HPA_REFERENCE, MIN_REPLICA, MAX_REPLICA, CURRENT_REPLICA,
22
- DESCRIPTION, SUB_TYPE, PERSISTENT_VOLUME_CLAIM, RECLAIM_POLICY, PV_REASON, WORKLOAD_HEALTH_SCALE, POD_RESTARTS,
22
+ ACCESS_KEY, DESCRIPTION, EXPIRES, EXPIRY_STATE, LAST_USED, SUB_TYPE, AGE_NORMAN, SCOPE_NORMAN, PERSISTENT_VOLUME_CLAIM, RECLAIM_POLICY, PV_REASON, WORKLOAD_HEALTH_SCALE, POD_RESTARTS,
23
23
  DURATION, MESSAGE, REASON, EVENT_TYPE, OBJECT, ROLE, ROLES, VERSION, INTERNAL_EXTERNAL_IP, KUBE_NODE_OS, CPU, RAM, SECRET_DATA,
24
24
  EVENT_LAST_SEEN_TIME,
25
25
  EVENT_FIRST_SEEN_TIME,
@@ -546,6 +546,16 @@ export function init(store) {
546
546
  AGE
547
547
  ]);
548
548
 
549
+ headers(NORMAN.TOKEN, [
550
+ EXPIRY_STATE,
551
+ ACCESS_KEY,
552
+ DESCRIPTION,
553
+ SCOPE_NORMAN,
554
+ LAST_USED,
555
+ EXPIRES,
556
+ AGE_NORMAN
557
+ ]);
558
+
549
559
  virtualType({
550
560
  label: store.getters['i18n/t']('clusterIndexPage.header'),
551
561
  group: 'Root',
@@ -1033,15 +1033,6 @@ export const SCOPE_NORMAN = {
1033
1033
  sort: ['clusterId'],
1034
1034
  };
1035
1035
 
1036
- export const NORMAN_KEY_DEPRECATION = {
1037
- name: 'isNormanKeyDeprecated',
1038
- labelKey: 'tableHeaders.isLegacy',
1039
- value: (row) => row.isDeprecated ? 'True' : undefined,
1040
- sort: 'isDeprecated',
1041
- align: 'left',
1042
- dashIfEmpty: true,
1043
- };
1044
-
1045
1036
  export const EXPIRES = {
1046
1037
  name: 'expires',
1047
1038
  value: 'expiresAt',
package/config/types.js CHANGED
@@ -269,7 +269,6 @@ export const EXT = {
269
269
  GROUP_MEMBERSHIP_REFRESH_REQUESTS: 'ext.cattle.io.groupmembershiprefreshrequest',
270
270
  PASSWORD_CHANGE_REQUESTS: 'ext.cattle.io.passwordchangerequest',
271
271
  KUBECONFIG: 'ext.cattle.io.kubeconfig',
272
- TOKEN: 'ext.cattle.io.token',
273
272
  };
274
273
 
275
274
  export const CAPI = {