@rancher/shell 3.0.12-rc.1 → 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 (376) 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/images/providers/entraid-black.svg +4 -0
  54. package/assets/images/providers/entraid.svg +9 -0
  55. package/assets/images/vendor/entraid.svg +9 -0
  56. package/assets/styles/app.scss +0 -1
  57. package/assets/styles/base/_variables.scss +2 -0
  58. package/assets/styles/fonts/_fontstack.scss +132 -8
  59. package/assets/translations/en-us.yaml +41 -22
  60. package/assets/translations/zh-hans.yaml +4 -8
  61. package/chart/__tests__/S3.test.ts +10 -3
  62. package/chart/monitoring/index.vue +10 -1
  63. package/components/ActionDropdownShell.vue +2 -1
  64. package/components/CountBox.vue +20 -0
  65. package/components/CreateDriver.vue +0 -12
  66. package/components/CruResourceFooter.vue +9 -5
  67. package/components/DetailText.vue +12 -3
  68. package/components/ExplorerProjectsNamespaces.vue +1 -1
  69. package/components/InstallHelmCharts.vue +2 -2
  70. package/components/LandingPagePreference.vue +14 -5
  71. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +15 -1
  72. package/components/Resource/Detail/Metadata/index.vue +6 -0
  73. package/components/Resource/Detail/ResourcePopover/index.vue +12 -1
  74. package/components/Resource/Detail/SpacedRow.vue +3 -1
  75. package/components/Resource/Detail/TitleBar/index.vue +10 -11
  76. package/components/ResourceList/Masthead.vue +12 -8
  77. package/components/SelectIconGrid.vue +5 -10
  78. package/components/SingleClusterInfo.vue +1 -0
  79. package/components/SortableTable/__tests__/sorting.test.ts +126 -0
  80. package/components/SortableTable/index.vue +6 -9
  81. package/components/SortableTable/selection.js +23 -5
  82. package/components/SortableTable/sorting.js +6 -3
  83. package/components/Wizard.vue +14 -13
  84. package/components/__tests__/CountBox.test.ts +72 -0
  85. package/components/__tests__/DetailText.test.ts +113 -0
  86. package/components/fleet/FleetBundles.vue +100 -12
  87. package/components/fleet/FleetClusterTargets/index.vue +54 -15
  88. package/components/fleet/__tests__/FleetClusterTargets.test.ts +149 -115
  89. package/components/fleet/__tests__/FleetClusters.test.ts +12 -12
  90. package/components/form/InputWithSelect.vue +18 -10
  91. package/components/form/KeyValue.vue +17 -1
  92. package/components/form/LabeledSelect.vue +101 -26
  93. package/components/form/NameNsDescription.vue +11 -0
  94. package/components/form/Security.vue +6 -2
  95. package/components/form/Select.vue +73 -56
  96. package/components/form/ServiceNameSelect.vue +13 -11
  97. package/components/form/WorkloadPorts.vue +2 -7
  98. package/components/form/__tests__/KeyValue.test.ts +66 -0
  99. package/components/form/__tests__/NodeScheduling.test.ts +9 -0
  100. package/components/form/__tests__/Security.test.ts +76 -0
  101. package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
  102. package/components/formatter/Autoscaler.vue +4 -4
  103. package/components/formatter/ClusterKubeVersion.vue +27 -0
  104. package/components/formatter/ClusterLink.vue +1 -7
  105. package/components/formatter/ClusterProvider.vue +6 -10
  106. package/components/formatter/FleetSummaryGraph.vue +0 -3
  107. package/components/formatter/MachineSummaryGraph.vue +1 -1
  108. package/components/formatter/PodsUsage.vue +2 -2
  109. package/components/formatter/__tests__/Autoscaler.test.ts +19 -22
  110. package/components/formatter/__tests__/FleetSummaryGraph.test.ts +216 -0
  111. package/components/formatter/__tests__/PodsUsage.test.ts +6 -10
  112. package/components/nav/Group.vue +7 -6
  113. package/components/nav/Header.vue +24 -3
  114. package/components/nav/NamespaceFilter.vue +2 -2
  115. package/components/nav/NotificationCenter/Notification.vue +4 -1
  116. package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
  117. package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
  118. package/components/nav/TopLevelMenu.helper.ts +15 -3
  119. package/components/nav/TopLevelMenu.vue +16 -5
  120. package/components/nav/Type.vue +8 -7
  121. package/components/nav/WindowManager/index.vue +2 -1
  122. package/components/nav/WorkspaceSwitcher.vue +13 -0
  123. package/components/nav/__tests__/Group.test.ts +67 -0
  124. package/components/nav/__tests__/Header.test.ts +235 -0
  125. package/components/nav/__tests__/TopLevelMenu.test.ts +145 -21
  126. package/components/nav/__tests__/Type.test.ts +20 -3
  127. package/components/templates/default.vue +34 -4
  128. package/components/templates/home.vue +30 -25
  129. package/components/templates/plain.vue +31 -26
  130. package/components/templates/standalone.vue +17 -0
  131. package/composables/useFormValidation.ts +93 -0
  132. package/composables/useLabeledFormElement.ts +10 -2
  133. package/composables/useLabeledSelect.ts +60 -0
  134. package/composables/useUserRetentionValidation.ts +1 -49
  135. package/composables/useVeeValidateField.test.ts +159 -0
  136. package/composables/useVeeValidateField.ts +67 -0
  137. package/config/cookies.js +0 -1
  138. package/config/labels-annotations.js +1 -0
  139. package/config/pagination-table-headers.js +18 -1
  140. package/config/product/manager.js +82 -21
  141. package/config/query-params.js +1 -0
  142. package/config/router/routes.js +6 -8
  143. package/config/table-headers.js +20 -1
  144. package/config/types.js +2 -1
  145. package/core/__tests__/plugin-products.test.ts +1505 -30
  146. package/core/plugin-products-base.ts +137 -20
  147. package/core/plugin-products-helpers.ts +5 -4
  148. package/core/plugin-products.ts +4 -0
  149. package/core/plugin-types.ts +129 -4
  150. package/core/plugin.ts +15 -7
  151. package/core/productDebugger.js +9 -4
  152. package/core/types-provisioning.ts +43 -30
  153. package/core/types.ts +58 -19
  154. package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
  155. package/detail/__tests__/pod.test.ts +41 -0
  156. package/detail/harvesterhci.io.management.cluster.vue +6 -2
  157. package/detail/management.cattle.io.fleetworkspace.vue +49 -0
  158. package/detail/pod.vue +1 -1
  159. package/detail/provisioning.cattle.io.cluster.vue +4 -10
  160. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
  161. package/edit/__tests__/kontainerDriver.test.ts +0 -13
  162. package/edit/__tests__/nodeDriver.test.ts +5 -11
  163. package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
  164. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  165. package/edit/auth/__tests__/azuread.test.ts +217 -34
  166. package/edit/auth/__tests__/oidc.test.ts +54 -0
  167. package/edit/auth/azuread.vue +123 -15
  168. package/edit/auth/oidc.vue +10 -2
  169. package/edit/kontainerDriver.vue +1 -2
  170. package/edit/networking.k8s.io.ingress/DefaultBackend.vue +13 -4
  171. package/edit/networking.k8s.io.ingress/RulePath.vue +8 -4
  172. package/edit/networking.k8s.io.ingress/index.vue +75 -20
  173. package/edit/nodeDriver.vue +0 -2
  174. package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
  175. package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
  176. package/edit/provisioning.cattle.io.cluster/__tests__/MachinePool.test.ts +104 -0
  177. package/edit/provisioning.cattle.io.cluster/index.vue +81 -106
  178. package/edit/provisioning.cattle.io.cluster/rke2.vue +8 -4
  179. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  180. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +37 -4
  181. package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +132 -7
  182. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -1
  183. package/edit/secret/__tests__/ssh.test.ts +5 -6
  184. package/edit/secret/basic.vue +31 -0
  185. package/edit/secret/index.vue +68 -17
  186. package/edit/secret/registry.vue +38 -0
  187. package/edit/secret/ssh.vue +29 -0
  188. package/edit/secret/tls.vue +30 -0
  189. package/edit/service.vue +4 -4
  190. package/edit/workload/Upgrading.vue +3 -3
  191. package/edit/workload/__tests__/Upgrading.test.ts +6 -9
  192. package/edit/workload/mixins/workload.js +2 -1
  193. package/initialize/App.vue +29 -2
  194. package/initialize/install-plugins.js +0 -2
  195. package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
  196. package/list/catalog.cattle.io.app.vue +25 -5
  197. package/list/fleet.cattle.io.bundle.vue +7 -104
  198. package/list/fleet.cattle.io.clusterregistrationtoken.vue +20 -0
  199. package/list/management.cattle.io.feature.vue +1 -1
  200. package/list/management.cattle.io.fleetworkspace.vue +8 -0
  201. package/list/provisioning.cattle.io.cluster.vue +262 -180
  202. package/list/utils/management.cattle.io.cluster.utils.ts +128 -0
  203. package/machine-config/amazonec2.vue +1 -0
  204. package/mixins/__tests__/chart.test.ts +112 -0
  205. package/mixins/brand.js +2 -1
  206. package/mixins/chart.js +50 -15
  207. package/mixins/resource-fetch-api-pagination.js +41 -5
  208. package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
  209. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
  210. package/models/__tests__/chart.test.ts +99 -6
  211. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +67 -67
  212. package/models/__tests__/management.cattle.io.cluster.test.ts +1 -1
  213. package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
  214. package/models/__tests__/management.cattle.io.node.ts +6 -5
  215. package/models/__tests__/management.cattle.io.nodepool.ts +5 -4
  216. package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
  217. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +32 -11
  218. package/models/base-cluster.x-k8s.io.js +26 -0
  219. package/models/catalog.cattle.io.app.js +21 -17
  220. package/models/catalog.cattle.io.clusterrepo.js +39 -11
  221. package/models/chart.js +33 -19
  222. package/models/cluster.js +1 -1
  223. package/models/cluster.x-k8s.io.machine.js +4 -22
  224. package/models/cluster.x-k8s.io.machinedeployment.js +2 -20
  225. package/models/cluster.x-k8s.io.machineset.js +2 -20
  226. package/models/compliance.cattle.io.clusterscan.js +130 -2
  227. package/models/ext.cattle.io.kubeconfig.ts +4 -7
  228. package/models/fleet-application.js +4 -2
  229. package/models/fleet.cattle.io.bundle.js +1 -1
  230. package/models/kontainerdriver.js +11 -0
  231. package/models/management.cattle.io.authconfig.js +5 -1
  232. package/models/management.cattle.io.cluster.js +402 -78
  233. package/models/management.cattle.io.feature.js +3 -3
  234. package/models/management.cattle.io.kontainerdriver.js +1 -26
  235. package/models/management.cattle.io.node.js +6 -4
  236. package/models/management.cattle.io.nodepool.js +1 -1
  237. package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
  238. package/models/networking.k8s.io.ingress.js +12 -4
  239. package/models/nodedriver.js +7 -0
  240. package/models/provisioning.cattle.io.cluster.js +47 -330
  241. package/models/rke.cattle.io.etcdsnapshot.js +1 -2
  242. package/package.json +20 -37
  243. package/pages/__tests__/readme.test.ts +49 -0
  244. package/pages/auth/setup.vue +2 -3
  245. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +265 -0
  246. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
  247. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
  248. package/pages/c/_cluster/apps/charts/chart.vue +275 -39
  249. package/pages/c/_cluster/apps/charts/index.vue +2 -2
  250. package/pages/c/_cluster/apps/charts/install.vue +18 -10
  251. package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
  252. package/pages/c/_cluster/explorer/__tests__/index.test.ts +23 -25
  253. package/pages/c/_cluster/explorer/index.vue +5 -49
  254. package/pages/c/_cluster/istio/__tests__/istio.index.test.ts +194 -0
  255. package/pages/c/_cluster/istio/index.vue +21 -6
  256. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
  257. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +40 -2
  258. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
  259. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +735 -13
  260. package/pages/c/_cluster/uiplugins/index.vue +226 -222
  261. package/pages/diagnostic.vue +13 -17
  262. package/pages/fail-whale.vue +18 -0
  263. package/pages/home.vue +77 -260
  264. package/pages/readme.vue +88 -0
  265. package/plugins/dashboard-store/__tests__/resource-class.test.ts +88 -0
  266. package/plugins/dashboard-store/actions.js +40 -18
  267. package/plugins/dashboard-store/resource-class.js +5 -2
  268. package/plugins/steve/__tests__/subscribe.spec.ts +6 -3
  269. package/plugins/steve/steve-pagination-utils.ts +11 -3
  270. package/plugins/steve/subscribe.js +35 -5
  271. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +211 -1
  272. package/rancher-components/Form/LabeledInput/LabeledInput.vue +37 -4
  273. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
  274. package/rancher-components/RcButton/RcButton.test.ts +37 -1
  275. package/rancher-components/RcButton/RcButton.vue +38 -8
  276. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +10 -8
  277. package/scripts/test-plugins-build.sh +5 -2
  278. package/server/server-middleware.js +2 -2
  279. package/static/humans.txt +1 -0
  280. package/static/robots.txt +34 -0
  281. package/static/welcome-cow.svg +18 -0
  282. package/store/__tests__/catalog.test.ts +276 -12
  283. package/store/__tests__/type-map.test.ts +556 -1
  284. package/store/action-menu.js +8 -3
  285. package/store/auth.js +1 -4
  286. package/store/aws.js +27 -16
  287. package/store/catalog.js +87 -11
  288. package/store/digitalocean.js +20 -38
  289. package/store/index.js +2 -0
  290. package/store/linode.js +25 -40
  291. package/store/pnap.js +1 -0
  292. package/store/type-map.js +111 -29
  293. package/tsconfig.paths.json +8 -8
  294. package/types/kube/kube-api.ts +14 -1
  295. package/types/rancher/steve.api.ts +12 -12
  296. package/types/resources/settings.d.ts +2 -1
  297. package/types/shell/index.d.ts +128 -24
  298. package/types/store/dashboard-store.types.ts +108 -11
  299. package/types/store/pagination.types.ts +6 -3
  300. package/utils/__tests__/alertmanagerconfig.test.ts +117 -0
  301. package/utils/__tests__/async.test.ts +87 -0
  302. package/utils/__tests__/aws.test.ts +140 -0
  303. package/utils/__tests__/banners.test.ts +176 -0
  304. package/utils/__tests__/chart.test.ts +64 -1
  305. package/utils/__tests__/color.test.ts +226 -0
  306. package/utils/__tests__/duration.test.ts +140 -0
  307. package/utils/__tests__/fleet.test.ts +340 -0
  308. package/utils/__tests__/git.test.ts +270 -0
  309. package/utils/__tests__/inactivity.test.ts +316 -0
  310. package/utils/__tests__/ingress.test.ts +553 -0
  311. package/utils/__tests__/kube.test.ts +68 -0
  312. package/utils/__tests__/namespace-filter.test.ts +109 -0
  313. package/utils/__tests__/object.test.ts +77 -0
  314. package/utils/__tests__/pagination-utils.test.ts +361 -0
  315. package/utils/__tests__/parse-externalid.test.ts +137 -0
  316. package/utils/__tests__/perf-setting.utils.test.ts +98 -0
  317. package/utils/__tests__/poller-sequential.test.ts +177 -0
  318. package/utils/__tests__/poller.test.ts +170 -0
  319. package/utils/__tests__/promise.test.ts +346 -0
  320. package/utils/__tests__/settings.test.ts +140 -0
  321. package/utils/__tests__/sort-utils.test.ts +301 -0
  322. package/utils/__tests__/string-utils.test.ts +798 -0
  323. package/utils/__tests__/string.test.ts +23 -1
  324. package/utils/__tests__/style.test.ts +154 -0
  325. package/utils/__tests__/svg-filter.test.ts +184 -0
  326. package/utils/__tests__/time.test.ts +14 -1
  327. package/utils/__tests__/units.test.ts +417 -0
  328. package/utils/__tests__/url.test.ts +246 -0
  329. package/utils/__tests__/versions.test.ts +128 -0
  330. package/utils/__tests__/xccdf.test.ts +391 -0
  331. package/utils/chart.js +36 -0
  332. package/utils/fleet.ts +13 -3
  333. package/utils/gatekeeper/__tests__/util.test.ts +174 -0
  334. package/utils/gc/__tests__/gc-interval.test.ts +119 -0
  335. package/utils/gc/__tests__/gc-root-store.test.ts +225 -0
  336. package/utils/gc/__tests__/gc-route-changed.test.ts +96 -0
  337. package/utils/gc/__tests__/gc.test.ts +487 -0
  338. package/utils/ingress.ts +9 -1
  339. package/utils/object.js +33 -2
  340. package/utils/pagination-utils.ts +2 -1
  341. package/utils/string.js +25 -2
  342. package/utils/time.ts +5 -0
  343. package/utils/uiplugins.ts +5 -5
  344. package/utils/validators/__tests__/cluster-name.test.ts +110 -0
  345. package/utils/validators/__tests__/cron-schedule.test.ts +79 -0
  346. package/utils/validators/__tests__/index.test.ts +481 -0
  347. package/utils/validators/__tests__/kubernetes-name.test.ts +163 -0
  348. package/utils/validators/__tests__/misc-validators.test.ts +246 -0
  349. package/utils/validators/__tests__/pod-affinity.test.ts +382 -0
  350. package/utils/validators/__tests__/prometheusrule.test.ts +211 -0
  351. package/utils/validators/__tests__/role-template.test.ts +149 -0
  352. package/utils/validators/__tests__/service.test.ts +283 -0
  353. package/utils/validators/__tests__/setting.test.js +32 -0
  354. package/utils/validators/formRules/__tests__/index.test.ts +50 -0
  355. package/utils/validators/formRules/index.ts +5 -5
  356. package/utils/validators/machine-pool.ts +1 -1
  357. package/utils/validators/setting.js +18 -3
  358. package/utils/xccdf.ts +418 -0
  359. package/vue.config.js +0 -9
  360. package/assets/fonts/lato/lato-v17-latin-700.woff +0 -0
  361. package/assets/fonts/lato/lato-v17-latin-700.woff2 +0 -0
  362. package/assets/fonts/lato/lato-v17-latin-regular.woff +0 -0
  363. package/assets/fonts/lato/lato-v17-latin-regular.woff2 +0 -0
  364. package/assets/images/providers/azuread-black.svg +0 -22
  365. package/assets/images/providers/azuread.svg +0 -25
  366. package/assets/images/vendor/azuread.svg +0 -18
  367. package/assets/styles/fonts/_dots.scss +0 -18
  368. package/components/EmberPage.vue +0 -622
  369. package/components/EmberPageView.vue +0 -39
  370. package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
  371. package/mixins/labeled-form-element.ts +0 -225
  372. package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
  373. package/pages/c/_cluster/manager/pages/_page.vue +0 -22
  374. package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
  375. package/plugins/ember-cookie.js +0 -17
  376. package/utils/ember-page.js +0 -30
