@rancher/shell 3.0.5-rc.8 → 3.0.5

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 (199) hide show
  1. package/assets/styles/base/_color.scss +4 -1
  2. package/assets/styles/global/_tooltip.scss +7 -4
  3. package/assets/styles/themes/_dark.scss +11 -0
  4. package/assets/styles/themes/_light.scss +13 -1
  5. package/assets/styles/themes/_modern.scss +22 -0
  6. package/assets/translations/en-us.yaml +147 -19
  7. package/assets/translations/zh-hans.yaml +0 -1
  8. package/chart/monitoring/grafana/index.vue +8 -2
  9. package/components/ActionMenuShell.vue +3 -1
  10. package/components/Cron/CronExpressionEditor.vue +299 -0
  11. package/components/Cron/CronExpressionEditorModal.vue +247 -0
  12. package/components/Cron/CronTooltip.vue +87 -0
  13. package/components/Cron/types.ts +13 -0
  14. package/components/ForceDirectedTreeChart/composable.ts +11 -0
  15. package/components/PodSecurityAdmission.vue +2 -0
  16. package/components/PromptModal.vue +1 -1
  17. package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +1 -0
  18. package/components/Resource/Detail/CopyToClipboard.vue +78 -0
  19. package/components/Resource/Detail/FetchLoader/__tests__/composables.test.ts +69 -0
  20. package/components/Resource/Detail/FetchLoader/composables.ts +27 -0
  21. package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
  22. package/components/Resource/Detail/Metadata/Annotations/index.vue +1 -1
  23. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +13 -61
  24. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +33 -6
  25. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +24 -38
  26. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +25 -5
  27. package/components/Resource/Detail/Metadata/KeyValue.vue +12 -23
  28. package/components/Resource/Detail/Metadata/KeyValueRow.vue +144 -0
  29. package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -0
  30. package/components/Resource/Detail/Metadata/Labels/index.vue +1 -0
  31. package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +30 -32
  32. package/components/Resource/Detail/Metadata/__tests__/KeyValueRow.test.ts +108 -0
  33. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +0 -3
  34. package/components/Resource/Detail/Metadata/__tests__/index.test.ts +12 -5
  35. package/components/Resource/Detail/Metadata/composables.ts +1 -4
  36. package/components/Resource/Detail/Metadata/index.vue +1 -0
  37. package/components/Resource/Detail/Preview/Content.vue +63 -0
  38. package/components/Resource/Detail/Preview/Preview.vue +128 -0
  39. package/components/Resource/Detail/Preview/__tests__/Content.spec.ts +71 -0
  40. package/components/Resource/Detail/Preview/__tests__/Preview.spec.ts +121 -0
  41. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +141 -0
  42. package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +136 -0
  43. package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +245 -0
  44. package/components/Resource/Detail/ResourcePopover/index.vue +226 -0
  45. package/components/Resource/Detail/SpacedRow.vue +1 -0
  46. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +0 -5
  47. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
  48. package/components/Resource/Detail/TitleBar/composables.ts +1 -3
  49. package/components/Resource/Detail/TitleBar/index.vue +2 -29
  50. package/components/Resource/Detail/ViewOptions/composable.ts +9 -0
  51. package/components/Resource/Detail/ViewOptions/index.vue +41 -0
  52. package/components/Resource/Detail/__tests__/CopyToClipboard.spec.ts +82 -0
  53. package/components/ResourceDetail/Masthead/legacy.vue +0 -19
  54. package/components/ResourceDetail/index.vue +1 -26
  55. package/components/ResourceTable.vue +24 -0
  56. package/components/SortableTable/index.vue +7 -1
  57. package/components/SortableTable/paging.js +3 -0
  58. package/components/Tabbed/Tab.vue +43 -1
  59. package/components/Tabbed/index.vue +3 -1
  60. package/components/__tests__/Cron/CronExpressionEditor.test.ts +151 -0
  61. package/components/__tests__/Cron/CronExpressionEditorModal.test.ts +81 -0
  62. package/components/auth/login/saml.vue +86 -0
  63. package/components/form/LabeledSelect.vue +8 -8
  64. package/components/form/ProjectMemberEditor.vue +2 -0
  65. package/components/form/ResourceTabs/composable.ts +54 -0
  66. package/components/form/ResourceTabs/index.vue +10 -7
  67. package/components/form/Select.vue +13 -10
  68. package/components/form/__tests__/LabeledSelect.test.ts +133 -0
  69. package/components/form/__tests__/Select.test.ts +134 -0
  70. package/components/nav/Header.vue +6 -5
  71. package/composables/useExtensionManager.ts +17 -0
  72. package/config/home-links.js +12 -0
  73. package/config/labels-annotations.js +0 -1
  74. package/config/page-actions.js +0 -1
  75. package/config/product/explorer.js +3 -1
  76. package/config/product/fleet.js +2 -7
  77. package/config/product/manager.js +0 -5
  78. package/config/query-params.js +1 -0
  79. package/config/router/navigation-guards/clusters.js +2 -1
  80. package/config/router/navigation-guards/products.js +1 -1
  81. package/config/store.js +2 -0
  82. package/core/extension-manager-impl.js +518 -0
  83. package/core/plugins.js +35 -468
  84. package/core/types.ts +8 -2
  85. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +1 -0
  86. package/detail/catalog.cattle.io.app.vue +7 -4
  87. package/detail/fleet.cattle.io.bundle.vue +1 -5
  88. package/detail/fleet.cattle.io.cluster.vue +3 -2
  89. package/detail/fleet.cattle.io.gitrepo.vue +76 -49
  90. package/detail/fleet.cattle.io.helmop.vue +78 -49
  91. package/dialog/AddonConfigConfirmationDialog.vue +1 -1
  92. package/dialog/GenericPrompt.vue +1 -1
  93. package/dialog/ImportDialog.vue +9 -2
  94. package/dialog/InstallExtensionDialog.vue +18 -10
  95. package/dialog/SloDialog.vue +1 -1
  96. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
  97. package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
  98. package/edit/auth/oidc.vue +106 -6
  99. package/edit/auth/saml.vue +5 -5
  100. package/edit/cloudcredential.vue +31 -17
  101. package/edit/constraints.gatekeeper.sh.constraint/index.vue +10 -2
  102. package/edit/fleet.cattle.io.cluster.vue +19 -0
  103. package/edit/fleet.cattle.io.gitrepo.vue +23 -16
  104. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +12 -11
  105. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -1
  106. package/edit/provisioning.cattle.io.cluster/index.vue +14 -19
  107. package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -3
  108. package/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest.vue +1 -0
  109. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +1 -0
  110. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
  111. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +1 -0
  112. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -0
  113. package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +6 -0
  114. package/edit/resources.cattle.io.restore.vue +5 -8
  115. package/initialize/install-plugins.js +1 -3
  116. package/list/__tests__/workload.test.ts +1 -0
  117. package/list/workload.vue +8 -1
  118. package/machine-config/components/GCEImage.vue +6 -5
  119. package/machine-config/google.vue +11 -6
  120. package/mixins/__tests__/auth-config.test.ts +4 -6
  121. package/mixins/__tests__/chart.test.ts +139 -1
  122. package/mixins/auth-config.js +33 -10
  123. package/mixins/chart.js +58 -18
  124. package/models/__tests__/namespace.test.ts +69 -0
  125. package/models/apps.statefulset.js +8 -10
  126. package/models/chart.js +5 -1
  127. package/models/fleet-application.js +16 -46
  128. package/models/fleet.cattle.io.bundle.js +1 -38
  129. package/models/fleet.cattle.io.gitrepo.js +4 -0
  130. package/models/fleet.cattle.io.helmop.js +4 -0
  131. package/models/management.cattle.io.cluster.js +1 -1
  132. package/models/management.cattle.io.project.js +12 -0
  133. package/models/namespace.js +30 -0
  134. package/models/workload.js +4 -1
  135. package/package.json +10 -10
  136. package/pages/auth/login.vue +8 -3
  137. package/pages/auth/logout.vue +6 -5
  138. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +26 -11
  139. package/pages/c/_cluster/apps/charts/chart.vue +29 -20
  140. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  141. package/pages/c/_cluster/apps/charts/install.vue +6 -5
  142. package/pages/c/_cluster/explorer/tools/__tests__/index.test.ts +102 -12
  143. package/pages/c/_cluster/explorer/tools/index.vue +145 -254
  144. package/pages/c/_cluster/manager/cloudCredential/index.vue +18 -1
  145. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +12 -2
  146. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
  147. package/pages/c/_cluster/uiplugins/__tests__/index.spec.ts +318 -0
  148. package/pages/c/_cluster/uiplugins/index.vue +221 -363
  149. package/pages/home.vue +1 -9
  150. package/plugins/axios.js +3 -2
  151. package/plugins/dashboard-store/resource-class.js +49 -0
  152. package/plugins/ember-cookie.js +7 -3
  153. package/plugins/steve/subscribe.js +4 -2
  154. package/public/index.html +2 -1
  155. package/rancher-components/Card/Card.vue +1 -1
  156. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  157. package/rancher-components/Form/Radio/RadioButton.vue +1 -1
  158. package/rancher-components/Form/Radio/RadioGroup.vue +1 -1
  159. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -11
  160. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.test.ts +53 -0
  161. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +65 -0
  162. package/rancher-components/Pill/RcCounterBadge/index.ts +1 -0
  163. package/rancher-components/Pill/RcCounterBadge/types.ts +7 -0
  164. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +1 -1
  165. package/rancher-components/Pill/RcStatusBadge/index.ts +1 -1
  166. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -3
  167. package/rancher-components/Pill/RcStatusIndicator/types.ts +1 -1
  168. package/rancher-components/Pill/RcTag/RcTag.test.ts +64 -0
  169. package/rancher-components/Pill/RcTag/RcTag.vue +94 -0
  170. package/rancher-components/Pill/RcTag/index.ts +1 -0
  171. package/rancher-components/Pill/RcTag/types.ts +9 -0
  172. package/rancher-components/Pill/types.ts +1 -0
  173. package/rancher-components/RcItemCard/RcItemCard.vue +1 -0
  174. package/rancher-components/RcItemCard/RcItemCardAction.vue +12 -0
  175. package/scripts/test-plugins-build.sh +0 -1
  176. package/store/__tests__/catalog.test.ts +63 -0
  177. package/store/__tests__/cookies.test.ts +72 -0
  178. package/store/auth.js +33 -10
  179. package/store/catalog.js +2 -2
  180. package/store/cookies.ts +30 -0
  181. package/store/prefs.js +10 -5
  182. package/store/type-map.js +3 -15
  183. package/types/extension-manager.ts +26 -0
  184. package/types/shell/index.d.ts +123 -27
  185. package/utils/__tests__/product.test.ts +129 -0
  186. package/utils/__tests__/resource.test.ts +87 -0
  187. package/utils/alertmanagerconfig.js +2 -2
  188. package/utils/auth.js +4 -77
  189. package/utils/product.ts +39 -0
  190. package/utils/resource.ts +35 -0
  191. package/utils/select.js +0 -24
  192. package/utils/validators/formRules/__tests__/index.test.ts +3 -0
  193. package/utils/validators/formRules/index.ts +2 -1
  194. package/vue.config.js +1 -1
  195. package/components/Resource/Detail/Metadata/Rectangle.vue +0 -34
  196. package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +0 -24
  197. package/components/ResourceDetail/Masthead/__tests__/legacy.test.ts +0 -65
  198. package/utils/cookie-universal.js +0 -10
  199. /package/components/{ForceDirectedTreeChart.vue → ForceDirectedTreeChart/index.vue} +0 -0
