@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.
- package/assets/brand/classic/metadata.json +3 -0
- package/assets/styles/app.scss +1 -0
- package/assets/styles/base/_color.scss +16 -0
- package/assets/styles/base/_helpers.scss +10 -0
- package/assets/styles/base/_variables.scss +18 -12
- package/assets/styles/fonts/_icons.scss +1 -32
- package/assets/styles/global/_layout.scss +1 -1
- package/assets/styles/themes/_dark.scss +262 -258
- package/assets/styles/themes/_light.scss +538 -509
- package/assets/styles/themes/_modern.scss +914 -0
- package/assets/translations/en-us.yaml +110 -29
- package/chart/__tests__/S3.test.ts +2 -1
- package/cloud-credential/generic.vue +18 -10
- package/cloud-credential/harvester.vue +1 -9
- package/components/AdvancedSection.vue +8 -0
- package/components/ChartReadme.vue +17 -7
- package/components/CodeMirror.vue +1 -1
- package/components/Drawer/Chrome.vue +0 -1
- package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +27 -28
- package/components/Drawer/ResourceDetailDrawer/composables.ts +4 -24
- package/components/Drawer/ResourceDetailDrawer/index.vue +18 -4
- package/components/InstallHelmCharts.vue +656 -0
- package/components/LazyImage.vue +60 -4
- package/components/Loading.vue +1 -1
- package/components/LocaleSelector.vue +7 -2
- package/components/Markdown.vue +4 -0
- package/components/PaginatedResourceTable.vue +46 -1
- package/components/PromptRestore.vue +22 -44
- package/components/Resource/Detail/Masthead/composable.ts +16 -0
- package/components/Resource/Detail/Masthead/index.vue +37 -0
- package/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts +10 -2
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +26 -7
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +8 -1
- package/components/Resource/Detail/Metadata/KeyValue.vue +12 -10
- package/components/Resource/Detail/Metadata/Rectangle.vue +3 -1
- package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +10 -17
- package/components/Resource/Detail/Metadata/composables.ts +9 -7
- package/components/Resource/Detail/Metadata/index.vue +17 -2
- package/components/Resource/Detail/Page.vue +35 -21
- package/components/Resource/Detail/SpacedRow.vue +1 -1
- package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +8 -9
- package/components/Resource/Detail/TitleBar/composables.ts +5 -5
- package/components/Resource/Detail/TitleBar/index.vue +12 -3
- package/components/ResourceDetail/Masthead/legacy.vue +1 -1
- package/components/ResourceDetail/index.vue +569 -72
- package/components/ResourceList/index.vue +1 -0
- package/components/ResourceTable.vue +6 -1
- package/components/ResourceYaml.vue +1 -1
- package/components/RichTranslation.vue +106 -0
- package/components/SlideInPanelManager.vue +13 -10
- package/components/SortableTable/index.vue +5 -5
- package/components/SortableTable/selection.js +0 -1
- package/components/Tabbed/index.vue +35 -4
- package/components/__tests__/LazyImage.spec.ts +121 -0
- package/components/__tests__/PromptRestore.test.ts +1 -65
- package/components/__tests__/RichTranslation.test.ts +115 -0
- package/components/fleet/FleetStatus.vue +4 -0
- package/components/fleet/dashboard/ResourcePanel.vue +2 -1
- package/components/form/ClusterAppearance.vue +5 -0
- package/components/form/FileImageSelector.vue +1 -1
- package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
- package/components/form/NameNsDescription.vue +1 -0
- package/components/form/Networking.vue +24 -19
- package/components/form/ProjectMemberEditor.vue +1 -1
- package/components/form/ResourceLabeledSelect.vue +22 -8
- package/components/form/ResourceTabs/index.vue +20 -0
- package/components/form/SecretSelector.vue +9 -0
- package/components/form/SelectOrCreateAuthSecret.vue +6 -3
- package/components/form/__tests__/Networking.test.ts +116 -0
- package/components/form/labeled-select-utils/labeled-select-pagination.ts +3 -38
- package/components/formatter/FleetApplicationSource.vue +25 -17
- package/components/formatter/PodImages.vue +1 -1
- package/components/formatter/__tests__/LiveDate.test.ts +10 -2
- package/components/google/AccountAccess.vue +44 -46
- package/components/nav/Favorite.vue +4 -0
- package/components/nav/Group.vue +4 -1
- package/components/nav/NotificationCenter/Notification.vue +1 -27
- package/components/nav/WindowManager/index.vue +3 -3
- package/composables/resources.ts +2 -2
- package/config/labels-annotations.js +3 -2
- package/config/pagination-table-headers.js +8 -1
- package/config/product/explorer.js +27 -2
- package/config/product/manager.js +0 -1
- package/config/query-params.js +10 -0
- package/config/router/routes.js +21 -1
- package/config/system-namespaces.js +1 -1
- package/config/table-headers.js +30 -1
- package/config/types.js +1 -1
- package/config/version.js +1 -1
- package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +11 -0
- package/detail/__tests__/workload.test.ts +164 -0
- package/detail/configmap.vue +33 -75
- package/detail/projectsecret.vue +11 -0
- package/detail/provisioning.cattle.io.cluster.vue +351 -369
- package/detail/secret.vue +49 -308
- package/detail/workload/index.vue +38 -21
- package/dialog/InstallExtensionDialog.vue +8 -5
- package/dialog/RotateEncryptionKeyDialog.vue +10 -30
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
- package/edit/auth/ldap/__tests__/config.test.ts +14 -0
- package/edit/auth/ldap/config.vue +24 -0
- package/edit/compliance.cattle.io.clusterscan.vue +1 -1
- package/edit/configmap.vue +4 -1
- package/edit/fleet.cattle.io.gitrepo.vue +5 -6
- package/edit/fleet.cattle.io.helmop.vue +78 -56
- package/edit/logging.banzaicloud.io.output/index.vue +1 -1
- package/edit/logging.banzaicloud.io.output/providers/awsElasticsearch.vue +5 -6
- package/edit/networking.k8s.io.ingress/Certificate.vue +20 -22
- package/edit/networking.k8s.io.ingress/DefaultBackend.vue +8 -3
- package/edit/networking.k8s.io.ingress/Rule.vue +2 -5
- package/edit/networking.k8s.io.ingress/RulePath.vue +17 -11
- package/edit/networking.k8s.io.ingress/__tests__/Certificate.test.ts +165 -0
- package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +11 -10
- package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -3
- package/edit/networking.k8s.io.networkpolicy/index.vue +17 -17
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +3 -2
- package/edit/provisioning.cattle.io.cluster/rke2.vue +123 -61
- package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +9 -7
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +22 -13
- package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +10 -12
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +39 -38
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +41 -19
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +16 -3
- package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +32 -33
- package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryMirrors.vue +9 -10
- package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +1 -3
- package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +16 -9
- package/edit/secret/basic.vue +1 -0
- package/edit/secret/index.vue +126 -15
- package/edit/workload/index.vue +5 -14
- package/list/projectsecret.vue +345 -0
- package/list/provisioning.cattle.io.cluster.vue +1 -69
- package/list/secret.vue +109 -0
- package/machine-config/__tests__/vmwarevsphere.test.ts +5 -7
- package/machine-config/google.vue +9 -1
- package/machine-config/vmwarevsphere.vue +7 -17
- package/mixins/__tests__/brand.spec.ts +2 -2
- package/mixins/chart.js +0 -2
- package/mixins/create-edit-view/impl.js +10 -1
- package/mixins/resource-fetch-api-pagination.js +11 -12
- package/mixins/resource-fetch.js +3 -1
- package/models/__tests__/chart.test.ts +111 -80
- package/models/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
- package/models/__tests__/node.test.ts +7 -63
- package/models/catalog.cattle.io.app.js +1 -1
- package/models/catalog.cattle.io.operation.js +1 -1
- package/models/chart.js +36 -20
- package/models/cloudcredential.js +2 -163
- package/models/cluster/node.js +7 -7
- package/models/cluster.x-k8s.io.machine.js +3 -3
- package/models/cluster.x-k8s.io.machinedeployment.js +11 -2
- package/models/compliance.cattle.io.clusterscan.js +2 -2
- package/models/configmap.js +4 -0
- package/models/constraints.gatekeeper.sh.constraint.js +1 -1
- package/models/fleet-application.js +0 -17
- package/models/fleet.cattle.io.cluster.js +2 -2
- package/models/fleet.cattle.io.gitrepo.js +15 -1
- package/models/fleet.cattle.io.helmop.js +26 -22
- package/models/management.cattle.io.setting.js +4 -0
- package/models/persistentvolumeclaim.js +1 -1
- package/models/pod.js +2 -2
- package/models/provisioning.cattle.io.cluster.js +39 -67
- package/models/rke.cattle.io.etcdsnapshot.js +1 -1
- package/models/secret.js +161 -2
- package/models/storage.k8s.io.storageclass.js +2 -2
- package/models/workload.js +3 -3
- package/package.json +11 -10
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +1 -0
- package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +4 -1
- package/pages/c/_cluster/apps/charts/__tests__/AppChartCardFooter.spec.js +41 -0
- package/pages/c/_cluster/apps/charts/chart.vue +422 -174
- package/pages/c/_cluster/apps/charts/index.vue +46 -35
- package/pages/c/_cluster/apps/charts/install.vue +1 -1
- package/pages/c/_cluster/explorer/projectsecret.vue +24 -0
- package/pages/c/_cluster/fleet/__tests__/index.test.ts +608 -314
- package/pages/c/_cluster/fleet/index.vue +103 -45
- package/pages/c/_cluster/manager/cloudCredential/index.vue +2 -59
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +10 -3
- package/pages/c/_cluster/uiplugins/index.vue +36 -25
- package/plugins/dashboard-store/__tests__/normalize.test.ts +223 -0
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +191 -0
- package/plugins/dashboard-store/__tests__/utils/normalize-usecases.ts +1526 -0
- package/plugins/dashboard-store/actions.js +42 -22
- package/plugins/dashboard-store/normalize.js +29 -17
- package/plugins/dashboard-store/resource-class.js +83 -17
- package/plugins/steve/__tests__/getters.test.ts +1 -1
- package/plugins/steve/__tests__/subscribe.spec.ts +259 -1
- package/plugins/steve/getters.js +8 -2
- package/plugins/steve/resourceWatcher.js +10 -3
- package/plugins/steve/steve-pagination-utils.ts +14 -3
- package/plugins/steve/subscribe.js +192 -19
- package/plugins/steve/worker/web-worker.advanced.js +2 -0
- package/rancher-components/Card/Card.vue +0 -18
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.test.ts +15 -0
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +65 -0
- package/rancher-components/Pill/RcStatusBadge/index.ts +2 -0
- package/rancher-components/Pill/RcStatusBadge/types.ts +5 -0
- package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.test.ts +33 -0
- package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +75 -0
- package/rancher-components/Pill/RcStatusIndicator/index.ts +2 -0
- package/rancher-components/Pill/RcStatusIndicator/types.ts +7 -0
- package/rancher-components/Pill/types.ts +2 -0
- package/rancher-components/RcButton/RcButton.vue +1 -1
- package/rancher-components/RcDropdown/RcDropdown.test.ts +98 -0
- package/rancher-components/RcDropdown/RcDropdown.vue +5 -0
- package/rancher-components/RcDropdown/RcDropdownItem.vue +7 -1
- package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +2 -1
- package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +2 -1
- package/rancher-components/RcDropdown/useDropdownContext.ts +21 -0
- package/rancher-components/RcDropdown/useDropdownItem.ts +30 -1
- package/rancher-components/RcItemCard/RcItemCard.test.ts +20 -0
- package/rancher-components/RcItemCard/RcItemCard.vue +40 -6
- package/store/__tests__/catalog.test.ts +93 -1
- package/store/aws.js +19 -8
- package/store/catalog.js +8 -3
- package/types/kube/kube-api.ts +12 -0
- package/types/resources/settings.d.ts +1 -1
- package/types/shell/index.d.ts +643 -585
- package/types/store/pagination.types.ts +16 -6
- package/types/uiplugins.ts +73 -0
- package/utils/__tests__/back-off.test.ts +354 -0
- package/utils/__tests__/create-yaml.test.ts +235 -0
- package/utils/__tests__/kontainer.test.ts +19 -0
- package/utils/__tests__/uiplugins.test.ts +84 -0
- package/utils/back-off.ts +176 -0
- package/utils/create-yaml.js +103 -9
- package/utils/dynamic-importer.js +8 -0
- package/utils/kontainer.ts +3 -5
- package/utils/pagination-utils.ts +18 -0
- package/utils/style.ts +3 -0
- package/utils/uiplugins.ts +29 -2
- package/utils/validators/__tests__/setting.test.js +92 -0
- package/utils/validators/formRules/__tests__/index.test.ts +88 -7
- package/utils/validators/formRules/index.ts +83 -8
- package/utils/validators/setting.js +17 -0
- package/cloud-credential/__tests__/harvester.test.ts +0 -18
- package/components/ResourceDetail/__tests__/index.test.ts +0 -135
- package/components/ResourceDetail/legacy.vue +0 -562
- package/components/formatter/CloudCredExpired.vue +0 -69
- package/models/etcdbackup.js +0 -45
- package/pages/explorer/resource/detail/configmap.vue +0 -42
- package/pages/explorer/resource/detail/secret.vue +0 -50
- 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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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="
|
|
1401
|
+
:key="row.key"
|
|
1402
1402
|
>
|
|
1403
1403
|
<slot
|
|
1404
1404
|
name="main-row"
|
|
@@ -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
|
-
|
|
252
|
-
|
|
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="
|
|
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
|
|
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 {
|