@rancher/shell 3.0.5-rc.6 → 3.0.5-rc.8

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 (243) hide show
  1. package/assets/brand/classic/metadata.json +3 -0
  2. package/assets/styles/app.scss +1 -0
  3. package/assets/styles/base/_color.scss +16 -0
  4. package/assets/styles/base/_helpers.scss +10 -0
  5. package/assets/styles/base/_variables.scss +18 -12
  6. package/assets/styles/fonts/_icons.scss +1 -32
  7. package/assets/styles/global/_layout.scss +1 -1
  8. package/assets/styles/themes/_dark.scss +262 -258
  9. package/assets/styles/themes/_light.scss +538 -509
  10. package/assets/styles/themes/_modern.scss +914 -0
  11. package/assets/translations/en-us.yaml +110 -29
  12. package/chart/__tests__/S3.test.ts +2 -1
  13. package/cloud-credential/generic.vue +18 -10
  14. package/cloud-credential/harvester.vue +1 -9
  15. package/components/AdvancedSection.vue +8 -0
  16. package/components/ChartReadme.vue +17 -7
  17. package/components/CodeMirror.vue +1 -1
  18. package/components/Drawer/Chrome.vue +0 -1
  19. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +27 -28
  20. package/components/Drawer/ResourceDetailDrawer/composables.ts +4 -24
  21. package/components/Drawer/ResourceDetailDrawer/index.vue +18 -4
  22. package/components/InstallHelmCharts.vue +656 -0
  23. package/components/LazyImage.vue +60 -4
  24. package/components/Loading.vue +1 -1
  25. package/components/LocaleSelector.vue +7 -2
  26. package/components/Markdown.vue +4 -0
  27. package/components/PaginatedResourceTable.vue +46 -1
  28. package/components/PromptRestore.vue +22 -44
  29. package/components/Resource/Detail/Masthead/composable.ts +16 -0
  30. package/components/Resource/Detail/Masthead/index.vue +37 -0
  31. package/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts +10 -2
  32. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +26 -7
  33. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +8 -1
  34. package/components/Resource/Detail/Metadata/KeyValue.vue +12 -10
  35. package/components/Resource/Detail/Metadata/Rectangle.vue +3 -1
  36. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +10 -17
  37. package/components/Resource/Detail/Metadata/composables.ts +9 -7
  38. package/components/Resource/Detail/Metadata/index.vue +17 -2
  39. package/components/Resource/Detail/Page.vue +35 -21
  40. package/components/Resource/Detail/SpacedRow.vue +1 -1
  41. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +8 -9
  42. package/components/Resource/Detail/TitleBar/composables.ts +5 -5
  43. package/components/Resource/Detail/TitleBar/index.vue +12 -3
  44. package/components/ResourceDetail/Masthead/legacy.vue +1 -1
  45. package/components/ResourceDetail/index.vue +569 -72
  46. package/components/ResourceList/index.vue +1 -0
  47. package/components/ResourceTable.vue +6 -1
  48. package/components/ResourceYaml.vue +1 -1
  49. package/components/RichTranslation.vue +106 -0
  50. package/components/SlideInPanelManager.vue +13 -10
  51. package/components/SortableTable/index.vue +5 -5
  52. package/components/SortableTable/selection.js +0 -1
  53. package/components/Tabbed/index.vue +35 -4
  54. package/components/__tests__/LazyImage.spec.ts +121 -0
  55. package/components/__tests__/PromptRestore.test.ts +1 -65
  56. package/components/__tests__/RichTranslation.test.ts +115 -0
  57. package/components/fleet/FleetStatus.vue +4 -0
  58. package/components/fleet/dashboard/ResourcePanel.vue +2 -1
  59. package/components/form/ClusterAppearance.vue +5 -0
  60. package/components/form/FileImageSelector.vue +1 -1
  61. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  62. package/components/form/NameNsDescription.vue +1 -0
  63. package/components/form/Networking.vue +24 -19
  64. package/components/form/ProjectMemberEditor.vue +1 -1
  65. package/components/form/ResourceLabeledSelect.vue +22 -8
  66. package/components/form/ResourceTabs/index.vue +20 -0
  67. package/components/form/SecretSelector.vue +9 -0
  68. package/components/form/SelectOrCreateAuthSecret.vue +6 -3
  69. package/components/form/__tests__/Networking.test.ts +116 -0
  70. package/components/form/labeled-select-utils/labeled-select-pagination.ts +3 -38
  71. package/components/formatter/FleetApplicationSource.vue +25 -17
  72. package/components/formatter/PodImages.vue +1 -1
  73. package/components/formatter/__tests__/LiveDate.test.ts +10 -2
  74. package/components/google/AccountAccess.vue +44 -46
  75. package/components/nav/Favorite.vue +4 -0
  76. package/components/nav/Group.vue +4 -1
  77. package/components/nav/NotificationCenter/Notification.vue +1 -27
  78. package/components/nav/WindowManager/index.vue +3 -3
  79. package/composables/resources.ts +2 -2
  80. package/config/labels-annotations.js +3 -2
  81. package/config/pagination-table-headers.js +8 -1
  82. package/config/product/explorer.js +27 -2
  83. package/config/product/manager.js +0 -1
  84. package/config/query-params.js +10 -0
  85. package/config/router/routes.js +21 -1
  86. package/config/system-namespaces.js +1 -1
  87. package/config/table-headers.js +30 -1
  88. package/config/types.js +1 -1
  89. package/config/version.js +1 -1
  90. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +11 -0
  91. package/detail/__tests__/workload.test.ts +164 -0
  92. package/detail/configmap.vue +33 -75
  93. package/detail/projectsecret.vue +11 -0
  94. package/detail/provisioning.cattle.io.cluster.vue +351 -369
  95. package/detail/secret.vue +49 -308
  96. package/detail/workload/index.vue +38 -21
  97. package/dialog/InstallExtensionDialog.vue +8 -5
  98. package/dialog/RotateEncryptionKeyDialog.vue +10 -30
  99. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
  100. package/edit/auth/ldap/__tests__/config.test.ts +14 -0
  101. package/edit/auth/ldap/config.vue +24 -0
  102. package/edit/compliance.cattle.io.clusterscan.vue +1 -1
  103. package/edit/configmap.vue +4 -1
  104. package/edit/fleet.cattle.io.gitrepo.vue +5 -6
  105. package/edit/fleet.cattle.io.helmop.vue +78 -56
  106. package/edit/logging.banzaicloud.io.output/index.vue +1 -1
  107. package/edit/logging.banzaicloud.io.output/providers/awsElasticsearch.vue +5 -6
  108. package/edit/networking.k8s.io.ingress/Certificate.vue +20 -22
  109. package/edit/networking.k8s.io.ingress/DefaultBackend.vue +8 -3
  110. package/edit/networking.k8s.io.ingress/Rule.vue +2 -5
  111. package/edit/networking.k8s.io.ingress/RulePath.vue +17 -11
  112. package/edit/networking.k8s.io.ingress/__tests__/Certificate.test.ts +165 -0
  113. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +11 -10
  114. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -3
  115. package/edit/networking.k8s.io.networkpolicy/index.vue +17 -17
  116. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +3 -2
  117. package/edit/provisioning.cattle.io.cluster/rke2.vue +123 -61
  118. package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +9 -7
  119. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +22 -13
  120. package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +10 -12
  121. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +39 -38
  122. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +41 -19
  123. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +16 -3
  124. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +32 -33
  125. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryMirrors.vue +9 -10
  126. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +1 -3
  127. package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +16 -9
  128. package/edit/secret/basic.vue +1 -0
  129. package/edit/secret/index.vue +126 -15
  130. package/edit/workload/index.vue +5 -14
  131. package/list/projectsecret.vue +345 -0
  132. package/list/provisioning.cattle.io.cluster.vue +1 -69
  133. package/list/secret.vue +109 -0
  134. package/machine-config/__tests__/vmwarevsphere.test.ts +5 -7
  135. package/machine-config/google.vue +9 -1
  136. package/machine-config/vmwarevsphere.vue +7 -17
  137. package/mixins/__tests__/brand.spec.ts +2 -2
  138. package/mixins/chart.js +0 -2
  139. package/mixins/create-edit-view/impl.js +10 -1
  140. package/mixins/resource-fetch-api-pagination.js +11 -12
  141. package/mixins/resource-fetch.js +3 -1
  142. package/models/__tests__/chart.test.ts +111 -80
  143. package/models/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
  144. package/models/__tests__/node.test.ts +7 -63
  145. package/models/catalog.cattle.io.app.js +1 -1
  146. package/models/catalog.cattle.io.operation.js +1 -1
  147. package/models/chart.js +36 -20
  148. package/models/cloudcredential.js +2 -163
  149. package/models/cluster/node.js +7 -7
  150. package/models/cluster.x-k8s.io.machine.js +3 -3
  151. package/models/cluster.x-k8s.io.machinedeployment.js +11 -2
  152. package/models/compliance.cattle.io.clusterscan.js +2 -2
  153. package/models/configmap.js +4 -0
  154. package/models/constraints.gatekeeper.sh.constraint.js +1 -1
  155. package/models/fleet-application.js +0 -17
  156. package/models/fleet.cattle.io.cluster.js +2 -2
  157. package/models/fleet.cattle.io.gitrepo.js +15 -1
  158. package/models/fleet.cattle.io.helmop.js +26 -22
  159. package/models/management.cattle.io.setting.js +4 -0
  160. package/models/persistentvolumeclaim.js +1 -1
  161. package/models/pod.js +2 -2
  162. package/models/provisioning.cattle.io.cluster.js +39 -67
  163. package/models/rke.cattle.io.etcdsnapshot.js +1 -1
  164. package/models/secret.js +161 -2
  165. package/models/storage.k8s.io.storageclass.js +2 -2
  166. package/models/workload.js +3 -3
  167. package/package.json +11 -10
  168. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +1 -0
  169. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +4 -1
  170. package/pages/c/_cluster/apps/charts/__tests__/AppChartCardFooter.spec.js +41 -0
  171. package/pages/c/_cluster/apps/charts/chart.vue +422 -174
  172. package/pages/c/_cluster/apps/charts/index.vue +46 -35
  173. package/pages/c/_cluster/apps/charts/install.vue +1 -1
  174. package/pages/c/_cluster/explorer/projectsecret.vue +24 -0
  175. package/pages/c/_cluster/fleet/__tests__/index.test.ts +608 -314
  176. package/pages/c/_cluster/fleet/index.vue +103 -45
  177. package/pages/c/_cluster/manager/cloudCredential/index.vue +2 -59
  178. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +10 -3
  179. package/pages/c/_cluster/uiplugins/index.vue +36 -25
  180. package/plugins/dashboard-store/__tests__/normalize.test.ts +223 -0
  181. package/plugins/dashboard-store/__tests__/resource-class.test.ts +191 -0
  182. package/plugins/dashboard-store/__tests__/utils/normalize-usecases.ts +1526 -0
  183. package/plugins/dashboard-store/actions.js +42 -22
  184. package/plugins/dashboard-store/normalize.js +29 -17
  185. package/plugins/dashboard-store/resource-class.js +83 -17
  186. package/plugins/steve/__tests__/getters.test.ts +1 -1
  187. package/plugins/steve/__tests__/subscribe.spec.ts +259 -1
  188. package/plugins/steve/getters.js +8 -2
  189. package/plugins/steve/resourceWatcher.js +10 -3
  190. package/plugins/steve/steve-pagination-utils.ts +14 -3
  191. package/plugins/steve/subscribe.js +192 -19
  192. package/plugins/steve/worker/web-worker.advanced.js +2 -0
  193. package/rancher-components/Card/Card.vue +0 -18
  194. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.test.ts +15 -0
  195. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +65 -0
  196. package/rancher-components/Pill/RcStatusBadge/index.ts +2 -0
  197. package/rancher-components/Pill/RcStatusBadge/types.ts +5 -0
  198. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.test.ts +33 -0
  199. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +75 -0
  200. package/rancher-components/Pill/RcStatusIndicator/index.ts +2 -0
  201. package/rancher-components/Pill/RcStatusIndicator/types.ts +7 -0
  202. package/rancher-components/Pill/types.ts +2 -0
  203. package/rancher-components/RcButton/RcButton.vue +1 -1
  204. package/rancher-components/RcDropdown/RcDropdown.test.ts +98 -0
  205. package/rancher-components/RcDropdown/RcDropdown.vue +5 -0
  206. package/rancher-components/RcDropdown/RcDropdownItem.vue +7 -1
  207. package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +2 -1
  208. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +2 -1
  209. package/rancher-components/RcDropdown/useDropdownContext.ts +21 -0
  210. package/rancher-components/RcDropdown/useDropdownItem.ts +30 -1
  211. package/rancher-components/RcItemCard/RcItemCard.test.ts +20 -0
  212. package/rancher-components/RcItemCard/RcItemCard.vue +40 -6
  213. package/store/__tests__/catalog.test.ts +93 -1
  214. package/store/aws.js +19 -8
  215. package/store/catalog.js +8 -3
  216. package/types/kube/kube-api.ts +12 -0
  217. package/types/resources/settings.d.ts +1 -1
  218. package/types/shell/index.d.ts +643 -585
  219. package/types/store/pagination.types.ts +16 -6
  220. package/types/uiplugins.ts +73 -0
  221. package/utils/__tests__/back-off.test.ts +354 -0
  222. package/utils/__tests__/create-yaml.test.ts +235 -0
  223. package/utils/__tests__/kontainer.test.ts +19 -0
  224. package/utils/__tests__/uiplugins.test.ts +84 -0
  225. package/utils/back-off.ts +176 -0
  226. package/utils/create-yaml.js +103 -9
  227. package/utils/dynamic-importer.js +8 -0
  228. package/utils/kontainer.ts +3 -5
  229. package/utils/pagination-utils.ts +18 -0
  230. package/utils/style.ts +3 -0
  231. package/utils/uiplugins.ts +29 -2
  232. package/utils/validators/__tests__/setting.test.js +92 -0
  233. package/utils/validators/formRules/__tests__/index.test.ts +88 -7
  234. package/utils/validators/formRules/index.ts +83 -8
  235. package/utils/validators/setting.js +17 -0
  236. package/cloud-credential/__tests__/harvester.test.ts +0 -18
  237. package/components/ResourceDetail/__tests__/index.test.ts +0 -135
  238. package/components/ResourceDetail/legacy.vue +0 -562
  239. package/components/formatter/CloudCredExpired.vue +0 -69
  240. package/models/etcdbackup.js +0 -45
  241. package/pages/explorer/resource/detail/configmap.vue +0 -42
  242. package/pages/explorer/resource/detail/secret.vue +0 -50
  243. package/utils/aws.js +0 -0
