@rancher/shell 3.0.9-rc.6 → 3.0.10

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 (82) 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 +2 -5
  5. package/components/CopyToClipboard.vue +28 -0
  6. package/components/CopyToClipboardText.vue +4 -0
  7. package/components/CruResource.vue +1 -0
  8. package/components/GlobalRoleBindings.vue +1 -5
  9. package/components/IconOrSvg.vue +61 -42
  10. package/components/ResourceDetail/index.vue +0 -21
  11. package/components/SortableTable/index.vue +2 -2
  12. package/components/__tests__/CruResource.test.ts +35 -1
  13. package/components/form/BannerSettings.vue +2 -2
  14. package/components/form/NotificationSettings.vue +2 -2
  15. package/composables/useIsNewDetailPageEnabled.test.ts +98 -0
  16. package/composables/useIsNewDetailPageEnabled.ts +12 -0
  17. package/config/product/explorer.js +11 -1
  18. package/config/product/manager.js +0 -1
  19. package/config/table-headers.js +0 -9
  20. package/config/types.js +0 -1
  21. package/detail/fleet.cattle.io.cluster.vue +1 -1
  22. package/dialog/FeatureFlagListDialog.vue +1 -1
  23. package/edit/auth/github-app-steps.vue +2 -0
  24. package/edit/auth/github-steps.vue +2 -0
  25. package/edit/catalog.cattle.io.clusterrepo.vue +1 -1
  26. package/edit/management.cattle.io.user.vue +60 -35
  27. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/auth.spec.ts +145 -0
  28. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/index.test.ts +202 -0
  29. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/tls.spec.ts +226 -0
  30. package/edit/monitoring.coreos.com.alertmanagerconfig/auth.vue +24 -21
  31. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/opsgenie.spec.ts +157 -0
  32. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/pagerduty.spec.ts +132 -0
  33. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/slack.spec.ts +108 -0
  34. package/edit/monitoring.coreos.com.alertmanagerconfig/types/pagerduty.vue +2 -1
  35. package/edit/monitoring.coreos.com.receiver/__tests__/auth.spec.ts +165 -0
  36. package/edit/monitoring.coreos.com.receiver/__tests__/index.test.ts +153 -0
  37. package/edit/monitoring.coreos.com.receiver/__tests__/tls.spec.ts +115 -0
  38. package/edit/monitoring.coreos.com.receiver/types/__tests__/email.spec.ts +86 -0
  39. package/edit/monitoring.coreos.com.receiver/types/__tests__/opsgenie.spec.ts +209 -0
  40. package/edit/monitoring.coreos.com.receiver/types/__tests__/pagerduty.spec.ts +105 -0
  41. package/edit/monitoring.coreos.com.receiver/types/__tests__/slack.spec.ts +92 -0
  42. package/edit/monitoring.coreos.com.receiver/types/__tests__/webhook.spec.ts +131 -0
  43. package/edit/provisioning.cattle.io.cluster/ingress/IngressCards.vue +14 -12
  44. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -5
  45. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +18 -3
  46. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +100 -76
  47. package/edit/token.vue +29 -68
  48. package/list/provisioning.cattle.io.cluster.vue +2 -2
  49. package/models/__tests__/chart.test.ts +2 -2
  50. package/models/chart.js +3 -3
  51. package/models/token.js +0 -4
  52. package/package.json +8 -8
  53. package/pages/account/index.vue +67 -96
  54. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +108 -24
  55. package/pages/c/_cluster/apps/charts/index.vue +1 -11
  56. package/pages/c/_cluster/explorer/index.vue +2 -19
  57. package/pages/c/_cluster/explorer/tools/index.vue +1 -1
  58. package/pages/c/_cluster/manager/cloudCredential/index.vue +1 -1
  59. package/pages/c/_cluster/uiplugins/index.vue +1 -1
  60. package/pkg/auto-import.js +41 -0
  61. package/plugins/dashboard-store/resource-class.js +2 -2
  62. package/plugins/steve/__tests__/steve-class.test.ts +1 -1
  63. package/plugins/steve/steve-class.js +3 -3
  64. package/plugins/steve/steve-pagination-utils.ts +2 -4
  65. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +7 -7
  66. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +5 -2
  67. package/rancher-components/RcIcon/types.ts +2 -2
  68. package/rancher-components/RcItemCard/RcItemCard.vue +8 -1
  69. package/rancher-components/RcSection/RcSection.test.ts +323 -0
  70. package/rancher-components/RcSection/RcSection.vue +252 -0
  71. package/rancher-components/RcSection/RcSectionActions.test.ts +212 -0
  72. package/rancher-components/RcSection/RcSectionActions.vue +85 -0
  73. package/rancher-components/RcSection/RcSectionBadges.test.ts +149 -0
  74. package/rancher-components/RcSection/RcSectionBadges.vue +29 -0
  75. package/rancher-components/RcSection/index.ts +12 -0
  76. package/rancher-components/RcSection/types.ts +86 -0
  77. package/scripts/test-plugins-build.sh +5 -4
  78. package/types/shell/index.d.ts +93 -108
  79. package/utils/style.ts +17 -0
  80. package/utils/svg-filter.js +4 -3
  81. package/utils/units.js +14 -5
  82. package/models/ext.cattle.io.token.js +0 -48
