@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.
Files changed (171) hide show
  1. package/assets/styles/base/_color.scss +4 -1
  2. package/assets/styles/global/_tooltip.scss +7 -4
  3. package/assets/styles/themes/_dark.scss +11 -0
  4. package/assets/styles/themes/_light.scss +13 -1
  5. package/assets/styles/themes/_modern.scss +22 -0
  6. package/assets/translations/en-us.yaml +136 -14
  7. package/assets/translations/zh-hans.yaml +0 -1
  8. package/chart/monitoring/grafana/index.vue +8 -2
  9. package/components/ActionMenuShell.vue +3 -1
  10. package/components/Cron/CronExpressionEditor.vue +299 -0
  11. package/components/Cron/CronExpressionEditorModal.vue +247 -0
  12. package/components/Cron/CronTooltip.vue +87 -0
  13. package/components/Cron/types.ts +13 -0
  14. package/components/ForceDirectedTreeChart/composable.ts +11 -0
  15. package/components/PromptModal.vue +1 -1
  16. package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +1 -0
  17. package/components/Resource/Detail/CopyToClipboard.vue +78 -0
  18. package/components/Resource/Detail/FetchLoader/__tests__/composables.test.ts +69 -0
  19. package/components/Resource/Detail/FetchLoader/composables.ts +27 -0
  20. package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
  21. package/components/Resource/Detail/Metadata/Annotations/index.vue +1 -1
  22. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +13 -61
  23. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +33 -6
  24. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +24 -38
  25. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +25 -5
  26. package/components/Resource/Detail/Metadata/KeyValue.vue +12 -23
  27. package/components/Resource/Detail/Metadata/KeyValueRow.vue +144 -0
  28. package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -0
  29. package/components/Resource/Detail/Metadata/Labels/index.vue +1 -0
  30. package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +30 -32
  31. package/components/Resource/Detail/Metadata/__tests__/KeyValueRow.test.ts +108 -0
  32. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +0 -3
  33. package/components/Resource/Detail/Metadata/__tests__/index.test.ts +12 -5
  34. package/components/Resource/Detail/Metadata/composables.ts +1 -4
  35. package/components/Resource/Detail/Metadata/index.vue +1 -0
  36. package/components/Resource/Detail/Preview/Content.vue +63 -0
  37. package/components/Resource/Detail/Preview/Preview.vue +128 -0
  38. package/components/Resource/Detail/Preview/__tests__/Content.spec.ts +71 -0
  39. package/components/Resource/Detail/Preview/__tests__/Preview.spec.ts +121 -0
  40. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +141 -0
  41. package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +136 -0
  42. package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +245 -0
  43. package/components/Resource/Detail/ResourcePopover/index.vue +226 -0
  44. package/components/Resource/Detail/SpacedRow.vue +1 -0
  45. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +0 -5
  46. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
  47. package/components/Resource/Detail/TitleBar/composables.ts +1 -3
  48. package/components/Resource/Detail/TitleBar/index.vue +2 -29
  49. package/components/Resource/Detail/ViewOptions/composable.ts +9 -0
  50. package/components/Resource/Detail/ViewOptions/index.vue +41 -0
  51. package/components/Resource/Detail/__tests__/CopyToClipboard.spec.ts +82 -0
  52. package/components/ResourceDetail/Masthead/legacy.vue +0 -19
  53. package/components/ResourceDetail/index.vue +1 -26
  54. package/components/ResourceTable.vue +24 -0
  55. package/components/SortableTable/index.vue +7 -1
  56. package/components/SortableTable/paging.js +3 -0
  57. package/components/Tabbed/Tab.vue +43 -1
  58. package/components/Tabbed/index.vue +3 -1
  59. package/components/__tests__/Cron/CronExpressionEditor.test.ts +151 -0
  60. package/components/__tests__/Cron/CronExpressionEditorModal.test.ts +81 -0
  61. package/components/auth/login/saml.vue +86 -0
  62. package/components/form/LabeledSelect.vue +8 -8
  63. package/components/form/ResourceTabs/composable.ts +54 -0
  64. package/components/form/ResourceTabs/index.vue +10 -7
  65. package/components/form/Select.vue +13 -10
  66. package/components/form/__tests__/LabeledSelect.test.ts +133 -0
  67. package/components/form/__tests__/Select.test.ts +134 -0
  68. package/composables/useExtensionManager.ts +17 -0
  69. package/config/home-links.js +12 -0
  70. package/config/labels-annotations.js +0 -1
  71. package/config/page-actions.js +0 -1
  72. package/config/product/explorer.js +3 -1
  73. package/config/product/fleet.js +2 -7
  74. package/config/product/manager.js +0 -5
  75. package/config/query-params.js +1 -0
  76. package/config/router/navigation-guards/clusters.js +2 -1
  77. package/config/router/navigation-guards/products.js +1 -1
  78. package/core/extension-manager-impl.js +518 -0
  79. package/core/plugins.js +35 -468
  80. package/core/types.ts +8 -2
  81. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +1 -0
  82. package/detail/catalog.cattle.io.app.vue +7 -4
  83. package/detail/fleet.cattle.io.bundle.vue +1 -5
  84. package/detail/fleet.cattle.io.cluster.vue +3 -2
  85. package/detail/fleet.cattle.io.gitrepo.vue +76 -49
  86. package/detail/fleet.cattle.io.helmop.vue +78 -49
  87. package/dialog/AddonConfigConfirmationDialog.vue +1 -1
  88. package/dialog/GenericPrompt.vue +1 -1
  89. package/dialog/ImportDialog.vue +9 -2
  90. package/dialog/InstallExtensionDialog.vue +18 -10
  91. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
  92. package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
  93. package/edit/cloudcredential.vue +31 -17
  94. package/edit/constraints.gatekeeper.sh.constraint/index.vue +10 -2
  95. package/edit/fleet.cattle.io.cluster.vue +19 -0
  96. package/edit/fleet.cattle.io.gitrepo.vue +23 -16
  97. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +12 -11
  98. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -1
  99. package/edit/provisioning.cattle.io.cluster/index.vue +14 -19
  100. package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -3
  101. package/edit/resources.cattle.io.restore.vue +5 -8
  102. package/list/__tests__/workload.test.ts +1 -0
  103. package/list/workload.vue +8 -1
  104. package/machine-config/components/GCEImage.vue +6 -5
  105. package/machine-config/google.vue +11 -6
  106. package/mixins/__tests__/chart.test.ts +139 -1
  107. package/mixins/chart.js +58 -18
  108. package/models/__tests__/namespace.test.ts +69 -0
  109. package/models/apps.statefulset.js +8 -10
  110. package/models/chart.js +5 -1
  111. package/models/fleet-application.js +16 -46
  112. package/models/fleet.cattle.io.bundle.js +1 -38
  113. package/models/fleet.cattle.io.gitrepo.js +4 -0
  114. package/models/fleet.cattle.io.helmop.js +4 -0
  115. package/models/management.cattle.io.project.js +12 -0
  116. package/models/namespace.js +30 -0
  117. package/models/workload.js +3 -0
  118. package/package.json +10 -10
  119. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +26 -11
  120. package/pages/c/_cluster/apps/charts/chart.vue +29 -20
  121. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  122. package/pages/c/_cluster/apps/charts/install.vue +6 -5
  123. package/pages/c/_cluster/explorer/tools/__tests__/index.test.ts +102 -12
  124. package/pages/c/_cluster/explorer/tools/index.vue +145 -254
  125. package/pages/c/_cluster/manager/cloudCredential/index.vue +18 -1
  126. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +12 -2
  127. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
  128. package/pages/c/_cluster/uiplugins/__tests__/index.spec.ts +318 -0
  129. package/pages/c/_cluster/uiplugins/index.vue +221 -363
  130. package/pages/home.vue +1 -9
  131. package/plugins/dashboard-store/resource-class.js +49 -0
  132. package/public/index.html +2 -1
  133. package/rancher-components/Card/Card.vue +1 -1
  134. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  135. package/rancher-components/Form/Radio/RadioButton.vue +1 -1
  136. package/rancher-components/Form/Radio/RadioGroup.vue +1 -1
  137. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -11
  138. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.test.ts +53 -0
  139. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +65 -0
  140. package/rancher-components/Pill/RcCounterBadge/index.ts +1 -0
  141. package/rancher-components/Pill/RcCounterBadge/types.ts +7 -0
  142. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +1 -1
  143. package/rancher-components/Pill/RcStatusBadge/index.ts +1 -1
  144. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -3
  145. package/rancher-components/Pill/RcStatusIndicator/types.ts +1 -1
  146. package/rancher-components/Pill/RcTag/RcTag.test.ts +64 -0
  147. package/rancher-components/Pill/RcTag/RcTag.vue +94 -0
  148. package/rancher-components/Pill/RcTag/index.ts +1 -0
  149. package/rancher-components/Pill/RcTag/types.ts +9 -0
  150. package/rancher-components/Pill/types.ts +1 -0
  151. package/rancher-components/RcItemCard/RcItemCard.vue +1 -0
  152. package/rancher-components/RcItemCard/RcItemCardAction.vue +12 -0
  153. package/store/__tests__/catalog.test.ts +63 -0
  154. package/store/catalog.js +2 -2
  155. package/store/type-map.js +3 -15
  156. package/types/extension-manager.ts +26 -0
  157. package/types/shell/index.d.ts +121 -16
  158. package/utils/__tests__/product.test.ts +129 -0
  159. package/utils/__tests__/resource.test.ts +87 -0
  160. package/utils/alertmanagerconfig.js +2 -2
  161. package/utils/auth.js +3 -76
  162. package/utils/product.ts +39 -0
  163. package/utils/resource.ts +35 -0
  164. package/utils/select.js +0 -24
  165. package/utils/validators/formRules/__tests__/index.test.ts +3 -0
  166. package/utils/validators/formRules/index.ts +2 -1
  167. package/vue.config.js +1 -1
  168. package/components/Resource/Detail/Metadata/Rectangle.vue +0 -34
  169. package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +0 -24
  170. package/components/ResourceDetail/Masthead/__tests__/legacy.test.ts +0 -65
  171. /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
+ }
@@ -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.id }.spec.receivers`],
23
- routeSchema: schema.schemaDefinitions?.[`${ schema.schemaDefinition.id }.spec.route`],
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
  */
@@ -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: '.' }] }));