@@ -0,0 +1,106 @@
1
+ <script lang="ts">
2
+ import { defineComponent, h, VNode } from 'vue';
3
+ import { useStore } from 'vuex';
4
+ import { purifyHTML } from '@shell/plugins/clean-html';
5
+
6
+ const ALLOWED_TAGS = ['b', 'i', 'span', 'a']; // Add more as needed
7
+
8
+ /**
9
+ * A component for rendering translated strings with embedded HTML and custom Vue components.
10
+ *
11
+ * This component allows you to use a single translation key for a message that contains
12
+ * both standard HTML tags (like <b>, <i>, etc.) and custom Vue components (like <router-link>).
13
+ *
14
+ * @example
15
+ * // In your translation file (e.g., en-us.yaml):
16
+ * my:
17
+ * translation:
18
+ * key: 'This is a <b>bold</b> statement with a <customLink>link</customLink>.'
19
+ *
20
+ * // In your Vue component:
21
+ * <RichTranslation k="my.translation.key">
22
+ * <template #customLink="{ content }">
23
+ * <router-link to="{ name: 'some-path' }">{{ content }}</router-link>
24
+ * </template>
25
+ * </RichTranslation>
26
+ */
27
+ export default defineComponent({
28
+ name: 'RichTranslation',
29
+ props: {
30
+ /**
31
+ * The translation key for the message.
32
+ */
33
+ k: {
34
+ type: String,
35
+ required: true,
36
+ },
37
+ /**
38
+ * The HTML tag to use for the root element.
39
+ */
40
+ tag: {
41
+ type: String,
42
+ default: 'span'
43
+ },
44
+ },
45
+ setup(props, { slots }) {
46
+ const store = useStore();
47
+
48
+ return () => {
49
+ // Get the raw translation string, without any processing.
50
+ const rawStr = store.getters['i18n/t'](props.k, {}, true);
51
+
52
+ if (!rawStr || typeof rawStr !== 'string') {
53
+ return h(props.tag, {}, [rawStr]);
54
+ }
55
+
56
+ // This regex splits the string by the custom tags, keeping the tags in the resulting array.
57
+ const regex = /<([a-zA-Z0-9]+)>(.*?)<\/\1>|<([a-zA-Z0-9]+)\/>/g;
58
+ const children: (VNode | string)[] = [];
59
+ let lastIndex = 0;
60
+ let match;
61
+
62
+ // Iterate over all matches of the regex.
63
+ while ((match = regex.exec(rawStr)) !== null) {
64
+ // Add the text before the current match as a plain text node.
65
+ if (match.index > lastIndex) {
66
+ children.push(h('span', { innerHTML: purifyHTML(rawStr.substring(lastIndex, match.index)) }));
67
+ }
68
+
69
+ const enclosingTagName = match[1]; // Captures the tag name for enclosing tags (e.g., 'customLink' from <customLink>...</customLink>)
70
+ const selfClosingTagName = match[3]; // Captures the tag name for self-closing tags (e.g., 'anotherTag' from <anotherTag/>)
71
+ const tagName = enclosingTagName || selfClosingTagName;
72
+
73
+ if (tagName) {
74
+ const content = enclosingTagName ? match[2] : '';
75
+
76
+ if (slots[tagName]) {
77
+ // If a slot is provided for this tag, render the slot with the content.
78
+ children.push(slots[tagName]({ content: purifyHTML(content) }));
79
+ } else if (ALLOWED_TAGS.includes(tagName.toLowerCase())) {
80
+ // If it's an allowed HTML tag, render it directly.
81
+ if (content) {
82
+ children.push(h(tagName, { innerHTML: purifyHTML(content, { ALLOWED_TAGS }) }));
83
+ } else {
84
+ children.push(h(tagName));
85
+ }
86
+ } else {
87
+ // Otherwise, render the tag and its content as plain HTML.
88
+ children.push(h('span', { innerHTML: purifyHTML(match[0]) }));
89
+ }
90
+ }
91
+
92
+ // Update the last index to continue searching after the current match
93
+ lastIndex = regex.lastIndex;
94
+ }
95
+
96
+ // Add any remaining text after the last match.
97
+ if (lastIndex < rawStr.length) {
98
+ children.push(h('span', { innerHTML: purifyHTML(rawStr.substring(lastIndex)) }));
99
+ }
100
+
101
+ // Render the root element with the processed children.
102
+ return h(props.tag, {}, children);
103
+ };
104
+ }
105
+ });
106
+ </script>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { computed, onBeforeUnmount, watch } from 'vue';
2
+ import { computed, onBeforeUnmount, watch, useTemplateRef } from 'vue';
3
3
  import { useStore } from 'vuex';