@@ -29,3 +29,7 @@ $gray003: #FFF;
29
29
  $gray004: #6C6C76;
30
30
  $gray005: #4A4B52;
31
31
  $gray006: #1b1c21;
32
+ $gray007: #161C24;
33
+ $gray008: #262831;
34
+ $gray009: #6C6C77;
35
+ $gray010: #B6B6C3;
@@ -571,17 +571,17 @@
571
571
  --rc-success: #{$green001};
572
572
  --rc-success-secondary: #{$green002};
573
573
 
574
- --rc-warning: #{$yellow001};
575
- --rc-warning-secondary: #{$yellow002};
574
+ --rc-warning: #{$yellow002};
575
+ --rc-warning-secondary: #{$yellow001};
576
576
 
577
577
  --rc-error: #{$red001};
578
578
  --rc-error-secondary: #{$red002};
579
579
 
580
- --rc-unknown: #{$gray001};
581
- --rc-unknown-secondary: #{$gray004};
580
+ --rc-unknown: #{$gray004};
581
+ --rc-unknown-secondary: #{$gray001};
582
582
 
583
- --rc-none: #{$gray002};
584
- --rc-none-secondary: #{$gray004};
583
+ --rc-none: #{$gray004};
584
+ --rc-none-secondary: #{$gray002};
585
585
 
586
586
  --rc-primary-hover: #{$blue003};
587
587
 
