@rancher/shell 3.0.10 → 3.0.12-rc.1

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 (154) hide show
  1. package/assets/styles/base/_mixins.scss +31 -0
  2. package/assets/styles/base/_variables.scss +2 -0
  3. package/assets/styles/themes/_modern.scss +6 -5
  4. package/assets/translations/en-us.yaml +12 -9
  5. package/assets/translations/zh-hans.yaml +0 -3
  6. package/chart/__tests__/rancher-backup-index.test.ts +248 -0
  7. package/chart/rancher-backup/index.vue +41 -2
  8. package/components/BrandImage.vue +6 -5
  9. package/components/ConsumptionGauge.vue +12 -4
  10. package/components/DynamicContent/DynamicContentIcon.vue +3 -2
  11. package/components/EmptyProductPage.vue +76 -0
  12. package/components/ExplorerProjectsNamespaces.vue +1 -4
  13. package/components/LazyImage.vue +2 -1
  14. package/components/Resource/Detail/Card/Scaler.vue +4 -4
  15. package/components/Resource/Detail/CopyToClipboard.vue +1 -2
  16. package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
  17. package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
  18. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
  19. package/components/Resource/Detail/TitleBar/index.vue +1 -1
  20. package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
  21. package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
  22. package/components/Resource/Detail/ViewOptions/index.vue +2 -1
  23. package/components/ResourceList/Masthead.vue +25 -2
  24. package/components/SideNav.vue +13 -0
  25. package/components/Tabbed/index.vue +6 -0
  26. package/components/__tests__/ConsumptionGauge.test.ts +31 -0
  27. package/components/__tests__/PromptModal.test.ts +2 -0
  28. package/components/fleet/FleetClusters.vue +1 -0
  29. package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
  30. package/components/form/NodeScheduling.vue +17 -3
  31. package/components/form/PrivateRegistry.vue +69 -0
  32. package/components/form/ProjectMemberEditor.vue +0 -10
  33. package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
  34. package/components/formatter/WorkloadHealthScale.vue +3 -1
  35. package/components/nav/Group.vue +26 -3
  36. package/components/nav/Header.vue +32 -7
  37. package/components/nav/TopLevelMenu.helper.ts +7 -79
  38. package/components/nav/TopLevelMenu.vue +15 -1
  39. package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
  40. package/config/pagination-table-headers.js +8 -1
  41. package/config/private-label.js +2 -1
  42. package/config/product/apps.js +3 -1
  43. package/config/product/auth.js +1 -0
  44. package/config/product/backup.js +1 -0
  45. package/config/product/compliance.js +1 -1
  46. package/config/product/explorer.js +25 -6
  47. package/config/product/fleet.js +1 -0
  48. package/config/product/gatekeeper.js +1 -0
  49. package/config/product/istio.js +1 -0
  50. package/config/product/logging.js +1 -0
  51. package/config/product/longhorn.js +2 -1
  52. package/config/product/manager.js +1 -0
  53. package/config/product/monitoring.js +1 -0
  54. package/config/product/navlinks.js +1 -0
  55. package/config/product/neuvector.js +2 -1
  56. package/config/product/settings.js +1 -0
  57. package/config/product/uiplugins.js +1 -0
  58. package/core/__tests__/extension-manager-impl.test.js +187 -2
  59. package/core/__tests__/plugin-products-helpers.test.ts +454 -0
  60. package/core/__tests__/plugin-products.test.ts +3219 -0
  61. package/core/extension-manager-impl.js +34 -3
  62. package/core/plugin-helpers.ts +31 -0
  63. package/core/plugin-products-base.ts +375 -0
  64. package/core/plugin-products-extending.ts +44 -0
  65. package/core/plugin-products-helpers.ts +262 -0
  66. package/core/plugin-products-top-level.ts +66 -0
  67. package/core/plugin-products-type-guards.ts +33 -0
  68. package/core/plugin-products.ts +50 -0
  69. package/core/plugin-types.ts +222 -0
  70. package/core/plugin.ts +45 -10
  71. package/core/productDebugger.js +48 -0
  72. package/core/types.ts +95 -11
  73. package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
  74. package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
  75. package/detail/__tests__/node.test.ts +83 -0
  76. package/detail/fleet.cattle.io.bundle.vue +21 -34
  77. package/detail/management.cattle.io.oidcclient.vue +2 -1
  78. package/detail/node.vue +1 -0
  79. package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
  80. package/dialog/InstallExtensionDialog.vue +6 -27
  81. package/dialog/UninstallExistingExtensionDialog.vue +141 -0
  82. package/dialog/UninstallExtensionDialog.vue +4 -26
  83. package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
  84. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
  85. package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
  86. package/edit/cloudcredential.vue +2 -1
  87. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
  88. package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
  89. package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
  90. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
  91. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
  92. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
  93. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
  94. package/edit/secret/generic.vue +1 -0
  95. package/edit/secret/index.vue +2 -1
  96. package/edit/service.vue +2 -14
  97. package/list/management.cattle.io.feature.vue +7 -1
  98. package/list/provisioning.cattle.io.cluster.vue +0 -50
  99. package/list/workload.vue +11 -4
  100. package/mixins/brand.js +2 -1
  101. package/mixins/resource-fetch.js +12 -3
  102. package/models/catalog.cattle.io.clusterrepo.js +9 -0
  103. package/models/cluster.x-k8s.io.machinedeployment.js +8 -3
  104. package/models/management.cattle.io.authconfig.js +2 -1
  105. package/models/management.cattle.io.cluster.js +4 -3
  106. package/models/monitoring.coreos.com.receiver.js +11 -6
  107. package/models/pod.js +18 -0
  108. package/models/provisioning.cattle.io.cluster.js +2 -2
  109. package/models/workload.js +20 -2
  110. package/package.json +5 -6
  111. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
  112. package/pages/c/_cluster/apps/charts/index.vue +3 -8
  113. package/pages/c/_cluster/apps/charts/install.vue +8 -9
  114. package/pages/c/_cluster/istio/index.vue +4 -2
  115. package/pages/c/_cluster/longhorn/index.vue +2 -1
  116. package/pages/c/_cluster/monitoring/index.vue +2 -2
  117. package/pages/c/_cluster/neuvector/index.vue +2 -1
  118. package/pages/c/_cluster/settings/brand.vue +4 -4
  119. package/pages/c/_cluster/settings/performance.vue +0 -5
  120. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
  121. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
  122. package/pages/c/_cluster/uiplugins/index.vue +145 -38
  123. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
  124. package/plugins/dashboard-store/actions.js +3 -2
  125. package/plugins/dashboard-store/resource-class.js +62 -6
  126. package/plugins/plugin.js +16 -0
  127. package/plugins/steve/steve-pagination-utils.ts +8 -2
  128. package/plugins/steve/subscribe.js +29 -4
  129. package/rancher-components/RcButton/RcButton.vue +3 -3
  130. package/rancher-components/RcButtonSplit/RcButtonSplit.test.ts +253 -0
  131. package/rancher-components/RcButtonSplit/RcButtonSplit.vue +158 -0
  132. package/rancher-components/RcButtonSplit/index.ts +1 -0
  133. package/scripts/test-plugins-build.sh +4 -4
  134. package/scripts/typegen.sh +13 -1
  135. package/store/__tests__/type-map.test.ts +84 -24
  136. package/store/type-map.js +42 -3
  137. package/tsconfig.paths.json +1 -0
  138. package/types/resources/pod.ts +18 -0
  139. package/types/shell/index.d.ts +8506 -2908
  140. package/types/store/dashboard-store.types.ts +5 -0
  141. package/types/store/pagination.types.ts +6 -0
  142. package/utils/__tests__/require-asset.test.ts +98 -0
  143. package/utils/async.ts +1 -5
  144. package/utils/axios.js +1 -4
  145. package/utils/brand.ts +3 -1
  146. package/utils/dynamic-importer.js +3 -2
  147. package/utils/favicon.js +4 -3
  148. package/utils/pagination-utils.ts +1 -1
  149. package/utils/require-asset.ts +95 -0
  150. package/utils/uiplugins.ts +12 -16
  151. package/utils/validators/__tests__/private-registry.test.ts +76 -0
  152. package/utils/validators/private-registry.ts +28 -0
  153. package/vue.config.js +4 -3
  154. package/components/HarvesterServiceAddOnConfig.vue +0 -207