@@ -214,6 +214,72 @@ describe('chartMixin', () => {
214
214
  id: 'custom-ns/custom-name'
215
215
  });
216
216
  });
217
+
218
+ it('should fall back to matchingInstalledApps when version namespace lookup fails', async() => {
219
+ const installedApp = {
220
+ id: 'other-ns/custom-name',
221
+ spec: { chart: { metadata: { version: '0.9.0' } } }
222
+ };
223
+
224
+ const mockStore = {
225
+ dispatch: jest.fn((action, payload) => {
226
+ if (action === 'cluster/find') {
227
+ return Promise.reject(new Error('not found'));
228
+ }
229
+
230
+ return Promise.resolve();
231
+ }),
232
+ getters: {
233
+ currentCluster: () => {},
234
+ isRancher: () => true,
235
+ 'catalog/repo': () => {
236
+ return () => 'repo';
237
+ },
238
+ 'catalog/chart': () => {
239
+ return {
240
+ id: 'chart-id',
241
+ versions: [{ version: '1.0.0' }],
242
+ matchingInstalledApps: [installedApp]
243
+ };
244
+ },
245
+ 'catalog/version': () => ({
246
+ version: '1.0.0',
247
+ annotations: {
248
+ [CATALOG_ANNOTATIONS.NAMESPACE]: 'custom-ns',
249
+ [CATALOG_ANNOTATIONS.RELEASE_NAME]: 'custom-name',
250
+ }
251
+ }),
252
+ 'prefs/get': () => {},
253
+ 'i18n/t': () => jest.fn()
254
+ }
255
+ };
256
+
257
+ const DummyComponent = {
258
+ mixins: [ChartMixin],
259
+ template: '<div></div>',
260
+ };
261
+
262
+ const wrapper = mount(
263
+ DummyComponent,
264
+ {
265
+ global: {
266
+ mocks: {
267
+ $store: mockStore,
268
+ $route: {
269
+ query: {
270
+ chart: 'chart_name',
271
+ versionName: '1.0.0'
272
+ }
273
+ }
274
+ }
275
+ }
276
+ });
277
+
278
+ await wrapper.vm.fetchChart();
279
+
280
+ expect(wrapper.vm.existing).toStrictEqual(installedApp);
281
+ expect(wrapper.vm.mode).toStrictEqual('edit');
282
+ });
217
283
  });
