@rancher/shell 3.0.9 → 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 (45) 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/ResourceDetail/index.vue +0 -21
  10. package/components/__tests__/CruResource.test.ts +35 -1
  11. package/composables/useIsNewDetailPageEnabled.test.ts +98 -0
  12. package/composables/useIsNewDetailPageEnabled.ts +12 -0
  13. package/config/product/explorer.js +11 -1
  14. package/config/table-headers.js +0 -9
  15. package/config/types.js +0 -1
  16. package/edit/auth/github-app-steps.vue +2 -0
  17. package/edit/auth/github-steps.vue +2 -0
  18. package/edit/management.cattle.io.user.vue +60 -35
  19. package/edit/token.vue +29 -68
  20. package/models/token.js +0 -4
  21. package/package.json +8 -8
  22. package/pages/account/index.vue +67 -96
  23. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +66 -9
  24. package/pages/c/_cluster/explorer/index.vue +2 -19
  25. package/pkg/auto-import.js +41 -0
  26. package/plugins/dashboard-store/resource-class.js +2 -2
  27. package/plugins/steve/__tests__/steve-class.test.ts +1 -1
  28. package/plugins/steve/steve-class.js +3 -3
  29. package/plugins/steve/steve-pagination-utils.ts +2 -4
  30. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +7 -7
  31. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +5 -2
  32. package/rancher-components/RcIcon/types.ts +2 -2
  33. package/rancher-components/RcSection/RcSection.test.ts +323 -0
  34. package/rancher-components/RcSection/RcSection.vue +252 -0
  35. package/rancher-components/RcSection/RcSectionActions.test.ts +212 -0
  36. package/rancher-components/RcSection/RcSectionActions.vue +85 -0
  37. package/rancher-components/RcSection/RcSectionBadges.test.ts +149 -0
  38. package/rancher-components/RcSection/RcSectionBadges.vue +29 -0
  39. package/rancher-components/RcSection/index.ts +12 -0
  40. package/rancher-components/RcSection/types.ts +86 -0
  41. package/scripts/test-plugins-build.sh +5 -4
  42. package/types/shell/index.d.ts +92 -108
  43. package/utils/style.ts +17 -0
  44. package/utils/units.js +14 -5
  45. package/models/ext.cattle.io.token.js +0 -48
@@ -11,6 +11,43 @@ function replaceAll(str, find, replace) {
11
11
  return str.split(find).join(replace);
12
12
  }
13
13
 
