@rancher/shell 3.0.5-rc.8 → 3.0.5-rc.9
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/styles/base/_color.scss +4 -1
- package/assets/styles/global/_tooltip.scss +7 -4
- package/assets/styles/themes/_dark.scss +11 -0
- package/assets/styles/themes/_light.scss +13 -1
- package/assets/styles/themes/_modern.scss +22 -0
- package/assets/translations/en-us.yaml +136 -14
- package/assets/translations/zh-hans.yaml +0 -1
- package/chart/monitoring/grafana/index.vue +8 -2
- package/components/ActionMenuShell.vue +3 -1
- package/components/Cron/CronExpressionEditor.vue +299 -0
- package/components/Cron/CronExpressionEditorModal.vue +247 -0
- package/components/Cron/CronTooltip.vue +87 -0
- package/components/Cron/types.ts +13 -0
- package/components/ForceDirectedTreeChart/composable.ts +11 -0
- package/components/PromptModal.vue +1 -1
- package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +1 -0
- package/components/Resource/Detail/CopyToClipboard.vue +78 -0
- package/components/Resource/Detail/FetchLoader/__tests__/composables.test.ts +69 -0
- package/components/Resource/Detail/FetchLoader/composables.ts +27 -0
- package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/Metadata/Annotations/index.vue +1 -1
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +13 -61
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +33 -6
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +24 -38
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +25 -5
- package/components/Resource/Detail/Metadata/KeyValue.vue +12 -23
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +144 -0
- package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -0
- package/components/Resource/Detail/Metadata/Labels/index.vue +1 -0
- package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +30 -32
- package/components/Resource/Detail/Metadata/__tests__/KeyValueRow.test.ts +108 -0
- package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +0 -3
- package/components/Resource/Detail/Metadata/__tests__/index.test.ts +12 -5
- package/components/Resource/Detail/Metadata/composables.ts +1 -4
- package/components/Resource/Detail/Metadata/index.vue +1 -0
- package/components/Resource/Detail/Preview/Content.vue +63 -0
- package/components/Resource/Detail/Preview/Preview.vue +128 -0
- package/components/Resource/Detail/Preview/__tests__/Content.spec.ts +71 -0
- package/components/Resource/Detail/Preview/__tests__/Preview.spec.ts +121 -0
- package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +141 -0
- package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +136 -0
- package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +245 -0
- package/components/Resource/Detail/ResourcePopover/index.vue +226 -0
- package/components/Resource/Detail/SpacedRow.vue +1 -0
- package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +0 -5
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/TitleBar/composables.ts +1 -3
- package/components/Resource/Detail/TitleBar/index.vue +2 -29
- package/components/Resource/Detail/ViewOptions/composable.ts +9 -0
- package/components/Resource/Detail/ViewOptions/index.vue +41 -0
- package/components/Resource/Detail/__tests__/CopyToClipboard.spec.ts +82 -0
- package/components/ResourceDetail/Masthead/legacy.vue +0 -19
- package/components/ResourceDetail/index.vue +1 -26
- package/components/ResourceTable.vue +24 -0
- package/components/SortableTable/index.vue +7 -1
- package/components/SortableTable/paging.js +3 -0
- package/components/Tabbed/Tab.vue +43 -1
- package/components/Tabbed/index.vue +3 -1
- package/components/__tests__/Cron/CronExpressionEditor.test.ts +151 -0
- package/components/__tests__/Cron/CronExpressionEditorModal.test.ts +81 -0
- package/components/auth/login/saml.vue +86 -0
- package/components/form/LabeledSelect.vue +8 -8
- package/components/form/ResourceTabs/composable.ts +54 -0
- package/components/form/ResourceTabs/index.vue +10 -7
- package/components/form/Select.vue +13 -10
- package/components/form/__tests__/LabeledSelect.test.ts +133 -0
- package/components/form/__tests__/Select.test.ts +134 -0
- package/composables/useExtensionManager.ts +17 -0
- package/config/home-links.js +12 -0
- package/config/labels-annotations.js +0 -1
- package/config/page-actions.js +0 -1
- package/config/product/explorer.js +3 -1
- package/config/product/fleet.js +2 -7
- package/config/product/manager.js +0 -5
- package/config/query-params.js +1 -0
- package/config/router/navigation-guards/clusters.js +2 -1
- package/config/router/navigation-guards/products.js +1 -1
- package/core/extension-manager-impl.js +518 -0
- package/core/plugins.js +35 -468
- package/core/types.ts +8 -2
- package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +1 -0
- package/detail/catalog.cattle.io.app.vue +7 -4
- package/detail/fleet.cattle.io.bundle.vue +1 -5
- package/detail/fleet.cattle.io.cluster.vue +3 -2
- package/detail/fleet.cattle.io.gitrepo.vue +76 -49
- package/detail/fleet.cattle.io.helmop.vue +78 -49
- package/dialog/AddonConfigConfirmationDialog.vue +1 -1
- package/dialog/GenericPrompt.vue +1 -1
- package/dialog/ImportDialog.vue +9 -2
- package/dialog/InstallExtensionDialog.vue +18 -10
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
- package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
- package/edit/cloudcredential.vue +31 -17
- package/edit/constraints.gatekeeper.sh.constraint/index.vue +10 -2
- package/edit/fleet.cattle.io.cluster.vue +19 -0
- package/edit/fleet.cattle.io.gitrepo.vue +23 -16
- package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +12 -11
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -1
- package/edit/provisioning.cattle.io.cluster/index.vue +14 -19
- package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -3
- package/edit/resources.cattle.io.restore.vue +5 -8
- package/list/__tests__/workload.test.ts +1 -0
- package/list/workload.vue +8 -1
- package/machine-config/components/GCEImage.vue +6 -5
- package/machine-config/google.vue +11 -6
- package/mixins/__tests__/chart.test.ts +139 -1
- package/mixins/chart.js +58 -18
- package/models/__tests__/namespace.test.ts +69 -0
- package/models/apps.statefulset.js +8 -10
- package/models/chart.js +5 -1
- package/models/fleet-application.js +16 -46
- package/models/fleet.cattle.io.bundle.js +1 -38
- package/models/fleet.cattle.io.gitrepo.js +4 -0
- package/models/fleet.cattle.io.helmop.js +4 -0
- package/models/management.cattle.io.project.js +12 -0
- package/models/namespace.js +30 -0
- package/models/workload.js +3 -0
- package/package.json +10 -10
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +26 -11
- package/pages/c/_cluster/apps/charts/chart.vue +29 -20
- package/pages/c/_cluster/apps/charts/index.vue +1 -0
- package/pages/c/_cluster/apps/charts/install.vue +6 -5
- package/pages/c/_cluster/explorer/tools/__tests__/index.test.ts +102 -12
- package/pages/c/_cluster/explorer/tools/index.vue +145 -254
- package/pages/c/_cluster/manager/cloudCredential/index.vue +18 -1
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +12 -2
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
- package/pages/c/_cluster/uiplugins/__tests__/index.spec.ts +318 -0
- package/pages/c/_cluster/uiplugins/index.vue +221 -363
- package/pages/home.vue +1 -9
- package/plugins/dashboard-store/resource-class.js +49 -0
- package/public/index.html +2 -1
- package/rancher-components/Card/Card.vue +1 -1
- package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
- package/rancher-components/Form/Radio/RadioButton.vue +1 -1
- package/rancher-components/Form/Radio/RadioGroup.vue +1 -1
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -11
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.test.ts +53 -0
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +65 -0
- package/rancher-components/Pill/RcCounterBadge/index.ts +1 -0
- package/rancher-components/Pill/RcCounterBadge/types.ts +7 -0
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +1 -1
- package/rancher-components/Pill/RcStatusBadge/index.ts +1 -1
- package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -3
- package/rancher-components/Pill/RcStatusIndicator/types.ts +1 -1
- package/rancher-components/Pill/RcTag/RcTag.test.ts +64 -0
- package/rancher-components/Pill/RcTag/RcTag.vue +94 -0
- package/rancher-components/Pill/RcTag/index.ts +1 -0
- package/rancher-components/Pill/RcTag/types.ts +9 -0
- package/rancher-components/Pill/types.ts +1 -0
- package/rancher-components/RcItemCard/RcItemCard.vue +1 -0
- package/rancher-components/RcItemCard/RcItemCardAction.vue +12 -0
- package/store/__tests__/catalog.test.ts +63 -0
- package/store/catalog.js +2 -2
- package/store/type-map.js +3 -15
- package/types/extension-manager.ts +26 -0
- package/types/shell/index.d.ts +121 -16
- package/utils/__tests__/product.test.ts +129 -0
- package/utils/__tests__/resource.test.ts +87 -0
- package/utils/alertmanagerconfig.js +2 -2
- package/utils/auth.js +3 -76
- package/utils/product.ts +39 -0
- package/utils/resource.ts +35 -0
- package/utils/select.js +0 -24
- package/utils/validators/formRules/__tests__/index.test.ts +3 -0
- package/utils/validators/formRules/index.ts +2 -1
- package/vue.config.js +1 -1
- package/components/Resource/Detail/Metadata/Rectangle.vue +0 -34
- package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +0 -24
- package/components/ResourceDetail/Masthead/__tests__/legacy.test.ts +0 -65
- /package/components/{ForceDirectedTreeChart.vue → ForceDirectedTreeChart/index.vue} +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ClusterProvisionerContext } from '@shell/core/types';
|
|
2
|
+
export type ExtensionManager = {
|
|
3
|
+
internal(): any;
|
|
4
|
+
loadPluginAsync(plugin: any): Promise<void>;
|
|
5
|
+
loadAsync(id: string, mainFile: string): Promise<void>;
|
|
6
|
+
loadBuiltinExtensions(): void;
|
|
7
|
+
registerBuiltinExtension(id: string, module: any): void;
|
|
8
|
+
initBuiltinExtension(id: string, module: any): void;
|
|
9
|
+
logout(): Promise<void>;
|
|
10
|
+
removePlugin(name: string): Promise<void>;
|
|
11
|
+
removeTypeFromStore(store: any, storeName: string, types: string[]): any[];
|
|
12
|
+
applyPlugin(plugin: any): void;
|
|
13
|
+
register(type: string, name: string, fn: Function): void;
|
|
14
|
+
unregister(type: string, name: string, fn: Function): void;
|
|
15
|
+
getAll(): any;
|
|
16
|
+
getPlugins(): any;
|
|
17
|
+
getDynamic(typeName: string, name: string): any;
|
|
18
|
+
getValidator(name: string): any;
|
|
19
|
+
getUIConfig(type: string, uiArea: string): any[];
|
|
20
|
+
getAllUIConfig(): any;
|
|
21
|
+
getProviders(context: ClusterProvisionerContext): any[];
|
|
22
|
+
lastLoad: number;
|
|
23
|
+
listDynamic(typeName: string): string[];
|
|
24
|
+
products: any[];
|
|
25
|
+
loadProducts(loadPlugins: any[]): void;
|
|
26
|
+
}
|
package/types/shell/index.d.ts
CHANGED
|
@@ -121,7 +121,6 @@ export namespace FLEET {
|
|
|
121
121
|
export { CLUSTER_NAMESPACE_1 as CLUSTER_NAMESPACE };
|
|
122
122
|
export let CLUSTER: string;
|
|
123
123
|
export let CREATED_BY_USER_ID: string;
|
|
124
|
-
export let CREATED_BY_USER_NAME: string;
|
|
125
124
|
export let OCI_STORAGE_SECRET_DEFAULT: string;
|
|
126
125
|
export let OCI_STORAGE_SECRET_GENERATED: string;
|
|
127
126
|
}
|
|
@@ -199,6 +198,7 @@ export const _STAGE: "stage";
|
|
|
199
198
|
export const _IMPORT: "import";
|
|
200
199
|
export const LEGACY: "legacy";
|
|
201
200
|
export const AS: "as";
|
|
201
|
+
export const VIEW: "view";
|
|
202
202
|
export const _DETAIL: "detail";
|
|
203
203
|
export const _CONFIG: "config";
|
|
204
204
|
export const _YAML: "yaml";
|
|
@@ -2613,6 +2613,20 @@ export default class Namespace {
|
|
|
2613
2613
|
cleanForNew(): void;
|
|
2614
2614
|
metadata: any;
|
|
2615
2615
|
get hideDetailLocation(): boolean;
|
|
2616
|
+
get glance(): any[];
|
|
2617
|
+
get projectGlance(): {
|
|
2618
|
+
name: string;
|
|
2619
|
+
label: any;
|
|
2620
|
+
formatter: string;
|
|
2621
|
+
formatterOpts: {
|
|
2622
|
+
to: any;
|
|
2623
|
+
row: {};
|
|
2624
|
+
options: {
|
|
2625
|
+
internal: boolean;
|
|
2626
|
+
};
|
|
2627
|
+
};
|
|
2628
|
+
content: any;
|
|
2629
|
+
};
|
|
2616
2630
|
}
|
|
2617
2631
|
}
|
|
2618
2632
|
|
|
@@ -3275,6 +3289,112 @@ export default class Resource {
|
|
|
3275
3289
|
};
|
|
3276
3290
|
content: any;
|
|
3277
3291
|
})[];
|
|
3292
|
+
get glance(): ({
|
|
3293
|
+
name: string;
|
|
3294
|
+
label: any;
|
|
3295
|
+
formatter: string;
|
|
3296
|
+
formatterOpts: {
|
|
3297
|
+
row: this;
|
|
3298
|
+
to?: undefined;
|
|
3299
|
+
options?: undefined;
|
|
3300
|
+
};
|
|
3301
|
+
content: any;
|
|
3302
|
+
} | {
|
|
3303
|
+
name: string;
|
|
3304
|
+
label: any;
|
|
3305
|
+
formatter: string;
|
|
3306
|
+
formatterOpts: {
|
|
3307
|
+
to: {
|
|
3308
|
+
name: string;
|
|
3309
|
+
params: {
|
|
3310
|
+
product: any;
|
|
3311
|
+
cluster: any;
|
|
3312
|
+
resource: any;
|
|
3313
|
+
};
|
|
3314
|
+
};
|
|
3315
|
+
row: {};
|
|
3316
|
+
options: {
|
|
3317
|
+
internal: boolean;
|
|
3318
|
+
};
|
|
3319
|
+
};
|
|
3320
|
+
content: any;
|
|
3321
|
+
} | {
|
|
3322
|
+
name: string;
|
|
3323
|
+
label: any;
|
|
3324
|
+
formatter: string;
|
|
3325
|
+
formatterOpts: {
|
|
3326
|
+
to: {
|
|
3327
|
+
name: string;
|
|
3328
|
+
product: any;
|
|
3329
|
+
cluster: any;
|
|
3330
|
+
resource: any;
|
|
3331
|
+
};
|
|
3332
|
+
row: {};
|
|
3333
|
+
options: {
|
|
3334
|
+
internal: boolean;
|
|
3335
|
+
};
|
|
3336
|
+
};
|
|
3337
|
+
content: any;
|
|
3338
|
+
} | {
|
|
3339
|
+
name: string;
|
|
3340
|
+
label: any;
|
|
3341
|
+
formatter: string;
|
|
3342
|
+
content: any;
|
|
3343
|
+
formatterOpts?: undefined;
|
|
3344
|
+
})[];
|
|
3345
|
+
get _glance(): ({
|
|
3346
|
+
name: string;
|
|
3347
|
+
label: any;
|
|
3348
|
+
formatter: string;
|
|
3349
|
+
formatterOpts: {
|
|
3350
|
+
row: this;
|
|
3351
|
+
to?: undefined;
|
|
3352
|
+
options?: undefined;
|
|
3353
|
+
};
|
|
3354
|
+
content: any;
|
|
3355
|
+
} | {
|
|
3356
|
+
name: string;
|
|
3357
|
+
label: any;
|
|
3358
|
+
formatter: string;
|
|
3359
|
+
formatterOpts: {
|
|
3360
|
+
to: {
|
|
3361
|
+
name: string;
|
|
3362
|
+
params: {
|
|
3363
|
+
product: any;
|
|
3364
|
+
cluster: any;
|
|
3365
|
+
resource: any;
|
|
3366
|
+
};
|
|
3367
|
+
};
|
|
3368
|
+
row: {};
|
|
3369
|
+
options: {
|
|
3370
|
+
internal: boolean;
|
|
3371
|
+
};
|
|
3372
|
+
};
|
|
3373
|
+
content: any;
|
|
3374
|
+
} | {
|
|
3375
|
+
name: string;
|
|
3376
|
+
label: any;
|
|
3377
|
+
formatter: string;
|
|
3378
|
+
formatterOpts: {
|
|
3379
|
+
to: {
|
|
3380
|
+
name: string;
|
|
3381
|
+
product: any;
|
|
3382
|
+
cluster: any;
|
|
3383
|
+
resource: any;
|
|
3384
|
+
};
|
|
3385
|
+
row: {};
|
|
3386
|
+
options: {
|
|
3387
|
+
internal: boolean;
|
|
3388
|
+
};
|
|
3389
|
+
};
|
|
3390
|
+
content: any;
|
|
3391
|
+
} | {
|
|
3392
|
+
name: string;
|
|
3393
|
+
label: any;
|
|
3394
|
+
formatter: string;
|
|
3395
|
+
content: any;
|
|
3396
|
+
formatterOpts?: undefined;
|
|
3397
|
+
})[];
|
|
3278
3398
|
get t(): any;
|
|
3279
3399
|
findOwners(): any[];
|
|
3280
3400
|
getOwners(): any[];
|
|
@@ -3556,20 +3676,6 @@ export function parseAuthProvidersInfo(rows: any): {
|
|
|
3556
3676
|
};
|
|
3557
3677
|
enabled: any;
|
|
3558
3678
|
};
|
|
3559
|
-
/**
|
|
3560
|
-
* Attempt to set the product in our datastore if the route matches a known product. Otherwise show an error page instead.
|
|
3561
|
-
*/
|
|
3562
|
-
export function setProduct(store: any, to: any): any;
|
|
3563
|
-
/**
|
|
3564
|
-
* Check that the resource is valid, if not redirect to fail whale
|
|
3565
|
-
*
|
|
3566
|
-
* This requires that
|
|
3567
|
-
* - product is set
|
|
3568
|
-
* - product's store is set and setup (so we can check schema's within it)
|
|
3569
|
-
* - product's store has the schemaFor getter (extension stores might not have it)
|
|
3570
|
-
* - there's a resource associated with route (meta or param)
|
|
3571
|
-
*/
|
|
3572
|
-
export function validateResource(store: any, to: any): boolean;
|
|
3573
3679
|
/**
|
|
3574
3680
|
* Attempt to load the current user's principal
|
|
3575
3681
|
*/
|
|
@@ -4389,7 +4495,6 @@ export function routeRequiresInstallRedirect(to: any): boolean;
|
|
|
4389
4495
|
// @shell/utils/select
|
|
4390
4496
|
|
|
4391
4497
|
declare module '@shell/utils/select' {
|
|
4392
|
-
export function onClickOption(option: any, e: any): void;
|
|
4393
4498
|
export function calculatePosition(dropdownList: any, component: any, width: any, placement: any): void;
|
|
4394
4499
|
}
|
|
4395
4500
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { setProduct } from '@shell/utils/product';
|
|
2
|
+
import { getProductFromRoute } from '@shell/utils/router';
|
|
3
|
+
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
4
|
+
import { RouteLocation } from 'vue-router';
|
|
5
|
+
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
jest.mock('@shell/utils/router', () => ({ getProductFromRoute: jest.fn() }));
|
|
8
|
+
|
|
9
|
+
const mockGetProductFromRoute = getProductFromRoute as jest.Mock;
|
|
10
|
+
|
|
11
|
+
describe('utils/product', () => {
|
|
12
|
+
describe('setProduct', () => {
|
|
13
|
+
let mockStore: any;
|
|
14
|
+
let mockTo: RouteLocation;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.clearAllMocks();
|
|
18
|
+
|
|
19
|
+
// A stateful mock store to better simulate Vuex behavior
|
|
20
|
+
mockStore = {
|
|
21
|
+
productId: 'some-old-product',
|
|
22
|
+
products: {
|
|
23
|
+
'some-old-product': { name: 'some-old-product', inStore: 'store_a' },
|
|
24
|
+
'new-product-same-store': { name: 'new-product-same-store', inStore: 'store_a' },
|
|
25
|
+
'new-product-diff-store': { name: 'new-product-diff-store', inStore: 'store_b' },
|
|
26
|
+
[EXPLORER]: { name: EXPLORER, inStore: 'explorer_store' },
|
|
27
|
+
},
|
|
28
|
+
getters: {
|
|
29
|
+
'type-map/isProductRegistered': jest.fn().mockReturnValue(true),
|
|
30
|
+
'i18n/t': jest.fn((key, args) => `${ key } ${ JSON.stringify(args) || '' }`),
|
|
31
|
+
get currentProduct() {
|
|
32
|
+
return mockStore.products[mockStore.productId];
|
|
33
|
+
},
|
|
34
|
+
get productId() {
|
|
35
|
+
return mockStore.productId;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
commit: jest.fn((type, payload) => {
|
|
39
|
+
if (type === 'setProduct') {
|
|
40
|
+
mockStore.productId = payload;
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
dispatch: jest.fn(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
mockTo = { matched: [{ path: '/c/:cluster/some-product' }] } as unknown as RouteLocation;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should dispatch loadingError if product is not found (wildcard route)', () => {
|
|
50
|
+
const product = 'invalid-product';
|
|
51
|
+
|
|
52
|
+
mockGetProductFromRoute.mockReturnValue(product);
|
|
53
|
+
mockTo.matched = [{ path: '/c/:cluster/:product' }] as any;
|
|
54
|
+
|
|
55
|
+
setProduct(mockStore, mockTo);
|
|
56
|
+
|
|
57
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('loadingError', expect.any(Error));
|
|
58
|
+
expect(mockStore.getters['i18n/t']).toHaveBeenCalledWith('nav.failWhale.productNotFound', { productNotFound: product }, true);
|
|
59
|
+
expect(mockStore.commit).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should dispatch loadingError if product exists but route has no matches', () => {
|
|
63
|
+
const product = 'invalid-product';
|
|
64
|
+
|
|
65
|
+
mockGetProductFromRoute.mockReturnValue(product);
|
|
66
|
+
mockTo.matched = [] as any; // No matched routes
|
|
67
|
+
|
|
68
|
+
setProduct(mockStore, mockTo);
|
|
69
|
+
|
|
70
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('loadingError', expect.any(Error));
|
|
71
|
+
expect(mockStore.getters['i18n/t']).toHaveBeenCalledWith('nav.failWhale.productNotFound', { productNotFound: product }, true);
|
|
72
|
+
expect(mockStore.commit).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should dispatch loadingError if product is not registered', () => {
|
|
76
|
+
const product = 'unregistered-product';
|
|
77
|
+
|
|
78
|
+
mockGetProductFromRoute.mockReturnValue(product);
|
|
79
|
+
mockStore.getters['type-map/isProductRegistered'].mockReturnValue(false);
|
|
80
|
+
|
|
81
|
+
setProduct(mockStore, mockTo);
|
|
82
|
+
|
|
83
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('loadingError', expect.any(Error));
|
|
84
|
+
expect(mockStore.getters['i18n/t']).toHaveBeenCalledWith('nav.failWhale.productNotFound', { productNotFound: product }, true);
|
|
85
|
+
expect(mockStore.commit).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should default to EXPLORER product and reset catalog if store changes', () => {
|
|
89
|
+
mockGetProductFromRoute.mockReturnValue(null);
|
|
90
|
+
|
|
91
|
+
setProduct(mockStore, mockTo);
|
|
92
|
+
|
|
93
|
+
expect(mockStore.commit).toHaveBeenCalledWith('setProduct', EXPLORER);
|
|
94
|
+
expect(mockStore.commit).toHaveBeenCalledWith('catalog/reset');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should not call any commits if the product has not changed', () => {
|
|
98
|
+
const product = 'some-old-product';
|
|
99
|
+
|
|
100
|
+
mockGetProductFromRoute.mockReturnValue(product);
|
|
101
|
+
|
|
102
|
+
setProduct(mockStore, mockTo);
|
|
103
|
+
|
|
104
|
+
expect(mockStore.commit).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should change product but not reset catalog if inStore is the same', () => {
|
|
108
|
+
const newProduct = 'new-product-same-store';
|
|
109
|
+
|
|
110
|
+
mockGetProductFromRoute.mockReturnValue(newProduct);
|
|
111
|
+
|
|
112
|
+
setProduct(mockStore, mockTo);
|
|
113
|
+
|
|
114
|
+
expect(mockStore.commit).toHaveBeenCalledWith('setProduct', newProduct);
|
|
115
|
+
expect(mockStore.commit).not.toHaveBeenCalledWith('catalog/reset');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should change product and reset catalog if inStore is different', () => {
|
|
119
|
+
const newProduct = 'new-product-diff-store';
|
|
120
|
+
|
|
121
|
+
mockGetProductFromRoute.mockReturnValue(newProduct);
|
|
122
|
+
|
|
123
|
+
setProduct(mockStore, mockTo);
|
|
124
|
+
|
|
125
|
+
expect(mockStore.commit).toHaveBeenCalledWith('setProduct', newProduct);
|
|
126
|
+
expect(mockStore.commit).toHaveBeenCalledWith('catalog/reset');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { validateResource } from '@shell/utils/resource';
|
|
2
|
+
import { canViewResource } from '@shell/utils/auth';
|
|
3
|
+
import { getResourceFromRoute } from '@shell/utils/router';
|
|
4
|
+
import { RouteLocation } from 'vue-router';
|
|
5
|
+
|
|
6
|
+
// Mock dependencies from other modules
|
|
7
|
+
jest.mock('@shell/utils/auth', () => ({ canViewResource: jest.fn() }));
|
|
8
|
+
|
|
9
|
+
jest.mock('@shell/utils/router', () => ({ getResourceFromRoute: jest.fn() }));
|
|
10
|
+
|
|
11
|
+
// Typed mocks for better intellisense and type-checking
|
|
12
|
+
const mockCanViewResource = canViewResource as jest.Mock;
|
|
13
|
+
const mockGetResourceFromRoute = getResourceFromRoute as jest.Mock;
|
|
14
|
+
|
|
15
|
+
describe('validateResource', () => {
|
|
16
|
+
let mockStore: any;
|
|
17
|
+
let mockTo: RouteLocation;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Reset mocks before each test to ensure test isolation
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
|
|
23
|
+
// A basic mock for the Vuex store
|
|
24
|
+
mockStore = {
|
|
25
|
+
getters: {
|
|
26
|
+
currentProduct: { name: 'explorer' },
|
|
27
|
+
'i18n/t': jest.fn((key, args) => `i18n: ${ key } ${ JSON.stringify(args) || '' }`),
|
|
28
|
+
},
|
|
29
|
+
dispatch: jest.fn(),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// A mock for the route location object
|
|
33
|
+
mockTo = {
|
|
34
|
+
name: 'c-cluster-product-resource-id',
|
|
35
|
+
params: {
|
|
36
|
+
cluster: 'local',
|
|
37
|
+
product: 'explorer',
|
|
38
|
+
resource: 'pod',
|
|
39
|
+
id: 'my-pod'
|
|
40
|
+
}
|
|
41
|
+
} as unknown as RouteLocation;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return false if no product is set in the store', () => {
|
|
45
|
+
mockStore.getters.currentProduct = null;
|
|
46
|
+
mockGetResourceFromRoute.mockReturnValue('pod');
|
|
47
|
+
|
|
48
|
+
const result = validateResource(mockStore, mockTo);
|
|
49
|
+
|
|
50
|
+
expect(result).toBe(false);
|
|
51
|
+
expect(mockStore.dispatch).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return false if no resource is found in the route', () => {
|
|
55
|
+
mockGetResourceFromRoute.mockReturnValue(null);
|
|
56
|
+
|
|
57
|
+
const result = validateResource(mockStore, mockTo);
|
|
58
|
+
|
|
59
|
+
expect(result).toBe(false);
|
|
60
|
+
expect(mockStore.dispatch).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return false if the user is authorized to view the resource', () => {
|
|
64
|
+
mockGetResourceFromRoute.mockReturnValue('pod');
|
|
65
|
+
mockCanViewResource.mockReturnValue(true);
|
|
66
|
+
|
|
67
|
+
const result = validateResource(mockStore, mockTo);
|
|
68
|
+
|
|
69
|
+
expect(result).toBe(false);
|
|
70
|
+
expect(mockStore.dispatch).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should throw an error and dispatch loadingError if user cannot view the resource', () => {
|
|
74
|
+
const resource = 'pod';
|
|
75
|
+
|
|
76
|
+
mockGetResourceFromRoute.mockReturnValue(resource);
|
|
77
|
+
mockCanViewResource.mockReturnValue(false);
|
|
78
|
+
|
|
79
|
+
const errorMessage = `i18n: nav.failWhale.resourceNotFound {"resource":"${ resource }"}`;
|
|
80
|
+
|
|
81
|
+
mockStore.getters['i18n/t'].mockReturnValue(errorMessage);
|
|
82
|
+
|
|
83
|
+
expect(() => validateResource(mockStore, mockTo)).toThrow(errorMessage);
|
|
84
|
+
expect(mockStore.dispatch).toHaveBeenCalledWith('loadingError', expect.any(Error));
|
|
85
|
+
expect(mockStore.getters['i18n/t']).toHaveBeenCalledWith('nav.failWhale.resourceNotFound', { resource }, true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -19,8 +19,8 @@ export const fetchAlertManagerConfigSpecs = async($store) => {
|
|
|
19
19
|
await schema.fetchResourceFields();
|
|
20
20
|
|
|
21
21
|
return {
|
|
22
|
-
receiverSchema: schema.schemaDefinitions?.[`${ schema.schemaDefinition.
|
|
23
|
-
routeSchema: schema.schemaDefinitions?.[`${ schema.schemaDefinition.
|
|
22
|
+
receiverSchema: schema.schemaDefinitions?.[`${ schema.schemaDefinition.type }.spec.receivers`],
|
|
23
|
+
routeSchema: schema.schemaDefinitions?.[`${ schema.schemaDefinition.type }.spec.route`],
|
|
24
24
|
};
|
|
25
25
|
};
|
|
26
26
|
|
package/utils/auth.js
CHANGED
|
@@ -5,8 +5,6 @@ import {
|
|
|
5
5
|
} from '@shell/config/query-params';
|
|
6
6
|
import { MANAGEMENT, NORMAN } from '@shell/config/types';
|
|
7
7
|
import { allHash } from '@shell/utils/promise';
|
|
8
|
-
import { getProductFromRoute, getResourceFromRoute } from '@shell/utils/router';
|
|
9
|
-
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
10
8
|
import { findBy } from '@shell/utils/array';
|
|
11
9
|
import { onExtensionsReady } from '@shell/utils/uiplugins';
|
|
12
10
|
|
|
@@ -201,6 +199,9 @@ export const checkPermissions = (types, getters) => {
|
|
|
201
199
|
return allHash(hash);
|
|
202
200
|
};
|
|
203
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Checks if the current user has access to the specified resource type
|
|
204
|
+
*/
|
|
204
205
|
export const canViewResource = (store, resource) => {
|
|
205
206
|
// Note - don't use the current products store... because products can override stores for resources with `typeStoreMap`
|
|
206
207
|
const inStore = store.getters['currentStore'](resource);
|
|
@@ -218,80 +219,6 @@ export const canViewResource = (store, resource) => {
|
|
|
218
219
|
return !!validResource;
|
|
219
220
|
};
|
|
220
221
|
|
|
221
|
-
// ************************************************************
|
|
222
|
-
//
|
|
223
|
-
// BELOW ARE METHODS THAT ARE A PART OF THE AUTHENTICATED MIDDLEWARE REMOVAL. THIS IS A TEMPORARY HOME FOR THESE UTILS AND SHOULD BE REWRITTEN, MOVED OR DELETED.
|
|
224
|
-
//
|
|
225
|
-
// TODO: Remove and refactor everything below for more clarity and better organization. https://github.com/rancher/dashboard/issues/11111
|
|
226
|
-
//
|
|
227
|
-
// ************************************************************
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Attempt to set the product in our datastore if the route matches a known product. Otherwise show an error page instead.
|
|
231
|
-
*/
|
|
232
|
-
export function setProduct(store, to) {
|
|
233
|
-
let product = getProductFromRoute(to);
|
|
234
|
-
|
|
235
|
-
// since all products are hardcoded as routes (ex: c-local-explorer), if we match the wildcard route it means that the product does not exist
|
|
236
|
-
if ((product && (!to.matched.length || (to.matched.length && to.matched[0].path === '/c/:cluster/:product'))) ||
|
|
237
|
-
// if the product grabbed from the route is not registered, then we don't have it!
|
|
238
|
-
(product && !store.getters['type-map/isProductRegistered'](product))) {
|
|
239
|
-
const error = new Error(store.getters['i18n/t']('nav.failWhale.productNotFound', { productNotFound: product }, true));
|
|
240
|
-
|
|
241
|
-
return store.dispatch('loadingError', error);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if ( !product ) {
|
|
245
|
-
product = EXPLORER;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const oldProduct = store.getters['productId'];
|
|
249
|
-
const oldStore = store.getters['currentProduct']?.inStore;
|
|
250
|
-
|
|
251
|
-
if ( product !== oldProduct ) {
|
|
252
|
-
store.commit('setProduct', product);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const neuStore = store.getters['currentProduct']?.inStore;
|
|
256
|
-
|
|
257
|
-
if ( neuStore !== oldStore ) {
|
|
258
|
-
// If the product store changes, clear the catalog.
|
|
259
|
-
// There might be management catalog items in it vs cluster.
|
|
260
|
-
store.commit('catalog/reset');
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Check that the resource is valid, if not redirect to fail whale
|
|
266
|
-
*
|
|
267
|
-
* This requires that
|
|
268
|
-
* - product is set
|
|
269
|
-
* - product's store is set and setup (so we can check schema's within it)
|
|
270
|
-
* - product's store has the schemaFor getter (extension stores might not have it)
|
|
271
|
-
* - there's a resource associated with route (meta or param)
|
|
272
|
-
*/
|
|
273
|
-
export function validateResource(store, to) {
|
|
274
|
-
const product = store.getters['currentProduct'];
|
|
275
|
-
const resource = getResourceFromRoute(to);
|
|
276
|
-
|
|
277
|
-
// In order to check a resource is valid we need these
|
|
278
|
-
if (!product || !resource) {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (canViewResource(store, resource)) {
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Unknown resource, redirect to fail whale
|
|
287
|
-
|
|
288
|
-
const error = new Error(store.getters['i18n/t']('nav.failWhale.resourceNotFound', { resource }, true));
|
|
289
|
-
|
|
290
|
-
store.dispatch('loadingError', error);
|
|
291
|
-
|
|
292
|
-
throw error;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
222
|
/**
|
|
296
223
|
* Attempt to load the current user's principal
|
|
297
224
|
*/
|
package/utils/product.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getProductFromRoute } from '@shell/utils/router';
|
|
2
|
+
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
3
|
+
import { RouteLocation } from 'vue-router';
|
|
4
|
+
import { Store } from 'vuex';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Attempt to set the product in our datastore if the route matches a known product. Otherwise show an error page instead.
|
|
8
|
+
*/
|
|
9
|
+
export function setProduct(store: Store<any>, to: RouteLocation) {
|
|
10
|
+
let product = getProductFromRoute(to);
|
|
11
|
+
|
|
12
|
+
// since all products are hardcoded as routes (ex: c-local-explorer), if we match the wildcard route it means that the product does not exist
|
|
13
|
+
if ((product && (!to.matched.length || (to.matched.length && to.matched[0].path === '/c/:cluster/:product'))) ||
|
|
14
|
+
// if the product grabbed from the route is not registered, then we don't have it!
|
|
15
|
+
(product && !store.getters['type-map/isProductRegistered'](product))) {
|
|
16
|
+
const error = new Error(store.getters['i18n/t']('nav.failWhale.productNotFound', { productNotFound: product }, true));
|
|
17
|
+
|
|
18
|
+
return store.dispatch('loadingError', error);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if ( !product ) {
|
|
22
|
+
product = EXPLORER;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const oldProduct = store.getters['productId'];
|
|
26
|
+
const oldStore = store.getters['currentProduct']?.inStore;
|
|
27
|
+
|
|
28
|
+
if ( product !== oldProduct ) {
|
|
29
|
+
store.commit('setProduct', product);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const neuStore = store.getters['currentProduct']?.inStore;
|
|
33
|
+
|
|
34
|
+
if ( neuStore !== oldStore ) {
|
|
35
|
+
// If the product store changes, clear the catalog.
|
|
36
|
+
// There might be management catalog items in it vs cluster.
|
|
37
|
+
store.commit('catalog/reset');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { canViewResource } from '@shell/utils/auth';
|
|
2
|
+
import { getResourceFromRoute } from '@shell/utils/router';
|
|
3
|
+
import { RouteLocation } from 'vue-router';
|
|
4
|
+
import { Store } from 'vuex';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check that the resource is valid, if not redirect to fail whale
|
|
8
|
+
*
|
|
9
|
+
* This requires that
|
|
10
|
+
* - product is set
|
|
11
|
+
* - product's store is set and setup (so we can check schema's within it)
|
|
12
|
+
* - product's store has the schemaFor getter (extension stores might not have it)
|
|
13
|
+
* - there's a resource associated with route (meta or param)
|
|
14
|
+
*/
|
|
15
|
+
export function validateResource(store: Store<any>, to: RouteLocation) {
|
|
16
|
+
const product = store.getters['currentProduct'];
|
|
17
|
+
const resource = getResourceFromRoute(to);
|
|
18
|
+
|
|
19
|
+
// In order to check a resource is valid we need these
|
|
20
|
+
if (!product || !resource) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (canViewResource(store, resource)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Unknown resource, redirect to fail whale
|
|
29
|
+
|
|
30
|
+
const error = new Error(store.getters['i18n/t']('nav.failWhale.resourceNotFound', { resource }, true));
|
|
31
|
+
|
|
32
|
+
store.dispatch('loadingError', error);
|
|
33
|
+
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
package/utils/select.js
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
export function onClickOption(option, e) {
|
|
2
|
-
if (!this.$attrs.multiple) {
|
|
3
|
-
return;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const getValue = (opt) => (this.optionKey ? this.get(opt, this.optionKey) : this.getOptionLabel(opt));
|
|
7
|
-
const optionValue = getValue(option);
|
|
8
|
-
const value = this.value || [];
|
|
9
|
-
const optionIndex = value.findIndex((option) => getValue(option) === optionValue);
|
|
10
|
-
|
|
11
|
-
if (optionIndex < 0) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
this.value.splice(optionIndex, 1);
|
|
16
|
-
|
|
17
|
-
this.$emit('update:value', this.value);
|
|
18
|
-
e.preventDefault();
|
|
19
|
-
e.stopPropagation();
|
|
20
|
-
|
|
21
|
-
if (this.closeOnSelect) {
|
|
22
|
-
this.$refs['select-input'].closeSearchOptions();
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
1
|
|
|
26
2
|
// This is a simpler positionner for the dropdown for a select control
|
|
27
3
|
// We used to use popper for these, but it does not suppotr fractional pixel placements which
|
|
@@ -114,6 +114,8 @@ describe('formRules', () => {
|
|
|
114
114
|
['git@github.com:rancher/dashboard/', undefined],
|
|
115
115
|
['git@github.com:rancher/%20dashboard/', undefined],
|
|
116
116
|
['git@github.com:rancher/dashboard/%20', undefined],
|
|
117
|
+
['git@git.apps.local:fleet/fleet-local.git', undefined],
|
|
118
|
+
['git@git.apps.local:33333/fleet/fleet-local.git', undefined],
|
|
117
119
|
|
|
118
120
|
// Not valid HTTP(s)
|
|
119
121
|
['https://github.com/rancher/ dashboard.git', message],
|
|
@@ -140,6 +142,7 @@ describe('formRules', () => {
|
|
|
140
142
|
['git@githubcomrancher/dashboard', message],
|
|
141
143
|
['%20git@github.comrancher/dashboard', message],
|
|
142
144
|
['git@git%20hub.comrancher/dashboard', message],
|
|
145
|
+
['git@git.apps.local:/fleet/fleet-local.git', message],
|
|
143
146
|
['git@.git', message],
|
|
144
147
|
['git@', message],
|
|
145
148
|
|
|
@@ -191,6 +191,7 @@ export default function(
|
|
|
191
191
|
protocol,
|
|
192
192
|
authority,
|
|
193
193
|
host,
|
|
194
|
+
port,
|
|
194
195
|
path
|
|
195
196
|
} = parse(url);
|
|
196
197
|
|
|
@@ -205,7 +206,7 @@ export default function(
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
// Test ssh, authority must be valid (SSH user + host)
|
|
208
|
-
if (!protocol && !authority.endsWith(':')) {
|
|
209
|
+
if (!protocol && !port && (!authority.endsWith(':') || path.startsWith('/'))) {
|
|
209
210
|
return message;
|
|
210
211
|
}
|
|
211
212
|
|
package/vue.config.js
CHANGED
|
@@ -546,7 +546,7 @@ module.exports = function(dir, appConfig = {}) {
|
|
|
546
546
|
config.plugins.push(getVirtualModulesAutoImport(dir));
|
|
547
547
|
config.plugins.push(getPackageImport(dir));
|
|
548
548
|
config.plugins.push(createEnvVariablesPlugin(routerBasePath, rancherEnv));
|
|
549
|
-
config.plugins.push(new NodePolyfillPlugin()); // required from Webpack 5 to polyfill node modules
|
|
549
|
+
config.plugins.push(new NodePolyfillPlugin({ additionalAliases: ['process'] })); // required from Webpack 5 to polyfill node modules
|
|
550
550
|
|
|
551
551
|
// The static assets need to be in the built assets directory in order to get served (primarily the favicon)
|
|
552
552
|
config.plugins.push(new CopyWebpackPlugin({ patterns: [{ from: path.join(SHELL_ABS, 'static'), to: '.' }] }));
|