4
4
  import {
5
5
  DEFAULT_FOCUS_TRAP_OPTS,
@@ -10,6 +10,9 @@ import { useRouter } from 'vue-router';
10
10
 
11
11
  const HEADER_HEIGHT = 55;
12
12
 
13
+ const slideInPanelManager = useTemplateRef('SlideInPanelManager');
14
+ const slideInPanelManagerClose = useTemplateRef('SlideInPanelManagerClose');
15
+
13
16
  const store = useStore();
14
17
  const isOpen = computed(() => store.getters['slideInPanel/isOpen']);
15
18
  const isClosing = computed(() => store.getters['slideInPanel/isClosing']);
@@ -36,7 +39,6 @@ const panelTop = computed(() => {
36
39
  const panelHeight = computed(() => (currentProps?.value?.height) ? (currentProps?.value?.height) : `calc(100vh - ${ panelTop?.value })`);
37
40
  const panelWidth = computed(() => currentProps?.value?.width || '33%');
38
41
  const panelRight = computed(() => (isOpen?.value ? '0' : `-${ panelWidth?.value }`));
39
- const panelZIndex = computed(() => `${ (isOpen?.value ? 1 : 2) * (currentProps?.value?.zIndex ?? 1000) }`);
40
42
 
41
43
  const showHeader = computed(() => currentProps?.value?.showHeader ?? true);
42
44
  const panelTitle = showHeader.value ? computed(() => currentProps?.value?.title || 'Details') : null;
@@ -73,7 +75,9 @@ watch(
73
75
  }
74
76
 
75
77
  return returnFocusSelector || '.dashboard-root';
76
- }
78
+ },
79
+ // putting the initial focus on the first element that is not conditionally displayed
80
+ initialFocus: slideInPanelManagerClose.value
77
81
  };