14
+ // Injected at the top of every generated importTypes() function.
15
+ // Ensures both $extension (newer Rancher) and $plugin (older Rancher) are available on
16
+ // Vue globalProperties, regardless of which one the host injected. This makes all
17
+ // extensions compatible across Rancher versions without any per-extension code changes.
18
+ const COMPAT_SHIM = ` if (typeof document !== 'undefined') {
19
+ var patchGlobalProps = function() {
20
+ var __vueApp = document.getElementById('app').__vue_app__;
21
+
22
+ if (!__vueApp) {
23
+ // no __vue_app__, vueApp.mount('#app') has not been called yet
24
+ return false;
25
+ }
26
+
27
+ if (__vueApp.config && __vueApp.config.globalProperties) {
28
+ var __gp = __vueApp.config.globalProperties;
29
+ if (!__gp.$extension && __gp.$plugin) { __gp.$extension = __gp.$plugin; }
30
+ else if (!__gp.$plugin && __gp.$extension) { __gp.$plugin = __gp.$extension; }
31
+ return true;
32
+ }
33
+
34
+ // Fallback to failure case
35
+ return false;
36
+ };
37
+
38
+ if (!patchGlobalProps()) {
39
+ // Could not patch, keep retrying until it works
40
+ var __retry = setInterval(function() {
41
+ if (patchGlobalProps()) {
42
+ clearInterval(__retry);
43
+ }
44
+ }, 100);
45
+
46
+ // Fallback: clear interval after 10 seconds just in case
47
+ setTimeout(function() { clearInterval(__retry); }, 10000);
48
+ }
49
+ }\n`;
50
+
14
51
  function registerFile(file, type, pkg, f) {
15
52
  const importType = (f === 'models') ? 'require' : 'import';
16
53
  const chunkName = (f === 'l10n') ? '' : `/* webpackChunkName: "${ f }" */`;
@@ -31,6 +68,8 @@ function register(file, pkg, f) {
31
68
  function generateTypeImport(pkg, dir) {
32
69
  let content = 'export function importTypes($extension) { \n';
33
70
 
71
+ content += COMPAT_SHIM;
72
+
34
73
  // Auto-import if the folder exists
35
74
  contextFolders.forEach((f) => {
36
75
  const filePath = path.join(dir, f);
@@ -79,6 +118,8 @@ function generateDynamicTypeImport(pkg, dir) {
79
118
  const template = fs.readFileSync(path.join(__dirname, 'import.js'), { encoding: 'utf8' });
80
119
  let content = 'export function importTypes($extension) { \n';
81
120
 
121
+ content += COMPAT_SHIM;
122
+
82
123
  // Auto-import if the folder exists
83
124
  contextFolders.forEach((f) => {
84
125
  if (fs.existsSync(path.join(dir, f))) {
@@ -1193,7 +1193,7 @@ export default class Resource {
1193
1193
  * Allow to handle the response of the save request
1194
1194
  * @param {*} res Full request response
1195
1195
  */
1196
- processSaveResponse(res) { }
1196
+ processSaveResponse(res, opt = {}) { }
1197
1197
 
1198
1198
  async _save(opt = { }) {
1199
1199
  const forNew = !this.id;
@@ -1280,7 +1280,7 @@ export default class Resource {
1280
1280
  const res = await this.$dispatch('request', { opt, type: this.type } );
1281
1281
 
1282
1282
  // Allow to process response independently from the related models
1283
- this.processSaveResponse(res);
1283
+ this.processSaveResponse(res, opt);
1284
1284
 
1285
1285
  // Steve sometimes returns Table responses instead of the resource you just saved.. ignore
1286
1286
  if ( res && res.kind !== 'Table') {
@@ -74,7 +74,7 @@ describe('class: Steve', () => {
74
74
 
75
75
  steve.processSaveResponse(response);
76
76
 
77
- expect(parentProcessSaveResponse).toHaveBeenCalledWith(response);
77
+ expect(parentProcessSaveResponse).toHaveBeenCalledWith(response, {});
78
78
  });
79
79
 
80
80
  describe('growl notifications', () => {
@@ -70,11 +70,11 @@ export default class SteveModel extends HybridModel {
70
70
  *
71
71
  * @param {*} res
72
72
  */
73
- processSaveResponse(res) {
74
- super.processSaveResponse(res);
73
+ processSaveResponse(res, opt = {}) {
74
+ super.processSaveResponse(res, opt);
75
75
 
76
76
  // Conditionally show the growl for autogenerated names
77
- if (res && res._status === 201 && res.metadata?.generateName && res.id) {
77
+ if (res && res._status === 201 && res.metadata?.generateName && res.id && !opt.suppressSuccessToast) {
78
78
  // Split to remove the namespace if present (default/generated-xxx)
79
79
  const nameOnly = res.id.split('/').pop();
80
80
 
@@ -15,8 +15,7 @@ import {
15
15
  INGRESS,
16
16
  WORKLOAD_TYPES,
17
17
  HPA,
18
- SECRET,
19
- EXT
18
+ SECRET
20
19
  } from '@shell/config/types';
21
20
  import { CAPI as CAPI_LAB_AND_ANO, CATTLE_PUBLIC_ENDPOINTS, STORAGE, UI_PROJECT_SECRET_COPY } from '@shell/config/labels-annotations';
22
21
  import { Schema } from '@shell/plugins/steve/schema';
@@ -768,8 +767,7 @@ export const PAGINATION_SETTINGS_STORE_DEFAULTS: PaginationSettingsStores = {
768
767
  { resource: MANAGEMENT.CLUSTER, context: ['side-bar'] },
769
768
  { resource: CATALOG.APP, context: ['branding'] },
770
769
  SECRET,
771
- CAPI.MACHINE_SET,
772
- EXT.TOKEN
770
+ CAPI.MACHINE_SET
773
771
  ],
774
772
  generic: false,
775
773
  }
@@ -11,27 +11,27 @@ const displayCount = computed(() => props.count < 1000 ? props.count : '999+');
11
11
  :class="{[props.type]: true, disabled: props.disabled}"
12
12
  data-testid="rc-counter-badge"
13
13
  >
14
- {{ displayCount }}
14
+ <span class="count">{{ displayCount }}</span>
15
15
  </div>
16
16
  </template>
17
17
 
18
18
  <style lang="scss" scoped>
19
19
  .rc-counter-badge {
20
+ box-sizing: border-box;
21
+ height: 21px;
22
+
20
23
  display: inline-flex;
21
- padding: 1px 8px;
24
+ padding: 2px 8px;
22
25
  align-items: center;
23
- gap: 8px;
24
26
 
25
27
  border-radius: 30px;
26
28
  border: 1px solid var(--rc-active-border);
27
29
 
28
- overflow: hidden;
29
- text-overflow: ellipsis;
30
30
  font-family: Lato;
31
- font-size: 13px;
31
+ font-size: 12px;
32
32
  font-style: normal;
33
33
  font-weight: 400;
34
- line-height: 22px;
34
+ line-height: 17px;
35
35
  color: var(--body-text);
36
36
 
37
37
  &.active {
@@ -20,17 +20,20 @@ const { backgroundColor, borderColor, textColor } = useStatusColors(status, 'out
20
20
 
21
21
  <style lang="scss" scoped>
22
22
  .rc-status-badge {
23
+ box-sizing: border-box;
24
+ height: 21px;
25
+
23
26
  display: inline-flex;
24
27
  align-items: center;
25
28
  justify-content: center;
26
- padding: 1px 7px;
29
+ padding: 2px 7px;
27
30
 
28
31
  border: 1px solid transparent;
29
32
  border-radius: 30px;
30
33
 
31
34
  font-family: Lato;
32
35
  font-size: 12px;
33
- line-height: 19px;
36
+ line-height: 17px;
34
37
 
35
38
  background-color: v-bind(backgroundColor);
36
39
  border-color: v-bind(borderColor);
@@ -148,8 +148,8 @@ export const RcIconTypeToClass = {
148
148
  export const RcIconSizeToCSS = {
149
149
  xxlarge: '40px',
150
150
  xlarge: '32px',
151
- large: '25px',
152
- medium: '18px',
151
+ large: '24px',
152
+ medium: '16px',
153
153
  small: '14px',
154
154
  inherit: 'inherit'
155
155
  };
@@ -0,0 +1,323 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import RcSection from './RcSection.vue';
3
+
4
+ describe('component: RcSection', () => {
5
+ const defaultProps = {
6
+ type: 'primary' as const,
7
+ mode: 'with-header' as const,
8
+ background: 'primary' as const,
9
+ expandable: false,
10
+ title: 'Test title',
11
+ };
12
+
13
+ describe('type prop', () => {
14
+ it('should apply type-primary class when type is "primary"', () => {
15
+ const wrapper = mount(RcSection, { props: { ...defaultProps, type: 'primary' } });
16
+
17
+ expect(wrapper.find('.rc-section').classes()).toContain('type-primary');
18
+ });
19
+
20
+ it('should apply type-secondary class when type is "secondary"', () => {
21
+ const wrapper = mount(RcSection, { props: { ...defaultProps, type: 'secondary' } });
22
+
23
+ expect(wrapper.find('.rc-section').classes()).toContain('type-secondary');
24
+ });
25
+ });
26
+
27
+ describe('background prop', () => {
28
+ it('should apply bg-primary class when background is "primary"', () => {
29
+ const wrapper = mount(RcSection, { props: { ...defaultProps, background: 'primary' } });
30
+
31
+ expect(wrapper.find('.rc-section').classes()).toContain('bg-primary');
32
+ });
33
+
34
+ it('should apply bg-secondary class when background is "secondary"', () => {
35
+ const wrapper = mount(RcSection, { props: { ...defaultProps, background: 'secondary' } });
36
+
37
+ expect(wrapper.find('.rc-section').classes()).toContain('bg-secondary');
38
+ });
39
+
40
+ it('should default to "primary" background when no background prop and no parent', () => {
41
+ const { background: _, ...propsWithoutBg } = defaultProps;
42
+ const wrapper = mount(RcSection, { props: propsWithoutBg });
43
+
44
+ expect(wrapper.find('.rc-section').classes()).toContain('bg-primary');
45
+ });
46
+
47
+ it('should alternate background from parent via provide/inject', () => {
48
+ const wrapper = mount(RcSection, {
49
+ props: {
50
+ ...defaultProps, background: 'primary', expanded: true
51
+ },
52
+ slots: {
53
+ default: {
54
+ components: { RcSection },
55
+ template: '<RcSection type="secondary" mode="with-header" :expandable="false" title="Child" />',
56
+ },
57
+ },
58
+ });
59
+
60
+ const childSection = wrapper.findAll('.rc-section')[1];
61
+
62
+ expect(childSection.classes()).toContain('bg-secondary');
63
+ });
64
+
65
+ it('should allow explicit background to override the injected alternation', () => {
66
+ const wrapper = mount(RcSection, {
67
+ props: {
68
+ ...defaultProps, background: 'primary', expanded: true
69
+ },
70
+ slots: {
71
+ default: {
72
+ components: { RcSection },
73
+ template: '<RcSection type="secondary" mode="with-header" :expandable="false" background="primary" title="Child" />',
74
+ },
75
+ },
76
+ });
77
+
78
+ const childSection = wrapper.findAll('.rc-section')[1];
79
+
80
+ expect(childSection.classes()).toContain('bg-primary');
81
+ });
82
+ });
83
+
84
+ describe('mode prop', () => {
85
+ it('should render section-header when mode is "with-header"', () => {
86
+ const wrapper = mount(RcSection, { props: { ...defaultProps, mode: 'with-header' } });
87
+
88
+ expect(wrapper.find('.section-header').exists()).toBe(true);
89
+ });
90
+
91
+ it('should not render section-header when mode is "no-header"', () => {
92
+ const wrapper = mount(RcSection, { props: { ...defaultProps, mode: 'no-header' } });
93
+
94
+ expect(wrapper.find('.section-header').exists()).toBe(false);
95
+ });
96
+
97
+ it('should apply no-header class to content when mode is "no-header"', () => {
98
+ const wrapper = mount(RcSection, { props: { ...defaultProps, mode: 'no-header' } });
99
+
100
+ expect(wrapper.find('.section-content').classes()).toContain('no-header');
101
+ });
102
+ });
103
+
104
+ describe('title prop', () => {
105
+ it('should render the title text', () => {
106
+ const wrapper = mount(RcSection, { props: { ...defaultProps, title: 'My Section' } });
107
+
108
+ expect(wrapper.find('.title').text()).toBe('My Section');
109
+ });
110
+
111
+ it('should render the title slot when provided', () => {
112
+ const wrapper = mount(RcSection, {
113
+ props: { ...defaultProps },
114
+ slots: { title: '<span class="custom-title">Custom</span>' },
115
+ });
116
+
117
+ expect(wrapper.find('.custom-title').exists()).toBe(true);
118
+ expect(wrapper.find('.custom-title').text()).toBe('Custom');
119
+ });
120
+ });
121
+
122
+ describe('expandable behavior', () => {
123
+ it('should render toggle button when expandable is true', () => {
124
+ const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: true } });
125
+
126
+ expect(wrapper.find('.toggle-button').exists()).toBe(true);
127
+ });
128
+
129
+ it('should not render toggle button when expandable is false', () => {
130
+ const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: false } });
131
+
132
+ expect(wrapper.find('.toggle-button').exists()).toBe(false);
133
+ });
134
+
135
+ it('should set aria-expanded on toggle button when expandable', () => {
136
+ const wrapper = mount(RcSection, {
137
+ props: {
138
+ ...defaultProps, expandable: true, expanded: true
139
+ }
140
+ });
141
+
142
+ expect(wrapper.find('.toggle-button').attributes('aria-expanded')).toBe('true');
143
+ });
144
+
145
+ it('should set aria-expanded="false" on toggle button when collapsed', () => {
146
+ const wrapper = mount(RcSection, {
147
+ props: {
148
+ ...defaultProps, expandable: true, expanded: false
149
+ }
150
+ });
151
+
152
+ expect(wrapper.find('.toggle-button').attributes('aria-expanded')).toBe('false');
153
+ });
154
+
155
+ it('should set aria-label to "Collapse section" on toggle button when expanded', () => {
156
+ const wrapper = mount(RcSection, {
157
+ props: {
158
+ ...defaultProps, expandable: true, expanded: true
159
+ }
160
+ });
161
+
162
+ expect(wrapper.find('.toggle-button').attributes('aria-label')).toBe('Collapse section');
163
+ });
164
+
165
+ it('should set aria-label to "Expand section" on toggle button when collapsed', () => {
166
+ const wrapper = mount(RcSection, {
167
+ props: {
168
+ ...defaultProps, expandable: true, expanded: false
169
+ }
170
+ });
171
+
172
+ expect(wrapper.find('.toggle-button').attributes('aria-label')).toBe('Expand section');
173
+ });
174
+
175
+ it('should emit update:expanded with false when clicking an expanded header', async() => {
176
+ const wrapper = mount(RcSection, {
177
+ props: {
178
+ ...defaultProps, expandable: true, expanded: true
179
+ }
180
+ });
181
+
182
+ await wrapper.find('.section-header').trigger('click');
183
+
184
+ expect(wrapper.emitted('update:expanded')).toHaveLength(1);
185
+ expect(wrapper.emitted('update:expanded')![0]).toStrictEqual([false]);
186
+ });
187
+
188
+ it('should emit update:expanded with true when clicking a collapsed header', async() => {
189
+ const wrapper = mount(RcSection, {
190
+ props: {
191
+ ...defaultProps, expandable: true, expanded: false
192
+ }
193
+ });
194
+
195
+ await wrapper.find('.section-header').trigger('click');
196
+
197
+ expect(wrapper.emitted('update:expanded')).toHaveLength(1);
198
+ expect(wrapper.emitted('update:expanded')![0]).toStrictEqual([true]);
199
+ });
200
+
201
+ it('should not emit update:expanded when clicking a non-expandable header', async() => {
202
+ const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: false } });
203
+
204
+ await wrapper.find('.section-header').trigger('click');
205
+
206
+ expect(wrapper.emitted('update:expanded')).toBeUndefined();
207
+ });
208
+
209
+ it('should emit update:expanded when toggle button is clicked', async() => {
210
+ const wrapper = mount(RcSection, {
211
+ props: {
212
+ ...defaultProps, expandable: true, expanded: true
213
+ }
214
+ });
215
+
216
+ await wrapper.find('.toggle-button').trigger('click');
217
+
218
+ expect(wrapper.emitted('update:expanded')).toHaveLength(1);
219
+ expect(wrapper.emitted('update:expanded')![0]).toStrictEqual([false]);
220
+ });
221
+ });
222
+
223
+ describe('expanded prop', () => {
224
+ it('should default expanded to true', () => {
225
+ const wrapper = mount(RcSection, { props: { ...defaultProps, expandable: true } });
226
+
227
+ expect(wrapper.find('.section-content').exists()).toBe(true);
228
+ });
229
+
230
+ it('should render content when expanded is true', () => {
231
+ const wrapper = mount(RcSection, {
232
+ props: { ...defaultProps, expanded: true },
233
+ slots: { default: '<p>Content</p>' },
234
+ });
235
+
236
+ expect(wrapper.find('.section-content').exists()).toBe(true);
237
+ expect(wrapper.find('p').text()).toBe('Content');
238
+ });
239
+
240
+ it('should hide content when expanded is false', () => {
241
+ const wrapper = mount(RcSection, {
242
+ props: { ...defaultProps, expanded: false },
243
+ slots: { default: '<p>Content</p>' },
244
+ });
245
+
246
+ expect(wrapper.find('.section-content').exists()).toBe(false);
247
+ });
248
+
249
+ it('should apply expandable-content class when expandable is true', () => {
250
+ const wrapper = mount(RcSection, {
251
+ props: {
252
+ ...defaultProps, expandable: true, expanded: true
253
+ }
254
+ });
255
+
256
+ expect(wrapper.find('.section-content').classes()).toContain('expandable-content');
257
+ });
258
+
259
+ it('should not apply expandable-content class when expandable is false', () => {
260
+ const wrapper = mount(RcSection, {
261
+ props: {
262
+ ...defaultProps, expandable: false, expanded: true
263
+ }
264
+ });
265
+
266
+ expect(wrapper.find('.section-content').classes()).not.toContain('expandable-content');
267
+ });
268
+
269
+ it('should add collapsed class to header when not expanded', () => {
270
+ const wrapper = mount(RcSection, {
271
+ props: {
272
+ ...defaultProps, expandable: true, expanded: false
273
+ }
274
+ });
275
+
276
+ expect(wrapper.find('.section-header').classes()).toContain('collapsed');
277
+ });
278
+ });
279
+
280
+ describe('slots', () => {
281
+ it('should render badges slot inside right-wrapper', () => {
282
+ const wrapper = mount(RcSection, {
283
+ props: { ...defaultProps },
284
+ slots: { badges: '<span class="test-badge">Badge</span>' },
285
+ });
286
+
287
+ expect(wrapper.find('.right-wrapper .status-badges .test-badge').exists()).toBe(true);
288
+ });
289
+
290
+ it('should render actions slot inside right-wrapper', () => {
291
+ const wrapper = mount(RcSection, {
292
+ props: { ...defaultProps },
293
+ slots: { actions: '<button class="test-action">Act</button>' },
294
+ });
295
+
296
+ expect(wrapper.find('.right-wrapper .actions .test-action').exists()).toBe(true);
297
+ });
298
+
299
+ it('should not render right-wrapper when no badges or actions slots', () => {
300
+ const wrapper = mount(RcSection, { props: { ...defaultProps } });
301
+
302
+ expect(wrapper.find('.right-wrapper').exists()).toBe(false);
303
+ });
304
+
305
+ it('should render counter slot', () => {
306
+ const wrapper = mount(RcSection, {
307
+ props: { ...defaultProps },
308
+ slots: { counter: '<span class="test-counter">5</span>' },
309
+ });
310
+
311
+ expect(wrapper.find('.test-counter').exists()).toBe(true);
312
+ });
313
+
314
+ it('should render errors slot', () => {
315
+ const wrapper = mount(RcSection, {
316
+ props: { ...defaultProps },
317
+ slots: { errors: '<span class="test-error">!</span>' },
318
+ });
319
+
320
+ expect(wrapper.find('.test-error').exists()).toBe(true);
321
+ });
322
+ });
323
+ });