218
284
 
219
285
  describe('action', () => {
@@ -366,6 +432,52 @@ describe('chartMixin', () => {
366
432
  });
367
433
  });
368
434
 
435
+ describe('isChartTargeted', () => {
436
+ const DummyComponent = {
437
+ mixins: [ChartMixin],
438
+ template: '<div></div>',
439
+ };
440
+
441
+ const mockStore = {
442
+ dispatch: jest.fn(() => Promise.resolve()),
443
+ getters: { 'i18n/t': (key: string) => key }
444
+ };
445
+
446
+ it('should return truthy when version annotations have both namespace and release-name', () => {
447
+ const wrapper = mount(DummyComponent, {
448
+ data: () => ({
449
+ version: {
450
+ annotations: {
451
+ [CATALOG_ANNOTATIONS.NAMESPACE]: 'custom-ns',
452
+ [CATALOG_ANNOTATIONS.RELEASE_NAME]: 'custom-name',
453
+ }
454
+ }
455
+ }),
456
+ global: { mocks: { $store: mockStore } }
457
+ });
458
+
459
+ expect(wrapper.vm.isChartTargeted).toBeTruthy();
460
+ });
461
+
462
+ it('should return falsy when version annotations are missing', () => {
463
+ const wrapper = mount(DummyComponent, {
464
+ data: () => ({ version: { annotations: {} } }),
465
+ global: { mocks: { $store: mockStore } }
466
+ });
467
+
468
+ expect(wrapper.vm.isChartTargeted).toBeFalsy();
469
+ });
470
+
471
+ it('should return falsy when version is null', () => {
472
+ const wrapper = mount(DummyComponent, {
473
+ data: () => ({ version: null }),
474
+ global: { mocks: { $store: mockStore } }
475
+ });
476
+
477
+ expect(wrapper.vm.isChartTargeted).toBeFalsy();
478
+ });
479
+ });
480
+
369
481
  describe('mappedVersions', () => {
370
482
  it('should return versions sorted by semver (descending)', () => {
371
483
  const versions = [
package/mixins/brand.js CHANGED
@@ -183,7 +183,8 @@ export default {
183
183
  },
184
184
  setBodyClass() {
185
185
  const body = document.getElementsByTagName('body')[0];
186
- const cssClass = `overflow-hidden dashboard-body`;
186
+ const isStandalone = this.$route?.meta?.standalone;
187
+ const cssClass = isStandalone ? 'dashboard-body' : 'overflow-hidden dashboard-body';
187
188
  let bodyClass = `theme-${ this.theme } ${ cssClass }`;
188
189
 
189
190
  if ( this.brand ) {
package/mixins/chart.js CHANGED
@@ -13,7 +13,7 @@ import { CAPI, CATALOG } from '@shell/config/types';
13
13
  import { isPrerelease } from '@shell/utils/version';
14
14
  import { compareChartVersions } from '@shell/utils/chart';
15
15
  import difference from 'lodash/difference';
16
- import { LINUX, APP_UPGRADE_STATUS } from '@shell/store/catalog';
16
+ import { APP_UPGRADE_STATUS, isRancherRepo, getPermittedOSs } from '@shell/store/catalog';
17
17
  import { clone } from '@shell/utils/object';
18
18
  import { merge } from 'lodash';
19
19
 
@@ -28,6 +28,17 @@ export default {
28
28
  ignoreWarning: false,
29
29
 
30
30
  chart: null,
31
+
32
+ // Whether installing a new instance is allowed.
33
+ // This is false when the chart is targeted (has fixed namespace/name annotations)
34
+ // or when the URL query specifies a specific app to edit.
35
+ canInstallNew: true,
36
+
37
+ // Installed instances of this chart that can be selected for edit/upgrade
38
+ // on the chart detail page. When instances exist, `existing` is set to the
39
+ // first one by default, but the user can select a different instance or
40
+ // choose to install a new one.
41
+ installedInstances: [],
31
42
  };
32
43
  },
33
44
 
@@ -73,7 +84,8 @@ export default {
73
84
  keywords: version.keywords
74
85
  };
75
86
 
76
- const permittedSystems = (version?.annotations?.[CATALOG_ANNOTATIONS.PERMITTED_OS] || LINUX).split(',');
87
+ const isRancher = isRancherRepo(this.repo, this.chart);
88
+ const permittedSystems = getPermittedOSs(version?.annotations, isRancher);
77
89
 
78
90
  if (permittedSystems.length > 0 && difference(OSs, permittedSystems).length > 0) {
79
91
  nue.disabled = true;
@@ -260,7 +272,7 @@ export default {
260
272
  },
261
273
 
262
274
  isChartTargeted() {
263
- return this.chart?.targetNamespace && this.chart?.targetName;
275
+ return this.version?.annotations?.[CATALOG_ANNOTATIONS.NAMESPACE] && this.version?.annotations?.[CATALOG_ANNOTATIONS.RELEASE_NAME];
264
276
  },
265
277
 
266
278
  hasQuestions() {
@@ -376,6 +388,9 @@ export default {
376
388
  // If found, set the form to edit mode. If not, set the
377
389
  // form to create mode.
378
390
 
391
+ // This is a hard blocker - installing a new instance is NOT allowed.
392
+ this.canInstallNew = false;
393
+
379
394
  try {
380
395
  this.existing = await this.$store.dispatch('cluster/find', {
381
396
  type: CATALOG.APP,
@@ -396,10 +411,12 @@ export default {
396
411
  if ( targetNamespace && targetName ) {
397
412
  // If the app name and namespace values are not provided in the
398
413
  // query, fall back on target values defined in the Helm chart itself.
399
-
400
414
  // Ask to install a special chart with fixed namespace/name
401
415
  // or edit it if there's an existing install.
402
416
 
417
+ // This is a hard blocker - installing a new instance is NOT allowed.
418
+ this.canInstallNew = false;
419
+
403
420
  try {
404
421
  this.existing = await this.$store.dispatch('cluster/find', {
405
422
  type: CATALOG.APP,
@@ -407,22 +424,40 @@ export default {
407
424
  });
408
425
  this.mode = _EDIT;
409
426
  } catch (e) {
410
- this.mode = _CREATE;
411
- this.existing = null;
427
+ // Version targets a different namespace than where the app is installed.
428
+ // Fall back to matching installed apps (e.g. chart detail page).
429
+ const matching = this.chart?.matchingInstalledApps || [];
430
+
431
+ this.existing = matching[0] || null;
432
+ this.mode = this.existing ? _EDIT : _CREATE;
412
433
  }
413
- } else if (this.chart) {
414
- const matching = this.chart.matchingInstalledApps;
434
+ } else {
435
+ // Regular chart (not targeted) - check if there are installed instances.
436
+ // Installing new instances IS allowed (canInstallNew remains true).
437
+ const isChartDetailPage = this.$route.name === 'c-cluster-apps-charts-chart';
415
438
 
416
- if (matching.length === 1) {
417
- this.existing = matching[0];
418
- this.mode = _EDIT;
439
+ if (isChartDetailPage) {
440
+ const matching = this.chart?.matchingInstalledApps || [];
441
+
442
+ // Always refresh the available instances so stale values are removed.
443
+ this.installedInstances = [];
444
+
445
+ if (matching.length >= 1) {
446
+ // Populate the instance selector and preserve the current selection
447
+ // when it is still one of the matching installed apps.
448
+ this.installedInstances = matching;
449
+ const hasExistingMatch = this.existing?.id && matching.some((instance) => instance.id === this.existing.id);
450
+
451
+ if (!hasExistingMatch) {
452
+ this.existing = matching[0];
453
+ }
454
+ } else {
455
+ this.existing = null;
456
+ }
419
457
  } else {
458
+ // Regular create
420
459
  this.mode = _CREATE;
421
460
  }
422
- } else {
423
- // Regular create
424
-
425
- this.mode = _CREATE;
426
461
  }
427
462
  }
428
463
  }, // End of fetchChart
@@ -1,5 +1,5 @@
1
1
  import { NAMESPACE_FILTER_NAMESPACED_YES, NAMESPACE_FILTER_NAMESPACED_NO, NAMESPACE_FILTER_ALL } from '@shell/utils/namespace-filter';
2
- import { NAMESPACE } from '@shell/config/types';
2
+ import { MANAGEMENT, NAMESPACE } from '@shell/config/types';
3
3
  import { ALL_NAMESPACES } from '@shell/store/prefs';
4
4
  import { mapGetters } from 'vuex';
5
5
  import { ResourceListComponentName } from '../components/ResourceList/resource-list.config';
@@ -248,7 +248,7 @@ export default {
248
248
  namespaceFilters: {
249
249
  immediate: true,
250
250
  async handler(neu, old) {
251
- if (!this.canPaginate || !this.isNamespaced) {
251
+ if (!this.canPaginate || !this.isNamespaced || !this.currentProduct?.showNamespaceFilter) {
252
252
  return;
253
253
  }
254
254
 
@@ -283,7 +283,7 @@ export default {
283
283
  allNamespaces: this.$store.getters[`${ this.currentProduct?.inStore }/all`](NAMESPACE),
284
284
  selection: neu,
285
285
  isAllNamespaces: this.isAllNamespaces,
286
- isLocalCluster: this.$store.getters['currentCluster'].isLocal,
286
+ isLocalCluster: this.$store.getters['currentCluster']?.isLocal,
287
287
  showReservedRancherNamespaces: this.showDynamicRancherNamespaces,
288
288
  productHidesSystemNamespaces: this.productHidesSystemNamespaces,
289
289
  });
@@ -354,15 +354,51 @@ export default {
354
354
 
355
355
  async beforeUnmount() {
356
356
  if (this.havePaginated) {
357
+ const store = this.overrideInStore || this.inStore;
357
358
  // of type @STEVE_WATCH_PARAMS
358
359
  const watchArgs = {
359
360
  type: this.resource,
360
361
  mode: STEVE_WATCH_MODE.RESOURCE_CHANGES,
361
362
  };
362
363
 
363
- await this.$store.dispatch(`${ this.overrideInStore || this.inStore }/forgetType`, this.resource, (watchParams) => {
364
- return watchParams.type === watchArgs.type && watchParams.mode === watchArgs.type.mode;
364
+ // Ensure that a resource remains in the store... and it's the same reference as before
365
+ // We'll do this for the mgmt cluster if there's a currentCluster set, otherwise we become unstuck
366
+ // (on nav we ensure currentCluster exists, but beforeUnmount fires afterwards, removing the cluster from pages that are using it)
367
+ const retainResource = this.resource === MANAGEMENT.CLUSTER && this.currentCluster ? this.currentCluster : null;
368
+
369
+ // Forget (unwatch, clear from store) the list's resource
370
+ await this.$store.dispatch(`${ store }/forgetType`, {
371
+ type: this.resource,
372
+ compareWatches: (watchParams) => {
373
+ return watchParams.type === watchArgs.type && watchParams.mode === watchArgs.mode;
374
+ },
375
+ unwatch: true,
376
+ forget: !retainResource
365
377
  });
378
+
379
+ if (!!retainResource) {
380
+ // The end state should be just like we've dispatched `${store}/find`
381
+
382
+ try {
383
+ // Clone to ensure we can load each property of A into B without worrying about them being the same reference
384
+ const clone = await this.$store.dispatch(`${ store }/clone`, { resource: retainResource });
385
+
386
+ // Load the resource. It's already in the store but this makes sure any legacy state (like havePage) is reset
387
+ await this.$store.dispatch(`${ store }/load`, {
388
+ data: clone,
389
+ existing: retainResource,
390
+ invalidatePageCache: true,
391
+ });
392
+
393
+ // Watch the resource.
394
+ await this.$store.dispatch(`${ store }/watch`, {
395
+ type: this.resource,
396
+ id: retainResource.id,
397
+ });
398
+ } catch (error) {
399
+ console.error('Error occurred whilst trying to retain the management cluster in cache and correctly watched', error); // eslint-disable-line no-console
400
+ }
401
+ }
366
402
  }
367
403
  }
368
404
  };
@@ -87,6 +87,19 @@ const certManagerOfficialMatchingChart2 = {
87
87
  }]
88
88
  };
89
89
 
90
+ // Simulates the chart data fetched from the repository where the home URL has changed in newer versions
91
+ // AND the older installed version has been dropped/replaced from the repository index.
92
+ const certManagerOfficialMatchingChartNewHomeOnly = {
93
+ name: chartName,
94
+ repoName: certManagerOfficial.repoName,
95
+ versions: [{
96
+ version: latestVersion,
97
+ home: certManagerOfficial.home,
98
+ repoName: certManagerOfficial.repoName,
99
+ annotations: {},
100
+ }]
101
+ };
102
+
90
103
  const installedCertManagerAppCoFromRancherUI = {
91
104
  metadata: {
92
105
  annotations: { [CATALOG_ANNOTATIONS.SOURCE_REPO_NAME]: appCo.repoName },
@@ -130,7 +143,8 @@ describe('class CatalogApp', () => {
130
143
  [installedCertManagerOfficialFromRancherUI, [], APP_UPGRADE_STATUS.NO_UPGRADE],
131
144
  [installedCertManagerOfficialFromRancherUI, [certManagerOfficialMatchingChart1], APP_UPGRADE_STATUS.SINGLE_UPGRADE],
132
145
  [installedCertManagerOfficialFromRancherUI, [certManagerOfficialMatchingChart1, appCoMatchingChart1], APP_UPGRADE_STATUS.SINGLE_UPGRADE],
133
- [installedCertManagerOfficialFromRancherUI, [certManagerOfficialMatchingChart1, certManagerOfficialMatchingChart2], APP_UPGRADE_STATUS.MULTIPLE_UPGRADES]
146
+ [installedCertManagerOfficialFromRancherUI, [certManagerOfficialMatchingChart1, certManagerOfficialMatchingChart2], APP_UPGRADE_STATUS.MULTIPLE_UPGRADES],
147
+ [installedCertManagerOfficialFromRancherUI, [certManagerOfficialMatchingChartNewHomeOnly], APP_UPGRADE_STATUS.SINGLE_UPGRADE]
134
148
  ];
135
149
 
136
150
  it.each(testCases)('should return the correct upgrade status', (installedChart: Object, matchingCharts: any, expected: any) => {
@@ -0,0 +1,84 @@
1
+ import ClusterRepo from '../catalog.cattle.io.clusterrepo';
2
+
3
+ describe('clusterRepo', () => {
4
+ let model: any;
5
+
6
+ beforeEach(() => {
7
+ model = new ClusterRepo({
8
+ metadata: { name: 'test-repo' },
9
+ spec: { url: 'https://test-url.com' }
10
+ }, {
11
+ getters: {},
12
+ dispatch: jest.fn(),
13
+ rootGetters: {}
14
+ });
15
+
16
+ jest.spyOn(model, '_key', 'get').mockReturnValue('test-key');
17
+ jest.spyOn(model, 'save').mockImplementation().mockResolvedValue(true);
18
+ jest.spyOn(model, 'waitForState').mockImplementation().mockResolvedValue(true);
19
+ jest.spyOn(model, '$dispatch', 'get').mockReturnValue(jest.fn());
20
+ });
21
+
22
+ describe('refresh', () => {
23
+ it('updates forceUpdate, saves, waits for active state, and dispatches load by default', async() => {
24
+ // Mock Date to ensure deterministic forceUpdate value
25
+ const mockDate = new Date('2023-01-01T12:00:00.000Z');
26
+ const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any);
27
+
28
+ await model.refresh();
29
+
30
+ expect(model.spec.forceUpdate).toBe('2023-01-01T12:00:00Z');
31
+ expect(model.save).toHaveBeenCalledWith();
32
+ expect(model.waitForState).toHaveBeenCalledWith('active', 10000, 1000);
33
+ expect(model.$dispatch).toHaveBeenCalledWith('catalog/load', { force: true, repoKeys: [model._key] }, { root: true });
34
+
35
+ spy.mockRestore();
36
+ });
37
+
38
+ it('updates forceUpdate, saves, waits for active state, but DOES NOT dispatch load if dispatchLoad is false', async() => {
39
+ await model.refresh(false);
40
+
41
+ expect(model.save).toHaveBeenCalledWith();
42
+ expect(model.waitForState).toHaveBeenCalledWith('active', 10000, 1000);
43
+ expect(model.$dispatch).not.toHaveBeenCalled();
44
+ });
45
+
46
+ it('dispatches error to growl if save or waitForState fails', async() => {
47
+ const error = new Error('waitForState timeout');
48
+
49
+ model.waitForState.mockRejectedValue(error);
50
+ jest.spyOn(model, 't', 'get').mockReturnValue(jest.fn().mockReturnValue('Error refreshing repository'));
51
+ jest.spyOn(model, 'nameDisplay', 'get').mockReturnValue('Test Repo');
52
+
53
+ await model.refresh();
54
+
55
+ expect(model.$dispatch).toHaveBeenCalledWith('growl/fromError', {
56
+ title: 'Error refreshing repository',
57
+ err: error
58
+ }, { root: true });
59
+ });
60
+ });
61
+
62
+ describe('refreshBulk', () => {
63
+ it('calls refresh(false) on all items and then dispatches a single catalog/load with all repoKeys', async() => {
64
+ const mockItem1 = {
65
+ _key: 'repo-1',
66
+ refresh: jest.fn().mockResolvedValue(true)
67
+ };
68
+ const mockItem2 = {
69
+ _key: 'repo-2',
70
+ refresh: jest.fn().mockResolvedValue(true)
71
+ };
72
+
73
+ await model.refreshBulk([mockItem1, mockItem2]);
74
+
75
+ expect(mockItem1.refresh).toHaveBeenCalledWith(false);
76
+ expect(mockItem2.refresh).toHaveBeenCalledWith(false);
77
+
78
+ expect(model.$dispatch).toHaveBeenCalledWith('catalog/load', {
79
+ force: true,
80
+ repoKeys: ['repo-1', 'repo-2']
81
+ }, { root: true });
82
+ });
83
+ });
84
+ });
@@ -17,7 +17,7 @@ type MockChartContext = {
17
17
  };
18
18
 
19
19
  interface CardContent {
20
- subHeaderItems: { label: string, labelTooltip?: string}[];
20
+ subHeaderItems: { label: string, labelTooltip?: string, icon?: string, iconTooltip?: string }[];
21
21
  footerItems: { labels: string[]; icon?: string }[];
22
22
  statuses: { tooltip: { key?: string; text?: string }; color: string }[];
23
23
  }
@@ -166,7 +166,7 @@ describe('class Chart', () => {
166
166
  expect(chart.isInstalled).toBe(false);
167
167
  });
168
168
 
169
- it('is false when multiple apps match', () => {
169
+ it('is true when multiple apps match', () => {
170
170
  const app = makeInstalledApp();
171
171
 
172
172
  app.spec.chart.metadata.version = '1.2.3';
@@ -174,7 +174,7 @@ describe('class Chart', () => {
174
174
 
175
175
  const chart = new Chart(base, ctx);
176
176
 
177
- expect(chart.isInstalled).toBe(false);
177
+ expect(chart.isInstalled).toBe(true);
178
178
  });
179
179
  });
180
180
 
@@ -206,6 +206,58 @@ describe('class Chart', () => {
206
206
 
207
207
  expect(chart.upgradeable).toBe(false);
208
208
  });
209
+
210
+ it('is true when at least one of multiple installed apps is upgradeable', () => {
211
+ const app1 = makeInstalledApp(APP_UPGRADE_STATUS.NO_UPGRADE);
212
+ const app2 = makeInstalledApp(APP_UPGRADE_STATUS.SINGLE_UPGRADE);
213
+
214
+ ctx.rootGetters['cluster/all'] = () => [app1, app2];
215
+
216
+ const chart = new Chart(base, ctx);
217
+
218
+ expect(chart.upgradeable).toBe(true);
219
+ });
220
+
221
+ it('is false when none of multiple installed apps are upgradeable', () => {
222
+ const app1 = makeInstalledApp(APP_UPGRADE_STATUS.NO_UPGRADE);
223
+ const app2 = makeInstalledApp(APP_UPGRADE_STATUS.NO_UPGRADE);
224
+
225
+ ctx.rootGetters['cluster/all'] = () => [app1, app2];
226
+
227
+ const chart = new Chart(base, ctx);
228
+
229
+ expect(chart.upgradeable).toBe(false);
230
+ });
231
+ });
232
+
233
+ describe('installedCount', () => {
234
+ it('returns 0 when no apps are installed', () => {
235
+ const chart = new Chart(base, ctx);
236
+
237
+ expect(chart.installedCount).toBe(0);
238
+ });
239
+
240
+ it('returns 1 when one app is installed', () => {
241
+ const installedApp = makeInstalledApp();
242
+
243
+ ctx.rootGetters['cluster/all'] = () => [installedApp];
244
+
245
+ const chart = new Chart(base, ctx);
246
+
247
+ expect(chart.installedCount).toBe(1);
248
+ });
249
+
250
+ it('returns correct count when multiple apps are installed', () => {
251
+ const app1 = makeInstalledApp();
252
+ const app2 = makeInstalledApp();
253
+ const app3 = makeInstalledApp();
254
+
255
+ ctx.rootGetters['cluster/all'] = () => [app1, app2, app3];
256
+
257
+ const chart = new Chart(base, ctx);
258
+
259
+ expect(chart.installedCount).toBe(3);
260
+ });
209
261
  });
210
262
 
211
263
  describe('cardContent', () => {
@@ -274,6 +326,25 @@ describe('class Chart', () => {
274
326
  expect(installedStatus?.tooltip?.text).toContain(installedApp.spec.chart.metadata.version);
275
327
  });
276
328
 
329
+ it('does not include version in installed tooltip when multiple instances exist', () => {
330
+ const app1 = makeInstalledApp();
331
+ const app2 = makeInstalledApp();
332
+
333
+ app2.spec.chart.metadata.version = '1.2.0';
334
+ ctx.rootGetters['cluster/all'] = () => [app1, app2];
335
+
336
+ const chart = new Chart(base, ctx);
337
+
338
+ const result = chart.cardContent as CardContent;
339
+
340
+ const installedStatus = result.statuses.find((s) => s.tooltip?.text?.startsWith('generic.installedMultiple'));
341
+
342
+ expect(installedStatus).toBeDefined();
343
+ expect(installedStatus?.color).toBe('success');
344
+ // Should not contain version number when multiple instances
345
+ expect(installedStatus?.tooltip?.text).toBe('generic.installedMultiple');
346
+ });
347
+
277
348
  it('includes upgradeable status when upgrade is available', () => {
278
349
  const installedApp = makeInstalledApp(APP_UPGRADE_STATUS.SINGLE_UPGRADE);
279
350
 
@@ -332,10 +403,32 @@ describe('class Chart', () => {
332
403
  });
333
404
 
334
405
  const result = chart.cardContent as CardContent;
335
- const lastUpdatedItem = result.subHeaderItems[1];
336
406
 
337
- expect(lastUpdatedItem.label).toBe('generic.na');
338
- expect(lastUpdatedItem.labelTooltip).toBe('catalog.charts.appChartCard.subHeaderItem.missingVersionDate');
407
+ expect(result.subHeaderItems).toHaveLength(1);
408
+ expect(result.subHeaderItems[0].icon).toBe('icon-version-alt');
409
+ });
410
+
411
+ it('handles falsy time for last updated date', () => {
412
+ const chartWithFalsyTime = {
413
+ ...base,
414
+ versions: [{
415
+ ...base.versions[0],
416
+ created: '',
417
+ }]
418
+ };
419
+ const chart = new Chart(chartWithFalsyTime, {
420
+ rootGetters: {
421
+ 'cluster/all': () => [],
422
+ 'i18n/t': (key: string) => key,
423
+ currentCluster: { workerOSs: [] },
424
+ 'prefs/get': () => false,
425
+ },
426
+ });
427
+
428
+ const result = chart.cardContent as CardContent;
429
+
430
+ expect(result.subHeaderItems).toHaveLength(1);
431
+ expect(result.subHeaderItems[0].icon).toBe('icon-version-alt');
339
432
  });
340
433
  });
341
434
  });