78
82
 
79
83
  useWatcherBasedSetupFocusTrapWithDestroyIncluded(
@@ -84,7 +88,7 @@ watch(
84
88
 
85
89
  return isOpen?.value && !isClosing?.value;
86
90
  },
87
- '#slide-in-panel-manager',
91
+ slideInPanelManager.value as HTMLElement,
88
92
  opts,
89
93
  false
90
94
  );
@@ -129,6 +133,7 @@ function closePanel() {
129
133
  <Teleport to="#slides">
130
134
  <div
131
135
  id="slide-in-panel-manager"
136
+ ref="SlideInPanelManager"
132
137
  @keydown.escape="closePanel"
133
138
  >
134
139
  <div
@@ -136,9 +141,6 @@ function closePanel() {
136
141
  data-testid="slide-in-glass"
137
142
  class="slide-in-glass"
138
143
  :class="{ 'slide-in-glass-open': isOpen }"
139
- :style="{
140
- ['z-index']: panelZIndex
141
- }"
142
144
  @click="closePanel"
143
145
  />
144
146
  <aside
@@ -149,7 +151,6 @@ function closePanel() {
149
151
  right: panelRight,
150
152
  top: panelTop,
151
153
  height: panelHeight,
152
- ['z-index']: panelZIndex
153
154
  }"
154
155
  >
155
156
  <div
@@ -160,6 +161,7 @@ function closePanel() {
160
161
  {{ panelTitle }}
161
162
  </div>
162
163
  <i
164
+ ref="SlideInPanelManagerClose"
163
165
  class="icon icon-close"
164
166
  data-testid="slide-in-close"
165
167
  :tabindex="isOpen ? 0 : -1"
@@ -188,11 +190,11 @@ function closePanel() {
188
190
  left: 0;
189
191
  height: 100vh;
190
192
  width: 100vw;
193
+ z-index: z-index('slide-in');
191
194
  }
192
195
  .slide-in-glass-open {
193
- background-color: var(--body-bg);
196
+ background: var(--overlay-bg);
194
197
  display: block;
195
- opacity: 0.5;
196
198
  }
197
199
 
198
200
  .slide-in {
@@ -203,6 +205,7 @@ function closePanel() {
203
205
  transition: right 0.5s ease;
204
206
  border-left: 1px solid var(--border);
205
207
  background-color: var(--body-bg);
208
+ z-index: calc(z-index('slide-in') + 1);
206
209
  }
207
210
 
208
211
  .slide-in-open {
@@ -3,7 +3,7 @@ import { mapGetters, useStore } from 'vuex';
3
3
  import { defineAsyncComponent, ref, onMounted, onBeforeUnmount } from 'vue';
4
4
  import day from 'dayjs';
5
5
  import isEmpty from 'lodash/isEmpty';
6
- import { dasherize, ucFirst } from '@shell/utils/string';
6
+ import { dasherize, ucFirst, randomStr } from '@shell/utils/string';
7
7
  import { get, clone } from '@shell/utils/object';
8
8
  import { removeObject } from '@shell/utils/array';
9
9
  import { Checkbox } from '@components/Form/Checkbox';
@@ -130,7 +130,7 @@ export default {
130
130
  },
131
131
  groupSort: {
132
132
  // Field to order groups by, defaults to groupBy
133
- type: Array,
133
+ type: String,
134
134
  default: null
135
135
  },
136
136
 
@@ -735,7 +735,7 @@ export default {
735
735
  grp.rows.forEach((row) => {
736
736
  const rowData = {
737
737
  row,
738
- key: this.get(row, this.keyField),
738
+ key: this.get(row, this.keyField) ?? randomStr(),
739
739
  showSubRow: this.showSubRow(row, this.keyField),
740
740
  canRunBulkActionOfInterest: this.canRunBulkActionOfInterest(row),
741
741
  columns: []
@@ -1038,7 +1038,7 @@ export default {
1038
1038
  handleActionButtonClick(i, event) {
1039
1039
  // Each row in the table gets its own ref with
1040
1040
  // a number based on its index. If you are using
1041
- // an ActionMenu that doen't have a dependency on Vuex,
1041
+ // an ActionMenu that doesn't have a dependency on Vuex,
1042
1042
  // these refs are useful because you can reuse the
1043
1043
  // same ActionMenu component on a page with many different
1044
1044
  // target elements in a list,
@@ -1398,7 +1398,7 @@ export default {
1398
1398
  </slot>
1399
1399
  <template
1400
1400
  v-for="(row, i) in groupedRows.rows"
1401
- :key="i"
1401
+ :key="row.key"
1402
1402
  >
1403
1403
  <slot
1404
1404
  name="main-row"
@@ -344,7 +344,6 @@ export default {
344
344
  return;
345
345
  }
346
346
 
347
- e.preventDefault();
348
347
  e.stopPropagation();
349
348
 
350
349
  this.prevNode = node;
@@ -66,6 +66,19 @@ export default {
66
66
  resource: {
67
67
  type: Object,
68
68
  default: () => {}
69
+ },
70
+
71
+ showExtensionTabs: {
72
+ type: Boolean,
73
+ default: true,
74
+ },
75
+ /**
76
+ * Inherited global identifier prefix for tests
77
+ * Define a term based on the parent component to avoid conflicts on multiple components
78
+ */
79
+ componentTestid: {
80
+ type: String,
81
+ default: 'tabbed'
69
82
  }
70
83
  },
71
84
 
@@ -92,7 +105,7 @@ export default {
92
105
  },
93
106
 
94
107
  data() {
95
- const extensionTabs = getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.RESOURCE_DETAIL, this.$route, this, this.extensionParams) || [];
108
+ const extensionTabs = this.showExtensionTabs ? getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.RESOURCE_DETAIL, this.$route, this, this.extensionParams) || [] : [];
96
109
 
97
110
  const parsedExtTabs = extensionTabs.map((item) => {
98
111
  return {
@@ -248,8 +261,12 @@ export default {
248
261
 
249
262
  <template>
250
263
  <div
251
- :class="{'side-tabs': !!sideTabs, 'tabs-only': tabsOnly }"
252
- data-testid="tabbed"
264
+ class="tabbed-container"
265
+ :class="{
266
+ 'side-tabs': !!sideTabs,
267
+ 'tabs-only': tabsOnly
268
+ }"
269
+ :data-testid="componentTestid"
253
270
  >
254
271
  <ul
255
272
  v-if="!hideTabs"
@@ -257,7 +274,7 @@ export default {
257
274
  role="tablist"
258
275
  class="tabs"
259
276
  :class="{'clearfix':!sideTabs, 'vertical': sideTabs, 'horizontal': !sideTabs}"
260
- data-testid="tabbed-block"
277
+ :data-testid="`${componentTestid}-block`"
261
278
  tabindex="0"
262
279
  @keydown.right.prevent="selectNext(1)"
263
280
  @keydown.left.prevent="selectNext(-1)"
@@ -364,6 +381,10 @@ export default {
364
381
  </template>
365
382
 
366
383
  <style lang="scss" scoped>
384
+ .tabbed-container {
385
+ min-width: fit-content;
386
+ }
387
+
367
388
  .tabs {
368
389
  list-style-type: none;
369
390
  margin: 0;
@@ -536,6 +557,7 @@ export default {
536
557
  list-style: none;
537
558
  padding: 0;
538
559
  margin-top: auto;
560
+ z-index: z-index('default');
539
561
 
540
562
  li {
541
563
  display: flex;
@@ -545,16 +567,25 @@ export default {
545
567
  flex: 1 1;
546
568
  display: flex;
547
569
  justify-content: center;
570
+
571
+ &:focus-visible {
572
+ @include focus-outline;
573
+ }
548
574
  }
549
575
 
550
576
  button:first-of-type {
551
577
  border-top: solid 1px var(--border);
552
578
  border-right: solid 1px var(--border);
579
+ border-top-left-radius: 0;
553
580
  border-top-right-radius: 0;
581
+ border-bottom-right-radius: 0;
554
582
  }
555
583
  button:last-of-type {
556
584
  border-top: solid 1px var(--border);
585
+ border-top-right-radius: 0;
557
586
  border-top-left-radius: 0;
587
+ border-bottom-left-radius: 0;
588
+ border-bottom-right-radius: 0;
558
589
  }
559
590
  }
560
591
  }
@@ -0,0 +1,121 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import LazyImage from '@shell/components/LazyImage.vue';
3
+
4
+ describe('component: LazyImage.vue', () => {
5
+ const initialSrc = 'initial.jpg';
6
+ const src = 'test.jpg';
7
+ const errorSrc = 'error.jpg';
8
+
9
+ beforeEach(() => {
10
+ // Clear all mocks before each test to ensure test isolation
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+ it('renders the initial source image', () => {
15
+ const wrapper = mount(LazyImage, {
16
+ propsData: {
17
+ initialSrc,
18
+ src,
19
+ errorSrc
20
+ },
21
+ });
22
+
23
+ const img = wrapper.find('img');
24
+
25
+ expect(img.attributes('src')).toBe(initialSrc);
26
+ });
27
+
28
+ it('does not load the main src image if not in viewport', async() => {
29
+ const wrapper = mount(LazyImage, {
30
+ propsData: {
31
+ initialSrc,
32
+ src,
33
+ errorSrc
34
+ },
35
+ });
36
+
37
+ const callback = window.IntersectionObserver.mock.calls[0][0];
38
+
39
+ // eslint-disable-next-line node/no-callback-literal
40
+ callback([{ isIntersecting: false }]);
41
+ await wrapper.vm.$nextTick();
42
+
43
+ const img = wrapper.find('img');
44
+
45
+ expect(img.attributes('src')).toBe(initialSrc);
46
+ });
47
+
48
+ it('loads the main src image when it enters the viewport', async() => {
49
+ const wrapper = mount(LazyImage, {
50
+ propsData: {
51
+ initialSrc,
52
+ src,
53
+ errorSrc
54
+ },
55
+ });
56
+
57
+ // Manually trigger the intersection observer
58
+ const callback = window.IntersectionObserver.mock.calls[0][0];
59
+
60
+ // eslint-disable-next-line node/no-callback-literal
61
+ callback([{ isIntersecting: true }]);
62
+
63
+ await wrapper.vm.$nextTick();
64
+
65
+ const img = wrapper.find('img');
66
+
67
+ expect(img.attributes('src')).toBe(src);
68
+ });
69
+
70
+ it('loads the error image if the main src image fails to load', async() => {
71
+ const wrapper = mount(LazyImage, {
72
+ propsData: {
73
+ initialSrc,
74
+ src,
75
+ errorSrc
76
+ },
77
+ });
78
+
79
+ // Manually trigger the intersection observer
80
+ const callback = window.IntersectionObserver.mock.calls[0][0];
81
+
82
+ // eslint-disable-next-line node/no-callback-literal
83
+ callback([{ isIntersecting: true }]);
84
+
85
+ await wrapper.vm.$nextTick();
86
+
87
+ const img = wrapper.find('img');
88
+
89
+ img.trigger('error');
90
+
91
+ await wrapper.vm.$nextTick();
92
+
93
+ expect(img.attributes('src')).toBe(errorSrc);
94
+ });
95
+
96
+ it('loads a new src image if the src prop changes', async() => {
97
+ const wrapper = mount(LazyImage, {
98
+ propsData: {
99
+ initialSrc,
100
+ src,
101
+ errorSrc
102
+ },
103
+ });
104
+
105
+ // Manually trigger the intersection observer
106
+ const callback = window.IntersectionObserver.mock.calls[0][0];
107
+
108
+ // eslint-disable-next-line node/no-callback-literal
109
+ callback([{ isIntersecting: true }]);
110
+
111
+ await wrapper.vm.$nextTick();
112
+
113
+ const newSrc = 'new.jpg';
114
+
115
+ await wrapper.setProps({ src: newSrc });
116
+
117
+ const img = wrapper.find('img');
118
+
119
+ expect(img.attributes('src')).toBe(newSrc);
120
+ });
121
+ });
@@ -4,7 +4,7 @@ import PromptRestore from '@shell/components/PromptRestore.vue';
4
4
  import { createStore } from 'vuex';
5
5
  import { ExtendedVue, Vue } from 'vue/types/vue';
6
6
  import { DefaultProps } from 'vue/types/options';
7
- import { CAPI, NORMAN } from '@shell/config/types';
7
+ import { CAPI } from '@shell/config/types';
8
8
  import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
9
9
 
10
10
  const RKE2_CLUSTER_NAME = 'rke2_cluster_name';
@@ -33,32 +33,6 @@ const RKE2_FAILED_SNAPSHOT = {
33
33
  name: 'rke2_name_3'
34
34
  };
35
35
 
36
- const RKE1_CLUSTER_NAME = 'rke1_cluster_name';
37
- const RKE1_SUCCESSFUL_SNAPSHOT_1 = {
38
- clusterId: RKE1_CLUSTER_NAME,
39
- type: NORMAN.ETCD_BACKUP,
40
- state: STATES_ENUM.ACTIVE,
41
- created: 'Thu Jul 30 2023 11:11:39',
42
- id: 'rke1_id_1',
43
- name: 'rke1_name_1'
44
- };
45
- const RKE1_SUCCESSFUL_SNAPSHOT_2 = {
46
- clusterId: RKE1_CLUSTER_NAME,
47
- type: NORMAN.ETCD_BACKUP,
48
- state: STATES_ENUM.ACTIVE,
49
- created: 'Thu Jul 30 2022 11:11:39',
50
- id: 'rke1_id_2',
51
- name: 'rke1_name_2'
52
- };
53
- const RKE1_WITH_ERROR_SNAPSHOT = {
54
- clusterId: RKE1_CLUSTER_NAME,
55
- type: NORMAN.ETCD_BACKUP,
56
- state: STATES_ENUM.ERROR,
57
- created: 'Thu Jul 30 2021 11:11:39',
58
- id: 'rke1_id_3',
59
- name: 'rke1_name_3'
60
- };
61
-
62
36
  describe('component: PromptRestore', () => {
63
37
  const rke2TestCases = [
64
38
  [[], 0],
@@ -98,42 +72,4 @@ describe('component: PromptRestore', () => {
98
72
 
99
73
  expect(wrapper.vm.clusterSnapshots).toHaveLength(expected);
100
74
  });
101
-
102
- const rke1TestCases = [
103
- [[], 0],
104
- [[RKE1_WITH_ERROR_SNAPSHOT], 0],
105
- [[RKE1_SUCCESSFUL_SNAPSHOT_1], 1],
106
- [[RKE1_SUCCESSFUL_SNAPSHOT_1, RKE1_SUCCESSFUL_SNAPSHOT_2], 2],
107
- [[RKE1_WITH_ERROR_SNAPSHOT, RKE1_SUCCESSFUL_SNAPSHOT_1, RKE1_SUCCESSFUL_SNAPSHOT_2], 2]
108
- ];
109
-
110
- it.each(rke1TestCases)('should list RKE1 snapshots properly', async(snapShots, expected) => {
111
- const store = createStore({
112
- modules: {
113
- 'action-menu': {
114
- namespaced: true,
115
- state: {
116
- showPromptRestore: true,
117
- toRestore: [{
118
- type: CAPI.RANCHER_CLUSTER,
119
- metadata: { name: RKE1_CLUSTER_NAME },
120
- snapShots
121
- }]
122
- },
123
- },
124
- },
125
- getters: { 'i18n/t': () => jest.fn(), 'prefs/get': () => jest.fn() },
126
- actions: { 'rancher/findAll': jest.fn().mockResolvedValue(snapShots) }
127
- });
128
-
129
- const wrapper = shallowMount(
130
- PromptRestore as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>,
131
- { global: { mocks: { $store: store } } }
132
- );
133
-
134
- await wrapper.vm.fetchSnapshots();
135
- await nextTick();
136
-
137
- expect(wrapper.vm.clusterSnapshots).toHaveLength(expected);
138
- });
139
75
  });
@@ -0,0 +1,115 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import RichTranslation from '../RichTranslation.vue';
3
+ import { createStore } from 'vuex';
4
+ import { h } from 'vue';
5
+
6
+ // Mock the i18n store getter
7
+ const mockI18nStore = createStore({
8
+ getters: {
9
+ 'i18n/t': () => (key: string, args: any, noMarkup: boolean) => {
10
+ const translations: Record<string, string> = {
11
+ 'test.simple': 'Hello World',
12
+ 'test.html': 'This is <b>bold</b> and <i>italic</i>.',
13
+ 'test.custom': 'This has a <customLink>link</customLink> and <anotherTag/>.',
14
+ 'test.mixed': 'Text before <tag1>content1</tag1> text in middle <tag2/> text after.',
15
+ 'test.noString': 123,
16
+ };
17
+
18
+ return translations[key] || key;
19
+ },
20
+ },
21
+ });
22
+
23
+ describe('richTranslation', () => {
24
+ it('renders a simple translation correctly', () => {
25
+ const wrapper = mount(RichTranslation, {
26
+ props: { k: 'test.simple' },
27
+ global: { plugins: [mockI18nStore] },
28
+ });
29
+
30
+ expect(wrapper.text()).toBe('Hello World');
31
+ expect(wrapper.html()).toContain('<span>Hello World</span>');
32
+ });
33
+
34
+ it('renders HTML tags correctly', () => {
35
+ const wrapper = mount(RichTranslation, {
36
+ props: { k: 'test.html' },
37
+ global: { plugins: [mockI18nStore] },
38
+ });
39
+
40
+ expect(wrapper.html()).toContain('<span><span>This is </span><b>bold</b><span> and </span><i>italic</i><span>.</span></span>');
41
+ expect(wrapper.find('b').exists()).toBe(true);
42
+ expect(wrapper.find('i').exists()).toBe(true);
43
+ });
44
+
45
+ it('renders custom components via slots (enclosing tag)', () => {
46
+ const wrapper = mount(RichTranslation, {
47
+ props: { k: 'test.custom' },
48
+ slots: { customLink: ({ content }: { content: string }) => h('a', { href: '/test' }, content) },
49
+ global: { plugins: [mockI18nStore] },
50
+ });
51
+
52
+ expect(wrapper.html()).toContain('<a href="/test">link</a>');
53
+ expect(wrapper.find('a').text()).toBe('link');
54
+ });
55
+
56
+ it('renders custom components via slots (self-closing tag)', () => {
57
+ const wrapper = mount(RichTranslation, {
58
+ props: { k: 'test.custom' },
59
+ slots: { anotherTag: () => h('span', { class: 'self-closed' }, 'Self-closed content') },
60
+ global: { plugins: [mockI18nStore] },
61
+ });
62
+
63
+ expect(wrapper.html()).toContain('<span class="self-closed">Self-closed content</span>');
64
+ expect(wrapper.find('.self-closed').text()).toBe('Self-closed content');
65
+ });
66
+
67
+ it('handles mixed content with multiple custom components', () => {
68
+ const wrapper = mount(RichTranslation, {
69
+ props: { k: 'test.mixed' },
70
+ slots: {
71
+ tag1: ({ content }: { content: string }) => h('strong', {}, content),
72
+ tag2: () => h('em', {}, 'emphasized'),
73
+ },
74
+ global: { plugins: [mockI18nStore] },
75
+ });
76
+
77
+ expect(wrapper.html()).toContain('<span>Text before </span><strong>content1</strong><span> text in middle </span><em>emphasized</em><span> text after.</span>');
78
+ expect(wrapper.find('strong').text()).toBe('content1');
79
+ expect(wrapper.find('em').text()).toBe('emphasized');
80
+ });
81
+
82
+ it('renders correctly when translation is not a string', () => {
83
+ const wrapper = mount(RichTranslation, {
84
+ props: { k: 'test.noString' },
85
+ global: { plugins: [mockI18nStore] },
86
+ });
87
+
88
+ expect(wrapper.text()).toBe('123');
89
+ expect(wrapper.html()).toContain('<span>123</span>');
90
+ });
91
+
92
+ it('uses the specified root tag', () => {
93
+ const wrapper = mount(RichTranslation, {
94
+ props: {
95
+ k: 'test.simple',
96
+ tag: 'div',
97
+ },
98
+ global: { plugins: [mockI18nStore] },
99
+ });
100
+
101
+ expect(wrapper.html()).toContain('<div><span>Hello World</span></div>');
102
+ expect(wrapper.find('div').exists()).toBe(true);
103
+ expect(wrapper.find('span').exists()).toBe(true); // Inner span for content
104
+ });
105
+
106
+ it('falls back to raw tag content if slot is not provided for enclosing tag', () => {
107
+ const wrapper = mount(RichTranslation, {
108
+ props: { k: 'test.custom' }, // Contains <customLink> and <anotherTag/>
109
+ // No slots provided
110
+ global: { plugins: [mockI18nStore] },
111
+ });
112
+
113
+ expect(wrapper.find('a').exists()).toBe(false); // Should not render as <a>
114
+ });
115
+ });
@@ -213,6 +213,7 @@ function toPercent(value, min, max) {
213
213
  .fleet-status {
214
214
  display: flex;
215
215
  width: 100%;
216
+ min-width: fit-content;
216
217
  border: 1px solid var(--border);
217
218
  border-radius: 10px
218
219
  }
@@ -227,6 +228,9 @@ function toPercent(value, min, max) {
227
228
  width: 100%;
228
229
  padding: 15px;
229
230
 
231
+ .title {
232
+ margin-right: 16px;
233
+ }
230
234
  }
231
235
 
232
236
  .count {