@rancher/shell 3.0.12-rc.1 → 3.0.12-rc.2
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/assets/images/providers/entraid-black.svg +4 -0
- package/assets/images/providers/entraid.svg +9 -0
- package/assets/images/vendor/entraid.svg +9 -0
- package/assets/styles/app.scss +0 -1
- package/assets/translations/en-us.yaml +19 -17
- package/assets/translations/zh-hans.yaml +4 -8
- package/chart/__tests__/S3.test.ts +10 -3
- package/components/CountBox.vue +20 -0
- package/components/CreateDriver.vue +0 -12
- package/components/DetailText.vue +12 -3
- package/components/SelectIconGrid.vue +5 -0
- package/components/__tests__/CountBox.test.ts +72 -0
- package/components/__tests__/DetailText.test.ts +113 -0
- package/components/fleet/FleetClusterTargets/index.vue +18 -1
- package/components/form/InputWithSelect.vue +18 -10
- package/components/form/KeyValue.vue +17 -1
- package/components/form/LabeledSelect.vue +82 -24
- package/components/form/Select.vue +73 -56
- package/components/form/ServiceNameSelect.vue +13 -11
- package/components/form/__tests__/KeyValue.test.ts +66 -0
- package/components/form/__tests__/NodeScheduling.test.ts +9 -0
- package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
- package/components/nav/Group.vue +7 -6
- package/components/nav/Header.vue +24 -3
- package/components/nav/NotificationCenter/Notification.vue +4 -1
- package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
- package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
- package/components/nav/Type.vue +8 -7
- package/components/nav/WindowManager/index.vue +2 -1
- package/components/nav/WorkspaceSwitcher.vue +13 -0
- package/components/nav/__tests__/Group.test.ts +67 -0
- package/components/nav/__tests__/Header.test.ts +235 -0
- package/components/nav/__tests__/Type.test.ts +20 -3
- package/components/templates/default.vue +34 -4
- package/components/templates/home.vue +12 -25
- package/components/templates/plain.vue +13 -26
- package/composables/useLabeledFormElement.ts +10 -2
- package/composables/useLabeledSelect.ts +60 -0
- package/composables/useUserRetentionValidation.ts +1 -49
- package/config/cookies.js +0 -1
- package/config/labels-annotations.js +1 -0
- package/config/query-params.js +1 -0
- package/config/router/routes.js +0 -8
- package/core/__tests__/plugin-products.test.ts +616 -25
- package/core/plugin-products-base.ts +31 -14
- package/core/plugin-products-helpers.ts +5 -4
- package/core/plugin-types.ts +18 -3
- package/core/types.ts +3 -1
- package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
- package/detail/management.cattle.io.fleetworkspace.vue +49 -0
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
- package/edit/__tests__/kontainerDriver.test.ts +0 -13
- package/edit/__tests__/nodeDriver.test.ts +5 -11
- package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
- package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
- package/edit/auth/__tests__/oidc.test.ts +54 -0
- package/edit/auth/azuread.vue +1 -1
- package/edit/auth/oidc.vue +8 -0
- package/edit/kontainerDriver.vue +1 -2
- package/edit/nodeDriver.vue +0 -2
- package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
- package/initialize/App.vue +29 -2
- package/initialize/install-plugins.js +0 -2
- package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
- package/list/catalog.cattle.io.app.vue +25 -5
- package/list/management.cattle.io.feature.vue +1 -1
- package/list/management.cattle.io.fleetworkspace.vue +8 -0
- package/machine-config/amazonec2.vue +1 -0
- package/mixins/chart.js +40 -9
- package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
- package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
- package/models/__tests__/chart.test.ts +99 -6
- package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
- package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
- package/models/catalog.cattle.io.app.js +21 -17
- package/models/catalog.cattle.io.clusterrepo.js +39 -11
- package/models/chart.js +33 -19
- package/models/fleet-application.js +1 -1
- package/models/fleet.cattle.io.bundle.js +1 -1
- package/models/kontainerdriver.js +11 -0
- package/models/management.cattle.io.authconfig.js +5 -1
- package/models/management.cattle.io.cluster.js +0 -53
- package/models/management.cattle.io.feature.js +3 -3
- package/models/management.cattle.io.kontainerdriver.js +1 -26
- package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
- package/models/nodedriver.js +7 -0
- package/package.json +13 -12
- package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
- package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
- package/pages/c/_cluster/apps/charts/chart.vue +217 -33
- package/pages/c/_cluster/apps/charts/index.vue +2 -2
- package/pages/c/_cluster/apps/charts/install.vue +8 -3
- package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
- package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +15 -10
- package/pages/c/_cluster/uiplugins/index.vue +23 -25
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
- package/scripts/test-plugins-build.sh +5 -2
- package/server/server-middleware.js +2 -2
- package/static/humans.txt +1 -0
- package/static/robots.txt +34 -0
- package/static/welcome-cow.svg +18 -0
- package/store/__tests__/catalog.test.ts +161 -11
- package/store/auth.js +0 -3
- package/store/catalog.js +60 -8
- package/types/shell/index.d.ts +26 -22
- package/utils/__tests__/git.test.ts +270 -0
- package/utils/__tests__/inactivity.test.ts +316 -0
- package/utils/__tests__/object.test.ts +77 -0
- package/utils/__tests__/time.test.ts +14 -1
- package/utils/__tests__/url.test.ts +246 -0
- package/utils/object.js +33 -2
- package/utils/time.ts +5 -0
- package/vue.config.js +0 -9
- package/assets/images/providers/azuread-black.svg +0 -22
- package/assets/images/providers/azuread.svg +0 -25
- package/assets/images/vendor/azuread.svg +0 -18
- package/assets/styles/fonts/_dots.scss +0 -18
- package/components/EmberPage.vue +0 -622
- package/components/EmberPageView.vue +0 -39
- package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
- package/mixins/labeled-form-element.ts +0 -225
- package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
- package/pages/c/_cluster/manager/pages/_page.vue +0 -22
- package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
- package/plugins/ember-cookie.js +0 -17
- package/utils/ember-page.js +0 -30
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { PluginProduct } from '@shell/core/plugin-products';
|
|
2
2
|
import {
|
|
3
3
|
ProductMetadata, ProductSinglePage, ProductChildPage,
|
|
4
|
-
ProductChildGroup,
|
|
4
|
+
ProductChildGroup, ProductChildCustomPage, ProductChildResourcePage,
|
|
5
|
+
ProductChild, StandardProductNames
|
|
5
6
|
} from '@shell/core/plugin-types';
|
|
6
7
|
import { IExtension } from '@shell/core/types';
|
|
7
8
|
|
|
@@ -891,9 +892,9 @@ describe('pluginProduct', () => {
|
|
|
891
892
|
pluginProduct.apply(mockPlugin, mockStore);
|
|
892
893
|
|
|
893
894
|
// Verify default route points to the group's component page (not first child)
|
|
894
|
-
// When a group has a component,
|
|
895
|
+
// When a group has a component, the route includes the group's name for proper side-menu highlighting
|
|
895
896
|
expect(mockDSL.product).toHaveBeenCalledWith(
|
|
896
|
-
expect.objectContaining({ to: expect.objectContaining({ name: 'groupwithpage-
|
|
897
|
+
expect.objectContaining({ to: expect.objectContaining({ name: 'groupwithpage-settings' }) })
|
|
897
898
|
);
|
|
898
899
|
|
|
899
900
|
// Verify virtualType was still created for the group component
|
|
@@ -965,28 +966,6 @@ describe('pluginProduct', () => {
|
|
|
965
966
|
new PluginProduct(mockPlugin, productMetadata, badConfig);
|
|
966
967
|
}).toThrow('forEach');
|
|
967
968
|
});
|
|
968
|
-
|
|
969
|
-
it('should throw when extending standard product and group parent has component', () => {
|
|
970
|
-
const mockPlugin = createMockPlugin();
|
|
971
|
-
const config: ProductChildGroup[] = [
|
|
972
|
-
{
|
|
973
|
-
name: 'parent-group',
|
|
974
|
-
label: 'Parent Group',
|
|
975
|
-
component: { name: 'GroupComponent' },
|
|
976
|
-
children: [
|
|
977
|
-
{
|
|
978
|
-
name: 'child',
|
|
979
|
-
label: 'Child',
|
|
980
|
-
component: { name: 'ChildComponent' },
|
|
981
|
-
},
|
|
982
|
-
],
|
|
983
|
-
},
|
|
984
|
-
];
|
|
985
|
-
|
|
986
|
-
expect(() => {
|
|
987
|
-
new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, config);
|
|
988
|
-
}).toThrow('When extending an existing product, group parent items cannot have a component because of route matching conflicts.');
|
|
989
|
-
});
|
|
990
969
|
});
|
|
991
970
|
|
|
992
971
|
describe('state verification', () => {
|
|
@@ -3153,6 +3132,13 @@ describe('pluginProduct', () => {
|
|
|
3153
3132
|
virtualType: jest.fn(),
|
|
3154
3133
|
configureType: jest.fn(),
|
|
3155
3134
|
weightType: jest.fn(),
|
|
3135
|
+
headers: jest.fn(),
|
|
3136
|
+
hideBulkActions: jest.fn(),
|
|
3137
|
+
spoofedType: jest.fn(),
|
|
3138
|
+
mapGroup: jest.fn(),
|
|
3139
|
+
ignoreGroup: jest.fn(),
|
|
3140
|
+
mapType: jest.fn(),
|
|
3141
|
+
ignoreType: jest.fn(),
|
|
3156
3142
|
};
|
|
3157
3143
|
|
|
3158
3144
|
jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
|
|
@@ -3181,6 +3167,13 @@ describe('pluginProduct', () => {
|
|
|
3181
3167
|
virtualType: jest.fn(),
|
|
3182
3168
|
configureType: jest.fn(),
|
|
3183
3169
|
weightType: jest.fn(),
|
|
3170
|
+
headers: jest.fn(),
|
|
3171
|
+
hideBulkActions: jest.fn(),
|
|
3172
|
+
spoofedType: jest.fn(),
|
|
3173
|
+
mapGroup: jest.fn(),
|
|
3174
|
+
ignoreGroup: jest.fn(),
|
|
3175
|
+
mapType: jest.fn(),
|
|
3176
|
+
ignoreType: jest.fn(),
|
|
3184
3177
|
};
|
|
3185
3178
|
|
|
3186
3179
|
jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
|
|
@@ -3205,6 +3198,13 @@ describe('pluginProduct', () => {
|
|
|
3205
3198
|
virtualType: jest.fn(),
|
|
3206
3199
|
configureType: jest.fn(),
|
|
3207
3200
|
weightType: jest.fn(),
|
|
3201
|
+
headers: jest.fn(),
|
|
3202
|
+
hideBulkActions: jest.fn(),
|
|
3203
|
+
spoofedType: jest.fn(),
|
|
3204
|
+
mapGroup: jest.fn(),
|
|
3205
|
+
ignoreGroup: jest.fn(),
|
|
3206
|
+
mapType: jest.fn(),
|
|
3207
|
+
ignoreType: jest.fn(),
|
|
3208
3208
|
};
|
|
3209
3209
|
|
|
3210
3210
|
jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
|
|
@@ -3216,4 +3216,595 @@ describe('pluginProduct', () => {
|
|
|
3216
3216
|
expect(productCalls[0][0]).toStrictEqual(expect.objectContaining({ name: 'myproduct' }));
|
|
3217
3217
|
});
|
|
3218
3218
|
});
|
|
3219
|
+
|
|
3220
|
+
describe('documentation examples', () => {
|
|
3221
|
+
describe('quick start: string convenience method', () => {
|
|
3222
|
+
it('should create a new product from just a string name', () => {
|
|
3223
|
+
const mockPlugin = createMockPlugin();
|
|
3224
|
+
const pluginProduct = PluginProduct.fromName(mockPlugin, 'my-first-product');
|
|
3225
|
+
|
|
3226
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3227
|
+
expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
|
|
3228
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
|
|
3229
|
+
});
|
|
3230
|
+
});
|
|
3231
|
+
|
|
3232
|
+
describe('single page product', () => {
|
|
3233
|
+
it('should create a product with a single page component and no config', () => {
|
|
3234
|
+
const mockPlugin = createMockPlugin();
|
|
3235
|
+
const product: ProductSinglePage = {
|
|
3236
|
+
name: 'my-dashboard',
|
|
3237
|
+
label: 'My Dashboard',
|
|
3238
|
+
icon: 'globe',
|
|
3239
|
+
component: { name: 'DashboardPage' },
|
|
3240
|
+
};
|
|
3241
|
+
|
|
3242
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, []);
|
|
3243
|
+
|
|
3244
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3245
|
+
expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
|
|
3246
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
|
|
3247
|
+
});
|
|
3248
|
+
});
|
|
3249
|
+
|
|
3250
|
+
describe('product with custom pages', () => {
|
|
3251
|
+
it('should register routes for each custom page', () => {
|
|
3252
|
+
const mockPlugin = createMockPlugin();
|
|
3253
|
+
|
|
3254
|
+
const overviewPage: ProductChildCustomPage = {
|
|
3255
|
+
name: 'overview',
|
|
3256
|
+
label: 'Overview',
|
|
3257
|
+
component: { name: 'OverviewPage' },
|
|
3258
|
+
weight: 2,
|
|
3259
|
+
};
|
|
3260
|
+
|
|
3261
|
+
const settingsPage: ProductChildCustomPage = {
|
|
3262
|
+
name: 'settings',
|
|
3263
|
+
label: 'Settings',
|
|
3264
|
+
component: { name: 'SettingsPage' },
|
|
3265
|
+
weight: 1,
|
|
3266
|
+
};
|
|
3267
|
+
|
|
3268
|
+
const product: ProductMetadata = {
|
|
3269
|
+
name: 'my-app',
|
|
3270
|
+
label: 'My App',
|
|
3271
|
+
icon: 'gear',
|
|
3272
|
+
};
|
|
3273
|
+
|
|
3274
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [overviewPage, settingsPage]);
|
|
3275
|
+
|
|
3276
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3277
|
+
expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
|
|
3278
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(2);
|
|
3279
|
+
});
|
|
3280
|
+
});
|
|
3281
|
+
|
|
3282
|
+
describe('product with resource pages', () => {
|
|
3283
|
+
it('should register resource CRUD routes for resource page items', () => {
|
|
3284
|
+
const mockPlugin = createMockPlugin();
|
|
3285
|
+
|
|
3286
|
+
const clusterPage: ProductChildResourcePage = {
|
|
3287
|
+
type: 'provisioning.cattle.io.cluster',
|
|
3288
|
+
weight: 2,
|
|
3289
|
+
config: {
|
|
3290
|
+
displayName: 'Clusters',
|
|
3291
|
+
isCreatable: true,
|
|
3292
|
+
isEditable: true,
|
|
3293
|
+
isRemovable: true,
|
|
3294
|
+
canYaml: true,
|
|
3295
|
+
},
|
|
3296
|
+
};
|
|
3297
|
+
|
|
3298
|
+
const nodePage: ProductChildResourcePage = {
|
|
3299
|
+
type: 'management.cattle.io.node',
|
|
3300
|
+
weight: 1,
|
|
3301
|
+
};
|
|
3302
|
+
|
|
3303
|
+
const product: ProductMetadata = {
|
|
3304
|
+
name: 'my-resources',
|
|
3305
|
+
label: 'My Resources',
|
|
3306
|
+
};
|
|
3307
|
+
|
|
3308
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [clusterPage, nodePage]);
|
|
3309
|
+
|
|
3310
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3311
|
+
// Resource routes are only added once (shared CRUD routes) - mock generates list + detail = 2
|
|
3312
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(2);
|
|
3313
|
+
});
|
|
3314
|
+
});
|
|
3315
|
+
|
|
3316
|
+
describe('product with groups', () => {
|
|
3317
|
+
it('should register routes for a product with groups and standalone pages', () => {
|
|
3318
|
+
const mockPlugin = createMockPlugin();
|
|
3319
|
+
|
|
3320
|
+
const alertsPage: ProductChildCustomPage = {
|
|
3321
|
+
name: 'alerts',
|
|
3322
|
+
label: 'Alerts',
|
|
3323
|
+
component: { name: 'AlertsPage' },
|
|
3324
|
+
};
|
|
3325
|
+
|
|
3326
|
+
const metricsPage: ProductChildCustomPage = {
|
|
3327
|
+
name: 'metrics',
|
|
3328
|
+
label: 'Metrics',
|
|
3329
|
+
component: { name: 'MetricsPage' },
|
|
3330
|
+
};
|
|
3331
|
+
|
|
3332
|
+
const monitoringGroup: ProductChildGroup = {
|
|
3333
|
+
name: 'monitoring',
|
|
3334
|
+
label: 'Monitoring',
|
|
3335
|
+
weight: 2,
|
|
3336
|
+
children: [alertsPage, metricsPage],
|
|
3337
|
+
};
|
|
3338
|
+
|
|
3339
|
+
const overviewPage: ProductChildCustomPage = {
|
|
3340
|
+
name: 'overview',
|
|
3341
|
+
label: 'Overview',
|
|
3342
|
+
component: { name: 'OverviewPage' },
|
|
3343
|
+
weight: 3,
|
|
3344
|
+
};
|
|
3345
|
+
|
|
3346
|
+
const product: ProductMetadata = {
|
|
3347
|
+
name: 'my-platform',
|
|
3348
|
+
label: 'My Platform',
|
|
3349
|
+
};
|
|
3350
|
+
|
|
3351
|
+
const config: ProductChild[] = [overviewPage, monitoringGroup];
|
|
3352
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, config);
|
|
3353
|
+
|
|
3354
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3355
|
+
expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
|
|
3356
|
+
// 1 standalone page + 1 group parent route + 2 group children routes = 4
|
|
3357
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(4);
|
|
3358
|
+
});
|
|
3359
|
+
});
|
|
3360
|
+
|
|
3361
|
+
describe('extending an existing product', () => {
|
|
3362
|
+
it('should extend explorer with a custom page', () => {
|
|
3363
|
+
const mockPlugin = createMockPlugin();
|
|
3364
|
+
|
|
3365
|
+
const customPage: ProductChildCustomPage = {
|
|
3366
|
+
name: 'my-custom-view',
|
|
3367
|
+
label: 'My Custom View',
|
|
3368
|
+
component: { name: 'MyCustomView' },
|
|
3369
|
+
};
|
|
3370
|
+
|
|
3371
|
+
const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, [customPage]);
|
|
3372
|
+
|
|
3373
|
+
expect(pluginProduct.newProduct).toBe(false);
|
|
3374
|
+
expect(mockPlugin._registerTopLevelProduct).not.toHaveBeenCalled();
|
|
3375
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
|
|
3376
|
+
});
|
|
3377
|
+
});
|
|
3378
|
+
|
|
3379
|
+
describe('mixed pages: custom + resource', () => {
|
|
3380
|
+
it('should register routes for both custom pages and resource pages together', () => {
|
|
3381
|
+
const mockPlugin = createMockPlugin();
|
|
3382
|
+
|
|
3383
|
+
const dashboardPage: ProductChildCustomPage = {
|
|
3384
|
+
name: 'dashboard',
|
|
3385
|
+
label: 'Dashboard',
|
|
3386
|
+
component: { name: 'Dashboard' },
|
|
3387
|
+
weight: 3,
|
|
3388
|
+
};
|
|
3389
|
+
|
|
3390
|
+
const clusterPage: ProductChildResourcePage = {
|
|
3391
|
+
type: 'provisioning.cattle.io.cluster',
|
|
3392
|
+
weight: 2,
|
|
3393
|
+
};
|
|
3394
|
+
|
|
3395
|
+
const settingsPage: ProductChildCustomPage = {
|
|
3396
|
+
name: 'settings',
|
|
3397
|
+
label: 'Settings',
|
|
3398
|
+
component: { name: 'Settings' },
|
|
3399
|
+
weight: 1,
|
|
3400
|
+
};
|
|
3401
|
+
|
|
3402
|
+
const product: ProductMetadata = {
|
|
3403
|
+
name: 'my-platform',
|
|
3404
|
+
label: 'My Platform',
|
|
3405
|
+
};
|
|
3406
|
+
|
|
3407
|
+
const config: ProductChild[] = [dashboardPage, clusterPage, settingsPage];
|
|
3408
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, config);
|
|
3409
|
+
|
|
3410
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3411
|
+
// 2 custom page routes + resource CRUD routes (list + detail = 2 from mock) = 4
|
|
3412
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(4);
|
|
3413
|
+
// Verify addRoute was called for both custom pages and resource routes
|
|
3414
|
+
const routeCalls = (mockPlugin.addRoute as jest.Mock).mock.calls;
|
|
3415
|
+
const hasCustomRoutes = routeCalls.some((call) => call[0]?.name?.includes('dashboard'));
|
|
3416
|
+
const hasResourceRoutes = routeCalls.some((call) => call[0]?.name?.includes('provisioning.cattle.io.cluster'));
|
|
3417
|
+
|
|
3418
|
+
expect(hasCustomRoutes).toBe(true);
|
|
3419
|
+
expect(hasResourceRoutes).toBe(true);
|
|
3420
|
+
});
|
|
3421
|
+
});
|
|
3422
|
+
|
|
3423
|
+
describe('group with its own page', () => {
|
|
3424
|
+
it('should register a route for the group page itself and its children', () => {
|
|
3425
|
+
const mockPlugin = createMockPlugin();
|
|
3426
|
+
|
|
3427
|
+
const alertsPage: ProductChildCustomPage = {
|
|
3428
|
+
name: 'alerts',
|
|
3429
|
+
label: 'Alerts',
|
|
3430
|
+
component: { name: 'AlertsPage' },
|
|
3431
|
+
};
|
|
3432
|
+
|
|
3433
|
+
const metricsPage: ProductChildCustomPage = {
|
|
3434
|
+
name: 'metrics',
|
|
3435
|
+
label: 'Metrics',
|
|
3436
|
+
component: { name: 'MetricsPage' },
|
|
3437
|
+
};
|
|
3438
|
+
|
|
3439
|
+
const monitoringGroup: ProductChildGroup = {
|
|
3440
|
+
name: 'monitoring',
|
|
3441
|
+
label: 'Monitoring',
|
|
3442
|
+
component: { name: 'MonitoringOverview' },
|
|
3443
|
+
children: [alertsPage, metricsPage],
|
|
3444
|
+
};
|
|
3445
|
+
|
|
3446
|
+
const product: ProductMetadata = {
|
|
3447
|
+
name: 'my-platform',
|
|
3448
|
+
label: 'My Platform',
|
|
3449
|
+
};
|
|
3450
|
+
|
|
3451
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [monitoringGroup]);
|
|
3452
|
+
|
|
3453
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3454
|
+
// 1 group parent route (with component) + 2 children routes = 3
|
|
3455
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(3);
|
|
3456
|
+
});
|
|
3457
|
+
|
|
3458
|
+
it('should generate a route with the group name for proper side-menu highlighting', () => {
|
|
3459
|
+
const mockPlugin = createMockPlugin();
|
|
3460
|
+
|
|
3461
|
+
const childPage: ProductChildCustomPage = {
|
|
3462
|
+
name: 'child',
|
|
3463
|
+
label: 'Child',
|
|
3464
|
+
component: { name: 'ChildComponent' },
|
|
3465
|
+
};
|
|
3466
|
+
|
|
3467
|
+
const monitoringGroup: ProductChildGroup = {
|
|
3468
|
+
name: 'monitoring',
|
|
3469
|
+
label: 'Monitoring',
|
|
3470
|
+
component: { name: 'MonitoringOverview' },
|
|
3471
|
+
children: [childPage],
|
|
3472
|
+
};
|
|
3473
|
+
|
|
3474
|
+
const product: ProductMetadata = {
|
|
3475
|
+
name: 'my-platform',
|
|
3476
|
+
label: 'My Platform',
|
|
3477
|
+
};
|
|
3478
|
+
|
|
3479
|
+
new PluginProduct(mockPlugin, product, [monitoringGroup]);
|
|
3480
|
+
|
|
3481
|
+
// The group's own route should include the group name in the route name
|
|
3482
|
+
// This ensures the side-menu can highlight the correct item
|
|
3483
|
+
const routeCalls = (mockPlugin.addRoute as jest.Mock).mock.calls;
|
|
3484
|
+
const groupRoute = routeCalls.find((call) => call[0]?.name?.includes('monitoring'));
|
|
3485
|
+
|
|
3486
|
+
expect(groupRoute).toBeDefined();
|
|
3487
|
+
expect(groupRoute[0].name).toStrictEqual(expect.stringContaining('monitoring'));
|
|
3488
|
+
});
|
|
3489
|
+
});
|
|
3490
|
+
|
|
3491
|
+
describe('extending Cluster Explorer', () => {
|
|
3492
|
+
it('should extend explorer with a standalone page', () => {
|
|
3493
|
+
const mockPlugin = createMockPlugin();
|
|
3494
|
+
|
|
3495
|
+
const customPage: ProductChildCustomPage = {
|
|
3496
|
+
name: 'cost-analysis',
|
|
3497
|
+
label: 'Cost Analysis',
|
|
3498
|
+
component: { name: 'CostAnalysis' },
|
|
3499
|
+
};
|
|
3500
|
+
|
|
3501
|
+
const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, [customPage]);
|
|
3502
|
+
|
|
3503
|
+
expect(pluginProduct.newProduct).toBe(false);
|
|
3504
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
|
|
3505
|
+
});
|
|
3506
|
+
|
|
3507
|
+
it('should extend explorer with a group containing multiple pages', () => {
|
|
3508
|
+
const mockPlugin = createMockPlugin();
|
|
3509
|
+
|
|
3510
|
+
const costPage: ProductChildCustomPage = {
|
|
3511
|
+
name: 'cost-analysis',
|
|
3512
|
+
label: 'Cost Analysis',
|
|
3513
|
+
component: { name: 'CostAnalysis' },
|
|
3514
|
+
};
|
|
3515
|
+
|
|
3516
|
+
const usagePage: ProductChildCustomPage = {
|
|
3517
|
+
name: 'usage-report',
|
|
3518
|
+
label: 'Usage Report',
|
|
3519
|
+
component: { name: 'UsageReport' },
|
|
3520
|
+
};
|
|
3521
|
+
|
|
3522
|
+
const insightsGroup: ProductChildGroup = {
|
|
3523
|
+
name: 'insights',
|
|
3524
|
+
label: 'Insights',
|
|
3525
|
+
children: [costPage, usagePage],
|
|
3526
|
+
};
|
|
3527
|
+
|
|
3528
|
+
const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, [insightsGroup]);
|
|
3529
|
+
|
|
3530
|
+
expect(pluginProduct.newProduct).toBe(false);
|
|
3531
|
+
// 1 group parent route + 2 child routes = 3
|
|
3532
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(3);
|
|
3533
|
+
});
|
|
3534
|
+
});
|
|
3535
|
+
|
|
3536
|
+
describe('translation keys instead of labels', () => {
|
|
3537
|
+
it('should accept labelKey instead of label for product and pages', () => {
|
|
3538
|
+
const mockPlugin = createMockPlugin();
|
|
3539
|
+
|
|
3540
|
+
const product: ProductMetadata = {
|
|
3541
|
+
name: 'my-app',
|
|
3542
|
+
labelKey: 'product.myApp.label',
|
|
3543
|
+
icon: 'gear',
|
|
3544
|
+
};
|
|
3545
|
+
|
|
3546
|
+
const overviewPage: ProductChildCustomPage = {
|
|
3547
|
+
name: 'overview',
|
|
3548
|
+
labelKey: 'product.myApp.overview',
|
|
3549
|
+
component: { name: 'OverviewPage' },
|
|
3550
|
+
};
|
|
3551
|
+
|
|
3552
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [overviewPage]);
|
|
3553
|
+
|
|
3554
|
+
expect(pluginProduct.newProduct).toBe(true);
|
|
3555
|
+
expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
|
|
3556
|
+
expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
|
|
3557
|
+
});
|
|
3558
|
+
|
|
3559
|
+
it('should register labelKey on virtualType during apply', () => {
|
|
3560
|
+
const mockPlugin = createMockPlugin();
|
|
3561
|
+
const mockStore = createMockStore();
|
|
3562
|
+
const virtualTypeCalls: any[] = [];
|
|
3563
|
+
const mockDSL = {
|
|
3564
|
+
product: jest.fn(),
|
|
3565
|
+
basicType: jest.fn(),
|
|
3566
|
+
labelGroup: jest.fn(),
|
|
3567
|
+
setGroupDefaultType: jest.fn(),
|
|
3568
|
+
weightGroup: jest.fn(),
|
|
3569
|
+
virtualType: jest.fn((...args: any[]) => virtualTypeCalls.push(args)),
|
|
3570
|
+
configureType: jest.fn(),
|
|
3571
|
+
weightType: jest.fn(),
|
|
3572
|
+
};
|
|
3573
|
+
|
|
3574
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3575
|
+
|
|
3576
|
+
const product: ProductMetadata = {
|
|
3577
|
+
name: 'my-app',
|
|
3578
|
+
labelKey: 'product.myApp.label',
|
|
3579
|
+
};
|
|
3580
|
+
|
|
3581
|
+
const overviewPage: ProductChildCustomPage = {
|
|
3582
|
+
name: 'overview',
|
|
3583
|
+
labelKey: 'product.myApp.overview',
|
|
3584
|
+
component: { name: 'OverviewPage' },
|
|
3585
|
+
};
|
|
3586
|
+
|
|
3587
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [overviewPage]);
|
|
3588
|
+
|
|
3589
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3590
|
+
|
|
3591
|
+
expect(virtualTypeCalls).toHaveLength(1);
|
|
3592
|
+
expect(virtualTypeCalls[0][0]).toStrictEqual(expect.objectContaining({ labelKey: 'product.myApp.overview' }));
|
|
3593
|
+
});
|
|
3594
|
+
});
|
|
3595
|
+
|
|
3596
|
+
describe('duplicate page name detection', () => {
|
|
3597
|
+
it('should throw when two custom pages have the same name in a new product', () => {
|
|
3598
|
+
const mockPlugin = createMockPlugin();
|
|
3599
|
+
const mockStore = createMockStore();
|
|
3600
|
+
const mockDSL = {
|
|
3601
|
+
product: jest.fn(),
|
|
3602
|
+
basicType: jest.fn(),
|
|
3603
|
+
labelGroup: jest.fn(),
|
|
3604
|
+
setGroupDefaultType: jest.fn(),
|
|
3605
|
+
weightGroup: jest.fn(),
|
|
3606
|
+
virtualType: jest.fn(),
|
|
3607
|
+
configureType: jest.fn(),
|
|
3608
|
+
weightType: jest.fn(),
|
|
3609
|
+
};
|
|
3610
|
+
|
|
3611
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3612
|
+
|
|
3613
|
+
const page1: ProductChildCustomPage = {
|
|
3614
|
+
name: 'overview',
|
|
3615
|
+
label: 'Overview',
|
|
3616
|
+
component: { name: 'Page1' },
|
|
3617
|
+
};
|
|
3618
|
+
|
|
3619
|
+
const page2: ProductChildCustomPage = {
|
|
3620
|
+
name: 'overview',
|
|
3621
|
+
label: 'Overview Duplicate',
|
|
3622
|
+
component: { name: 'Page2' },
|
|
3623
|
+
};
|
|
3624
|
+
|
|
3625
|
+
const product: ProductMetadata = {
|
|
3626
|
+
name: 'my-app',
|
|
3627
|
+
label: 'My App',
|
|
3628
|
+
};
|
|
3629
|
+
|
|
3630
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [page1, page2]);
|
|
3631
|
+
|
|
3632
|
+
expect(() => {
|
|
3633
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3634
|
+
}).toThrow('Duplicate page name "overview"');
|
|
3635
|
+
});
|
|
3636
|
+
|
|
3637
|
+
it('should not throw when pages with the same name are in different groups (different resolved names)', () => {
|
|
3638
|
+
const mockPlugin = createMockPlugin();
|
|
3639
|
+
const mockStore = createMockStore();
|
|
3640
|
+
const mockDSL = {
|
|
3641
|
+
product: jest.fn(),
|
|
3642
|
+
basicType: jest.fn(),
|
|
3643
|
+
labelGroup: jest.fn(),
|
|
3644
|
+
setGroupDefaultType: jest.fn(),
|
|
3645
|
+
weightGroup: jest.fn(),
|
|
3646
|
+
virtualType: jest.fn(),
|
|
3647
|
+
configureType: jest.fn(),
|
|
3648
|
+
weightType: jest.fn(),
|
|
3649
|
+
};
|
|
3650
|
+
|
|
3651
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3652
|
+
|
|
3653
|
+
// Same page name 'overview' but in different groups produces different resolved names
|
|
3654
|
+
const standalonePage: ProductChildCustomPage = {
|
|
3655
|
+
name: 'cost-analysis',
|
|
3656
|
+
label: 'Cost Analysis',
|
|
3657
|
+
component: { name: 'CostAnalysis1' },
|
|
3658
|
+
};
|
|
3659
|
+
|
|
3660
|
+
const groupChildPage: ProductChildCustomPage = {
|
|
3661
|
+
name: 'cost-analysis',
|
|
3662
|
+
label: 'Cost Analysis',
|
|
3663
|
+
component: { name: 'CostAnalysis2' },
|
|
3664
|
+
};
|
|
3665
|
+
|
|
3666
|
+
const group: ProductChildGroup = {
|
|
3667
|
+
name: 'insights',
|
|
3668
|
+
label: 'Insights',
|
|
3669
|
+
children: [groupChildPage],
|
|
3670
|
+
};
|
|
3671
|
+
|
|
3672
|
+
const product: ProductMetadata = {
|
|
3673
|
+
name: 'my-app',
|
|
3674
|
+
label: 'My App',
|
|
3675
|
+
};
|
|
3676
|
+
|
|
3677
|
+
const config: ProductChild[] = [standalonePage, group];
|
|
3678
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, config);
|
|
3679
|
+
|
|
3680
|
+
// Different groups produce different resolved names (myapp-cost-analysis vs myapp-insights-cost-analysis)
|
|
3681
|
+
expect(() => {
|
|
3682
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3683
|
+
}).not.toThrow();
|
|
3684
|
+
});
|
|
3685
|
+
|
|
3686
|
+
it('should throw when two resource pages have the same type', () => {
|
|
3687
|
+
const mockPlugin = createMockPlugin();
|
|
3688
|
+
const mockStore = createMockStore();
|
|
3689
|
+
const mockDSL = {
|
|
3690
|
+
product: jest.fn(),
|
|
3691
|
+
basicType: jest.fn(),
|
|
3692
|
+
labelGroup: jest.fn(),
|
|
3693
|
+
setGroupDefaultType: jest.fn(),
|
|
3694
|
+
weightGroup: jest.fn(),
|
|
3695
|
+
virtualType: jest.fn(),
|
|
3696
|
+
configureType: jest.fn(),
|
|
3697
|
+
weightType: jest.fn(),
|
|
3698
|
+
};
|
|
3699
|
+
|
|
3700
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3701
|
+
|
|
3702
|
+
const resource1: ProductChildResourcePage = { type: 'provisioning.cattle.io.cluster' };
|
|
3703
|
+
|
|
3704
|
+
const resource2: ProductChildResourcePage = { type: 'provisioning.cattle.io.cluster' };
|
|
3705
|
+
|
|
3706
|
+
const product: ProductMetadata = {
|
|
3707
|
+
name: 'my-app',
|
|
3708
|
+
label: 'My App',
|
|
3709
|
+
};
|
|
3710
|
+
|
|
3711
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [resource1, resource2]);
|
|
3712
|
+
|
|
3713
|
+
expect(() => {
|
|
3714
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3715
|
+
}).toThrow('Duplicate resource type "provisioning.cattle.io.cluster"');
|
|
3716
|
+
});
|
|
3717
|
+
|
|
3718
|
+
it('should not throw when pages have different names', () => {
|
|
3719
|
+
const mockPlugin = createMockPlugin();
|
|
3720
|
+
const mockStore = createMockStore();
|
|
3721
|
+
const mockDSL = {
|
|
3722
|
+
product: jest.fn(),
|
|
3723
|
+
basicType: jest.fn(),
|
|
3724
|
+
labelGroup: jest.fn(),
|
|
3725
|
+
setGroupDefaultType: jest.fn(),
|
|
3726
|
+
weightGroup: jest.fn(),
|
|
3727
|
+
virtualType: jest.fn(),
|
|
3728
|
+
configureType: jest.fn(),
|
|
3729
|
+
weightType: jest.fn(),
|
|
3730
|
+
};
|
|
3731
|
+
|
|
3732
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3733
|
+
|
|
3734
|
+
const page1: ProductChildCustomPage = {
|
|
3735
|
+
name: 'overview',
|
|
3736
|
+
label: 'Overview',
|
|
3737
|
+
component: { name: 'Page1' },
|
|
3738
|
+
};
|
|
3739
|
+
|
|
3740
|
+
const page2: ProductChildCustomPage = {
|
|
3741
|
+
name: 'settings',
|
|
3742
|
+
label: 'Settings',
|
|
3743
|
+
component: { name: 'Page2' },
|
|
3744
|
+
};
|
|
3745
|
+
|
|
3746
|
+
const product: ProductMetadata = {
|
|
3747
|
+
name: 'my-app',
|
|
3748
|
+
label: 'My App',
|
|
3749
|
+
};
|
|
3750
|
+
|
|
3751
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [page1, page2]);
|
|
3752
|
+
|
|
3753
|
+
expect(() => {
|
|
3754
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3755
|
+
}).not.toThrow();
|
|
3756
|
+
});
|
|
3757
|
+
});
|
|
3758
|
+
|
|
3759
|
+
describe('group with component route naming', () => {
|
|
3760
|
+
it('should include the group name in the virtualType route for side-menu highlighting', () => {
|
|
3761
|
+
const mockPlugin = createMockPlugin();
|
|
3762
|
+
const mockStore = createMockStore();
|
|
3763
|
+
const virtualTypeCalls: any[] = [];
|
|
3764
|
+
const mockDSL = {
|
|
3765
|
+
product: jest.fn(),
|
|
3766
|
+
basicType: jest.fn(),
|
|
3767
|
+
labelGroup: jest.fn(),
|
|
3768
|
+
setGroupDefaultType: jest.fn(),
|
|
3769
|
+
weightGroup: jest.fn(),
|
|
3770
|
+
virtualType: jest.fn((...args: any[]) => virtualTypeCalls.push(args)),
|
|
3771
|
+
configureType: jest.fn(),
|
|
3772
|
+
weightType: jest.fn(),
|
|
3773
|
+
};
|
|
3774
|
+
|
|
3775
|
+
(mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
|
|
3776
|
+
|
|
3777
|
+
const childPage: ProductChildCustomPage = {
|
|
3778
|
+
name: 'alerts',
|
|
3779
|
+
label: 'Alerts',
|
|
3780
|
+
component: { name: 'AlertsPage' },
|
|
3781
|
+
};
|
|
3782
|
+
|
|
3783
|
+
const monitoringGroup: ProductChildGroup = {
|
|
3784
|
+
name: 'monitoring',
|
|
3785
|
+
label: 'Monitoring',
|
|
3786
|
+
component: { name: 'MonitoringOverview' },
|
|
3787
|
+
children: [childPage],
|
|
3788
|
+
};
|
|
3789
|
+
|
|
3790
|
+
const product: ProductMetadata = {
|
|
3791
|
+
name: 'my-app',
|
|
3792
|
+
label: 'My App',
|
|
3793
|
+
};
|
|
3794
|
+
|
|
3795
|
+
const pluginProduct = new PluginProduct(mockPlugin, product, [monitoringGroup]);
|
|
3796
|
+
|
|
3797
|
+
pluginProduct.apply(mockPlugin, mockStore);
|
|
3798
|
+
|
|
3799
|
+
// Find the virtualType call for the group (has exact + overview flags)
|
|
3800
|
+
const groupVirtualType = virtualTypeCalls.find((call) => call[0].exact === true && call[0].overview === true);
|
|
3801
|
+
|
|
3802
|
+
expect(groupVirtualType).toBeDefined();
|
|
3803
|
+
// The route name should contain the group name 'monitoring', not a generic 'group'
|
|
3804
|
+
expect(groupVirtualType[0].route.name).toStrictEqual(expect.stringContaining('monitoring'));
|
|
3805
|
+
// It should NOT be a generic route without the group name
|
|
3806
|
+
expect(groupVirtualType[0].route.name).not.toBe('myapp-c-cluster');
|
|
3807
|
+
});
|
|
3808
|
+
});
|
|
3809
|
+
});
|
|
3219
3810
|
});
|