@rancher/shell 3.0.12-rc.2 → 3.0.12-rc.3

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 (272) hide show
  1. package/apis/impl/apis.ts +6 -0
  2. package/apis/index.ts +26 -0
  3. package/apis/intf/resources-api/cluster-api.ts +18 -0
  4. package/apis/intf/resources-api/mgmt-api.ts +15 -0
  5. package/apis/intf/resources-api/resource-base.ts +107 -0
  6. package/apis/intf/resources-api/resource-constants.ts +147 -0
  7. package/apis/intf/resources-api/resources-api.ts +143 -0
  8. package/apis/intf/resources.ts +49 -0
  9. package/apis/intf/{modal.ts → shell-api/modal.ts} +21 -26
  10. package/apis/intf/shell-api/proxy.ts +216 -0
  11. package/apis/intf/{slide-in.ts → shell-api/slide-in.ts} +4 -3
  12. package/apis/intf/{system.ts → shell-api/system.ts} +4 -1
  13. package/apis/intf/shell.ts +12 -6
  14. package/apis/resources/__tests__/resources-api-class.test.ts +550 -0
  15. package/apis/resources/index.ts +22 -0
  16. package/apis/resources/resources-api-class.ts +187 -0
  17. package/apis/shell/__tests__/proxy.test.ts +369 -0
  18. package/apis/shell/index.ts +8 -1
  19. package/apis/shell/modal.ts +4 -1
  20. package/apis/shell/notifications.ts +9 -6
  21. package/apis/shell/proxy.ts +256 -0
  22. package/apis/shell/slide-in.ts +4 -1
  23. package/apis/vue-shim.d.ts +2 -1
  24. package/assets/data/aws-regions.json +4 -0
  25. package/assets/fonts/lato/LatoLatin-Black.woff +0 -0
  26. package/assets/fonts/lato/LatoLatin-Black.woff2 +0 -0
  27. package/assets/fonts/lato/LatoLatin-BlackItalic.woff +0 -0
  28. package/assets/fonts/lato/LatoLatin-BlackItalic.woff2 +0 -0
  29. package/assets/fonts/lato/LatoLatin-Bold.woff +0 -0
  30. package/assets/fonts/lato/LatoLatin-Bold.woff2 +0 -0
  31. package/assets/fonts/lato/LatoLatin-BoldItalic.woff +0 -0
  32. package/assets/fonts/lato/LatoLatin-BoldItalic.woff2 +0 -0
  33. package/assets/fonts/lato/LatoLatin-Heavy.woff +0 -0
  34. package/assets/fonts/lato/LatoLatin-Heavy.woff2 +0 -0
  35. package/assets/fonts/lato/LatoLatin-HeavyItalic.woff +0 -0
  36. package/assets/fonts/lato/LatoLatin-HeavyItalic.woff2 +0 -0
  37. package/assets/fonts/lato/LatoLatin-Italic.woff +0 -0
  38. package/assets/fonts/lato/LatoLatin-Italic.woff2 +0 -0
  39. package/assets/fonts/lato/LatoLatin-Light.woff +0 -0
  40. package/assets/fonts/lato/LatoLatin-Light.woff2 +0 -0
  41. package/assets/fonts/lato/LatoLatin-LightItalic.woff +0 -0
  42. package/assets/fonts/lato/LatoLatin-LightItalic.woff2 +0 -0
  43. package/assets/fonts/lato/LatoLatin-Medium.woff +0 -0
  44. package/assets/fonts/lato/LatoLatin-Medium.woff2 +0 -0
  45. package/assets/fonts/lato/LatoLatin-MediumItalic.woff +0 -0
  46. package/assets/fonts/lato/LatoLatin-MediumItalic.woff2 +0 -0
  47. package/assets/fonts/lato/LatoLatin-Regular.woff +0 -0
  48. package/assets/fonts/lato/LatoLatin-Regular.woff2 +0 -0
  49. package/assets/fonts/lato/LatoLatin-Semibold.woff +0 -0
  50. package/assets/fonts/lato/LatoLatin-Semibold.woff2 +0 -0
  51. package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff +0 -0
  52. package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff2 +0 -0
  53. package/assets/styles/base/_variables.scss +2 -0
  54. package/assets/styles/fonts/_fontstack.scss +132 -8
  55. package/assets/translations/en-us.yaml +22 -5
  56. package/chart/monitoring/index.vue +10 -1
  57. package/components/ActionDropdownShell.vue +2 -1
  58. package/components/CruResourceFooter.vue +9 -5
  59. package/components/ExplorerProjectsNamespaces.vue +1 -1
  60. package/components/InstallHelmCharts.vue +2 -2
  61. package/components/LandingPagePreference.vue +14 -5
  62. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +15 -1
  63. package/components/Resource/Detail/Metadata/index.vue +6 -0
  64. package/components/Resource/Detail/ResourcePopover/index.vue +12 -1
  65. package/components/Resource/Detail/SpacedRow.vue +3 -1
  66. package/components/Resource/Detail/TitleBar/index.vue +10 -11
  67. package/components/ResourceList/Masthead.vue +12 -8
  68. package/components/SelectIconGrid.vue +0 -10
  69. package/components/SingleClusterInfo.vue +1 -0
  70. package/components/SortableTable/__tests__/sorting.test.ts +126 -0
  71. package/components/SortableTable/index.vue +6 -9
  72. package/components/SortableTable/selection.js +23 -5
  73. package/components/SortableTable/sorting.js +6 -3
  74. package/components/Wizard.vue +14 -13
  75. package/components/fleet/FleetBundles.vue +100 -12
  76. package/components/fleet/FleetClusterTargets/index.vue +37 -15
  77. package/components/fleet/__tests__/FleetClusterTargets.test.ts +149 -115
  78. package/components/fleet/__tests__/FleetClusters.test.ts +12 -12
  79. package/components/form/LabeledSelect.vue +20 -3
  80. package/components/form/NameNsDescription.vue +11 -0
  81. package/components/form/Security.vue +6 -2
  82. package/components/form/WorkloadPorts.vue +2 -7
  83. package/components/form/__tests__/Security.test.ts +76 -0
  84. package/components/formatter/Autoscaler.vue +4 -4
  85. package/components/formatter/ClusterKubeVersion.vue +27 -0
  86. package/components/formatter/ClusterLink.vue +1 -7
  87. package/components/formatter/ClusterProvider.vue +6 -10
  88. package/components/formatter/FleetSummaryGraph.vue +0 -3
  89. package/components/formatter/MachineSummaryGraph.vue +1 -1
  90. package/components/formatter/PodsUsage.vue +2 -2
  91. package/components/formatter/__tests__/Autoscaler.test.ts +19 -22
  92. package/components/formatter/__tests__/FleetSummaryGraph.test.ts +216 -0
  93. package/components/formatter/__tests__/PodsUsage.test.ts +6 -10
  94. package/components/nav/NamespaceFilter.vue +2 -2
  95. package/components/nav/TopLevelMenu.helper.ts +15 -3
  96. package/components/nav/TopLevelMenu.vue +16 -5
  97. package/components/nav/__tests__/TopLevelMenu.test.ts +145 -21
  98. package/components/templates/home.vue +18 -0
  99. package/components/templates/plain.vue +18 -0
  100. package/components/templates/standalone.vue +17 -0
  101. package/composables/useFormValidation.ts +93 -0
  102. package/composables/useVeeValidateField.test.ts +159 -0
  103. package/composables/useVeeValidateField.ts +67 -0
  104. package/config/pagination-table-headers.js +18 -1
  105. package/config/product/manager.js +82 -21
  106. package/config/router/routes.js +6 -0
  107. package/config/table-headers.js +20 -1
  108. package/config/types.js +2 -1
  109. package/core/__tests__/plugin-products.test.ts +904 -20
  110. package/core/plugin-products-base.ts +107 -7
  111. package/core/plugin-products.ts +4 -0
  112. package/core/plugin-types.ts +111 -1
  113. package/core/plugin.ts +15 -7
  114. package/core/productDebugger.js +9 -4
  115. package/core/types-provisioning.ts +43 -30
  116. package/core/types.ts +57 -20
  117. package/detail/__tests__/pod.test.ts +41 -0
  118. package/detail/harvesterhci.io.management.cluster.vue +6 -2
  119. package/detail/pod.vue +1 -1
  120. package/detail/provisioning.cattle.io.cluster.vue +4 -10
  121. package/edit/auth/__tests__/azuread.test.ts +217 -34
  122. package/edit/auth/azuread.vue +122 -14
  123. package/edit/auth/oidc.vue +2 -2
  124. package/edit/networking.k8s.io.ingress/DefaultBackend.vue +13 -4
  125. package/edit/networking.k8s.io.ingress/RulePath.vue +8 -4
  126. package/edit/networking.k8s.io.ingress/index.vue +75 -20
  127. package/edit/provisioning.cattle.io.cluster/__tests__/MachinePool.test.ts +104 -0
  128. package/edit/provisioning.cattle.io.cluster/index.vue +11 -7
  129. package/edit/provisioning.cattle.io.cluster/rke2.vue +8 -4
  130. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  131. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +37 -4
  132. package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +132 -7
  133. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -1
  134. package/edit/secret/__tests__/ssh.test.ts +5 -6
  135. package/edit/secret/basic.vue +31 -0
  136. package/edit/secret/index.vue +68 -17
  137. package/edit/secret/registry.vue +38 -0
  138. package/edit/secret/ssh.vue +29 -0
  139. package/edit/secret/tls.vue +30 -0
  140. package/edit/service.vue +4 -4
  141. package/edit/workload/Upgrading.vue +3 -3
  142. package/edit/workload/__tests__/Upgrading.test.ts +6 -9
  143. package/edit/workload/mixins/workload.js +2 -1
  144. package/list/fleet.cattle.io.bundle.vue +7 -104
  145. package/list/fleet.cattle.io.clusterregistrationtoken.vue +20 -0
  146. package/list/provisioning.cattle.io.cluster.vue +262 -180
  147. package/list/utils/management.cattle.io.cluster.utils.ts +128 -0
  148. package/mixins/__tests__/chart.test.ts +112 -0
  149. package/mixins/brand.js +2 -1
  150. package/mixins/chart.js +12 -8
  151. package/mixins/resource-fetch-api-pagination.js +41 -5
  152. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +67 -67
  153. package/models/__tests__/management.cattle.io.cluster.test.ts +1 -1
  154. package/models/__tests__/management.cattle.io.node.ts +6 -5
  155. package/models/__tests__/management.cattle.io.nodepool.ts +5 -4
  156. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +32 -11
  157. package/models/base-cluster.x-k8s.io.js +26 -0
  158. package/models/cluster.js +1 -1
  159. package/models/cluster.x-k8s.io.machine.js +4 -22
  160. package/models/cluster.x-k8s.io.machinedeployment.js +2 -20
  161. package/models/cluster.x-k8s.io.machineset.js +2 -20
  162. package/models/compliance.cattle.io.clusterscan.js +130 -2
  163. package/models/ext.cattle.io.kubeconfig.ts +4 -7
  164. package/models/fleet-application.js +3 -1
  165. package/models/management.cattle.io.cluster.js +417 -40
  166. package/models/management.cattle.io.node.js +6 -4
  167. package/models/management.cattle.io.nodepool.js +1 -1
  168. package/models/networking.k8s.io.ingress.js +12 -4
  169. package/models/provisioning.cattle.io.cluster.js +47 -330
  170. package/models/rke.cattle.io.etcdsnapshot.js +1 -2
  171. package/package.json +11 -29
  172. package/pages/__tests__/readme.test.ts +49 -0
  173. package/pages/auth/setup.vue +2 -3
  174. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +76 -0
  175. package/pages/c/_cluster/apps/charts/chart.vue +60 -8
  176. package/pages/c/_cluster/apps/charts/install.vue +10 -7
  177. package/pages/c/_cluster/explorer/__tests__/index.test.ts +23 -25
  178. package/pages/c/_cluster/explorer/index.vue +5 -49
  179. package/pages/c/_cluster/istio/__tests__/istio.index.test.ts +194 -0
  180. package/pages/c/_cluster/istio/index.vue +21 -6
  181. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -0
  182. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +719 -2
  183. package/pages/c/_cluster/uiplugins/index.vue +203 -197
  184. package/pages/diagnostic.vue +13 -17
  185. package/pages/fail-whale.vue +18 -0
  186. package/pages/home.vue +77 -260
  187. package/pages/readme.vue +88 -0
  188. package/plugins/dashboard-store/__tests__/resource-class.test.ts +88 -0
  189. package/plugins/dashboard-store/actions.js +40 -18
  190. package/plugins/dashboard-store/resource-class.js +5 -2
  191. package/plugins/steve/__tests__/subscribe.spec.ts +6 -3
  192. package/plugins/steve/steve-pagination-utils.ts +11 -3
  193. package/plugins/steve/subscribe.js +35 -5
  194. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +10 -4
  195. package/rancher-components/Form/LabeledInput/LabeledInput.vue +7 -52
  196. package/rancher-components/RcButton/RcButton.test.ts +37 -1
  197. package/rancher-components/RcButton/RcButton.vue +38 -8
  198. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +10 -8
  199. package/store/__tests__/catalog.test.ts +115 -1
  200. package/store/__tests__/type-map.test.ts +556 -1
  201. package/store/action-menu.js +8 -3
  202. package/store/auth.js +1 -1
  203. package/store/aws.js +27 -16
  204. package/store/catalog.js +27 -3
  205. package/store/digitalocean.js +20 -38
  206. package/store/index.js +2 -0
  207. package/store/linode.js +25 -40
  208. package/store/pnap.js +1 -0
  209. package/store/type-map.js +111 -29
  210. package/tsconfig.paths.json +8 -8
  211. package/types/kube/kube-api.ts +14 -1
  212. package/types/rancher/steve.api.ts +12 -12
  213. package/types/resources/settings.d.ts +2 -1
  214. package/types/shell/index.d.ts +102 -2
  215. package/types/store/dashboard-store.types.ts +108 -11
  216. package/types/store/pagination.types.ts +6 -3
  217. package/utils/__tests__/alertmanagerconfig.test.ts +117 -0
  218. package/utils/__tests__/async.test.ts +87 -0
  219. package/utils/__tests__/aws.test.ts +140 -0
  220. package/utils/__tests__/banners.test.ts +176 -0
  221. package/utils/__tests__/chart.test.ts +64 -1
  222. package/utils/__tests__/color.test.ts +226 -0
  223. package/utils/__tests__/duration.test.ts +140 -0
  224. package/utils/__tests__/fleet.test.ts +340 -0
  225. package/utils/__tests__/ingress.test.ts +553 -0
  226. package/utils/__tests__/kube.test.ts +68 -0
  227. package/utils/__tests__/namespace-filter.test.ts +109 -0
  228. package/utils/__tests__/pagination-utils.test.ts +361 -0
  229. package/utils/__tests__/parse-externalid.test.ts +137 -0
  230. package/utils/__tests__/perf-setting.utils.test.ts +98 -0
  231. package/utils/__tests__/poller-sequential.test.ts +177 -0
  232. package/utils/__tests__/poller.test.ts +170 -0
  233. package/utils/__tests__/promise.test.ts +346 -0
  234. package/utils/__tests__/settings.test.ts +140 -0
  235. package/utils/__tests__/sort-utils.test.ts +301 -0
  236. package/utils/__tests__/string-utils.test.ts +798 -0
  237. package/utils/__tests__/string.test.ts +23 -1
  238. package/utils/__tests__/style.test.ts +154 -0
  239. package/utils/__tests__/svg-filter.test.ts +184 -0
  240. package/utils/__tests__/units.test.ts +417 -0
  241. package/utils/__tests__/versions.test.ts +128 -0
  242. package/utils/__tests__/xccdf.test.ts +391 -0
  243. package/utils/chart.js +36 -0
  244. package/utils/fleet.ts +13 -3
  245. package/utils/gatekeeper/__tests__/util.test.ts +174 -0
  246. package/utils/gc/__tests__/gc-interval.test.ts +119 -0
  247. package/utils/gc/__tests__/gc-root-store.test.ts +225 -0
  248. package/utils/gc/__tests__/gc-route-changed.test.ts +96 -0
  249. package/utils/gc/__tests__/gc.test.ts +487 -0
  250. package/utils/ingress.ts +9 -1
  251. package/utils/pagination-utils.ts +2 -1
  252. package/utils/string.js +25 -2
  253. package/utils/uiplugins.ts +5 -5
  254. package/utils/validators/__tests__/cluster-name.test.ts +110 -0
  255. package/utils/validators/__tests__/cron-schedule.test.ts +79 -0
  256. package/utils/validators/__tests__/index.test.ts +481 -0
  257. package/utils/validators/__tests__/kubernetes-name.test.ts +163 -0
  258. package/utils/validators/__tests__/misc-validators.test.ts +246 -0
  259. package/utils/validators/__tests__/pod-affinity.test.ts +382 -0
  260. package/utils/validators/__tests__/prometheusrule.test.ts +211 -0
  261. package/utils/validators/__tests__/role-template.test.ts +149 -0
  262. package/utils/validators/__tests__/service.test.ts +283 -0
  263. package/utils/validators/__tests__/setting.test.js +32 -0
  264. package/utils/validators/formRules/__tests__/index.test.ts +50 -0
  265. package/utils/validators/formRules/index.ts +5 -5
  266. package/utils/validators/machine-pool.ts +1 -1
  267. package/utils/validators/setting.js +18 -3
  268. package/utils/xccdf.ts +418 -0
  269. package/assets/fonts/lato/lato-v17-latin-700.woff +0 -0
  270. package/assets/fonts/lato/lato-v17-latin-700.woff2 +0 -0
  271. package/assets/fonts/lato/lato-v17-latin-regular.woff +0 -0
  272. package/assets/fonts/lato/lato-v17-latin-regular.woff2 +0 -0