package/core/plugin.ts CHANGED
@@ -19,12 +19,20 @@ import {
19
19
  PaginationTableColumn,
20
20
  ExtensionEnvironment,
21
21
  ServerSidePaginationExtensionConfig,
22
- TableAction
22
+ TableAction,
23
23
  } from './types';
24
+ import {
25
+ ProductMetadata,
26
+ ProductSinglePage,
27
+ ProductChild,
28
+ StandardProductName,
29
+ RouteRecordRawWithParams
30
+ } from './plugin-types';
24
31
  import coreStore, { coreStoreModule, coreStoreState } from '@shell/plugins/dashboard-store';
25
32
  import { defineAsyncComponent, markRaw, Component } from 'vue';
26
33
  import { getVersionData, CURRENT_RANCHER_VERSION } from '@shell/config/version';
27
34
  import { ExtensionManagerTypes } from '@shell/types/extension-manager';
35
+ import { PluginProduct } from './plugin-products';
28
36
 
29
37
  /** Registration IDs used for different extension points in the extensions catalog */
30
38
  export const EXT_IDS = {
@@ -42,18 +50,20 @@ export type ProductFunction = (plugin: IPlugin, store: any) => void;
42
50
  export class Plugin implements IPlugin {
43
51
  public id: string;
44
52
  public name: string;
53
+ public topLevelProduct = false;
45
54
  public types: ExtensionManagerTypes = {};
46
55
  public l10n: { [key: string]: Function[] } = {};
47
56
  public modelExtensions: { [key: string]: Function[] } = {};
48
57
  public locales: { locale: string, label: string}[] = [];
49
58
  public products: ProductFunction[] = [];
50
59
  public productNames: string[] = [];
51
- public routes: { parent?: string, route: RouteRecordRaw }[] = [];
60
+ public routes: { parent?: string, route: RouteRecordRaw | RouteRecordRawWithParams }[] = [];
52
61
  public stores: { storeName: string, register: RegisterStore, unregister: UnregisterStore }[] = [];
53
62
  public onEnter: OnNavToPackage = () => Promise.resolve();
54
63
  public onLeave: OnNavAwayFromPackage = () => Promise.resolve();
55
64
  public _onLogOut: OnLogOut = () => Promise.resolve();
56
65
  public onLogIn: OnLogIn = () => Promise.resolve();
66
+ public productConfigs: PluginProduct[] = [];
57
67
 
58
68
  public uiConfig: { [key: string]: any } = {};
59
69
 
@@ -110,7 +120,12 @@ export class Plugin implements IPlugin {
110
120
  this._validators = vals;
111
121
  }
112
122
 
123
+ _registerTopLevelProduct() {
124
+ this.topLevelProduct = true;
125
+ }
126
+
113
127
  // Track which products the plugin creates
128
+ // Legacy DSL method
114
129
  DSL(store: any, productName: string) {
115
130
  const storeDSL = STORE_DSL(store, productName);
116
131
 
@@ -119,8 +134,28 @@ export class Plugin implements IPlugin {
119
134
  return storeDSL;
120
135
  }
121
136
 
122
- addProduct(product: ProductFunction): void {
123
- this.products.push(product);
137
+ addProduct(product: ProductFunction | ProductMetadata | ProductSinglePage | string, config?: ProductChild[]): void {
138
+ if (typeof product === 'string') {
139
+ this.productConfigs.push(PluginProduct.fromName(this, product));
140
+ } else if (product?.name) {
141
+ if (!config) {
142
+ const p = product as ProductSinglePage;
143
+
144
+ this.productConfigs.push(new PluginProduct(this, p, []));
145
+ } else {
146
+ const p = product as ProductMetadata;
147
+
148
+ this.productConfigs.push(new PluginProduct(this, p, config));
149
+ }
150
+ } else {
151
+ this.products.push(product as ProductFunction);
152
+ }
153
+ }
154
+
155
+ extendProduct(product: StandardProductName | string, config: ProductChild[] | ProductChild): void {
156
+ const arrayConfig = Array.isArray(config) ? config : [config];
157
+
158
+ this.productConfigs.push(new PluginProduct(this, product, arrayConfig));
124
159
  }
125
160
 
126
161
  addLocale(locale: string, label: string): void {
@@ -131,8 +166,8 @@ export class Plugin implements IPlugin {
131
166
  this.register('l10n', locale, fn);
132
167
  }
133
168
 
134
- addRoutes(routes: PluginRouteRecordRaw[] | RouteRecordRaw[]) {
135
- routes.forEach((r: PluginRouteRecordRaw | RouteRecordRaw) => {
169
+ addRoutes(routes: PluginRouteRecordRaw[] | RouteRecordRawWithParams[] | RouteRecordRaw[]) {
170
+ routes.forEach((r: PluginRouteRecordRaw | RouteRecordRawWithParams | RouteRecordRaw) => {
136
171
  if (Object.keys(r).includes('parent')) {
137
172
  const pConfig = r as PluginRouteRecordRaw;
138
173
 
@@ -142,16 +177,16 @@ export class Plugin implements IPlugin {
142
177
  this.addRoute(pConfig.route);
143
178
  }
144
179
  } else {
145
- this.addRoute(r as RouteRecordRaw);
180
+ this.addRoute(r as RouteRecordRaw | RouteRecordRawWithParams);
146
181
  }
147
182
  });
148
183
  }
149
184
 
150
- addRoute(parentOrRoute: RouteRecordRaw | string, optionalRoute?: RouteRecordRaw): void {
185
+ addRoute(parentOrRoute: RouteRecordRaw | RouteRecordRawWithParams | string, optionalRoute?: RouteRecordRaw | RouteRecordRawWithParams): void {
151
186
  // Always add the pkg name to the route metadata
152
187
  const hasParent = typeof (parentOrRoute) === 'string';
153
188
  const parent: string | undefined = hasParent ? parentOrRoute as string : undefined;
154
- const route: RouteRecordRaw = hasParent ? optionalRoute as RouteRecordRaw : parentOrRoute as RouteRecordRaw;
189
+ const route: RouteRecordRaw | RouteRecordRawWithParams = hasParent ? (optionalRoute as RouteRecordRaw | RouteRecordRawWithParams) : parentOrRoute as RouteRecordRaw | RouteRecordRawWithParams;
155
190
 
156
191
  let parentOverride;
157
192
 
@@ -390,7 +425,7 @@ export class Plugin implements IPlugin {
390
425
  const allowPaths = ['models', 'image'];
391
426
  const nparts = name.split('/');
392
427
 
393
- // Support components in a sub-folder - component_name/index.vue (and ignore other componnets in that folder)
428
+ // Support components in a sub-folder - component_name/index.vue (and ignore other components in that folder)
394
429
  // Allow store-scoped models via sub-folder - pkgname/models/storename/type will be registered as storename/type to avoid overwriting shell/models/type
395
430
  if (nparts.length === 2 && !allowPaths.includes(type)) {
396
431
  if (nparts[1] !== 'index') {
@@ -0,0 +1,48 @@
1
+
2
+ export function DSLRegistrationsPerProduct(store, prodName) {
3
+ const parsedData = {};
4
+ const typeMapData = store._state.data['type-map'];
5
+
6
+ const relevantKeys = [
7
+ 'basicGroupWeights',
8
+ 'basicTypeWeights',
9
+ 'groupDefaultTypes',
10
+ 'groupLabels',
11
+ // 'headers',
12
+ // 'spoofedTypes',
13
+ // 'typeOptions',
14
+ // 'typeWeights'
15
+ ];
16
+
17
+ Object.keys(typeMapData).forEach((dataType) => {
18
+ // prod reg
19
+ if (dataType === 'products' && typeMapData[dataType].filter((item) => item.name === prodName)) {
20
+ parsedData[dataType] = typeMapData[dataType].filter((item) => item.name === prodName);
21
+ }
22
+
23
+ // prod configureType
24
+ if (dataType === 'typeOptions' && typeMapData[dataType].filter((item) => item.customRoute && item.customRoute.name.includes(prodName))) {
25
+ parsedData['configureType'] = typeMapData[dataType].filter((item) => item.customRoute && item.customRoute.name.includes(prodName));
26
+ }
27
+
28
+ // other types which map with prodName
29
+ if (typeMapData[dataType]?.[prodName]) {
30
+ parsedData[dataType] = typeMapData[dataType]?.[prodName];
31
+ }
32
+
33
+ // other relevant data
34
+ if (relevantKeys.includes(dataType)) {
35
+ parsedData[dataType] = typeMapData[dataType];
36
+ }
37
+ });
38
+
39
+ console.error('*** PRODUCT DATA DEBUGGER **** DSLRegistrationsPerProduct', parsedData); // eslint-disable-line no-console
40
+ }
41
+
42
+ export function registeredRoutes(store, prodName) {
43
+ const routes = store.$router.getRoutes();
44
+
45
+ const parsedData = routes.filter((route) => route.path.includes(prodName));
46
+
47
+ console.error('*** PRODUCT DATA DEBUGGER **** registeredRoutes', parsedData); // eslint-disable-line no-console
48
+ }
package/core/types.ts CHANGED
@@ -2,6 +2,11 @@ import { ProductFunction } from './plugin';
2
2
  import { RouteRecordRaw } from 'vue-router';
3
3
  import type { ExtensionManager } from '@shell/types/extension-manager';
4
4
  import { PaginationSettingsStores } from '@shell/types/resources/settings';
5
+ import type {
6
+ ProductMetadata, ProductSinglePage,
7
+ StandardProductName, RouteRecordRawWithParams, ProductChildGroup,
8
+ ProductChildPage
9
+ } from './plugin-types';
5
10
 
6
11
  // Cluster Provisioning types
7
12
  export * from './types-provisioning';
@@ -21,6 +26,8 @@ export interface PackageMetadata {
21
26
  // children: Route[];
22
27
  // }
23
28
 
29
+ export type PluginRouteRecordRaw = { [key: string]: any }
30
+
24
31
  export type VuexStoreObject = { [key: string]: any }
25
32
  export type CoreStoreSpecifics = { state: () => VuexStoreObject, getters: VuexStoreObject, mutations: VuexStoreObject, actions: VuexStoreObject }
26
33
  export type CoreStoreConfig = { namespace: string, baseUrl?: string, modelBaseClass?: string, supportsStream?: boolean, isClusterStore?: boolean }
@@ -28,8 +35,6 @@ export type CoreStoreInit = (store: any, ctx: any) => void;
28
35
  export type RegisterStore = () => (store: any) => void
29
36
  export type UnregisterStore = (store: any) => void
30
37
 
31
- export type PluginRouteRecordRaw = { [key: string]: any }
32
-
33
38
  export type OnEnterLeavePackageConfig = {
34
39
  clusterId: string,
35
40
  product: string,
@@ -265,6 +270,11 @@ export interface ProductOptions {
265
270
  */
266
271
  showClusterSwitcher?: boolean;
267
272
 
273
+ /**
274
+ * Indicates whether UI Extensions can add pages to this product
275
+ */
276
+ extendable?: boolean;
277
+
268
278
  /**
269
279
  * Show the namespace filter in the header
270
280
  */
@@ -290,6 +300,18 @@ export interface ProductOptions {
290
300
  */
291
301
  name?: string;
292
302
 
303
+ /**
304
+ *
305
+ */
306
+ label?: string;
307
+
308
+ labelKey?: string;
309
+
310
+ iconHeader?: string;
311
+
312
+ // Do not use - internal use only
313
+ version?: number;
314
+
293
315
  /**
294
316
  * Leaving these here for completeness but I don't think these should be advertised as useable to plugin creators.
295
317
  */
@@ -368,6 +390,17 @@ export interface HeaderOptions {
368
390
  */
369
391
  export type PaginationHeaderOptions = Omit<HeaderOptions, 'getValue'>
370
392
 
393
+ export type ResourceTypeConfig = {
394
+ options?: {
395
+ isCreatable?: boolean;
396
+ isEditable?: boolean;
397
+ },
398
+ listHeaders?: {
399
+ legacy?: HeaderOptions[];
400
+ paginated?: PaginationHeaderOptions[];
401
+ }
402
+ };
403
+
371
404
  /**
372
405
  * External extension configuration for @HeaderOptions
373
406
  */
@@ -489,9 +522,11 @@ export interface ConfigureVirtualTypeOptions extends ConfigureTypeOptions {
489
522
  name: string;
490
523
 
491
524
  /**
492
- * The route that this type should correspond to {@link PluginRouteRecordRaw} {@link RouteRecordRaw}
525
+ * The route that this type should correspond to {@link PluginRouteRecordRaw} {@link RouteRecordRaw} {@link RouteRecordRawWithParams}
493
526
  */
494
- route: PluginRouteRecordRaw | RouteRecordRaw | Object;
527
+ route: PluginRouteRecordRaw | RouteRecordRaw | RouteRecordRawWithParams | Object;
528
+
529
+ weight?: number;
495
530
  }
496
531
 
497
532
  export interface DSLReturnType {
@@ -501,7 +536,7 @@ export interface DSLReturnType {
501
536
  * @param group Conditionally a group you want to places all the types in
502
537
  * @returns {@link void}
503
538
  */
504
- basicType: (types: string[], group?: string) => void;
539
+ basicType: (types: string[] | string, group?: string) => void;
505
540
 
506
541
  /**
507
542
  * Configure a myriad of options for the specified type
@@ -572,6 +607,10 @@ export interface DSLReturnType {
572
607
  // moveType: (match, group)
573
608
  // setGroupDefaultType: (input, defaultType)
574
609
  // spoofedType: (obj)
610
+
611
+ labelGroup: (group: string, label: string | undefined, labelKey?: string) => void;
612
+
613
+ setGroupDefaultType: (group: string, defaultType: string) => void;
575
614
  }
576
615
 
577
616
  /**
@@ -610,15 +649,53 @@ export type ModelExtensionContext = {
610
649
  export type ModelExtensionConstructor = (context: ModelExtensionContext) => Object;
611
650
 
612
651
  /**
613
- * Interface for a Dashboard plugin
652
+ * Interface for a UI Extension
614
653
  */
615
- export interface IPlugin {
654
+ export interface IExtension {
655
+ /**
656
+ * Register a top-level product as a flag on the plugin
657
+ * @internal - DO NOT USE - Internal API only
658
+ */
659
+ _registerTopLevelProduct(): void;
660
+
661
+ /**
662
+ * Add a product to the sidebar, with children and a side menu for navigation for internal pages
663
+ * @param name
664
+ * @param config
665
+ */
666
+ addProduct(product: ProductMetadata, config: ProductChildGroup[]): void;
667
+ addProduct(product: ProductMetadata, config: ProductChildPage[]): void;
668
+
669
+ /**
670
+ * Add a product to the sidebar, without children (no side menu, single page only)
671
+ * @param product
672
+ */
673
+ addProduct(product: ProductSinglePage): void;
674
+
616
675
  /**
617
- * Add a product
676
+ * Add a product with just a name (convenience/bridge method for quick setup).
677
+ * Creates a basic product with an empty page component automatically.
678
+ * This is useful for getting started quickly - expand to the full API once you're ready to add custom pages.
679
+ * @param productName Simple product name - will be used as both the name and label
680
+ */
681
+ addProduct(productName: string): void;
682
+
683
+ /**
684
+ * Add a product to the sidebar (deprecated, use other signatures of addProduct instead)
685
+ * @deprecated Use other `addProduct` signatures instead
618
686
  * @param importFn Function that will import the module containing a product definition
619
687
  */
620
688
  addProduct(importFn: ProductFunction): void;
621
689
 
690
+ /**
691
+ * Extend an existing product in Rancher, with children and a side menu for navigation for internal pages
692
+ *
693
+ * @param product Product to be extended
694
+ * @param config Product extension configuration
695
+ */
696
+ extendProduct(product: StandardProductName | string, config: ProductChildGroup[]): void;
697
+ extendProduct(product: StandardProductName | string, config: ProductChildPage[]): void;
698
+
622
699
  /**
623
700
  * Add a locale to the i18n store
624
701
  * @param locale Locale id (e.g. en-us)
@@ -644,8 +721,8 @@ export interface IPlugin {
644
721
  /**
645
722
  * Add a route to the Vue Router
646
723
  */
647
- addRoute(route: RouteRecordRaw): void;
648
- addRoute(parent: string, route: RouteRecordRaw): void;
724
+ addRoute(route: RouteRecordRawWithParams | RouteRecordRaw): void;
725
+ addRoute(parent: string, route: RouteRecordRawWithParams | RouteRecordRaw): void;
649
726
 
650
727
  /**
651
728
  * Adds an action/button to the UI
@@ -698,7 +775,7 @@ export interface IPlugin {
698
775
  /**
699
776
  * Add routes to the Vue Router
700
777
  */
701
- addRoutes(routes: PluginRouteRecordRaw[] | RouteRecordRaw[]): void;
778
+ addRoutes(routes: PluginRouteRecordRaw[] | RouteRecordRawWithParams[] | RouteRecordRaw[]): void;
702
779
 
703
780
  /**
704
781
  * Add a hook to be called when the plugin is uninstalled
@@ -755,6 +832,7 @@ export interface IPlugin {
755
832
 
756
833
  /**
757
834
  * Will return all of the configuration functions used for creating a new product.
835
+ * @deprecated Should use `addProduct` and `extendProduct` instead and avoid using this directly
758
836
  * @param store The store that was passed to the function that's passed to `plugin.addProduct(function)`
759
837
  * @param productName The name of the new product. This name is displayed in the navigation.
760
838
  */
@@ -766,6 +844,12 @@ export interface IPlugin {
766
844
  get environment(): ExtensionEnvironment;
767
845
  }
768
846
 
847
+ /**
848
+ * Legacy interface for a plugin, which is just an extension but with the `DSL` function.
849
+ * @deprecated Should use `IExtension` interface instead
850
+ */
851
+ export type IPlugin = IExtension;
852
+
769
853
  // Internal interface
770
854
  // Built-in extensions may use this, but external extensions should not, as this is subject to change
771
855
  // Defined as any for now
@@ -0,0 +1,52 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`view: fleet.cattle.io.bundle should render resources when bundle deployments exist 1`] = `
4
+ <div class="tabbed-container resource-tabs view" data-testid="tabbed">
5
+ <ul role="tablist" class="tabs clearfix horizontal" data-testid="tabbed-block" tabindex="0">
6
+ <!-- This is the tabs link... tabs appear here because they are injected from the "Tab" component -->
7
+ <li id="resources" data-testid="resources" class="tab active"><a id="tab-resources" data-testid="btn-resources" aria-controls="resources" aria-selected="true" aria-label="Resources" role="tab" tabindex="0">
8
+ <!--v-if--><span>Resources</span>
9
+ <!--v-if-->
10
+ <!--v-if-->
11
+ </a></li>
12
+ <!--v-if-->
13
+ <!--v-if-->
14
+ </ul>
15
+ <div class="tab-container">
16
+ <!-- This is where "normal" tab content goes... -->
17
+ <section id="resources" aria-hidden="false" role="tabpanel" aria-labelledby="tab-resources" style="">
18
+ <!--v-if-->
19
+ <resource-table-stub rows="[object Object],[object Object]" loading="false" altloading="false" keyfield="tableKey" headers="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" search="true" tableactions="false" paginglabel="sortableTable.paging.resource" rowactions="false" groupoptions="[object Object],[object Object]" groupdefault="namespace" grouptooltip="resourceTable.groupBy.namespace" overflowx="false" overflowy="false" ignorefilter="false" hasadvancedfiltering="false" advfilterhidelabelsascols="false" advfilterpreventfilteringlabels="false" usequeryparamsforsimplefiltering="false" forceupdateliveanddelayed="0" externalpaginationenabled="false" default-sort-by="state"></resource-table-stub>
20
+ </section>
21
+ <!--v-if-->
22
+ <!--v-if-->
23
+ <!--v-if-->
24
+ <!-- Extension tabs content goes here... -->
25
+ </div>
26
+ </div>
27
+ `;
28
+
29
+ exports[`view: fleet.cattle.io.bundle should render the bundle detail page 1`] = `<resource-tabs-stub mode="view" needconditions="true" needevents="true" needrelated="false" usehash="true" value="[object Object]"></resource-tabs-stub>`;
30
+
31
+ exports[`view: fleet.cattle.io.bundle should render the bundle detail page with full mount 1`] = `
32
+ <div class="tabbed-container resource-tabs view" data-testid="tabbed">
33
+ <ul role="tablist" class="tabs clearfix horizontal" data-testid="tabbed-block" tabindex="0">
34
+ <!-- This is the tabs link... tabs appear here because they are injected from the "Tab" component -->
35
+ <!--v-if-->
36
+ <!--v-if-->
37
+ </ul>
38
+ <div class="">
39
+ <!-- This is where "normal" tab content goes... -->
40
+ <section id="resources" aria-hidden="true" role="tabpanel" aria-labelledby="tab-resources" style="display: none;">
41
+ <!--v-if-->
42
+ <resource-table-stub rows="" loading="false" altloading="false" keyfield="tableKey" headers="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" search="true" tableactions="false" paginglabel="sortableTable.paging.resource" rowactions="false" groupoptions="[object Object],[object Object]" groupdefault="namespace" grouptooltip="resourceTable.groupBy.namespace" overflowx="false" overflowy="false" ignorefilter="false" hasadvancedfiltering="false" advfilterhidelabelsascols="false" advfilterpreventfilteringlabels="false" usequeryparamsforsimplefiltering="false" forceupdateliveanddelayed="0" externalpaginationenabled="false" default-sort-by="state"></resource-table-stub>
43
+ </section>
44
+ <!--v-if-->
45
+ <!--v-if-->
46
+ <!--v-if-->
47
+ <!-- Extension tabs content goes here... -->
48
+ </div>
49
+ </div>
50
+ `;
51
+
52
+ exports[`view: fleet.cattle.io.bundle should show Loading when fetch is pending 1`] = `<loading-stub loading="true" mode="content" nodelay="false"></loading-stub>`;
@@ -0,0 +1,171 @@
1
+ import { shallowMount, mount } from '@vue/test-utils';
2
+ import BundleDetail from '@shell/detail/fleet.cattle.io.bundle.vue';
3
+
4
+ describe('view: fleet.cattle.io.bundle', () => {
5
+ const mockStore = {
6
+ getters: {
7
+ 'i18n/t': (text: string) => text,
8
+ 'i18n/exists': jest.fn(),
9
+ currentStore: () => 'management',
10
+ 'management/schemaFor': jest.fn(),
11
+ 'management/all': () => [],
12
+ 'management/pathExistsInSchema': () => false,
13
+ 'type-map/optionsFor': () => ({}),
14
+ 'type-map/headersFor': () => [],
15
+ 'cluster/schemaFor': jest.fn(),
16
+ 'resource-fetch/refreshFlag': () => false,
17
+ },
18
+ dispatch: jest.fn(),
19
+ };
20
+
21
+ const mockValue = {
22
+ id: 'fleet-default/test-bundle',
23
+ type: 'fleet.cattle.io.bundle',
24
+ metadata: {
25
+ name: 'test-bundle',
26
+ namespace: 'fleet-default',
27
+ labels: {},
28
+ },
29
+ spec: { targets: [] },
30
+ status: { conditions: [] },
31
+ targetClusters: [],
32
+ };
33
+
34
+ const mockValueWithResources = {
35
+ id: 'fleet-default/test-bundle',
36
+ type: 'fleet.cattle.io.bundle',
37
+ metadata: {
38
+ name: 'test-bundle',
39
+ namespace: 'fleet-default',
40
+ labels: {},
41
+ },
42
+ spec: { targets: [] },
43
+ status: {
44
+ conditions: [
45
+ {
46
+ type: 'Ready',
47
+ status: 'True',
48
+ }
49
+ ]
50
+ },
51
+ targetClusters: [
52
+ {
53
+ id: 'fleet-default/cluster-1',
54
+ nameDisplay: 'cluster-1',
55
+ metadata: { labels: { 'management.cattle.io/cluster-name': 'c-m-abc123' } },
56
+ },
57
+ ],
58
+ };
59
+
60
+ const mockBundleDeployments = [
61
+ {
62
+ metadata: {
63
+ labels: {
64
+ 'fleet.cattle.io/bundle-namespace': 'fleet-default',
65
+ 'fleet.cattle.io/bundle-name': 'test-bundle',
66
+ 'fleet.cattle.io/cluster-namespace': 'fleet-default',
67
+ 'fleet.cattle.io/cluster': 'cluster-1',
68
+ },
69
+ },
70
+ status: {
71
+ resources: [
72
+ {
73
+ kind: 'Deployment',
74
+ apiVersion: 'apps/v1',
75
+ namespace: 'default',
76
+ name: 'nginx',
77
+ },
78
+ {
79
+ kind: 'Service',
80
+ apiVersion: 'v1',
81
+ namespace: 'default',
82
+ name: 'nginx-svc',
83
+ },
84
+ ],
85
+ },
86
+ },
87
+ ];
88
+
89
+ const fullMountGlobal = {
90
+ mocks: {
91
+ $store: mockStore,
92
+ $fetchState: { pending: false },
93
+ $route: { query: {}, hash: '' },
94
+ $router: {
95
+ applyQuery: jest.fn(),
96
+ replace: jest.fn(),
97
+ currentRoute: { _value: { hash: '' } },
98
+ },
99
+ },
100
+ stubs: {
101
+ ResourceTable: true,
102
+ teleport: true,
103
+ },
104
+ };
105
+
106
+ const createWrapper = (props = {}) => {
107
+ return shallowMount(BundleDetail, {
108
+ props: {
109
+ value: mockValue,
110
+ ...props,
111
+ },
112
+ global: {
113
+ mocks: {
114
+ $store: mockStore,
115
+ $fetchState: { pending: false },
116
+ $route: { query: {} },
117
+ $router: { applyQuery: jest.fn() },
118
+ },
119
+ },
120
+ });
121
+ };
122
+
123
+ it('should render the bundle detail page', () => {
124
+ const wrapper = createWrapper();
125
+
126
+ expect(wrapper.html()).toMatchSnapshot();
127
+ });
128
+
129
+ it('should render the bundle detail page with full mount', () => {
130
+ const wrapper = mount(BundleDetail, {
131
+ props: { value: mockValue },
132
+ global: fullMountGlobal,
133
+ });
134
+
135
+ expect(wrapper.html()).toMatchSnapshot();
136
+ });
137
+
138
+ it('should show Loading when fetch is pending', () => {
139
+ const wrapper = shallowMount(BundleDetail, {
140
+ props: { value: mockValue },
141
+ global: {
142
+ mocks: {
143
+ $store: mockStore,
144
+ $fetchState: { pending: true },
145
+ $route: { query: {} },
146
+ $router: { applyQuery: jest.fn() },
147
+ },
148
+ },
149
+ });
150
+
151
+ expect(wrapper.html()).toMatchSnapshot();
152
+ });
153
+
154
+ it('should render resources when bundle deployments exist', async() => {
155
+ const wrapper = mount(BundleDetail, {
156
+ props: { value: mockValueWithResources },
157
+ global: fullMountGlobal,
158
+ });
159
+
160
+ // Simulate fetch completion by setting allBundleDeployments
161
+ (wrapper.vm as any).allBundleDeployments = mockBundleDeployments;
162
+ await wrapper.vm.$nextTick();
163
+
164
+ expect(wrapper.html()).toMatchSnapshot();
165
+
166
+ // Verify computed bundleResources are populated
167
+ expect((wrapper.vm as any).bundleResources).toHaveLength(2);
168
+ expect((wrapper.vm as any).bundleResources[0].name).toBe('nginx');
169
+ expect((wrapper.vm as any).bundleResources[1].name).toBe('nginx-svc');
170
+ });
171
+ });
@@ -0,0 +1,83 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import node from '@shell/detail/node.vue';
3
+ import ConsumptionGauge from '@shell/components/ConsumptionGauge.vue';
4
+
5
+ describe('view: node detail', () => {
6
+ const mockStore = {
7
+ getters: {
8
+ 'cluster/schemaFor': () => undefined,
9
+ 'cluster/paginationEnabled': () => false,
10
+ 'type-map/headersFor': jest.fn(),
11
+ 'i18n/t': (key: string) => key,
12
+ currentCluster: { id: 'local' },
13
+ },
14
+ dispatch: jest.fn(),
15
+ };
16
+
17
+ const mocks = {
18
+ $store: mockStore,
19
+ $fetchState: { pending: false },
20
+ };
21
+
22
+ const defaultNodeValue = {
23
+ metadata: { name: 'test-node' },
24
+ status: {
25
+ nodeInfo: {}, images: [], conditions: []
26
+ },
27
+ spec: { taints: [] },
28
+ pods: [],
29
+ cpuCapacity: 4,
30
+ cpuUsage: 2,
31
+ ramReserved: 8000,
32
+ ramUsage: 4000,
33
+ podCapacity: 110,
34
+ podConsumed: 5,
35
+ isPidPressureOk: true,
36
+ isDiskPressureOk: true,
37
+ isMemoryPressureOk: true,
38
+ isKubeletOk: true,
39
+ internalIp: '10.0.0.1',
40
+ };
41
+
42
+ function createWrapper(nodeOverrides = {}) {
43
+ return shallowMount(node, {
44
+ props: { value: { ...defaultNodeValue, ...nodeOverrides } },
45
+ global: { mocks },
46
+ });
47
+ }
48
+
49
+ function findGaugeByResource(wrapper: ReturnType<typeof createWrapper>, resourceKey: string) {
50
+ const gauges = wrapper.findAllComponents(ConsumptionGauge);
51
+
52
+ return gauges.find(
53
+ (g) => g.attributes('resourcename')?.includes(resourceKey)
54
+ );
55
+ }
56
+
57
+ it('should pass the "running" translation key as usedlabel to the pods ConsumptionGauge', () => {
58
+ const wrapper = createWrapper();
59
+
60
+ const podsGauge = findGaugeByResource(wrapper, 'consumptionGauge.pods');
61
+
62
+ expect(podsGauge).toBeDefined();
63
+ expect(podsGauge!.attributes('usedlabel')).toContain('consumptionGauge.running');
64
+ });
65
+
66
+ it('should NOT pass a usedlabel to the CPU ConsumptionGauge', () => {
67
+ const wrapper = createWrapper();
68
+
69
+ const cpuGauge = findGaugeByResource(wrapper, 'consumptionGauge.cpu');
70
+
71
+ expect(cpuGauge).toBeDefined();
72
+ expect(cpuGauge!.attributes('usedlabel')).toBeUndefined();
73
+ });
74
+
75
+ it('should NOT pass a usedlabel to the Memory ConsumptionGauge', () => {
76
+ const wrapper = createWrapper();
77
+
78
+ const memoryGauge = findGaugeByResource(wrapper, 'consumptionGauge.memory');
79
+
80
+ expect(memoryGauge).toBeDefined();
81
+ expect(memoryGauge!.attributes('usedlabel')).toBeUndefined();
82
+ });
83
+ });