@@ -0,0 +1,128 @@
1
+ <script lang="ts" setup>
2
+ import CopyToClipboard from '@shell/components/Resource/Detail/CopyToClipboard.vue';
3
+ import Content from '@shell/components/Resource/Detail/Preview/Content.vue';
4
+ import { useBasicSetupFocusTrap } from '@shell/composables/focusTrap';
5
+ import { computed, onMounted, ref } from 'vue';
6
+
7
+ export interface Props {
8
+ title: string;
9
+ value: string;
10
+ anchorElement: HTMLElement | null;
11
+ }
12
+
13
+ defineOptions({ inheritAttrs: false });
14
+
15
+ const props = defineProps<Props>();
16
+ const emit = defineEmits<{(e: 'close', keyboardExit: boolean): void}>();
17
+ const boundingRect = computed(() => props.anchorElement?.getBoundingClientRect());
18
+ const top = computed(() => `${ (boundingRect.value?.top || 0) - 28 }px`);
19
+ const right = computed(() => `${ (document.documentElement.clientWidth - (boundingRect.value?.left || 0)) + 16 }px`);
20
+ const containerRef = ref<HTMLElement | null>(null);
21
+ const escapePressed = ref(false);
22
+ const isMouseInteraction = ref(false);
23
+
24
+ const onFocusOut = (e: FocusEvent) => {
25
+ // Refocus the container if the user clicks a child element (copy to clipboard)
26
+ if (!escapePressed.value && containerRef.value?.contains(e.relatedTarget as Node)) {
27
+ if (isMouseInteraction.value) {
28
+ containerRef.value.focus();
29
+ }
30
+ } else {
31
+ emit('close', escapePressed.value);
32
+ }
33
+ };
34
+
35
+ const onKeydown = (event: KeyboardEvent) => {
36
+ if (event.key === 'Escape') {
37
+ escapePressed.value = true;
38
+ containerRef.value?.blur();
39
+ }
40
+ };
41
+
42
+ onMounted(() => {
43
+ containerRef.value?.focus();
44
+ });
45
+
46
+ useBasicSetupFocusTrap('#focus-trap-preview-container-element');
47
+
48
+ </script>
49
+ <template>
50
+ <Teleport to="#preview">
51
+ <div
52
+ id="focus-trap-preview-container-element"
53
+ ref="containerRef"
54
+ class="preview"
55
+ tabindex="-1"
56
+ @keydown="onKeydown"
57
+ @focusout="onFocusOut"
58
+ @mousedown="isMouseInteraction=true"
59
+ @mouseup="isMouseInteraction=false"
60
+ >
61
+ <div class="title">
62
+ {{ props.title }}
63
+ </div>
64
+ <Content
65
+ class="content"
66
+ :value="props.value"
67
+ />
68
+ <CopyToClipboard
69
+ class="copy-to-clipboard"
70
+ :value="props.value"
71
+ />
72
+ </div>
73
+ </Teleport>
74
+ </template>
75
+
76
+ <style lang="scss" scoped>
77
+ .preview-mouse-catcher {
78
+ cursor: default;
79
+ position: fixed;
80
+ top: 0;
81
+ bottom: 0;
82
+ left: 0;
83
+ right: 0;
84
+ z-index: 120;
85
+ }
86
+ .preview {
87
+ cursor: default;
88
+ position: fixed;
89
+ right: v-bind(right);
90
+ top: v-bind(top);
91
+ z-index: 121;
92
+ display: flex;
93
+ flex-direction: column;
94
+ min-width: 420px;
95
+ max-width: 550px;
96
+ max-height: 550px;
97
+
98
+ padding: 16px;
99
+
100
+ background-color: var(--body-bg);
101
+ border: 1px solid var(--border);
102
+ border-radius: var(--border-radius-md);
103
+
104
+ &:focus {
105
+ outline: none;
106
+ }
107
+
108
+ .title {
109
+ margin-bottom: 16px;
110
+
111
+ font-size: 14px;
112
+ font-style: normal;
113
+ font-weight: 400;
114
+ }
115
+
116
+ .content {
117
+ flex: 1;
118
+
119
+ overflow: scroll;
120
+ }
121
+
122
+ .copy-to-clipboard {
123
+ position: absolute;
124
+ right: -8px;
125
+ top: -8px;
126
+ }
127
+ }
128
+ </style>
@@ -0,0 +1,71 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import Content from '@shell/components/Resource/Detail/Preview/Content.vue';
3
+ import CodeMirror from '@shell/components/CodeMirror.vue';
4
+
5
+ describe('component: Resource Detail Preview Content', () => {
6
+ const toTextDirective = (el: any, binding: any) => {
7
+ el.textContent = binding.value;
8
+ };
9
+ const global = {
10
+ directives: {
11
+ cleanHtml: toTextDirective,
12
+ t: toTextDirective
13
+ },
14
+ stubs: { CodeMirror: true }
15
+ };
16
+
17
+ it('should display an empty message when value is empty', () => {
18
+ const wrapper = shallowMount(Content, {
19
+ props: { value: '' },
20
+ global
21
+ });
22
+
23
+ const span = wrapper.find('span');
24
+
25
+ expect(span.exists()).toBe(true);
26
+ expect(span.text()).toBe('detailText.empty');
27
+ });
28
+
29
+ it('should display a CodeMirror component for a valid JSON string', () => {
30
+ const jsonValue = '{"key":"value"}';
31
+ const wrapper = shallowMount(Content, {
32
+ props: { value: jsonValue },
33
+ global
34
+ });
35
+
36
+ const codeMirror = wrapper.findComponent(CodeMirror);
37
+
38
+ expect(codeMirror.exists()).toBe(true);
39
+
40
+ const expectedFormattedJson = JSON.stringify(JSON.parse(jsonValue), null, 2);
41
+
42
+ expect(codeMirror.props('value')).toBe(expectedFormattedJson);
43
+ });
44
+
45
+ it('should display a plain text message for a non-JSON string', () => {
46
+ const textValue = 'line 1';
47
+ const wrapper = shallowMount(Content, {
48
+ props: { value: textValue },
49
+ global
50
+ });
51
+
52
+ const span = wrapper.find('[data-testid="detail-top_html"]');
53
+
54
+ expect(span.exists()).toBe(true);
55
+ expect(span.text()).toBe('line 1');
56
+ expect(span.classes()).toContain('monospace');
57
+ });
58
+
59
+ it('should display a plain text message for a string that looks like JSON but is invalid', () => {
60
+ const invalidJson = '{';
61
+ const wrapper = shallowMount(Content, {
62
+ props: { value: invalidJson },
63
+ global
64
+ });
65
+
66
+ const span = wrapper.find('[data-testid="detail-top_html"]');
67
+
68
+ expect(span.exists()).toBe(true);
69
+ expect(span.text()).toBe('{');
70
+ });
71
+ });
@@ -0,0 +1,121 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import Preview from '@shell/components/Resource/Detail/Preview/Preview.vue';
3
+ import { useBasicSetupFocusTrap } from '@shell/composables/focusTrap';
4
+
5
+ jest.mock('@shell/utils/clipboard', () => ({ copyTextToClipboard: jest.fn() }));
6
+ jest.mock('@shell/composables/focusTrap', () => ({ useBasicSetupFocusTrap: jest.fn() }));
7
+
8
+ const teleportTarget = document.createElement('div');
9
+
10
+ teleportTarget.id = 'preview';
11
+ document.body.appendChild(teleportTarget);
12
+
13
+ describe('component: Resource Detail Preview', () => {
14
+ const global = {
15
+ stubs: {
16
+ Teleport: true,
17
+ Content: true,
18
+ CopyToClipboard: true,
19
+ },
20
+ };
21
+ const anchorElement = document.createElement('div');
22
+
23
+ // Mock getBoundingClientRect
24
+ anchorElement.getBoundingClientRect = () => ({
25
+ x: 100,
26
+ y: 100,
27
+ width: 50,
28
+ height: 20,
29
+ top: 100,
30
+ left: 100,
31
+ right: 150,
32
+ bottom: 120,
33
+ toJSON: () => ({}),
34
+ });
35
+
36
+ it('should render title and pass value to children', () => {
37
+ const wrapper = mount(Preview, {
38
+ props: {
39
+ title: 'My Test Title',
40
+ value: 'My test value',
41
+ anchorElement,
42
+ },
43
+ global
44
+ });
45
+
46
+ // Test title
47
+ const titleDiv = wrapper.find('.title');
48
+
49
+ expect(titleDiv.exists()).toBe(true);
50
+ expect(titleDiv.text()).toBe('My Test Title');
51
+
52
+ // Test props passed to Content
53
+ const content = wrapper.findComponent({ name: 'Content' });
54
+
55
+ expect(content.exists()).toBe(true);
56
+ expect(content.props('value')).toBe('My test value');
57
+
58
+ // Test props passed to CopyToClipboard
59
+ const copy = wrapper.findComponent({ name: 'CopyToClipboard' });
60
+
61
+ expect(copy.exists()).toBe(true);
62
+ expect(copy.props('value')).toBe('My test value');
63
+ });
64
+
65
+ it('should emit close on focusout', async() => {
66
+ const wrapper = mount(Preview, {
67
+ props: {
68
+ title: 'Test',
69
+ value: 'Value',
70
+ anchorElement,
71
+ },
72
+ global
73
+ });
74
+
75
+ const previewDiv = wrapper.find('.preview');
76
+
77
+ await previewDiv.trigger('focusout');
78
+
79
+ expect(wrapper.emitted('close')).toBeTruthy();
80
+ expect(wrapper.emitted('close')?.[0]).toStrictEqual([false]);
81
+ });
82
+
83
+ it('should emit close with true on Escape keydown', async() => {
84
+ const wrapper = mount(Preview, {
85
+ props: {
86
+ title: 'Test',
87
+ value: 'Value',
88
+ anchorElement,
89
+ },
90
+ global
91
+ });
92
+
93
+ const previewDiv = wrapper.find('.preview');
94
+
95
+ // Spy on blur to see if it's called
96
+ const blurSpy = jest.spyOn(previewDiv.element, 'blur');
97
+
98
+ await previewDiv.trigger('keydown.Escape');
99
+
100
+ expect(blurSpy).toHaveBeenCalledWith();
101
+
102
+ // Manually trigger focusout as jsdom doesn't do it automatically on blur()
103
+ await previewDiv.trigger('focusout');
104
+
105
+ expect(wrapper.emitted('close')).toBeTruthy();
106
+ expect(wrapper.emitted('close')?.[0]).toStrictEqual([true]);
107
+ });
108
+
109
+ it('should call focus trap composable', () => {
110
+ mount(Preview, {
111
+ props: {
112
+ title: 'Test',
113
+ value: 'Value',
114
+ anchorElement,
115
+ },
116
+ global
117
+ });
118
+
119
+ expect(useBasicSetupFocusTrap).toHaveBeenCalledWith('#focus-trap-preview-container-element');
120
+ });
121
+ });
@@ -0,0 +1,141 @@
1
+ <script lang="ts">
2
+ import Card from '@shell/components/Resource/Detail/Card/index.vue';
3
+ import { useStore } from 'vuex';
4
+ import ActionMenu from '@shell/components/ActionMenuShell.vue';
5
+ import { useI18n } from '@shell/composables/useI18n';
6
+
7
+ export interface Props {
8
+ resource: any;
9
+ }
10
+ </script>
11
+
12
+ <script setup lang="ts">
13
+ const emit = defineEmits(['action-invoked']);
14
+ const props = defineProps<Props>();
15
+ const store = useStore();
16
+ const i18n = useI18n(store);
17
+
18
+ const getGlanceItemValueId = (glanceItem: any): string => `value-${ glanceItem.label }:${ glanceItem.content }`.toLowerCase().replaceAll(' ', '');
19
+ </script>
20
+
21
+ <template>
22
+ <Card
23
+ class="resource-popover-card"
24
+ :title="resource.nameDisplay"
25
+ >
26
+ <template #heading-action>
27
+ <ActionMenu
28
+ :resource="props.resource"
29
+ :button-aria-label="i18n.t('component.resource.detail.glance.ariaLabel.actionMenu', { resource: props.resource.nameDisplay })"
30
+ data-testid="resource-popover-action-menu"
31
+ @action-invoked="emit('action-invoked')"
32
+ />
33
+ </template>
34
+
35
+ <div>
36
+ <div
37
+ v-for="(glanceItem, i) in props.resource.glance"
38
+ :key="glanceItem.label"
39
+ class="row"
40
+ >
41
+ <label
42
+ class="label text-deemphasized"
43
+ :for="getGlanceItemValueId(glanceItem)"
44
+ >
45
+ {{ glanceItem.label }}
46
+ </label>
47
+ <div
48
+ :id="getGlanceItemValueId(glanceItem)"
49
+ class="value"
50
+ >
51
+ <component
52
+ :is="glanceItem.formatter"
53
+ v-if="glanceItem.formatter"
54
+ v-bind="glanceItem.formatterOpts"
55
+ :id="i === 0 ? 'first-glance-item' : undefined"
56
+ :value="glanceItem.content"
57
+ />
58
+ <span
59
+ v-else
60
+ :id="i === 0 ? 'first-glance-item' : undefined"
61
+ >
62
+ {{ glanceItem.content }}
63
+ </span>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </Card>
68
+ </template>
69
+
70
+ <style lang="scss" scoped>
71
+ .resource-popover-card {
72
+ width: 288px;
73
+
74
+ .dropdown-item {
75
+ display: inline-block;
76
+ padding: 0;
77
+ margin: 0;
78
+ border: none;
79
+
80
+ &:hover {
81
+ background: none;
82
+ }
83
+ }
84
+
85
+ &:deep() {
86
+ .badge-state {
87
+ height: 20px;
88
+ font-size: 12px;
89
+ }
90
+
91
+ .heading {
92
+ height: 24px;
93
+
94
+ .title {
95
+ font-size: 16px;
96
+ font-weight: 600;
97
+ line-height: 24px;
98
+ }
99
+ }
100
+
101
+ .v-popper, .btn.role-link {
102
+ height: 24px;
103
+ min-height: initial;
104
+ padding: 0;
105
+ }
106
+
107
+ .v-popper {
108
+ padding: 0;
109
+ }
110
+
111
+ .btn.role-link {
112
+ color: #141419;
113
+ padding: 0 12px;
114
+ i {
115
+ display: inline-flex;
116
+ justify-content: center;
117
+ font-size: 12px;
118
+ width: 2.5px;
119
+ }
120
+
121
+ &:hover {
122
+ background-color: transparent
123
+ }
124
+ }
125
+ }
126
+
127
+ .row {
128
+ display: flex;
129
+ flex-direction: row;
130
+ line-height: 21px;
131
+
132
+ &:not(:first-of-type) {
133
+ margin-top: 4px;
134
+ }
135
+
136
+ .label {
137
+ width: 50%;
138
+ }
139
+ }
140
+ }
141
+ </style>
@@ -0,0 +1,136 @@
1
+ import { mount, VueWrapper } from '@vue/test-utils';
2
+ import { createStore } from 'vuex';
3
+ import ResourcePopoverCard from '@shell/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue';
4
+ import Card from '@shell/components/Resource/Detail/Card/index.vue';
5
+ import ActionMenu from '@shell/components/ActionMenuShell.vue';
6
+
7
+ const mockI18n = { t: (key: string) => key };
8
+
9
+ jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => mockI18n }));
10
+
11
+ describe('component: ResourcePopoverCard.vue', () => {
12
+ let wrapper: VueWrapper<any>;
13
+
14
+ const mockResource = {
15
+ nameDisplay: 'My Test Resource',
16
+ glance: [
17
+ {
18
+ label: 'Status',
19
+ content: 'Active',
20
+ },
21
+ {
22
+ label: 'Type',
23
+ content: 'SomeType',
24
+ formatter: 'SomeFormatterComponent',
25
+ formatterOpts: { opt1: 'value1' }
26
+ },
27
+ {
28
+ label: 'Created',
29
+ content: '2023-01-01',
30
+ }
31
+ ],
32
+ };
33
+
34
+ const store = createStore({ getters: { 'i18n/t': () => (key: string) => key } });
35
+
36
+ const SomeFormatterComponent = {
37
+ name: 'SomeFormatterComponent',
38
+ props: ['value', 'opt1', 'id'],
39
+ template: '<div :id="id">Formatted: {{ value }} with {{ opt1 }}</div>',
40
+ };
41
+
42
+ function createWrapper(resource: any) {
43
+ return mount(ResourcePopoverCard, {
44
+ props: { resource },
45
+ global: {
46
+ plugins: [store],
47
+ stubs: { ActionMenu: true },
48
+ components: { SomeFormatterComponent }
49
+ },
50
+ });
51
+ }
52
+
53
+ beforeEach(() => {
54
+ wrapper = createWrapper(mockResource);
55
+ });
56
+
57
+ afterEach(() => {
58
+ wrapper.unmount();
59
+ });
60
+
61
+ it('should render the Card with the correct title', () => {
62
+ const card = wrapper.findComponent(Card);
63
+
64
+ expect(card.exists()).toBe(true);
65
+ expect(card.props('title')).toBe(mockResource.nameDisplay);
66
+ });
67
+
68
+ it('should render the ActionMenu with the correct resource prop', () => {
69
+ const actionMenu = wrapper.findComponent(ActionMenu);
70
+
71
+ expect(actionMenu.exists()).toBe(true);
72
+ expect(actionMenu.props('resource')).toStrictEqual(mockResource);
73
+ });
74
+
75
+ it('should emit "action-invoked" when ActionMenu emits it', async() => {
76
+ const actionMenu = wrapper.findComponent(ActionMenu);
77
+
78
+ await actionMenu.vm.$emit('action-invoked');
79
+
80
+ expect(wrapper.emitted('action-invoked')).toHaveLength(1);
81
+ });
82
+
83
+ it('should render a row for each item in resource.glance', () => {
84
+ const rows = wrapper.findAll('.row');
85
+
86
+ expect(rows).toHaveLength(mockResource.glance.length);
87
+ });
88
+
89
+ it('should render the correct label and value for each glance item', () => {
90
+ const rows = wrapper.findAll('.row');
91
+
92
+ rows.forEach((row, i) => {
93
+ const glanceItem = mockResource.glance[i];
94
+ const label = row.find('label');
95
+ const value = row.find('.value');
96
+
97
+ expect(label.text()).toBe(glanceItem.label);
98
+ if (glanceItem.formatter) {
99
+ return;
100
+ }
101
+
102
+ expect(value.text()).toBe(glanceItem.content);
103
+ });
104
+ });
105
+
106
+ it('should render a dynamic component when a formatter is provided', () => {
107
+ const formatterComponent = wrapper.findComponent(SomeFormatterComponent);
108
+
109
+ expect(formatterComponent.exists()).toBe(true);
110
+ expect(formatterComponent.props('value')).toBe(mockResource.glance[1].content);
111
+ expect(formatterComponent.props('opt1')).toBe(mockResource.glance[1].formatterOpts.opt1);
112
+ });
113
+
114
+ it('should generate a unique ID for label `for` and value `id` attributes', () => {
115
+ const firstGlanceItem = mockResource.glance[0];
116
+ const expectedId = `value-${ firstGlanceItem.label }:${ firstGlanceItem.content }`.toLowerCase().replaceAll(' ', '');
117
+
118
+ const label = wrapper.find('label');
119
+ const value = wrapper.find('.value');
120
+
121
+ expect(label.attributes('for')).toBe(expectedId);
122
+ expect(value.attributes('id')).toBe(expectedId);
123
+ });
124
+
125
+ it('should add a specific ID to the first glance item value', () => {
126
+ const firstValueSpan = wrapper.find('#first-glance-item');
127
+
128
+ expect(firstValueSpan.exists()).toBe(true);
129
+ // This will be the span inside the first .value div
130
+ expect(firstValueSpan.text()).toBe(mockResource.glance[0].content);
131
+
132
+ const secondValue = wrapper.findAll('.value')[1];
133
+
134
+ expect(secondValue.find('#first-glance-item').exists()).toBe(false);
135
+ });
136
+ });