@@ -1,4 +1,5 @@
1
1
  import { PluginProduct } from '@shell/core/plugin-products';
2
+ import { Plugin } from '@shell/core/plugin';
2
3
  import {
3
4
  ProductMetadata, ProductSinglePage, ProductChildPage,
4
5
  ProductChildGroup, ProductChildCustomPage, ProductChildResourcePage,
@@ -57,9 +58,10 @@ jest.mock('@shell/core/productDebugger', () => ({
57
58
  // Create mock factories
58
59
  function createMockPlugin(): IExtension {
59
60
  return {
60
- _registerTopLevelProduct: jest.fn(),
61
- addRoute: jest.fn(),
62
- DSL: jest.fn((store, productName) => ({
61
+ _registerTopLevelProduct: jest.fn(),
62
+ addRoute: jest.fn(),
63
+ enableServerSidePagination: jest.fn(),
64
+ DSL: jest.fn((store, productName) => ({
63
65
  basicType: jest.fn(),
64
66
  labelGroup: jest.fn(),
65
67
  setGroupDefaultType: jest.fn(),
@@ -68,12 +70,24 @@ function createMockPlugin(): IExtension {
68
70
  configureType: jest.fn(),
69
71
  weightType: jest.fn(),
70
72
  product: jest.fn(),
73
+ headers: jest.fn(),
74
+ hideBulkActions: jest.fn(),
75
+ mapGroup: jest.fn(),
76
+ ignoreGroup: jest.fn(),
77
+ mapType: jest.fn(),
78
+ ignoreType: jest.fn(),
79
+ moveType: jest.fn(),
71
80
  })),
72
81
  } as any;
73
82
  }
74
83
 
75
- function createMockStore(extendableProducts: string[] = Object.values(StandardProductNames)): any {
76
- return { getters: { 'type-map/productByName': (productName: string) => (extendableProducts.includes(productName) ? { name: productName, extendable: true } : undefined) } };
84
+ function createMockStore(extendableProducts: string[] = Object.values(StandardProductNames), managementSchemas: string[] = []): any {
85
+ return {
86
+ getters: {
87
+ 'type-map/productByName': (productName: string) => (extendableProducts.includes(productName) ? { name: productName, extendable: true } : undefined),
88
+ 'management/schemaFor': (type: string) => (managementSchemas.includes(type) ? { id: type } : undefined),
89
+ }
90
+ };
77
91
  }
78
92
 
79
93
  describe('pluginProduct', () => {
@@ -3134,11 +3148,11 @@ describe('pluginProduct', () => {
3134
3148
  weightType: jest.fn(),
3135
3149
  headers: jest.fn(),
3136
3150
  hideBulkActions: jest.fn(),
3137
- spoofedType: jest.fn(),
3138
- mapGroup: jest.fn(),
3139
- ignoreGroup: jest.fn(),
3140
- mapType: jest.fn(),
3141
- ignoreType: jest.fn(),
3151
+
3152
+ mapGroup: jest.fn(),
3153
+ ignoreGroup: jest.fn(),
3154
+ mapType: jest.fn(),
3155
+ ignoreType: jest.fn(),
3142
3156
  };
3143
3157
 
3144
3158
  jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
@@ -3169,11 +3183,11 @@ describe('pluginProduct', () => {
3169
3183
  weightType: jest.fn(),
3170
3184
  headers: jest.fn(),
3171
3185
  hideBulkActions: jest.fn(),
3172
- spoofedType: jest.fn(),
3173
- mapGroup: jest.fn(),
3174
- ignoreGroup: jest.fn(),
3175
- mapType: jest.fn(),
3176
- ignoreType: jest.fn(),
3186
+
3187
+ mapGroup: jest.fn(),
3188
+ ignoreGroup: jest.fn(),
3189
+ mapType: jest.fn(),
3190
+ ignoreType: jest.fn(),
3177
3191
  };
3178
3192
 
3179
3193
  jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
@@ -3200,11 +3214,11 @@ describe('pluginProduct', () => {
3200
3214
  weightType: jest.fn(),
3201
3215
  headers: jest.fn(),
3202
3216
  hideBulkActions: jest.fn(),
3203
- spoofedType: jest.fn(),
3204
- mapGroup: jest.fn(),
3205
- ignoreGroup: jest.fn(),
3206
- mapType: jest.fn(),
3207
- ignoreType: jest.fn(),
3217
+
3218
+ mapGroup: jest.fn(),
3219
+ ignoreGroup: jest.fn(),
3220
+ mapType: jest.fn(),
3221
+ ignoreType: jest.fn(),
3208
3222
  };
3209
3223
 
3210
3224
  jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
@@ -3805,6 +3819,876 @@ describe('pluginProduct', () => {
3805
3819
  // It should NOT be a generic route without the group name
3806
3820
  expect(groupVirtualType[0].route.name).not.toBe('myapp-c-cluster');
3807
3821
  });
3822
+ describe('resource page: headers support', () => {
3823
+ it('should call DSL headers method when resource page has headers configured', () => {
3824
+ const mockPlugin = createMockPlugin();
3825
+ const mockStore = createMockStore();
3826
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
3827
+
3828
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3829
+
3830
+ const productMetadata: ProductMetadata = {
3831
+ name: 'headers-test',
3832
+ label: 'Headers Test',
3833
+ };
3834
+
3835
+ const testHeaders = [
3836
+ {
3837
+ name: 'name', label: 'Name', value: 'metadata.name'
3838
+ },
3839
+ {
3840
+ name: 'status', labelKey: 'tableHeaders.status', value: 'status'
3841
+ },
3842
+ ];
3843
+
3844
+ const config: ProductChildPage[] = [
3845
+ { type: 'some.resource.type', headers: testHeaders },
3846
+ ];
3847
+
3848
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3849
+
3850
+ pluginProduct.apply(mockPlugin, mockStore);
3851
+
3852
+ expect(mockDSL.headers).toHaveBeenCalledWith('some.resource.type', testHeaders, undefined);
3853
+ });
3854
+
3855
+ it('should call DSL headers method with sspHeaders when configured', () => {
3856
+ const mockPlugin = createMockPlugin();
3857
+ const mockStore = createMockStore();
3858
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
3859
+
3860
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3861
+
3862
+ const productMetadata: ProductMetadata = {
3863
+ name: 'ssp-headers-test',
3864
+ label: 'SSP Headers Test',
3865
+ };
3866
+
3867
+ const testSspHeaders = [
3868
+ {
3869
+ name: 'name', label: 'Name', value: 'metadata.name'
3870
+ },
3871
+ {
3872
+ name: 'namespace', labelKey: 'tableHeaders.namespace', value: 'metadata.namespace'
3873
+ },
3874
+ ];
3875
+
3876
+ const config: ProductChildPage[] = [
3877
+ { type: 'some.resource.type', sspHeaders: testSspHeaders },
3878
+ ];
3879
+
3880
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3881
+
3882
+ pluginProduct.apply(mockPlugin, mockStore);
3883
+
3884
+ expect(mockDSL.headers).toHaveBeenCalledWith('some.resource.type', undefined, testSspHeaders);
3885
+ });
3886
+
3887
+ it('should call DSL headers method with both headers and sspHeaders when both are configured', () => {
3888
+ const mockPlugin = createMockPlugin();
3889
+ const mockStore = createMockStore();
3890
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
3891
+
3892
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3893
+
3894
+ const productMetadata: ProductMetadata = {
3895
+ name: 'both-headers-test',
3896
+ label: 'Both Headers Test',
3897
+ };
3898
+
3899
+ const testHeaders = [
3900
+ {
3901
+ name: 'name', label: 'Name', value: 'metadata.name'
3902
+ },
3903
+ ];
3904
+
3905
+ const testSspHeaders = [
3906
+ {
3907
+ name: 'name', label: 'Name', value: 'metadata.name'
3908
+ },
3909
+ ];
3910
+
3911
+ const config: ProductChildPage[] = [
3912
+ {
3913
+ type: 'some.resource.type',
3914
+ headers: testHeaders,
3915
+ sspHeaders: testSspHeaders,
3916
+ },
3917
+ ];
3918
+
3919
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3920
+
3921
+ pluginProduct.apply(mockPlugin, mockStore);
3922
+
3923
+ expect(mockDSL.headers).toHaveBeenCalledWith('some.resource.type', testHeaders, testSspHeaders);
3924
+ });
3925
+
3926
+ it('should not call DSL headers method when resource page has no headers or sspHeaders', () => {
3927
+ const mockPlugin = createMockPlugin();
3928
+ const mockStore = createMockStore();
3929
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
3930
+
3931
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3932
+
3933
+ const productMetadata: ProductMetadata = {
3934
+ name: 'no-headers-test',
3935
+ label: 'No Headers Test',
3936
+ };
3937
+
3938
+ const config: ProductChildPage[] = [{ type: 'some.resource.type' }];
3939
+
3940
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3941
+
3942
+ pluginProduct.apply(mockPlugin, mockStore);
3943
+
3944
+ expect(mockDSL.headers).not.toHaveBeenCalled();
3945
+ });
3946
+ });
3947
+
3948
+ describe('resource page: overrideListResourceName (mapType) support', () => {
3949
+ it('should call DSL mapType when resource page has overrideListResourceName', () => {
3950
+ const mockPlugin = createMockPlugin();
3951
+ const mockStore = createMockStore();
3952
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
3953
+
3954
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3955
+
3956
+ const productMetadata: ProductMetadata = {
3957
+ name: 'maptype-test',
3958
+ label: 'MapType Test',
3959
+ };
3960
+
3961
+ const config: ProductChildPage[] = [
3962
+ { type: 'apps.deployment', overrideListResourceName: 'Deployments' },
3963
+ ];
3964
+
3965
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3966
+
3967
+ pluginProduct.apply(mockPlugin, mockStore);
3968
+
3969
+ expect(mockDSL.mapType).toHaveBeenCalledWith('apps.deployment', 'Deployments');
3970
+ });
3971
+
3972
+ it('should not call mapType when overrideListResourceName is not set', () => {
3973
+ const mockPlugin = createMockPlugin();
3974
+ const mockStore = createMockStore();
3975
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
3976
+
3977
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3978
+
3979
+ const productMetadata: ProductMetadata = {
3980
+ name: 'no-maptype',
3981
+ label: 'No MapType',
3982
+ };
3983
+
3984
+ const config: ProductChildPage[] = [{ type: 'apps.deployment' }];
3985
+
3986
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3987
+
3988
+ pluginProduct.apply(mockPlugin, mockStore);
3989
+
3990
+ expect(mockDSL.mapType).not.toHaveBeenCalled();
3991
+ });
3992
+ });
3993
+
3994
+ describe('resource page: hideFromNav (ignoreType) support', () => {
3995
+ it('should call DSL ignoreType when resource page has hideFromNav set', () => {
3996
+ const mockPlugin = createMockPlugin();
3997
+ const mockStore = createMockStore();
3998
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
3999
+
4000
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4001
+
4002
+ const productMetadata: ProductMetadata = {
4003
+ name: 'ignoretype-test',
4004
+ label: 'IgnoreType Test',
4005
+ };
4006
+
4007
+ const config: ProductChildPage[] = [
4008
+ { type: 'secret.type', hideFromNav: true },
4009
+ ];
4010
+
4011
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4012
+
4013
+ pluginProduct.apply(mockPlugin, mockStore);
4014
+
4015
+ expect(mockDSL.ignoreType).toHaveBeenCalledWith('secret.type');
4016
+ });
4017
+
4018
+ it('should not call ignoreType when hideFromNav is not set', () => {
4019
+ const mockPlugin = createMockPlugin();
4020
+ const mockStore = createMockStore();
4021
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4022
+
4023
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4024
+
4025
+ const productMetadata: ProductMetadata = {
4026
+ name: 'no-ignoretype',
4027
+ label: 'No IgnoreType',
4028
+ };
4029
+
4030
+ const config: ProductChildPage[] = [{ type: 'apps.deployment' }];
4031
+
4032
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4033
+
4034
+ pluginProduct.apply(mockPlugin, mockStore);
4035
+
4036
+ expect(mockDSL.ignoreType).not.toHaveBeenCalled();
4037
+ });
4038
+ });
4039
+
4040
+ describe('resource page: hideBulkActions support', () => {
4041
+ it('should call DSL hideBulkActions when resource page has hideBulkActions set', () => {
4042
+ const mockPlugin = createMockPlugin();
4043
+ const mockStore = createMockStore();
4044
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4045
+
4046
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4047
+
4048
+ const productMetadata: ProductMetadata = {
4049
+ name: 'bulk-actions-test',
4050
+ label: 'Bulk Actions Test',
4051
+ };
4052
+
4053
+ const config: ProductChildPage[] = [
4054
+ { type: 'management.cattle.io.feature', hideBulkActions: true },
4055
+ ];
4056
+
4057
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4058
+
4059
+ pluginProduct.apply(mockPlugin, mockStore);
4060
+
4061
+ expect(mockDSL.hideBulkActions).toHaveBeenCalledWith('management.cattle.io.feature', true);
4062
+ });
4063
+ });
4064
+
4065
+ describe('resource page: ordering - weight is set last', () => {
4066
+ it('should call weightType after configureType for resource pages', () => {
4067
+ const mockPlugin = createMockPlugin();
4068
+ const mockStore = createMockStore();
4069
+ const callOrder: string[] = [];
4070
+ const mockDSL = {
4071
+ product: jest.fn(),
4072
+ basicType: jest.fn(),
4073
+ labelGroup: jest.fn(),
4074
+ setGroupDefaultType: jest.fn(),
4075
+ weightGroup: jest.fn(),
4076
+ virtualType: jest.fn(),
4077
+ configureType: jest.fn(() => callOrder.push('configureType')),
4078
+ weightType: jest.fn(() => callOrder.push('weightType')),
4079
+ headers: jest.fn(() => callOrder.push('headers')),
4080
+ hideBulkActions: jest.fn(() => callOrder.push('hideBulkActions')),
4081
+
4082
+ mapGroup: jest.fn(),
4083
+ ignoreGroup: jest.fn(),
4084
+ mapType: jest.fn(() => callOrder.push('mapType')),
4085
+ ignoreType: jest.fn(() => callOrder.push('ignoreType')),
4086
+ };
4087
+
4088
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4089
+
4090
+ const productMetadata: ProductMetadata = {
4091
+ name: 'order-test',
4092
+ label: 'Order Test',
4093
+ };
4094
+
4095
+ const config: ProductChildPage[] = [
4096
+ {
4097
+ type: 'apps.deployment',
4098
+ weight: 10,
4099
+ headers: [{ name: 'col1', label: 'Col1' }],
4100
+ hideBulkActions: true,
4101
+ overrideListResourceName: 'Deployments',
4102
+ hideFromNav: true,
4103
+ },
4104
+ ];
4105
+
4106
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4107
+
4108
+ pluginProduct.apply(mockPlugin, mockStore);
4109
+
4110
+ // weight must be the last thing set
4111
+ const weightIndex = callOrder.indexOf('weightType');
4112
+ const configureIndex = callOrder.indexOf('configureType');
4113
+
4114
+ expect(weightIndex).toBeGreaterThan(configureIndex);
4115
+ expect(callOrder[callOrder.length - 1]).toBe('weightType');
4116
+ });
4117
+ });
4118
+
4119
+ describe('product-level: renameGroups support', () => {
4120
+ it('should call DSL mapGroup for each renameGroups entry in product metadata', () => {
4121
+ const mockPlugin = createMockPlugin();
4122
+ const mockStore = createMockStore();
4123
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4124
+
4125
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4126
+
4127
+ const productMetadata: ProductMetadata = {
4128
+ name: 'maptogroup-test',
4129
+ label: 'MapToGroup Test',
4130
+ renameGroups: [
4131
+ { groupSelector: /some\.regex/, newName: 'my-group' },
4132
+ { groupSelector: 'exact.match', newName: 'other-group' },
4133
+ ],
4134
+ };
4135
+
4136
+ const config: ProductChildPage[] = [
4137
+ {
4138
+ name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4139
+ },
4140
+ ];
4141
+
4142
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4143
+
4144
+ pluginProduct.apply(mockPlugin, mockStore);
4145
+
4146
+ expect(mockDSL.mapGroup).toHaveBeenCalledTimes(2);
4147
+ expect(mockDSL.mapGroup).toHaveBeenCalledWith(/some\.regex/, 'my-group');
4148
+ expect(mockDSL.mapGroup).toHaveBeenCalledWith('exact.match', 'other-group');
4149
+ });
4150
+
4151
+ it('should not call mapGroup when no renameGroups entries exist', () => {
4152
+ const mockPlugin = createMockPlugin();
4153
+ const mockStore = createMockStore();
4154
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4155
+
4156
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4157
+
4158
+ const productMetadata: ProductMetadata = {
4159
+ name: 'no-maptogroup',
4160
+ label: 'No MapToGroup',
4161
+ };
4162
+
4163
+ const config: ProductChildPage[] = [
4164
+ {
4165
+ name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4166
+ },
4167
+ ];
4168
+
4169
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4170
+
4171
+ pluginProduct.apply(mockPlugin, mockStore);
4172
+
4173
+ expect(mockDSL.mapGroup).not.toHaveBeenCalled();
4174
+ });
4175
+ });
4176
+
4177
+ describe('product-level: ignoreGroups support', () => {
4178
+ it('should call DSL ignoreGroup with callback when condition is provided', () => {
4179
+ const mockPlugin = createMockPlugin();
4180
+ const mockStore = createMockStore();
4181
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4182
+
4183
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4184
+
4185
+ const cbFn = jest.fn(() => true);
4186
+
4187
+ const productMetadata: ProductMetadata = {
4188
+ name: 'ignoregroups-test',
4189
+ label: 'IgnoreGroups Test',
4190
+ ignoreGroups: [
4191
+ { groupSelector: 'hidden-group', condition: cbFn },
4192
+ ],
4193
+ };
4194
+
4195
+ const config: ProductChildPage[] = [
4196
+ {
4197
+ name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4198
+ },
4199
+ ];
4200
+
4201
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4202
+
4203
+ pluginProduct.apply(mockPlugin, mockStore);
4204
+
4205
+ expect(mockDSL.ignoreGroup).toHaveBeenCalledTimes(1);
4206
+ expect(mockDSL.ignoreGroup).toHaveBeenCalledWith('hidden-group', cbFn);
4207
+ });
4208
+
4209
+ it('should call DSL ignoreGroup without callback when condition is not provided (unconditional hide)', () => {
4210
+ const mockPlugin = createMockPlugin();
4211
+ const mockStore = createMockStore();
4212
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4213
+
4214
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4215
+
4216
+ const productMetadata: ProductMetadata = {
4217
+ name: 'ignoregroups-unconditional',
4218
+ label: 'IgnoreGroups Unconditional',
4219
+ ignoreGroups: [
4220
+ { groupSelector: 'always-hidden' },
4221
+ ],
4222
+ };
4223
+
4224
+ const config: ProductChildPage[] = [
4225
+ {
4226
+ name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4227
+ },
4228
+ ];
4229
+
4230
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4231
+
4232
+ pluginProduct.apply(mockPlugin, mockStore);
4233
+
4234
+ expect(mockDSL.ignoreGroup).toHaveBeenCalledTimes(1);
4235
+ expect(mockDSL.ignoreGroup).toHaveBeenCalledWith('always-hidden');
4236
+ });
4237
+
4238
+ it('should support regex patterns in ignoreGroups', () => {
4239
+ const mockPlugin = createMockPlugin();
4240
+ const mockStore = createMockStore();
4241
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4242
+
4243
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4244
+
4245
+ const productMetadata: ProductMetadata = {
4246
+ name: 'ignoregroups-regex',
4247
+ label: 'IgnoreGroups Regex',
4248
+ ignoreGroups: [
4249
+ { groupSelector: /^internal-.*/ },
4250
+ ],
4251
+ };
4252
+
4253
+ const config: ProductChildPage[] = [
4254
+ {
4255
+ name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4256
+ },
4257
+ ];
4258
+
4259
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4260
+
4261
+ pluginProduct.apply(mockPlugin, mockStore);
4262
+
4263
+ expect(mockDSL.ignoreGroup).toHaveBeenCalledTimes(1);
4264
+ expect(mockDSL.ignoreGroup).toHaveBeenCalledWith(/^internal-.*/);
4265
+ });
4266
+
4267
+ it('should not call ignoreGroup when no ignoreGroups entries exist', () => {
4268
+ const mockPlugin = createMockPlugin();
4269
+ const mockStore = createMockStore();
4270
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4271
+
4272
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4273
+
4274
+ const productMetadata: ProductMetadata = {
4275
+ name: 'no-ignoregroups',
4276
+ label: 'No IgnoreGroups',
4277
+ };
4278
+
4279
+ const config: ProductChildPage[] = [
4280
+ {
4281
+ name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4282
+ },
4283
+ ];
4284
+
4285
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4286
+
4287
+ pluginProduct.apply(mockPlugin, mockStore);
4288
+
4289
+ expect(mockDSL.ignoreGroup).not.toHaveBeenCalled();
4290
+ });
4291
+ });
4292
+
4293
+ describe('product-level DSL options are not called when extending', () => {
4294
+ it('should not call mapGroup, ignoreGroup, or moveType when extending an existing product', () => {
4295
+ const mockPlugin = createMockPlugin();
4296
+ const mockStore = createMockStore();
4297
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4298
+
4299
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4300
+
4301
+ const config: ProductChildPage[] = [
4302
+ {
4303
+ name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4304
+ },
4305
+ ];
4306
+
4307
+ const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, config);
4308
+
4309
+ pluginProduct.apply(mockPlugin, mockStore);
4310
+
4311
+ expect(mockDSL.mapGroup).not.toHaveBeenCalled();
4312
+ expect(mockDSL.ignoreGroup).not.toHaveBeenCalled();
4313
+ expect(mockDSL.moveType).not.toHaveBeenCalled();
4314
+ });
4315
+ });
4316
+
4317
+ describe('product-level: moveToGroup support', () => {
4318
+ it('should call basicType and moveType to move a resource type into a group', () => {
4319
+ const mockPlugin = createMockPlugin();
4320
+ const mockStore = createMockStore();
4321
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4322
+
4323
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4324
+
4325
+ const monitoringGroup: ProductChildGroup = {
4326
+ name: 'monitoring',
4327
+ label: 'Monitoring',
4328
+ children: [
4329
+ {
4330
+ name: 'alerts', label: 'Alerts', component: { name: 'AlertsPage' }
4331
+ },
4332
+ ],
4333
+ };
4334
+
4335
+ const productMetadata: ProductMetadata = {
4336
+ name: 'my-app',
4337
+ label: 'My App',
4338
+ moveToGroup: [
4339
+ { entryId: 'pod', groupName: 'monitoring' },
4340
+ ],
4341
+ };
4342
+
4343
+ const config: ProductChild[] = [
4344
+ monitoringGroup,
4345
+ { type: 'pod' },
4346
+ ];
4347
+
4348
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4349
+
4350
+ pluginProduct.apply(mockPlugin, mockStore);
4351
+
4352
+ // basicType re-registers the page under the resolved group for the nav tree
4353
+ expect(mockDSL.basicType).toHaveBeenCalledWith(['pod'], 'myapp-monitoring');
4354
+ // moveType also called for resource types (non-basic view modes)
4355
+ expect(mockDSL.moveType).toHaveBeenCalledTimes(1);
4356
+ expect(mockDSL.moveType).toHaveBeenCalledWith('pod', 'myapp-monitoring', undefined);
4357
+ });
4358
+
4359
+ it('should call basicType but NOT moveType when moving a custom page into a group', () => {
4360
+ const mockPlugin = createMockPlugin();
4361
+ const mockStore = createMockStore();
4362
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4363
+
4364
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4365
+
4366
+ const monitoringGroup: ProductChildGroup = {
4367
+ name: 'monitoring',
4368
+ label: 'Monitoring',
4369
+ children: [
4370
+ {
4371
+ name: 'alerts', label: 'Alerts', component: { name: 'AlertsPage' }
4372
+ },
4373
+ ],
4374
+ };
4375
+
4376
+ const customPage: ProductChildCustomPage = {
4377
+ name: 'dashboard', label: 'Dashboard', component: { name: 'DashboardPage' }
4378
+ };
4379
+
4380
+ const productMetadata: ProductMetadata = {
4381
+ name: 'my-app',
4382
+ label: 'My App',
4383
+ moveToGroup: [
4384
+ { entryId: 'dashboard', groupName: 'monitoring' },
4385
+ ],
4386
+ };
4387
+
4388
+ const config: ProductChild[] = [monitoringGroup, customPage];
4389
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4390
+
4391
+ pluginProduct.apply(mockPlugin, mockStore);
4392
+
4393
+ // basicType re-registers the custom page under the resolved group
4394
+ expect(mockDSL.basicType).toHaveBeenCalledWith(['myapp-dashboard'], 'myapp-monitoring');
4395
+ // moveType is NOT called for custom pages (no schema to match against)
4396
+ expect(mockDSL.moveType).not.toHaveBeenCalled();
4397
+ });
4398
+
4399
+ it('should pass weight to DSL moveType when specified', () => {
4400
+ const mockPlugin = createMockPlugin();
4401
+ const mockStore = createMockStore();
4402
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4403
+
4404
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4405
+
4406
+ const myGroup: ProductChildGroup = {
4407
+ name: 'resources',
4408
+ label: 'Resources',
4409
+ children: [
4410
+ {
4411
+ name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4412
+ },
4413
+ ],
4414
+ };
4415
+
4416
+ const productMetadata: ProductMetadata = {
4417
+ name: 'my-app',
4418
+ label: 'My App',
4419
+ moveToGroup: [
4420
+ {
4421
+ entryId: 'apps.deployment', groupName: 'resources', weight: 10
4422
+ },
4423
+ ],
4424
+ };
4425
+
4426
+ const config: ProductChild[] = [myGroup, { type: 'apps.deployment' }];
4427
+
4428
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4429
+
4430
+ pluginProduct.apply(mockPlugin, mockStore);
4431
+
4432
+ expect(mockDSL.basicType).toHaveBeenCalledWith(['apps.deployment'], 'myapp-resources');
4433
+ expect(mockDSL.moveType).toHaveBeenCalledWith('apps.deployment', 'myapp-resources', 10);
4434
+ });
4435
+
4436
+ it('should throw when moveToGroup references a groupName that does not exist in the config', () => {
4437
+ const mockPlugin = createMockPlugin();
4438
+ const mockStore = createMockStore();
4439
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4440
+
4441
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4442
+
4443
+ const productMetadata: ProductMetadata = {
4444
+ name: 'my-app',
4445
+ label: 'My App',
4446
+ moveToGroup: [
4447
+ { entryId: 'pod', groupName: 'nonexistent-group' },
4448
+ ],
4449
+ };
4450
+
4451
+ const config: ProductChildPage[] = [
4452
+ { type: 'pod' },
4453
+ ];
4454
+
4455
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4456
+
4457
+ expect(() => {
4458
+ pluginProduct.apply(mockPlugin, mockStore);
4459
+ }).toThrow('moveToGroup target group "nonexistent-group" not found');
4460
+ });
4461
+
4462
+ it('should throw when moveToGroup entryId does not match any registered page', () => {
4463
+ const mockPlugin = createMockPlugin();
4464
+ const mockStore = createMockStore();
4465
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4466
+
4467
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4468
+
4469
+ const myGroup: ProductChildGroup = {
4470
+ name: 'monitoring',
4471
+ label: 'Monitoring',
4472
+ children: [
4473
+ {
4474
+ name: 'alerts', label: 'Alerts', component: { name: 'AlertsPage' }
4475
+ },
4476
+ ],
4477
+ };
4478
+
4479
+ const productMetadata: ProductMetadata = {
4480
+ name: 'my-app',
4481
+ label: 'My App',
4482
+ moveToGroup: [
4483
+ { entryId: 'nonexistent-page', groupName: 'monitoring' },
4484
+ ],
4485
+ };
4486
+
4487
+ const config: ProductChild[] = [myGroup];
4488
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4489
+
4490
+ expect(() => {
4491
+ pluginProduct.apply(mockPlugin, mockStore);
4492
+ }).toThrow('moveToGroup entryId "nonexistent-page" not found');
4493
+ });
4494
+
4495
+ it('should not call moveType when no moveToGroup entries exist', () => {
4496
+ const mockPlugin = createMockPlugin();
4497
+ const mockStore = createMockStore();
4498
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4499
+
4500
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4501
+
4502
+ const productMetadata: ProductMetadata = {
4503
+ name: 'no-move',
4504
+ label: 'No Move',
4505
+ };
4506
+
4507
+ const config: ProductChildPage[] = [
4508
+ {
4509
+ name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4510
+ },
4511
+ ];
4512
+
4513
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4514
+
4515
+ pluginProduct.apply(mockPlugin, mockStore);
4516
+
4517
+ expect(mockDSL.moveType).not.toHaveBeenCalled();
4518
+ });
4519
+
4520
+ it('should support multiple moveToGroup entries targeting different groups', () => {
4521
+ const mockPlugin = createMockPlugin();
4522
+ const mockStore = createMockStore();
4523
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4524
+
4525
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4526
+
4527
+ const networkingGroup: ProductChildGroup = {
4528
+ name: 'networking',
4529
+ label: 'Networking',
4530
+ children: [
4531
+ {
4532
+ name: 'net-overview', label: 'Overview', component: { name: 'NetOverview' }
4533
+ },
4534
+ ],
4535
+ };
4536
+
4537
+ const storageGroup: ProductChildGroup = {
4538
+ name: 'storage',
4539
+ label: 'Storage',
4540
+ children: [
4541
+ {
4542
+ name: 'storage-overview', label: 'Overview', component: { name: 'StorageOverview' }
4543
+ },
4544
+ ],
4545
+ };
4546
+
4547
+ const productMetadata: ProductMetadata = {
4548
+ name: 'my-app',
4549
+ label: 'My App',
4550
+ moveToGroup: [
4551
+ { entryId: 'networking.ingress', groupName: 'networking' },
4552
+ { entryId: 'storage.pvc', groupName: 'storage' },
4553
+ ],
4554
+ };
4555
+
4556
+ const config: ProductChild[] = [
4557
+ networkingGroup,
4558
+ storageGroup,
4559
+ { type: 'networking.ingress' },
4560
+ { type: 'storage.pvc' },
4561
+ ];
4562
+
4563
+ const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4564
+
4565
+ pluginProduct.apply(mockPlugin, mockStore);
4566
+
4567
+ expect(mockDSL.basicType).toHaveBeenCalledWith(['networking.ingress'], 'myapp-networking');
4568
+ expect(mockDSL.basicType).toHaveBeenCalledWith(['storage.pvc'], 'myapp-storage');
4569
+ expect(mockDSL.moveType).toHaveBeenCalledTimes(2);
4570
+ expect(mockDSL.moveType).toHaveBeenCalledWith('networking.ingress', 'myapp-networking', undefined);
4571
+ expect(mockDSL.moveType).toHaveBeenCalledWith('storage.pvc', 'myapp-storage', undefined);
4572
+ });
4573
+ });
4574
+
4575
+ describe('resource page DSL options work when extending a product', () => {
4576
+ it('should support headers, hideBulkActions, overrideListResourceName, hideFromNav on resource pages when extending', () => {
4577
+ const mockPlugin = createMockPlugin();
4578
+ const mockStore = createMockStore();
4579
+ const mockDSL = (mockPlugin.DSL as jest.Mock)();
4580
+
4581
+ (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4582
+
4583
+ const testHeaders = [{ name: 'col1', label: 'Column 1' }];
4584
+
4585
+ const config: ProductChildPage[] = [
4586
+ {
4587
+ type: 'custom.resource.type',
4588
+ headers: testHeaders,
4589
+ hideBulkActions: true,
4590
+ overrideListResourceName: 'Custom Name',
4591
+ hideFromNav: true,
4592
+ },
4593
+ ];
4594
+
4595
+ const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, config);
4596
+
4597
+ pluginProduct.apply(mockPlugin, mockStore);
4598
+
4599
+ expect(mockDSL.headers).toHaveBeenCalledWith('custom.resource.type', testHeaders, undefined);
4600
+ expect(mockDSL.hideBulkActions).toHaveBeenCalledWith('custom.resource.type', true);
4601
+ expect(mockDSL.mapType).toHaveBeenCalledWith('custom.resource.type', 'Custom Name');
4602
+ expect(mockDSL.ignoreType).toHaveBeenCalledWith('custom.resource.type');
4603
+ });
4604
+ });
4605
+ });
4606
+ });
4607
+
4608
+ describe('addProduct duplicate guard', () => {
4609
+ it('should throw when addProduct is called twice with the same product name (object form)', () => {
4610
+ const plugin = new Plugin('test-extension');
4611
+
4612
+ const product: ProductMetadata = {
4613
+ name: 'my-product',
4614
+ label: 'My Product',
4615
+ };
4616
+
4617
+ const config: ProductChildPage[] = [
4618
+ {
4619
+ name: 'page-a', label: 'Page A', component: { name: 'PageA' }
4620
+ },
4621
+ ];
4622
+
4623
+ plugin.addProduct(product, config);
4624
+
4625
+ expect(() => {
4626
+ plugin.addProduct(product, [{
4627
+ name: 'page-b', label: 'Page B', component: { name: 'PageB' }
4628
+ }]);
4629
+ }).toThrow('addProduct can only be called once per product');
4630
+ });
4631
+
4632
+ it('should throw when addProduct is called twice with the same product name (string form)', () => {
4633
+ const plugin = new Plugin('test-extension');
4634
+
4635
+ plugin.addProduct('my-product');
4636
+
4637
+ expect(() => {
4638
+ plugin.addProduct('my-product');
4639
+ }).toThrow('addProduct can only be called once per product');
4640
+ });
4641
+
4642
+ it('should throw when addProduct is called twice mixing string and object form for the same name', () => {
4643
+ const plugin = new Plugin('test-extension');
4644
+
4645
+ plugin.addProduct('my-product');
4646
+
4647
+ expect(() => {
4648
+ plugin.addProduct({ name: 'my-product', label: 'My Product' }, []);
4649
+ }).toThrow('addProduct can only be called once per product');
4650
+ });
4651
+
4652
+ it('should allow addProduct for different product names', () => {
4653
+ const plugin = new Plugin('test-extension');
4654
+
4655
+ plugin.addProduct('product-a');
4656
+
4657
+ expect(() => {
4658
+ plugin.addProduct('product-b');
4659
+ }).not.toThrow();
4660
+
4661
+ expect(plugin.productConfigs).toHaveLength(2);
4662
+ });
4663
+
4664
+ it('should allow addProduct and extendProduct for the same name (extending is separate)', () => {
4665
+ const plugin = new Plugin('test-extension');
4666
+
4667
+ plugin.addProduct('my-product');
4668
+
4669
+ expect(() => {
4670
+ plugin.extendProduct('explorer', [{
4671
+ name: 'extra-page', label: 'Extra', component: { name: 'Extra' }
4672
+ }]);
4673
+ }).not.toThrow();
4674
+
4675
+ expect(plugin.productConfigs).toHaveLength(2);
4676
+ });
4677
+
4678
+ it('should throw when addProduct is called twice with single page product form', () => {
4679
+ const plugin = new Plugin('test-extension');
4680
+
4681
+ const singlePage: ProductSinglePage = {
4682
+ name: 'my-dashboard',
4683
+ label: 'My Dashboard',
4684
+ component: { name: 'DashboardPage' },
4685
+ };
4686
+
4687
+ plugin.addProduct(singlePage);
4688
+
4689
+ expect(() => {
4690
+ plugin.addProduct(singlePage);
4691
+ }).toThrow('addProduct can only be called once per product');
3808
4692
  });
3809
4693
  });
3810
4694
  });