@rancher/shell 3.0.12-rc.6 → 3.0.12-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apis/intf/shell-api/slide-in.ts +46 -17
- package/apis/shell/__tests__/slide-in.test.ts +83 -2
- package/apis/shell/slide-in.ts +20 -0
- package/components/ExplorerProjectsNamespaces.vue +2 -2
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +9 -0
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +5 -1
- package/components/SlideInPanelManager.vue +103 -25
- package/components/__tests__/SlideInPanelManager.spec.ts +153 -10
- package/components/auth/AuthBanner.vue +1 -1
- package/core/plugins-loader.js +2 -0
- package/models/__tests__/namespace.test.ts +133 -8
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +57 -1
- package/models/namespace.js +34 -2
- package/models/provisioning.cattle.io.cluster.js +20 -13
- package/package.json +1 -1
- package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +136 -1
- package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +69 -1
- package/pages/c/_cluster/fleet/index.vue +5 -9
- package/pkg/vue.config.js +4 -3
- package/plugins/codemirror.js +2 -0
- package/plugins/dashboard-store/resource-class.js +3 -6
- package/store/__tests__/action-menu.test.ts +622 -0
- package/store/__tests__/slideInPanel.test.ts +143 -43
- package/store/__tests__/wm.test.ts +503 -0
- package/store/aws.js +19 -4
- package/store/slideInPanel.ts +15 -3
- package/types/shell/index.d.ts +26 -2
- package/utils/__tests__/fleet-appco.test.ts +23 -0
- package/utils/fleet-appco.ts +6 -1
- package/utils/sort.js +1 -1
|
@@ -5,7 +5,7 @@ import SlideInPanelManager from '@shell/components/SlideInPanelManager.vue';
|
|
|
5
5
|
|
|
6
6
|
const MockComponent = {
|
|
7
7
|
template: '<div data-testid="slide-in-panel-component">Mock Panel Content</div>',
|
|
8
|
-
props: ['
|
|
8
|
+
props: ['title', 'extraProp']
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
describe('slideInPanelManager.vue with Teleport', () => {
|
|
@@ -23,7 +23,7 @@ describe('slideInPanelManager.vue with Teleport', () => {
|
|
|
23
23
|
'slideInPanel/isOpen': () => true,
|
|
24
24
|
'slideInPanel/component': () => MockComponent,
|
|
25
25
|
'slideInPanel/componentProps': () => ({
|
|
26
|
-
width: '
|
|
26
|
+
width: 'default', title: 'Test Title', extraProp: 'extra'
|
|
27
27
|
})
|
|
28
28
|
};
|
|
29
29
|
|
|
@@ -61,15 +61,14 @@ describe('slideInPanelManager.vue with Teleport', () => {
|
|
|
61
61
|
|
|
62
62
|
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
63
63
|
|
|
64
|
-
expect(styleAttr).toContain('width:
|
|
64
|
+
expect(styleAttr).toContain('width: 33%');
|
|
65
65
|
expect(styleAttr).toContain('top: 55px');
|
|
66
66
|
expect(styleAttr).toContain('height: calc(100vh - 55px)');
|
|
67
67
|
expect(styleAttr).toContain('right: 0');
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
it('
|
|
71
|
-
|
|
72
|
-
getters['slideInPanel/componentProps'] = () => ({ width: '40%' });
|
|
70
|
+
it('hides header when no title is provided', async() => {
|
|
71
|
+
getters['slideInPanel/componentProps'] = () => ({});
|
|
73
72
|
store = createStore({
|
|
74
73
|
getters,
|
|
75
74
|
mutations: { 'slideInPanel/close': jest.fn() }
|
|
@@ -77,9 +76,9 @@ describe('slideInPanelManager.vue with Teleport', () => {
|
|
|
77
76
|
factory();
|
|
78
77
|
await nextTick();
|
|
79
78
|
|
|
80
|
-
const
|
|
79
|
+
const header = document.querySelector('#slides #slide-in-panel-manager .header') as HTMLElement;
|
|
81
80
|
|
|
82
|
-
expect(
|
|
81
|
+
expect(header).toBeNull();
|
|
83
82
|
});
|
|
84
83
|
|
|
85
84
|
it('computes panelTop correctly when a banner exists', async() => {
|
|
@@ -122,8 +121,8 @@ describe('slideInPanelManager.vue with Teleport', () => {
|
|
|
122
121
|
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
123
122
|
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
124
123
|
|
|
125
|
-
// With
|
|
126
|
-
expect(styleAttr).toContain('right: -
|
|
124
|
+
// With default width preset (33%), panelRight should be "-33%" when closed.
|
|
125
|
+
expect(styleAttr).toContain('right: -33%');
|
|
127
126
|
});
|
|
128
127
|
|
|
129
128
|
it('calls store commit when clicking on the slide-in glass overlay', async() => {
|
|
@@ -163,4 +162,148 @@ describe('slideInPanelManager.vue with Teleport', () => {
|
|
|
163
162
|
|
|
164
163
|
expect(closeMutation).toHaveBeenCalledWith({}, undefined);
|
|
165
164
|
});
|
|
165
|
+
|
|
166
|
+
describe('width preset', () => {
|
|
167
|
+
it('resolves width "wide" to 73%', async() => {
|
|
168
|
+
getters['slideInPanel/componentProps'] = () => ({ width: 'wide', title: 'Test' });
|
|
169
|
+
store = createStore({
|
|
170
|
+
getters,
|
|
171
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
172
|
+
});
|
|
173
|
+
factory();
|
|
174
|
+
await nextTick();
|
|
175
|
+
|
|
176
|
+
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
177
|
+
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
178
|
+
|
|
179
|
+
expect(styleAttr).toContain('width: 73%');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('defaults to 33% when no width config is provided', async() => {
|
|
183
|
+
getters['slideInPanel/componentProps'] = () => ({ title: 'Test' });
|
|
184
|
+
store = createStore({
|
|
185
|
+
getters,
|
|
186
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
187
|
+
});
|
|
188
|
+
factory();
|
|
189
|
+
await nextTick();
|
|
190
|
+
|
|
191
|
+
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
192
|
+
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
193
|
+
|
|
194
|
+
expect(styleAttr).toContain('width: 33%');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('height preset', () => {
|
|
199
|
+
it('resolves height "full" to 100vh with top 0', async() => {
|
|
200
|
+
getters['slideInPanel/componentProps'] = () => ({ height: 'full', title: 'Test' });
|
|
201
|
+
store = createStore({
|
|
202
|
+
getters,
|
|
203
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
204
|
+
});
|
|
205
|
+
factory();
|
|
206
|
+
await nextTick();
|
|
207
|
+
|
|
208
|
+
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
209
|
+
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
210
|
+
|
|
211
|
+
expect(styleAttr).toContain('height: 100vh');
|
|
212
|
+
expect(styleAttr).toContain('top: 0');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('applies elevated z-index when panelHeight is "full"', async() => {
|
|
216
|
+
getters['slideInPanel/componentProps'] = () => ({ height: 'full', title: 'Test' });
|
|
217
|
+
store = createStore({
|
|
218
|
+
getters,
|
|
219
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
220
|
+
});
|
|
221
|
+
factory();
|
|
222
|
+
await nextTick();
|
|
223
|
+
|
|
224
|
+
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
225
|
+
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
226
|
+
|
|
227
|
+
expect(styleAttr).toContain('z-index: 102');
|
|
228
|
+
|
|
229
|
+
const slideGlass = document.querySelector('[data-testid="slide-in-glass"]') as HTMLElement;
|
|
230
|
+
const glassStyle = slideGlass.getAttribute('style') || '';
|
|
231
|
+
|
|
232
|
+
expect(glassStyle).toContain('z-index: 101');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('does not apply elevated z-index for default panelHeight', async() => {
|
|
236
|
+
getters['slideInPanel/componentProps'] = () => ({ title: 'Test' });
|
|
237
|
+
store = createStore({
|
|
238
|
+
getters,
|
|
239
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
240
|
+
});
|
|
241
|
+
factory();
|
|
242
|
+
await nextTick();
|
|
243
|
+
|
|
244
|
+
const slidePanel = document.querySelector('#slides .slide-in') as HTMLElement;
|
|
245
|
+
const styleAttr = slidePanel.getAttribute('style') || '';
|
|
246
|
+
|
|
247
|
+
expect(styleAttr).not.toContain('z-index: 102');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('showHeader inferred from title', () => {
|
|
252
|
+
it('shows header when title is provided', async() => {
|
|
253
|
+
getters['slideInPanel/componentProps'] = () => ({ title: 'My Panel' });
|
|
254
|
+
store = createStore({
|
|
255
|
+
getters,
|
|
256
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
257
|
+
});
|
|
258
|
+
factory();
|
|
259
|
+
await nextTick();
|
|
260
|
+
|
|
261
|
+
const header = document.querySelector('#slides .slide-in .header') as HTMLElement;
|
|
262
|
+
|
|
263
|
+
expect(header).toBeTruthy();
|
|
264
|
+
expect(header.textContent?.trim()).toContain('My Panel');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('hides header when no title is provided', async() => {
|
|
268
|
+
getters['slideInPanel/componentProps'] = () => ({});
|
|
269
|
+
store = createStore({
|
|
270
|
+
getters,
|
|
271
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
272
|
+
});
|
|
273
|
+
factory();
|
|
274
|
+
await nextTick();
|
|
275
|
+
|
|
276
|
+
const header = document.querySelector('#slides .slide-in .header') as HTMLElement;
|
|
277
|
+
|
|
278
|
+
expect(header).toBeNull();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('respects deprecated showHeader=true even without title', async() => {
|
|
282
|
+
getters['slideInPanel/componentProps'] = () => ({ showHeader: true });
|
|
283
|
+
store = createStore({
|
|
284
|
+
getters,
|
|
285
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
286
|
+
});
|
|
287
|
+
factory();
|
|
288
|
+
await nextTick();
|
|
289
|
+
|
|
290
|
+
const header = document.querySelector('#slides .slide-in .header') as HTMLElement;
|
|
291
|
+
|
|
292
|
+
expect(header).toBeTruthy();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('respects deprecated showHeader=false even with title', async() => {
|
|
296
|
+
getters['slideInPanel/componentProps'] = () => ({ showHeader: false, title: 'Should Not Show' });
|
|
297
|
+
store = createStore({
|
|
298
|
+
getters,
|
|
299
|
+
mutations: { 'slideInPanel/close': jest.fn() }
|
|
300
|
+
});
|
|
301
|
+
factory();
|
|
302
|
+
await nextTick();
|
|
303
|
+
|
|
304
|
+
const header = document.querySelector('#slides .slide-in .header') as HTMLElement;
|
|
305
|
+
|
|
306
|
+
expect(header).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
166
309
|
});
|
package/core/plugins-loader.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as vue3 from 'vue';
|
|
2
|
+
import * as vueRouter from 'vue-router';
|
|
2
3
|
import $ from 'jquery';
|
|
3
4
|
import JSZip from 'jszip';
|
|
4
5
|
import jsyaml from 'js-yaml';
|
|
@@ -25,6 +26,7 @@ export default function({
|
|
|
25
26
|
|
|
26
27
|
// Global libraries - allows us to externalise these to reduce package bundle size
|
|
27
28
|
window.Vue = vue3;
|
|
29
|
+
window.__vueRouter = vueRouter;
|
|
28
30
|
window.$ = $;
|
|
29
31
|
window.__jszip = JSZip;
|
|
30
32
|
window.__jsyaml = jsyaml;
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import Namespace from '@shell/models/namespace';
|
|
2
2
|
import { SYSTEM_NAMESPACE } from '@shell/config/labels-annotations';
|
|
3
3
|
import SYSTEM_NAMESPACES from '@shell/config/system-namespaces';
|
|
4
|
+
import { LOCAL_CLUSTER } from '@shell/config/types';
|
|
5
|
+
import { NAME as MANAGER } from '@shell/config/product/manager';
|
|
6
|
+
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
7
|
+
import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
|
|
8
|
+
|
|
9
|
+
jest.mock('@shell/components/nav/TopLevelMenu.helper', () => ({
|
|
10
|
+
__esModule: true,
|
|
11
|
+
default: {
|
|
12
|
+
helper: {
|
|
13
|
+
clustersPinned: [],
|
|
14
|
+
clustersOthers: [],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
4
18
|
|
|
5
19
|
describe('class Namespace', () => {
|
|
6
20
|
describe('checking if isSystem', () => {
|
|
@@ -201,24 +215,64 @@ describe('class Namespace', () => {
|
|
|
201
215
|
expect(namespace.listLocation.name).toBe(name);
|
|
202
216
|
});
|
|
203
217
|
|
|
204
|
-
it('should
|
|
218
|
+
it('should route to local cluster explorer when in manager product', () => {
|
|
205
219
|
const namespace = new Namespace({});
|
|
206
220
|
|
|
207
221
|
jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({
|
|
208
222
|
isRancher: true,
|
|
209
|
-
|
|
223
|
+
productId: MANAGER,
|
|
224
|
+
clusterId: '_',
|
|
225
|
+
currentProduct: { inStore: '' },
|
|
210
226
|
});
|
|
211
227
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
228
|
+
expect(namespace.listLocation.params.cluster).toBe(LOCAL_CLUSTER);
|
|
229
|
+
expect(namespace.listLocation.params.product).toBe(EXPLORER);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should use current cluster and always use explorer product when not in manager', () => {
|
|
233
|
+
const namespace = new Namespace({});
|
|
216
234
|
|
|
217
|
-
|
|
235
|
+
jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({
|
|
236
|
+
isRancher: true,
|
|
237
|
+
productId: EXPLORER,
|
|
238
|
+
clusterId: 'c-abc',
|
|
239
|
+
currentProduct: { inStore: '' },
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(namespace.listLocation.params.cluster).toBe('c-abc');
|
|
243
|
+
expect(namespace.listLocation.params.product).toBe(EXPLORER);
|
|
218
244
|
});
|
|
219
245
|
});
|
|
220
246
|
|
|
221
|
-
|
|
247
|
+
describe('handling _detailLocation', () => {
|
|
248
|
+
const mockDetailLocation = (namespace: any, overrides: Record<string, any>) => {
|
|
249
|
+
jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue(overrides);
|
|
250
|
+
jest.spyOn(namespace, '$getters', 'get').mockReturnValue({ schemaFor: () => ({ attributes: { namespaced: false } }) });
|
|
251
|
+
Object.defineProperty(namespace, 'isProdRegistrationV2TopLevelProductResoure', { get: () => false });
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
it('should route to local cluster explorer when in manager product', () => {
|
|
255
|
+
const namespace = new Namespace({});
|
|
256
|
+
|
|
257
|
+
mockDetailLocation(namespace, { productId: MANAGER, clusterId: '_' });
|
|
258
|
+
|
|
259
|
+
const loc = namespace._detailLocation;
|
|
260
|
+
|
|
261
|
+
expect(loc.params.cluster).toBe(LOCAL_CLUSTER);
|
|
262
|
+
expect(loc.params.product).toBe(EXPLORER);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should use current cluster and product when not in manager product', () => {
|
|
266
|
+
const namespace = new Namespace({});
|
|
267
|
+
|
|
268
|
+
mockDetailLocation(namespace, { productId: EXPLORER, clusterId: 'c-abc' });
|
|
269
|
+
|
|
270
|
+
const loc = namespace._detailLocation;
|
|
271
|
+
|
|
272
|
+
expect(loc.params.cluster).toBe('c-abc');
|
|
273
|
+
expect(loc.params.product).toBe(EXPLORER);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
222
276
|
it.todo('should return the resourceQuota');
|
|
223
277
|
it.todo('should set the resourceQuota as reactive Vue property');
|
|
224
278
|
it.todo('should reset project with cleanForNew');
|
|
@@ -263,6 +317,7 @@ describe('class Namespace', () => {
|
|
|
263
317
|
const namespace = new Namespace({});
|
|
264
318
|
|
|
265
319
|
jest.spyOn(namespace, 'project', 'get').mockReturnValue(null);
|
|
320
|
+
jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({ productId: EXPLORER });
|
|
266
321
|
Object.defineProperty(namespace, '_glance', { get: jest.fn(() => [{ name: 'namespace' }, { name: 'other' }]) });
|
|
267
322
|
|
|
268
323
|
const result = namespace.glance;
|
|
@@ -270,6 +325,76 @@ describe('class Namespace', () => {
|
|
|
270
325
|
expect(result).toHaveLength(1);
|
|
271
326
|
expect(result[0].name).toBe('other');
|
|
272
327
|
});
|
|
328
|
+
|
|
329
|
+
it('should remove type Link formatter when in manager product without local cluster access', () => {
|
|
330
|
+
const namespace = new Namespace({});
|
|
331
|
+
|
|
332
|
+
sideNavService.helper.clustersPinned.length = 0;
|
|
333
|
+
sideNavService.helper.clustersOthers.length = 0;
|
|
334
|
+
|
|
335
|
+
jest.spyOn(namespace, 'project', 'get').mockReturnValue(null);
|
|
336
|
+
jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({ productId: MANAGER });
|
|
337
|
+
Object.defineProperty(namespace, '_glance', {
|
|
338
|
+
get: jest.fn(() => [
|
|
339
|
+
{
|
|
340
|
+
name: 'type', formatter: 'Link', formatterOpts: { to: {}, row: {} }
|
|
341
|
+
},
|
|
342
|
+
{ name: 'other' },
|
|
343
|
+
])
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const result = namespace.glance;
|
|
347
|
+
const typeItem = result.find((item: any) => item.name === 'type');
|
|
348
|
+
|
|
349
|
+
expect(typeItem.formatter).toBeUndefined();
|
|
350
|
+
expect(typeItem.formatterOpts).toBeUndefined();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should keep type Link formatter when in manager product with local cluster access', () => {
|
|
354
|
+
const namespace = new Namespace({});
|
|
355
|
+
|
|
356
|
+
sideNavService.helper.clustersPinned.length = 0;
|
|
357
|
+
sideNavService.helper.clustersPinned.push({ id: LOCAL_CLUSTER } as any);
|
|
358
|
+
sideNavService.helper.clustersOthers.length = 0;
|
|
359
|
+
|
|
360
|
+
jest.spyOn(namespace, 'project', 'get').mockReturnValue(null);
|
|
361
|
+
jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({ productId: MANAGER });
|
|
362
|
+
Object.defineProperty(namespace, '_glance', {
|
|
363
|
+
get: jest.fn(() => [
|
|
364
|
+
{
|
|
365
|
+
name: 'type', formatter: 'Link', formatterOpts: { to: {}, row: {} }
|
|
366
|
+
},
|
|
367
|
+
{ name: 'other' },
|
|
368
|
+
])
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const result = namespace.glance;
|
|
372
|
+
const typeItem = result.find((item: any) => item.name === 'type');
|
|
373
|
+
|
|
374
|
+
expect(typeItem.formatter).toBe('Link');
|
|
375
|
+
expect(typeItem.formatterOpts).toBeDefined();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should keep type Link formatter when not in manager product', () => {
|
|
379
|
+
const namespace = new Namespace({});
|
|
380
|
+
|
|
381
|
+
jest.spyOn(namespace, 'project', 'get').mockReturnValue(null);
|
|
382
|
+
jest.spyOn(namespace, '$rootGetters', 'get').mockReturnValue({ productId: EXPLORER });
|
|
383
|
+
Object.defineProperty(namespace, '_glance', {
|
|
384
|
+
get: jest.fn(() => [
|
|
385
|
+
{
|
|
386
|
+
name: 'type', formatter: 'Link', formatterOpts: { to: {}, row: {} }
|
|
387
|
+
},
|
|
388
|
+
{ name: 'other' },
|
|
389
|
+
])
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const result = namespace.glance;
|
|
393
|
+
const typeItem = result.find((item: any) => item.name === 'type');
|
|
394
|
+
|
|
395
|
+
expect(typeItem.formatter).toBe('Link');
|
|
396
|
+
expect(typeItem.formatterOpts).toBeDefined();
|
|
397
|
+
});
|
|
273
398
|
});
|
|
274
399
|
|
|
275
400
|
describe('projectGlance', () => {
|
|
@@ -3,11 +3,23 @@ import MgmtCluster from '@shell/models/management.cattle.io.cluster';
|
|
|
3
3
|
import { IMPORTED_DAY_2_OPS } from '@shell/config/features';
|
|
4
4
|
import { OPERATION_ANNOTATIONS } from '@shell/config/labels-annotations';
|
|
5
5
|
import { SETTING } from '@shell/config/settings';
|
|
6
|
-
import { MANAGEMENT, OPERATION } from '@shell/config/types';
|
|
6
|
+
import { MANAGEMENT, OPERATION, LOCAL_CLUSTER, NAMESPACE } from '@shell/config/types';
|
|
7
7
|
import { createOperationCR } from '@shell/utils/operation-cr';
|
|
8
|
+
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
9
|
+
import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
|
|
8
10
|
|
|
9
11
|
jest.mock('@shell/utils/operation-cr', () => ({ createOperationCR: jest.fn() }));
|
|
10
12
|
|
|
13
|
+
jest.mock('@shell/components/nav/TopLevelMenu.helper', () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: {
|
|
16
|
+
helper: {
|
|
17
|
+
clustersPinned: [],
|
|
18
|
+
clustersOthers: []
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}));
|
|
22
|
+
|
|
11
23
|
jest.mock('@shell/utils/provider', () => ({
|
|
12
24
|
isHostedProvider: jest.fn().mockImplementation((context, provider) => {
|
|
13
25
|
return ['GKE', 'EKS', 'AKS'].includes(provider);
|
|
@@ -597,4 +609,48 @@ describe('class ProvCluster', () => {
|
|
|
597
609
|
expect((createOperationCR as jest.Mock).mock.calls[0][4]).toBe('imported-cluster');
|
|
598
610
|
});
|
|
599
611
|
});
|
|
612
|
+
|
|
613
|
+
describe('namespaceLocation', () => {
|
|
614
|
+
const setLocalClusterAccess = ({ pinned, others }: { pinned: boolean, others: boolean }) => {
|
|
615
|
+
sideNavService.helper.clustersPinned.length = 0;
|
|
616
|
+
sideNavService.helper.clustersOthers.length = 0;
|
|
617
|
+
|
|
618
|
+
if (pinned) {
|
|
619
|
+
sideNavService.helper.clustersPinned.push({ id: LOCAL_CLUSTER } as any);
|
|
620
|
+
}
|
|
621
|
+
if (others) {
|
|
622
|
+
sideNavService.helper.clustersOthers.push({ id: LOCAL_CLUSTER } as any);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
it('should route to the local cluster explorer when local is in the pinned side-nav clusters', () => {
|
|
627
|
+
setLocalClusterAccess({ pinned: true, others: false });
|
|
628
|
+
const cluster = new ProvCluster({ metadata: { namespace: 'fleet-default' } });
|
|
629
|
+
|
|
630
|
+
expect(cluster.namespaceLocation).toStrictEqual({
|
|
631
|
+
name: 'c-cluster-product-resource-id',
|
|
632
|
+
params: {
|
|
633
|
+
cluster: LOCAL_CLUSTER,
|
|
634
|
+
product: EXPLORER,
|
|
635
|
+
resource: NAMESPACE,
|
|
636
|
+
id: 'fleet-default',
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('should route to the local cluster explorer when local is in the unpinned side-nav clusters', () => {
|
|
642
|
+
setLocalClusterAccess({ pinned: false, others: true });
|
|
643
|
+
const cluster = new ProvCluster({ metadata: { namespace: 'fleet-default' } });
|
|
644
|
+
|
|
645
|
+
expect(cluster.namespaceLocation?.params.cluster).toBe(LOCAL_CLUSTER);
|
|
646
|
+
expect(cluster.namespaceLocation?.params.product).toBe(EXPLORER);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('should return null when the user has no access to the local cluster', () => {
|
|
650
|
+
setLocalClusterAccess({ pinned: false, others: false });
|
|
651
|
+
const cluster = new ProvCluster({ metadata: { namespace: 'fleet-default' } });
|
|
652
|
+
|
|
653
|
+
expect(cluster.namespaceLocation).toBeNull();
|
|
654
|
+
});
|
|
655
|
+
});
|
|
600
656
|
});
|
package/models/namespace.js
CHANGED
|
@@ -2,12 +2,15 @@ import SYSTEM_NAMESPACES from '@shell/config/system-namespaces';
|
|
|
2
2
|
import {
|
|
3
3
|
PROJECT, SYSTEM_NAMESPACE, ISTIO as ISTIO_LABELS, FLEET, RESOURCE_QUOTA
|
|
4
4
|
} from '@shell/config/labels-annotations';
|
|
5
|
-
import { ISTIO, MANAGEMENT } from '@shell/config/types';
|
|
5
|
+
import { ISTIO, MANAGEMENT, LOCAL_CLUSTER } from '@shell/config/types';
|
|
6
6
|
|
|
7
7
|
import { get, set } from '@shell/utils/object';
|
|
8
8
|
import { insertAt, isArray } from '@shell/utils/array';
|
|
9
9
|
import SteveModel from '@shell/plugins/steve/steve-class';
|
|
10
10
|
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
|
11
|
+
import { NAME as MANAGER } from '@shell/config/product/manager';
|
|
12
|
+
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
13
|
+
import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
|
|
11
14
|
import { hasPSALabels, getPSATooltipsDescription, getPSALabels } from '@shell/utils/pod-security-admission';
|
|
12
15
|
import { PSAIconsDisplay, PSALabelsNamespaceVersion } from '@shell/config/pod-security-admission';
|
|
13
16
|
|
|
@@ -188,7 +191,14 @@ export default class Namespace extends SteveModel {
|
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
get listLocation() {
|
|
191
|
-
const
|
|
194
|
+
const productId = this.$rootGetters['productId'];
|
|
195
|
+
const isManagerProduct = productId === MANAGER;
|
|
196
|
+
const cluster = isManagerProduct ? LOCAL_CLUSTER : this.$rootGetters['clusterId'];
|
|
197
|
+
|
|
198
|
+
const listLocation = {
|
|
199
|
+
name: this.$rootGetters['isRancher'] ? 'c-cluster-product-projectsnamespaces' : 'c-cluster-product-resource',
|
|
200
|
+
params: { cluster, product: EXPLORER },
|
|
201
|
+
};
|
|
192
202
|
|
|
193
203
|
// Harvester uses these resource directly... but has different routes. listLocation covers routes leading back to route
|
|
194
204
|
if (this.$rootGetters['currentProduct'].inStore === HARVESTER) {
|
|
@@ -202,6 +212,12 @@ export default class Namespace extends SteveModel {
|
|
|
202
212
|
get _detailLocation() {
|
|
203
213
|
const _detailLocation = super._detailLocation;
|
|
204
214
|
|
|
215
|
+
// Namespace detail pages belong to the local cluster's explorer, not Cluster Management
|
|
216
|
+
if (this.$rootGetters['productId'] === MANAGER) {
|
|
217
|
+
_detailLocation.params.cluster = LOCAL_CLUSTER;
|
|
218
|
+
_detailLocation.params.product = EXPLORER;
|
|
219
|
+
}
|
|
220
|
+
|
|
205
221
|
return _detailLocation;
|
|
206
222
|
}
|
|
207
223
|
|
|
@@ -285,6 +301,22 @@ export default class Namespace extends SteveModel {
|
|
|
285
301
|
glance.splice(namespaceIndex, 1, this.projectGlance);
|
|
286
302
|
}
|
|
287
303
|
|
|
304
|
+
// In Cluster Management, the type link points to the local cluster's explorer.
|
|
305
|
+
// If the user cannot access the local cluster, show type as plain text.
|
|
306
|
+
const productId = this.$rootGetters['productId'];
|
|
307
|
+
|
|
308
|
+
if (productId === MANAGER) {
|
|
309
|
+
const hasLocalCluster = sideNavService.helper.clustersPinned.some((c) => c.id === LOCAL_CLUSTER) ||
|
|
310
|
+
sideNavService.helper.clustersOthers.some((c) => c.id === LOCAL_CLUSTER);
|
|
311
|
+
const typeIndex = glance.findIndex((item) => item.name === 'type');
|
|
312
|
+
|
|
313
|
+
if (!hasLocalCluster && typeIndex > -1) {
|
|
314
|
+
glance[typeIndex] = {
|
|
315
|
+
...glance[typeIndex], formatter: undefined, formatterOpts: undefined
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
288
320
|
// projectGlance could be undefined
|
|
289
321
|
return glance.filter(Boolean);
|
|
290
322
|
}
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
CONFIG_MAP, AUTOSCALER_CONFIG_MAP_ID,
|
|
4
4
|
EVENT, OPERATION
|
|
5
5
|
} from '@shell/config/types';
|
|
6
|
+
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
7
|
+
import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
|
|
6
8
|
import SteveModel from '@shell/plugins/steve/steve-class';
|
|
7
9
|
import { findBy } from '@shell/utils/array';
|
|
8
10
|
import { get, set } from '@shell/utils/object';
|
|
@@ -788,21 +790,26 @@ export default class ProvCluster extends SteveModel {
|
|
|
788
790
|
}
|
|
789
791
|
|
|
790
792
|
get namespaceLocation() {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
id: this.namespace
|
|
801
|
-
}
|
|
802
|
-
};
|
|
793
|
+
// Check side nav for local cluster access — `management/byId` only reflects what's
|
|
794
|
+
// currently cached, which on a downstream detail page is just the downstream cluster
|
|
795
|
+
// (so admins would falsely look like they have no local access). The side nav reflects
|
|
796
|
+
// permission, not cache state, so it's accurate for both admins and downstream-only users.
|
|
797
|
+
const hasLocalCluster = sideNavService.helper.clustersPinned.some((c) => c.id === LOCAL_CLUSTER) ||
|
|
798
|
+
sideNavService.helper.clustersOthers.some((c) => c.id === LOCAL_CLUSTER);
|
|
799
|
+
|
|
800
|
+
if (!hasLocalCluster) {
|
|
801
|
+
return null;
|
|
803
802
|
}
|
|
804
803
|
|
|
805
|
-
return
|
|
804
|
+
return {
|
|
805
|
+
name: 'c-cluster-product-resource-id',
|
|
806
|
+
params: {
|
|
807
|
+
cluster: LOCAL_CLUSTER,
|
|
808
|
+
product: EXPLORER,
|
|
809
|
+
resource: NAMESPACE,
|
|
810
|
+
id: this.namespace
|
|
811
|
+
}
|
|
812
|
+
};
|
|
806
813
|
}
|
|
807
814
|
|
|
808
815
|
/**
|