@@ -701,17 +701,17 @@ BODY, .theme-light {
701
701
  --rc-success: #{$green001};
702
702
  --rc-success-secondary: #{$green002};
703
703
 
704
- --rc-warning: #{$yellow001};
705
- --rc-warning-secondary: #{$yellow002};
704
+ --rc-warning: #{$yellow002};
705
+ --rc-warning-secondary: #{$yellow001};
706
706
 
707
707
  --rc-error: #{$red001};
708
708
  --rc-error-secondary: #{$red002};
709
709
 
710
- --rc-unknown: #{$gray001};
711
- --rc-unknown-secondary: #{$gray004};
710
+ --rc-unknown: #{$gray004};
711
+ --rc-unknown-secondary: #{$gray001};
712
712
 
713
- --rc-none: #{$gray002};
714
- --rc-none-secondary: #{$gray004};
713
+ --rc-none: #{$gray004};
714
+ --rc-none-secondary: #{$gray002};
715
715
 
716
716
  --rc-primary-hover: #{$blue003};
717
717
 
@@ -724,6 +724,10 @@ BODY, .theme-light {
724
724
  --rc-disabled-background: #{$gray001};
725
725
  --rc-disabled-text-color: #{$gray004};
726
726
 
727
+ --rc-section-background-primary: #{$lightest};
728
+ --rc-section-background-secondary: #{$lighter};
729
+ --rc-section-action-color: #{$gray009};
730
+
727
731
  --rc-image-bg: #{$lightest};
728
732
  --rc-image-color: #{$darkest};
729
733
 
@@ -1068,6 +1072,10 @@ BODY, .theme-dark {
1068
1072
  --rc-disabled-background: #{$gray005};
1069
1073
  --rc-disabled-text-color: #{$gray004};
1070
1074
 
1075
+ --rc-section-background-primary: #{$gray007};
1076
+ --rc-section-background-secondary: #{$gray008};
1077
+ --rc-section-action-color: #{$gray010};
1078
+
1071
1079
  --rc-image-bg: #{$lightest};
1072
1080
  --rc-image-color: #{$darkest};
1073
1081
 
@@ -30,6 +30,7 @@ generic:
30
30
  comma: ', '
31
31
  copy: Copy
32
32
  copyToClipboard: Copy text to Clipboard
33
+ copyValueToClipboard: 'Copy {value} to Clipboard'
33
34
  copiedToClipboard: Text copied to Clipboard
34
35
  create: Create
35
36
  created: Created
@@ -438,7 +439,6 @@ accountAndKeys:
438
439
  notAllowed: You do not have permission to manage API Keys
439
440
  apiEndpoint: "API Endpoint:"
440
441
  copyApiEnpoint: Copy API Endpoint to clipboard
441
- normanTokenDeprecation: The API Keys feature is being migrated to a new API. Any existing API Keys from the legacy API will continue to work, but new API Keys will be created using the new API.
442
442
  add:
443
443
  description:
444
444
  label: Description
@@ -464,9 +464,7 @@ accountAndKeys:
464
464
  month: Months
465
465
  year: Years
466
466
  scope: Scope
467
- userPrincipal: User Principal
468
467
  noScope: No Scope
469
- enabled: Token enabled
470
468
  info:
471
469
  accessKey: Access Key
472
470
  secretKey: Secret Key
@@ -475,7 +473,7 @@ accountAndKeys:
475
473
  keyCreated: A new API Key has been created
476
474
  bearerTokenTip: "Access Key and Secret Key can be sent as the username and password for HTTP Basic auth to authorize requests. You can also combine them to use as a Bearer token:"
477
475
  ttlLimitedWarning: The Expiry time for this API Key was reduced due to system configuration
478
- expiryOptionsWithNever: Since "auth-token-max-ttl-minutes" is set to <= 0, the API Key will not expire unless the "Automatically expire" option is set to "Custom" and a custom expiry time is set.
476
+
479
477
  addClusterMemberDialog:
480
478
  title: Add Cluster Member
481
479
 
@@ -6857,7 +6855,6 @@ storageClass:
6857
6855
  tooltip: By default the default storage class on the host Harvester cluster is used.
6858
6856
 
6859
6857
  tableHeaders:
6860
- isLegacy: Legacy
6861
6858
  assuredConcurrencyShares: Assured Concurrency Shares
6862
6859
  autoscaler: Autoscaler
6863
6860
  accessKey: Access Key
@@ -38,7 +38,35 @@ export default {
38
38
  success-label="Copied!"
39
39
  error-label="Error Copying"
40
40
  v-bind="$attrs"
41
+ :success-color="$attrs['action-color'] || 'role-primary'"
42
+ :waiting-color="$attrs['action-color'] || 'role-primary'"
41
43
  :delay="2000"
42
44
  @click="clicked"
43
45
  />
44
46
  </template>
47
+
48
+ <style lang="scss" scoped>
49
+ .icon-btn {
50
+ min-height: 24px;
51
+ min-width: 24px;
52
+ justify-content: center;
53
+ }
54
+
55
+ .bg-transparent {
56
+ &:active {
57
+ background-color: var(--primary-keyboard-focus);
58
+ color: var(--primary-text);
59
+ }
60
+
61
+ &:focus-visible {
62
+ @include focus-outline;
63
+ }
64
+ }
65
+
66
+ .role-primary {
67
+ &:active {
68
+ background-color: var(--primary-keyboard-focus);
69
+ color: var(--primary-text);
70
+ }
71
+ }
72
+ </style>
@@ -75,6 +75,10 @@ export default {
75
75
  }
76
76
  }
77
77
 
78
+ &:active {
79
+ color: var(--primary-keyboard-focus);
80
+ }
81
+
78
82
  &.copied {
79
83
  pointer-events: none;
80
84
  color: var(--success);
@@ -782,6 +782,7 @@ export default {
782
782
  </div>
783
783
  <slot name="form-footer">
784
784
  <CruResourceFooter
785
+ v-if="!isView"
785
786
  class="cru__footer"
786
787
  :mode="mode"
787
788
  :is-form="showAsForm"
@@ -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();
@@ -18,9 +18,9 @@
18
18
  */
19
19
  import { Solver } from '@shell/utils/svg-filter';
20
20
  import { colorToRgb, mapStandardColors, normalizeHex } from '@shell/utils/color';
21
+ import { mapGetters } from 'vuex';
21
22
 
22
23
  const filterCache = {};
23
- const cssCache = {};
24
24
 
25
25
  const colors = {
26
26
  header: {
@@ -33,7 +33,7 @@ const colors = {
33
33
  },
34
34
  primary: {
35
35
  color: '--on-tertiary',
36
- hover: '--link',
36
+ hover: '--tertiary-hover-app-bar',
37
37
  colorFallback: '--on-tertiary',
38
38
  hoverFallback: '--primary-hover-text',
39
39
  active: '--on-active',
@@ -63,7 +63,12 @@ export default {
63
63
  },
64
64
 
65
65
  data() {
66
- return { className: '' };
66
+ return {
67
+ className: '',
68
+ mainFilter: null,
69
+ hoverFilter: null,
70
+ activeFilter: null,
71
+ };
67
72
  },
68
73
 
69
74
  created() {
@@ -72,6 +77,18 @@ export default {
72
77
  }
73
78
  },
74
79
 
80
+ computed: {
81
+ ...mapGetters({
82
+ brand: 'management/brand',
83
+ theme: 'prefs/theme',
84
+ })
85
+ },
86
+
87
+ watch: {
88
+ brand: 'recomputeColor',
89
+ theme: 'recomputeColor',
90
+ },
91
+
75
92
  methods: {
76
93
  getComputedStyleFor(cssVar, fallback) {
77
94
  const value = window.getComputedStyle(document.body).getPropertyValue(cssVar).trim();
@@ -86,11 +103,11 @@ export default {
86
103
 
87
104
  const solver = new Solver(rgb);
88
105
  const res = solver.solve();
89
- const filter = res?.filter;
106
+ const filterVal = res?.filterVal;
90
107
 
91
- filterCache[cacheKey] = filter;
108
+ filterCache[cacheKey] = filterVal;
92
109
 
93
- return filter;
110
+ return filterVal;
94
111
  },
95
112
 
96
113
  setColor() {
@@ -111,43 +128,23 @@ export default {
111
128
 
112
129
  const className = `svg-icon-${ uiColorStr }-${ hoverColorStr }`;
113
130
 
114
- if (!cssCache[className]) {
115
- const hoverFilter = this.resolveColorFilter(hoverColor, hoverColorRGB);
116
- const mainFilter = this.resolveColorFilter(uiColor, uiColorRGB);
117
- const activeFilter = this.resolveColorFilter(activeColor, activeColorRGB);
118
-
119
- // Add stylesheet (added as global styles)
120
- const styles = `
121
- img.${ className } {
122
- ${ mainFilter };
123
- }
124
- img.${ className }:hover {
125
- ${ hoverFilter };
126
- }
127
- button:hover > img.${ className } {
128
- ${ hoverFilter };
129
- }
130
- li:hover > img.${ className } {
131
- ${ hoverFilter };
132
- }
133
- a.option:hover > img.${ className } {
134
- ${ hoverFilter };
135
- }
136
- a.option.active-menu-link > img.${ className } {
137
- ${ activeFilter };
138
- }
139
- `;
140
-
141
- const styleSheet = document.createElement('style');
142
-
143
- styleSheet.innerText = styles;
144
- document.head.appendChild(styleSheet);
145
-
146
- cssCache[className] = true;
147
- }
131
+ this.hoverFilter = this.resolveColorFilter(hoverColor, hoverColorRGB);
132
+ this.mainFilter = this.resolveColorFilter(uiColor, uiColorRGB);
133
+ this.activeFilter = this.resolveColorFilter(activeColor, activeColorRGB);
148
134
 
149
135
  this['className'] = className;
150
- }
136
+ },
137
+
138
+ recomputeColor() {
139
+ if (!this.src) {
140
+ return;
141
+ }
142
+
143
+ this.mainFilter = null;
144
+ this.hoverFilter = null;
145
+ this.activeFilter = null;
146
+ this.setColor();
147
+ },
151
148
  }
152
149
  };
153
150
  </script>
@@ -172,8 +169,30 @@ export default {
172
169
  </template>
173
170
 
174
171
  <style lang="scss" scoped>
175
- .svg-icon {
172
+ img.svg-icon {
173
+ filter: v-bind(mainFilter);
174
+ }
175
+
176
+ button:hover > img.svg-icon,
177
+ li:hover > img.svg-icon {
178
+ filter: v-bind(hoverFilter);
179
+ }
180
+
181
+ .side-menu .category div a > img.svg-icon {
176
182
  height: 24px;
177
183
  width: 24px;
184
+ filter: v-bind(mainFilter);
185
+ }
186
+
187
+ .side-menu .category div a:hover > img.svg-icon {
188
+ filter: v-bind(hoverFilter);
189
+ }
190
+
191
+ .side-menu .category div a.active-menu-link > img.svg-icon {
192
+ filter: v-bind(activeFilter);
193
+
194
+ &:hover {
195
+ filter: v-bind(activeFilter);
196
+ }
178
197
  }
179
198
  </style>
@@ -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
 
@@ -2099,13 +2099,13 @@ export default {
2099
2099
 
2100
2100
  $header-padding: 20px;
2101
2101
  .sub-header-row {
2102
- padding: 0 0 $header-padding / 2 0;
2102
+ padding: 0 0 calc($header-padding / 2) 0;
2103
2103
  }
2104
2104
 
2105
2105
  .fixed-header-actions {
2106
2106
  padding: 0 0 $header-padding 0;
2107
2107
  &.with-sub-header {
2108
- padding: 0 0 $header-padding / 4 0;
2108
+ padding: 0 0 calc($header-padding / 4) 0;
2109
2109
  }
2110
2110
 
2111
2111
  width: 100%;
@@ -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, {
@@ -9,7 +9,7 @@ import { ToggleSwitch } from '@components/Form/ToggleSwitch';
9
9
  import { TextAreaAutoGrow } from '@components/Form/TextArea';
10
10
  import { Banner } from '@components/Banner';
11
11
 
12
- export default ({
12
+ export default {
13
13
  name: 'BannerSettings',
14
14
 
15
15
  props: {
@@ -112,7 +112,7 @@ export default ({
112
112
  return this.bannerType === 'bannerConsent';
113
113
  }
114
114
  }
115
- });
115
+ };
116
116
  </script>
117
117
 
118
118
  <template>
@@ -3,7 +3,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
3
3
  import { Checkbox } from '@components/Form/Checkbox';
4
4
  import { _EDIT, _VIEW } from '@shell/config/query-params';
5
5
 
6
- export default ({
6
+ export default {
7
7
 
8
8
  name: 'NotificationSettings',
9
9
 
@@ -29,7 +29,7 @@ export default ({
29
29
  },
30
30
  },
31
31
 
32
- });
32
+ };
33
33
  </script>
34
34
 
35
35
  <template>
@@ -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
  }
@@ -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',
@@ -202,7 +202,6 @@ export function init(store) {
202
202
  ]);
203
203
 
204
204
  headers(EXT.KUBECONFIG, [
205
- STATE,
206
205
  {
207
206
  name: 'clusters',
208
207
  labelKey: 'tableHeaders.clusters',
@@ -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 = {