@rancher/shell 3.0.12-rc.5 → 3.0.12-rc.6
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/resources-api/resource-base.ts +42 -2
- package/apis/intf/resources-api/resource-instance.ts +101 -0
- package/apis/intf/resources-api/resources-api.ts +177 -23
- package/apis/intf/resources.ts +8 -6
- package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
- package/apis/resources/resources-api-class.ts +232 -43
- package/assets/images/providers/traefik.png +0 -0
- package/assets/translations/en-us.yaml +10 -4
- package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
- package/chart/rancher-monitoring-dashboards/index.vue +155 -0
- package/cloud-credential/__tests__/generic.test.ts +132 -0
- package/cloud-credential/generic.vue +33 -3
- package/components/EtcdInfoBanner.vue +2 -12
- package/components/GrafanaDashboard.vue +2 -8
- package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
- package/components/Resource/Detail/Masthead/index.vue +8 -3
- package/components/form/Security.vue +1 -1
- package/components/formatter/Translate.vue +22 -1
- package/components/nav/Group.vue +5 -10
- package/components/nav/TopLevelMenu.vue +1 -30
- package/components/nav/__tests__/Group.test.ts +61 -0
- package/config/labels-annotations.js +1 -0
- package/config/table-headers.js +5 -2
- package/core/__tests__/plugin-products-apply.test.ts +787 -0
- package/core/__tests__/plugin-products-extend.test.ts +189 -0
- package/core/__tests__/plugin-products-helpers.test.ts +81 -78
- package/core/__tests__/plugin-products-new.test.ts +389 -0
- package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
- package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
- package/core/plugin-products-base.ts +201 -93
- package/core/plugin-products-extending.ts +1 -1
- package/core/plugin-products-external.ts +380 -0
- package/core/plugin-products-helpers.ts +28 -24
- package/core/plugin-products-internal.ts +207 -0
- package/core/plugin-products-top-level.ts +10 -5
- package/core/plugin-products-type-guards.ts +17 -6
- package/core/plugin-products.ts +5 -7
- package/core/plugin-types.ts +1 -389
- package/core/plugin.ts +10 -11
- package/core/types.ts +6 -202
- package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
- package/detail/pod.vue +1 -1
- package/detail/provisioning.cattle.io.cluster.vue +0 -2
- package/edit/auth/AuthProviderWarningBanners.vue +12 -0
- package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
- package/edit/auth/__tests__/azuread.test.ts +1 -0
- package/edit/auth/__tests__/github.test.ts +1 -0
- package/edit/auth/__tests__/oidc.test.ts +1 -0
- package/edit/auth/__tests__/saml.test.ts +1 -0
- package/edit/auth/ldap/__tests__/index.test.ts +1 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
- package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
- package/list/management.cattle.io.user.vue +24 -5
- package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
- package/machine-config/__tests__/generic.test.ts +163 -0
- package/machine-config/components/EC2Networking.vue +31 -7
- package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
- package/machine-config/generic.vue +32 -5
- package/mixins/resource-fetch.js +1 -1
- package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
- package/models/pod.js +12 -10
- package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
- package/package.json +1 -1
- package/pages/auth/login.vue +1 -1
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
- package/pages/c/_cluster/apps/charts/install.vue +29 -0
- package/pages/c/_cluster/auth/config/index.vue +11 -4
- package/pages/c/_cluster/monitoring/index.vue +6 -1
- package/plugins/dashboard-store/resource-class.js +10 -3
- package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
- package/plugins/steve/steve-class.js +43 -0
- package/plugins/steve/subscribe.js +11 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
- package/store/__tests__/auth.test.ts +18 -0
- package/store/__tests__/notifications.test.ts +466 -1
- package/store/__tests__/ui-context.test.ts +255 -0
- package/store/auth.js +11 -1
- package/store/features.js +1 -0
- package/store/plugins.js +6 -0
- package/types/rancher/steve.api.ts +2 -2
- package/types/shell/index.d.ts +58 -0
- package/types/store/dashboard-store.types.ts +2 -2
- package/types/store/type-map.ts +262 -1
- package/utils/async.ts +2 -0
- package/utils/grafana.js +2 -17
- package/utils/monitoring.js +38 -1
- package/core/__tests__/plugin-products.test.ts +0 -4694
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { PluginProduct } from '@shell/core/plugin-products';
|
|
2
|
+
import { IExtension } from '@shell/core/types';
|
|
3
|
+
import { ProductChildGroup, ProductChildPage, ProductMetadata, StandardProductNames } from '@shell/core/plugin-products-external';
|
|
4
|
+
|
|
5
|
+
// Mock the helper functions
|
|
6
|
+
jest.mock('@shell/core/plugin-products-helpers', () => ({
|
|
7
|
+
gatherChildrenOrdering: jest.fn((config) => config),
|
|
8
|
+
generateTopLevelExtensionSimpleBaseRoute: jest.fn((name, opts) => ({
|
|
9
|
+
name: `${ name }-simple`,
|
|
10
|
+
path: opts?.omitPath ? '' : `/${ name }`,
|
|
11
|
+
component: opts?.component,
|
|
12
|
+
})),
|
|
13
|
+
generateVirtualTypeRoute: jest.fn((parentName, childName, opts) => ({
|
|
14
|
+
name: childName ? `${ parentName }-${ childName }` : `${ parentName }-group`,
|
|
15
|
+
path: opts?.omitPath ? '' : `/${ parentName }/${ childName || 'group' }`,
|
|
16
|
+
component: opts?.component,
|
|
17
|
+
})),
|
|
18
|
+
generateConfigureTypeRoute: jest.fn((parentName, page, opts) => {
|
|
19
|
+
const routeName = opts?.extendProduct ? `c-cluster-${ parentName }-resource` : `${ parentName }-c-cluster-resource`;
|
|
20
|
+
const routePath = opts?.extendProduct ? `c/:cluster/${ parentName }/:resource` : `${ parentName }/c/:cluster/:resource`;
|
|
21
|
+
const cluster = opts?.extendProduct ? undefined : '__BLANK_CLUSTER__';
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
name: routeName,
|
|
25
|
+
path: opts?.omitPath ? '' : routePath,
|
|
26
|
+
params: cluster ? {
|
|
27
|
+
product: parentName.replace(/-/g, ''),
|
|
28
|
+
cluster,
|
|
29
|
+
resource: page?.type,
|
|
30
|
+
} : {
|
|
31
|
+
product: parentName,
|
|
32
|
+
resource: page?.type,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}),
|
|
36
|
+
generateResourceRoutes: jest.fn((parentName, child) => [
|
|
37
|
+
{
|
|
38
|
+
name: `${ parentName }-${ child.type }-list`,
|
|
39
|
+
path: `/${ parentName }/${ child.type }`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: `${ parentName }-${ child.type }-detail`,
|
|
43
|
+
path: `/${ parentName }/${ child.type }/:id`,
|
|
44
|
+
},
|
|
45
|
+
]),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
jest.mock('@shell/core/productDebugger', () => ({
|
|
49
|
+
DSLRegistrationsPerProduct: jest.fn(),
|
|
50
|
+
registeredRoutes: jest.fn(),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// Create mock factories
|
|
54
|
+
function createMockPlugin(): IExtension {
|
|
55
|
+
return {
|
|
56
|
+
_registerTopLevelProduct: jest.fn(),
|
|
57
|
+
addRoute: jest.fn(),
|
|
58
|
+
enableServerSidePagination: jest.fn(),
|
|
59
|
+
DSL: jest.fn(),
|
|
60
|
+
} as any as IExtension;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createMockStore(extendableProducts: string[] = Object.values(StandardProductNames), managementSchemas: string[] = []): any {
|
|
64
|
+
return {
|
|
65
|
+
getters: {
|
|
66
|
+
'type-map/productByName': (productName: string) => (extendableProducts.includes(productName) ? { name: productName, extendable: true } : undefined),
|
|
67
|
+
'management/schemaFor': (type: string) => (managementSchemas.includes(type) ? { id: type } : undefined),
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('pluginProduct', () => {
|
|
73
|
+
describe('apply stage - product registration', () => {
|
|
74
|
+
it('should register new product via DSL during apply', () => {
|
|
75
|
+
const mockPlugin = createMockPlugin();
|
|
76
|
+
const mockStore = createMockStore();
|
|
77
|
+
const mockDSL = {
|
|
78
|
+
product: jest.fn(),
|
|
79
|
+
basicType: jest.fn(),
|
|
80
|
+
labelGroup: jest.fn(),
|
|
81
|
+
setGroupDefaultType: jest.fn(),
|
|
82
|
+
weightGroup: jest.fn(),
|
|
83
|
+
virtualType: jest.fn(),
|
|
84
|
+
configureType: jest.fn(),
|
|
85
|
+
weightType: jest.fn(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
89
|
+
|
|
90
|
+
const productMetadata: ProductMetadata = {
|
|
91
|
+
name: 'new-product',
|
|
92
|
+
label: 'New Product',
|
|
93
|
+
};
|
|
94
|
+
const config: ProductChildPage[] = [
|
|
95
|
+
{
|
|
96
|
+
name: 'page1',
|
|
97
|
+
label: 'Page 1',
|
|
98
|
+
component: { name: 'Page1' },
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
103
|
+
|
|
104
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
105
|
+
|
|
106
|
+
expect(mockDSL.product).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(mockDSL.product).toHaveBeenCalledWith(
|
|
108
|
+
expect.objectContaining({
|
|
109
|
+
name: 'newproduct',
|
|
110
|
+
inStore: 'management',
|
|
111
|
+
version: 2,
|
|
112
|
+
showClusterSwitcher: false,
|
|
113
|
+
category: 'global',
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should not register product when extending standard product during apply', () => {
|
|
119
|
+
const mockPlugin = createMockPlugin();
|
|
120
|
+
const mockStore = createMockStore();
|
|
121
|
+
const mockDSL = {
|
|
122
|
+
product: jest.fn(),
|
|
123
|
+
basicType: jest.fn(),
|
|
124
|
+
labelGroup: jest.fn(),
|
|
125
|
+
setGroupDefaultType: jest.fn(),
|
|
126
|
+
weightGroup: jest.fn(),
|
|
127
|
+
virtualType: jest.fn(),
|
|
128
|
+
configureType: jest.fn(),
|
|
129
|
+
weightType: jest.fn(),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
133
|
+
|
|
134
|
+
const validStandardProduct = StandardProductNames.EXPLORER;
|
|
135
|
+
|
|
136
|
+
const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, []);
|
|
137
|
+
|
|
138
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
139
|
+
|
|
140
|
+
expect(mockDSL.product).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should configure virtualType items during apply', () => {
|
|
144
|
+
const mockPlugin = createMockPlugin();
|
|
145
|
+
const mockStore = createMockStore();
|
|
146
|
+
const mockDSL = {
|
|
147
|
+
product: jest.fn(),
|
|
148
|
+
basicType: jest.fn(),
|
|
149
|
+
labelGroup: jest.fn(),
|
|
150
|
+
setGroupDefaultType: jest.fn(),
|
|
151
|
+
weightGroup: jest.fn(),
|
|
152
|
+
virtualType: jest.fn(),
|
|
153
|
+
configureType: jest.fn(),
|
|
154
|
+
weightType: jest.fn(),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
158
|
+
|
|
159
|
+
const productMetadata: ProductMetadata = {
|
|
160
|
+
name: 'product-with-pages',
|
|
161
|
+
label: 'Product',
|
|
162
|
+
};
|
|
163
|
+
const config: ProductChildPage[] = [
|
|
164
|
+
{
|
|
165
|
+
name: 'overview',
|
|
166
|
+
label: 'Overview',
|
|
167
|
+
component: { name: 'OverviewComponent' },
|
|
168
|
+
sideMenu: { weight: 10 },
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
173
|
+
|
|
174
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
175
|
+
|
|
176
|
+
expect(mockDSL.virtualType).toHaveBeenCalledTimes(1);
|
|
177
|
+
expect(mockDSL.virtualType).toHaveBeenCalledWith(
|
|
178
|
+
expect.objectContaining({
|
|
179
|
+
name: 'productwithpages-overview',
|
|
180
|
+
label: 'Overview',
|
|
181
|
+
weight: 10,
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should configure configureType (resource) items during apply', () => {
|
|
187
|
+
const mockPlugin = createMockPlugin();
|
|
188
|
+
const mockStore = createMockStore();
|
|
189
|
+
const mockDSL = {
|
|
190
|
+
product: jest.fn(),
|
|
191
|
+
basicType: jest.fn(),
|
|
192
|
+
labelGroup: jest.fn(),
|
|
193
|
+
setGroupDefaultType: jest.fn(),
|
|
194
|
+
weightGroup: jest.fn(),
|
|
195
|
+
virtualType: jest.fn(),
|
|
196
|
+
configureType: jest.fn(),
|
|
197
|
+
weightType: jest.fn(),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
201
|
+
|
|
202
|
+
const productMetadata: ProductMetadata = {
|
|
203
|
+
name: 'resource-product',
|
|
204
|
+
label: 'Resources',
|
|
205
|
+
};
|
|
206
|
+
const config: ProductChildPage[] = [
|
|
207
|
+
{
|
|
208
|
+
type: 'custom.resource',
|
|
209
|
+
sideMenu: { weight: 5 },
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
214
|
+
|
|
215
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
216
|
+
|
|
217
|
+
expect(mockDSL.configureType).toHaveBeenCalledWith(
|
|
218
|
+
'custom.resource',
|
|
219
|
+
expect.objectContaining({
|
|
220
|
+
isCreatable: true,
|
|
221
|
+
isEditable: true,
|
|
222
|
+
isRemovable: true,
|
|
223
|
+
canYaml: true,
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
expect(mockDSL.weightType).toHaveBeenCalledWith('custom.resource', 5, true);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('grouped items', () => {
|
|
231
|
+
it('should handle product with grouped items', () => {
|
|
232
|
+
const mockPlugin = createMockPlugin();
|
|
233
|
+
const mockStore = createMockStore();
|
|
234
|
+
const mockDSL = {
|
|
235
|
+
product: jest.fn(),
|
|
236
|
+
basicType: jest.fn(),
|
|
237
|
+
labelGroup: jest.fn(),
|
|
238
|
+
setGroupDefaultType: jest.fn(),
|
|
239
|
+
weightGroup: jest.fn(),
|
|
240
|
+
virtualType: jest.fn(),
|
|
241
|
+
configureType: jest.fn(),
|
|
242
|
+
weightType: jest.fn(),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
246
|
+
|
|
247
|
+
const productMetadata: ProductMetadata = {
|
|
248
|
+
name: 'grouped-product',
|
|
249
|
+
label: 'Grouped',
|
|
250
|
+
};
|
|
251
|
+
const groupedConfig: ProductChildGroup[] = [
|
|
252
|
+
{
|
|
253
|
+
name: 'settings',
|
|
254
|
+
label: 'Settings',
|
|
255
|
+
sideMenu: {
|
|
256
|
+
children: [
|
|
257
|
+
{
|
|
258
|
+
name: 'general',
|
|
259
|
+
label: 'General',
|
|
260
|
+
component: { name: 'GeneralSettings' },
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'advanced',
|
|
264
|
+
label: 'Advanced',
|
|
265
|
+
component: { name: 'AdvancedSettings' },
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, groupedConfig);
|
|
274
|
+
|
|
275
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
276
|
+
|
|
277
|
+
expect(mockDSL.basicType).toHaveBeenCalledTimes(2);
|
|
278
|
+
expect(mockDSL.labelGroup).toHaveBeenCalledWith(
|
|
279
|
+
expect.stringContaining('settings'),
|
|
280
|
+
'Settings',
|
|
281
|
+
undefined
|
|
282
|
+
);
|
|
283
|
+
expect(mockDSL.virtualType).toHaveBeenCalledTimes(2);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should set group default type when group has no component', () => {
|
|
287
|
+
const mockPlugin = createMockPlugin();
|
|
288
|
+
const mockStore = createMockStore();
|
|
289
|
+
const mockDSL = {
|
|
290
|
+
product: jest.fn(),
|
|
291
|
+
basicType: jest.fn(),
|
|
292
|
+
labelGroup: jest.fn(),
|
|
293
|
+
setGroupDefaultType: jest.fn(),
|
|
294
|
+
weightGroup: jest.fn(),
|
|
295
|
+
virtualType: jest.fn(),
|
|
296
|
+
configureType: jest.fn(),
|
|
297
|
+
weightType: jest.fn(),
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
301
|
+
|
|
302
|
+
const productMetadata: ProductMetadata = {
|
|
303
|
+
name: 'group-no-component',
|
|
304
|
+
label: 'Group No Component',
|
|
305
|
+
};
|
|
306
|
+
const config: ProductChildGroup[] = [
|
|
307
|
+
{
|
|
308
|
+
name: 'group',
|
|
309
|
+
label: 'Group Without Component',
|
|
310
|
+
sideMenu: {
|
|
311
|
+
children: [
|
|
312
|
+
{
|
|
313
|
+
name: 'child1',
|
|
314
|
+
label: 'Child 1',
|
|
315
|
+
component: { name: 'Child1Component' },
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
324
|
+
|
|
325
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
326
|
+
|
|
327
|
+
expect(mockDSL.setGroupDefaultType).toHaveBeenCalledWith(
|
|
328
|
+
expect.stringContaining('group'),
|
|
329
|
+
expect.stringContaining('child1')
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should set group default type to itself when group has component', () => {
|
|
334
|
+
const mockPlugin = createMockPlugin();
|
|
335
|
+
const mockStore = createMockStore();
|
|
336
|
+
const mockDSL = {
|
|
337
|
+
product: jest.fn(),
|
|
338
|
+
basicType: jest.fn(),
|
|
339
|
+
labelGroup: jest.fn(),
|
|
340
|
+
setGroupDefaultType: jest.fn(),
|
|
341
|
+
weightGroup: jest.fn(),
|
|
342
|
+
virtualType: jest.fn(),
|
|
343
|
+
configureType: jest.fn(),
|
|
344
|
+
weightType: jest.fn(),
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
348
|
+
|
|
349
|
+
const productMetadata: ProductMetadata = {
|
|
350
|
+
name: 'group-with-component',
|
|
351
|
+
label: 'Group With Component',
|
|
352
|
+
};
|
|
353
|
+
const config: ProductChildGroup[] = [
|
|
354
|
+
{
|
|
355
|
+
name: 'group',
|
|
356
|
+
label: 'Group With Component',
|
|
357
|
+
component: { name: 'GroupOverviewComponent' },
|
|
358
|
+
sideMenu: {
|
|
359
|
+
children: [
|
|
360
|
+
{
|
|
361
|
+
name: 'child1',
|
|
362
|
+
label: 'Child 1',
|
|
363
|
+
component: { name: 'Child1Component' },
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
},
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
372
|
+
|
|
373
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
374
|
+
|
|
375
|
+
// When a group has a component, setGroupDefaultType should be called with the group name itself
|
|
376
|
+
// This ensures clicking the group in nav routes to the group's page, not bypassing to first child
|
|
377
|
+
expect(mockDSL.setGroupDefaultType).toHaveBeenCalledWith(
|
|
378
|
+
'groupwithcomponent-group',
|
|
379
|
+
'groupwithcomponent-group'
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should apply group weight when specified', () => {
|
|
384
|
+
const mockPlugin = createMockPlugin();
|
|
385
|
+
const mockStore = createMockStore();
|
|
386
|
+
const mockDSL = {
|
|
387
|
+
product: jest.fn(),
|
|
388
|
+
basicType: jest.fn(),
|
|
389
|
+
labelGroup: jest.fn(),
|
|
390
|
+
setGroupDefaultType: jest.fn(),
|
|
391
|
+
weightGroup: jest.fn(),
|
|
392
|
+
virtualType: jest.fn(),
|
|
393
|
+
configureType: jest.fn(),
|
|
394
|
+
weightType: jest.fn(),
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
398
|
+
|
|
399
|
+
const productMetadata: ProductMetadata = {
|
|
400
|
+
name: 'weighted-group',
|
|
401
|
+
label: 'Weighted',
|
|
402
|
+
};
|
|
403
|
+
const config: ProductChildGroup[] = [
|
|
404
|
+
{
|
|
405
|
+
name: 'group',
|
|
406
|
+
label: 'Group',
|
|
407
|
+
sideMenu: {
|
|
408
|
+
weight: 50,
|
|
409
|
+
children: [
|
|
410
|
+
{
|
|
411
|
+
name: 'child',
|
|
412
|
+
label: 'Child',
|
|
413
|
+
component: { name: 'ChildComponent' },
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
421
|
+
|
|
422
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
423
|
+
|
|
424
|
+
expect(mockDSL.weightGroup).toHaveBeenCalledWith(
|
|
425
|
+
expect.stringContaining('group'),
|
|
426
|
+
50,
|
|
427
|
+
true
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe('default route determination', () => {
|
|
433
|
+
it('should use first config item as default route for new product', () => {
|
|
434
|
+
const mockPlugin = createMockPlugin();
|
|
435
|
+
const mockStore = createMockStore();
|
|
436
|
+
const mockDSL = {
|
|
437
|
+
product: jest.fn(),
|
|
438
|
+
basicType: jest.fn(),
|
|
439
|
+
labelGroup: jest.fn(),
|
|
440
|
+
setGroupDefaultType: jest.fn(),
|
|
441
|
+
weightGroup: jest.fn(),
|
|
442
|
+
virtualType: jest.fn(),
|
|
443
|
+
configureType: jest.fn(),
|
|
444
|
+
weightType: jest.fn(),
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
448
|
+
|
|
449
|
+
const productMetadata: ProductMetadata = {
|
|
450
|
+
name: 'default-route-product',
|
|
451
|
+
label: 'Default Route',
|
|
452
|
+
};
|
|
453
|
+
const config: ProductChildPage[] = [
|
|
454
|
+
{
|
|
455
|
+
name: 'first',
|
|
456
|
+
label: 'First',
|
|
457
|
+
component: { name: 'FirstComponent' },
|
|
458
|
+
sideMenu: { weight: 1 },
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: 'second',
|
|
462
|
+
label: 'Second',
|
|
463
|
+
component: { name: 'SecondComponent' },
|
|
464
|
+
sideMenu: { weight: 2 },
|
|
465
|
+
},
|
|
466
|
+
];
|
|
467
|
+
|
|
468
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
469
|
+
|
|
470
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
471
|
+
|
|
472
|
+
expect(mockDSL.product).toHaveBeenCalledWith(
|
|
473
|
+
expect.objectContaining({ to: expect.objectContaining({ name: expect.stringContaining('first') }) })
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('should use first group child as default route when first item is group', () => {
|
|
478
|
+
const mockPlugin = createMockPlugin();
|
|
479
|
+
const mockStore = createMockStore();
|
|
480
|
+
const mockDSL = {
|
|
481
|
+
product: jest.fn(),
|
|
482
|
+
basicType: jest.fn(),
|
|
483
|
+
labelGroup: jest.fn(),
|
|
484
|
+
setGroupDefaultType: jest.fn(),
|
|
485
|
+
weightGroup: jest.fn(),
|
|
486
|
+
virtualType: jest.fn(),
|
|
487
|
+
configureType: jest.fn(),
|
|
488
|
+
weightType: jest.fn(),
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
492
|
+
|
|
493
|
+
const productMetadata: ProductMetadata = {
|
|
494
|
+
name: 'group-default-route',
|
|
495
|
+
label: 'Group Default',
|
|
496
|
+
};
|
|
497
|
+
const config: ProductChildGroup[] = [
|
|
498
|
+
{
|
|
499
|
+
name: 'settings',
|
|
500
|
+
label: 'Settings',
|
|
501
|
+
sideMenu: {
|
|
502
|
+
children: [
|
|
503
|
+
{
|
|
504
|
+
name: 'general',
|
|
505
|
+
label: 'General',
|
|
506
|
+
component: { name: 'GeneralComponent' },
|
|
507
|
+
},
|
|
508
|
+
]
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
},
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
515
|
+
|
|
516
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
517
|
+
|
|
518
|
+
expect(mockDSL.product).toHaveBeenCalledWith(
|
|
519
|
+
expect.objectContaining({ to: expect.objectContaining({ name: expect.stringContaining('general') }) })
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('should use first group child with resource type as default route when first item is group', () => {
|
|
524
|
+
const mockPlugin = createMockPlugin();
|
|
525
|
+
const mockStore = createMockStore();
|
|
526
|
+
const mockDSL = {
|
|
527
|
+
product: jest.fn(),
|
|
528
|
+
basicType: jest.fn(),
|
|
529
|
+
labelGroup: jest.fn(),
|
|
530
|
+
setGroupDefaultType: jest.fn(),
|
|
531
|
+
weightGroup: jest.fn(),
|
|
532
|
+
virtualType: jest.fn(),
|
|
533
|
+
configureType: jest.fn(),
|
|
534
|
+
weightType: jest.fn(),
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
538
|
+
|
|
539
|
+
const productMetadata: ProductMetadata = {
|
|
540
|
+
name: 'group-resource-default',
|
|
541
|
+
label: 'Group Resource Default',
|
|
542
|
+
};
|
|
543
|
+
const config: ProductChildGroup[] = [
|
|
544
|
+
{
|
|
545
|
+
name: 'resources',
|
|
546
|
+
label: 'Resources',
|
|
547
|
+
sideMenu: {
|
|
548
|
+
children: [
|
|
549
|
+
{ type: 'provisioning.cattle.io.cluster' },
|
|
550
|
+
],
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
},
|
|
554
|
+
];
|
|
555
|
+
|
|
556
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
557
|
+
|
|
558
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
559
|
+
|
|
560
|
+
expect(mockDSL.product).toHaveBeenCalledWith(
|
|
561
|
+
expect.objectContaining({
|
|
562
|
+
to: expect.objectContaining({
|
|
563
|
+
name: 'groupresourcedefault-c-cluster-resource',
|
|
564
|
+
params: expect.objectContaining({
|
|
565
|
+
product: 'groupresourcedefault',
|
|
566
|
+
cluster: '__BLANK_CLUSTER__',
|
|
567
|
+
resource: 'provisioning.cattle.io.cluster',
|
|
568
|
+
}),
|
|
569
|
+
}),
|
|
570
|
+
})
|
|
571
|
+
);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('should use resource type as default route when first item is configureType', () => {
|
|
575
|
+
const mockPlugin = createMockPlugin();
|
|
576
|
+
const mockStore = createMockStore();
|
|
577
|
+
const mockDSL = {
|
|
578
|
+
product: jest.fn(),
|
|
579
|
+
basicType: jest.fn(),
|
|
580
|
+
labelGroup: jest.fn(),
|
|
581
|
+
setGroupDefaultType: jest.fn(),
|
|
582
|
+
weightGroup: jest.fn(),
|
|
583
|
+
virtualType: jest.fn(),
|
|
584
|
+
configureType: jest.fn(),
|
|
585
|
+
weightType: jest.fn(),
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
589
|
+
|
|
590
|
+
const productMetadata: ProductMetadata = {
|
|
591
|
+
name: 'resource-first',
|
|
592
|
+
label: 'Resource First',
|
|
593
|
+
};
|
|
594
|
+
const config: ProductChildPage[] = [
|
|
595
|
+
{ type: 'apps.deployment', sideMenu: { weight: 1 } },
|
|
596
|
+
{
|
|
597
|
+
name: 'overview',
|
|
598
|
+
label: 'Overview',
|
|
599
|
+
component: { name: 'OverviewComponent' },
|
|
600
|
+
sideMenu: { weight: 2 },
|
|
601
|
+
},
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
605
|
+
|
|
606
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
607
|
+
|
|
608
|
+
expect(mockDSL.product).toHaveBeenCalledWith(
|
|
609
|
+
expect.objectContaining({
|
|
610
|
+
to: expect.objectContaining({
|
|
611
|
+
name: 'resourcefirst-c-cluster-resource',
|
|
612
|
+
params: expect.objectContaining({
|
|
613
|
+
product: 'resourcefirst',
|
|
614
|
+
cluster: '__BLANK_CLUSTER__',
|
|
615
|
+
resource: 'apps.deployment',
|
|
616
|
+
}),
|
|
617
|
+
}),
|
|
618
|
+
})
|
|
619
|
+
);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('should use group component as default route when group has component but no children', () => {
|
|
623
|
+
const mockPlugin = createMockPlugin();
|
|
624
|
+
const mockStore = createMockStore();
|
|
625
|
+
const mockDSL = {
|
|
626
|
+
product: jest.fn(),
|
|
627
|
+
basicType: jest.fn(),
|
|
628
|
+
labelGroup: jest.fn(),
|
|
629
|
+
setGroupDefaultType: jest.fn(),
|
|
630
|
+
weightGroup: jest.fn(),
|
|
631
|
+
virtualType: jest.fn(),
|
|
632
|
+
configureType: jest.fn(),
|
|
633
|
+
weightType: jest.fn(),
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
637
|
+
|
|
638
|
+
const productMetadata: ProductMetadata = {
|
|
639
|
+
name: 'empty-group',
|
|
640
|
+
label: 'Empty Group',
|
|
641
|
+
};
|
|
642
|
+
const config: ProductChildGroup[] = [
|
|
643
|
+
{
|
|
644
|
+
name: 'empty-group-with-page',
|
|
645
|
+
label: 'Empty Group With Page',
|
|
646
|
+
component: { name: 'EmptyGroupComponent' },
|
|
647
|
+
sideMenu: { children: [] }
|
|
648
|
+
|
|
649
|
+
},
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
653
|
+
|
|
654
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
655
|
+
|
|
656
|
+
// Verify default route points to the group's component page (not a child, since there are none)
|
|
657
|
+
expect(mockDSL.product).toHaveBeenCalledWith(
|
|
658
|
+
expect.objectContaining({ to: expect.objectContaining({ name: expect.stringContaining('emptygroup') }) })
|
|
659
|
+
);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('should use group component as default route when group has both component and children', () => {
|
|
663
|
+
const mockPlugin = createMockPlugin();
|
|
664
|
+
const mockStore = createMockStore();
|
|
665
|
+
const mockDSL = {
|
|
666
|
+
product: jest.fn(),
|
|
667
|
+
basicType: jest.fn(),
|
|
668
|
+
labelGroup: jest.fn(),
|
|
669
|
+
setGroupDefaultType: jest.fn(),
|
|
670
|
+
weightGroup: jest.fn(),
|
|
671
|
+
virtualType: jest.fn(),
|
|
672
|
+
configureType: jest.fn(),
|
|
673
|
+
weightType: jest.fn(),
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
677
|
+
|
|
678
|
+
const productMetadata: ProductMetadata = {
|
|
679
|
+
name: 'group-with-page',
|
|
680
|
+
label: 'Group With Page',
|
|
681
|
+
};
|
|
682
|
+
const config: ProductChildGroup[] = [
|
|
683
|
+
{
|
|
684
|
+
name: 'settings',
|
|
685
|
+
label: 'Settings',
|
|
686
|
+
component: { name: 'SettingsOverviewComponent' },
|
|
687
|
+
sideMenu: {
|
|
688
|
+
children: [
|
|
689
|
+
{
|
|
690
|
+
name: 'general',
|
|
691
|
+
label: 'General Settings',
|
|
692
|
+
component: { name: 'GeneralComponent' },
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: 'advanced',
|
|
696
|
+
label: 'Advanced Settings',
|
|
697
|
+
component: { name: 'AdvancedComponent ' },
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
},
|
|
703
|
+
];
|
|
704
|
+
|
|
705
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
|
|
706
|
+
|
|
707
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
708
|
+
|
|
709
|
+
// Verify default route points to the group's component page (not first child)
|
|
710
|
+
// When a group has a component, the route includes the group's name for proper side-menu highlighting
|
|
711
|
+
expect(mockDSL.product).toHaveBeenCalledWith(
|
|
712
|
+
expect.objectContaining({ to: expect.objectContaining({ name: 'groupwithpage-settings' }) })
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
// Verify virtualType was still created for the group component
|
|
716
|
+
expect(mockDSL.virtualType).toHaveBeenCalledWith(
|
|
717
|
+
expect.objectContaining({
|
|
718
|
+
name: 'groupwithpage-settings',
|
|
719
|
+
exact: true,
|
|
720
|
+
overview: true,
|
|
721
|
+
})
|
|
722
|
+
);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
describe('mixed config types', () => {
|
|
727
|
+
it('should handle product with mixed virtualType and configureType items', () => {
|
|
728
|
+
const mockPlugin = createMockPlugin();
|
|
729
|
+
const mockStore = createMockStore();
|
|
730
|
+
const mockDSL = {
|
|
731
|
+
product: jest.fn(),
|
|
732
|
+
basicType: jest.fn(),
|
|
733
|
+
labelGroup: jest.fn(),
|
|
734
|
+
setGroupDefaultType: jest.fn(),
|
|
735
|
+
weightGroup: jest.fn(),
|
|
736
|
+
virtualType: jest.fn(),
|
|
737
|
+
configureType: jest.fn(),
|
|
738
|
+
weightType: jest.fn(),
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
742
|
+
|
|
743
|
+
const productMetadata: ProductMetadata = {
|
|
744
|
+
name: 'mixed-product',
|
|
745
|
+
label: 'Mixed Content',
|
|
746
|
+
};
|
|
747
|
+
const mixedConfig: ProductChildPage[] = [
|
|
748
|
+
{
|
|
749
|
+
name: 'overview',
|
|
750
|
+
label: 'Overview',
|
|
751
|
+
component: { name: 'OverviewComponent' },
|
|
752
|
+
},
|
|
753
|
+
{ type: 'resources.io' },
|
|
754
|
+
];
|
|
755
|
+
|
|
756
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, mixedConfig);
|
|
757
|
+
|
|
758
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
759
|
+
|
|
760
|
+
expect(mockDSL.virtualType).toHaveBeenCalledTimes(1);
|
|
761
|
+
expect(mockDSL.configureType).toHaveBeenCalledWith('resources.io', expect.any(Object));
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
describe('state verification', () => {
|
|
766
|
+
it('should set newProduct flag for new products', () => {
|
|
767
|
+
const mockPlugin = createMockPlugin();
|
|
768
|
+
const productMetadata: ProductMetadata = {
|
|
769
|
+
name: 'new-prod',
|
|
770
|
+
label: 'New',
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
const pluginProduct = new PluginProduct(mockPlugin, productMetadata, []);
|
|
774
|
+
|
|
775
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('should not set newProduct flag for standard product extensions', () => {
|
|
779
|
+
const mockPlugin = createMockPlugin();
|
|
780
|
+
const validStandardProduct = StandardProductNames.EXPLORER;
|
|
781
|
+
|
|
782
|
+
const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, []);
|
|
783
|
+
|
|
784
|
+
expect(pluginProduct.newProduct).toBe(false);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
});
|