@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,245 @@
1
+ import { mount, RouterLinkStub } from '@vue/test-utils';
2
+ import { createStore } from 'vuex';
3
+ import ResourcePopover from '@shell/components/Resource/Detail/ResourcePopover/index.vue';
4
+
5
+ const mockI18n = { t: (key: string, args: any) => JSON.stringify({ key, args }) };
6
+ const mockFocusTrap = jest.fn();
7
+
8
+ jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => mockI18n }));
9
+ jest.mock('@shell/composables/focusTrap', () => ({ useWatcherBasedSetupFocusTrapWithDestroyIncluded: (...args: any) => mockFocusTrap(...args) }));
10
+
11
+ const mockResource = {
12
+ id: 'test-ns/test-pod',
13
+ type: 'pod',
14
+ nameDisplay: 'My Test Pod',
15
+ stateBackground: 'bg-success',
16
+ detailLocation: { name: 'pod-detail', params: { id: 'test-pod' } },
17
+ glance: [{ label: 'Status', content: 'Active' }],
18
+ parentNameOverride: 'Overridden Pod',
19
+ };
20
+
21
+ describe('component: ResourcePopover/index.vue', () => {
22
+ let store: any;
23
+ const mockClusterFind = jest.fn();
24
+ const mockSomethingFind = jest.fn();
25
+
26
+ const createWrapper = async(props: any = { type: 'pod', id: 'test-ns/test-pod' }) => {
27
+ store = createStore({
28
+ getters: {
29
+ 'i18n/t': () => (key: string) => key,
30
+ currentStore: () => () => 'cluster',
31
+ 'cluster/schemaFor': () => () => ({ id: 'pod' }),
32
+ 'type-map/labelFor': () => () => 'Pod',
33
+ },
34
+ actions: { 'cluster/find': mockClusterFind, 'something/find': mockSomethingFind }
35
+ });
36
+
37
+ const wrapper = mount(ResourcePopover, {
38
+ props,
39
+ global: {
40
+ plugins: [store],
41
+ stubs: {
42
+ 'v-dropdown': { name: 'v-dropdown', template: '<div><slot /><slot name="popper" /></div>' },
43
+ 'router-link': RouterLinkStub,
44
+ ResourcePopoverCard: {
45
+ name: 'ResourcePopoverCard',
46
+ template: '<div id="resource-popover-card" />'
47
+ },
48
+ RcStatusIndicator: true,
49
+ },
50
+ },
51
+ });
52
+
53
+ // Requires two so the fetch can be resolved
54
+ await wrapper.vm.$nextTick();
55
+ await wrapper.vm.$nextTick();
56
+
57
+ return wrapper;
58
+ };
59
+
60
+ // Reset mocks before each test
61
+ beforeEach(() => {
62
+ jest.clearAllMocks();
63
+ });
64
+
65
+ describe('data Fetching and Initial State', () => {
66
+ it('should show loading state initially', async() => {
67
+ const wrapper = await createWrapper(); // Override default fetch value
68
+
69
+ expect(wrapper.find('.display').exists()).toBe(false);
70
+ expect(wrapper.text()).toContain('...');
71
+ });
72
+
73
+ it('should call the fetch composable and dispatch a store action', async() => {
74
+ await createWrapper();
75
+
76
+ expect(mockClusterFind).toHaveBeenCalledWith(expect.objectContaining({}), expect.objectContaining({ type: 'pod', id: 'test-ns/test-pod' }));
77
+ });
78
+
79
+ it('should dispatch to the specified store', async() => {
80
+ await createWrapper({
81
+ type: 'pod', id: 'test-ns/test-pod', currentStore: 'something'
82
+ });
83
+
84
+ expect(mockSomethingFind).toHaveBeenCalledWith(expect.objectContaining({}), expect.objectContaining({ type: 'pod', id: 'test-ns/test-pod' }));
85
+ });
86
+
87
+ it('should render resource name and link when data is loaded', async() => {
88
+ mockClusterFind.mockReturnValue(mockResource);
89
+ const wrapper = await createWrapper();
90
+ const link = wrapper.findComponent(RouterLinkStub);
91
+
92
+ expect(link.exists()).toBe(true);
93
+ expect(link.text()).toBe(mockResource.nameDisplay);
94
+ expect(link.props('to')).toStrictEqual(mockResource.detailLocation);
95
+ });
96
+
97
+ it('should use `detailLocation` prop for router-link if provided', async() => {
98
+ const detailLocationProp = { name: 'custom-route' };
99
+
100
+ const wrapper = await createWrapper({
101
+ type: 'pod',
102
+ id: 'test-ns/test-pod',
103
+ detailLocation: detailLocationProp
104
+ });
105
+
106
+ const link = wrapper.findComponent(RouterLinkStub);
107
+
108
+ expect(link.props('to')).toStrictEqual(detailLocationProp);
109
+ });
110
+ });
111
+
112
+ describe('computed Properties', () => {
113
+ it('should return empty resourceTypeLabel if no data', async() => {
114
+ mockClusterFind.mockReturnValue(null);
115
+
116
+ const wrapper = await createWrapper();
117
+
118
+ expect(wrapper.vm.resourceTypeLabel).toBe('');
119
+ });
120
+
121
+ it('should return resourceTypeLabel from type-map getter', async() => {
122
+ const resourceWithoutOverride = { ...mockResource, parentNameOverride: undefined };
123
+
124
+ mockClusterFind.mockReturnValue(resourceWithoutOverride);
125
+ const wrapper = await createWrapper();
126
+
127
+ expect(wrapper.vm.resourceTypeLabel).toBe('Pod');
128
+ });
129
+
130
+ it('should generate correct aria-labels', async() => {
131
+ mockClusterFind.mockReturnValue(mockResource);
132
+ const wrapper = await createWrapper();
133
+
134
+ const expectedAriaLabel = JSON.stringify({ key: 'component.resource.detail.glance.ariaLabel.showDetails', args: { name: mockResource.nameDisplay, resource: mockResource.parentNameOverride } });
135
+ const dropdown = wrapper.findComponent({ name: 'v-dropdown' });
136
+ const button = wrapper.find('.focus-button');
137
+
138
+ expect(dropdown.attributes('aria-label')).toBe(expectedAriaLabel);
139
+ expect(button.attributes('aria-label')).toBe(expectedAriaLabel);
140
+ });
141
+ });
142
+
143
+ describe('popover Visibility', () => {
144
+ it('should not show popover card initially', async() => {
145
+ mockClusterFind.mockReturnValue(mockResource);
146
+ const wrapper = await createWrapper();
147
+
148
+ expect(wrapper.findComponent({ name: 'ResourcePopoverCard' }).exists()).toBe(false);
149
+ });
150
+
151
+ it('should show popover on mouseenter', async() => {
152
+ mockClusterFind.mockReturnValue(mockResource);
153
+ const wrapper = await createWrapper();
154
+
155
+ await wrapper.find('.display').trigger('mouseenter');
156
+ expect(wrapper.vm.showPopover).toBe(true);
157
+ await wrapper.vm.$nextTick();
158
+ expect(wrapper.findComponent({ name: 'ResourcePopoverCard' }).exists()).toBe(true);
159
+ });
160
+
161
+ it('should hide popover on mouseleave', async() => {
162
+ mockClusterFind.mockReturnValue(mockResource);
163
+ const wrapper = await createWrapper();
164
+
165
+ await wrapper.find('.display').trigger('mouseenter');
166
+ expect(wrapper.vm.showPopover).toBe(true);
167
+
168
+ await wrapper.find('.resource-popover').trigger('mouseleave');
169
+ expect(wrapper.vm.showPopover).toBe(false);
170
+ await wrapper.vm.$nextTick();
171
+ expect(wrapper.findComponent({ name: 'ResourcePopoverCard' }).exists()).toBe(false);
172
+ });
173
+
174
+ it('should show popover on focus button click', async() => {
175
+ mockClusterFind.mockReturnValue(mockResource);
176
+ const wrapper = await createWrapper();
177
+
178
+ await wrapper.find('.focus-button').trigger('click');
179
+ expect(wrapper.vm.showPopover).toBe(true);
180
+ expect(wrapper.vm.focusOpen).toBe(true);
181
+ await wrapper.vm.$nextTick();
182
+ expect(wrapper.findComponent({ name: 'ResourcePopoverCard' }).exists()).toBe(true);
183
+ });
184
+
185
+ it('should hide popover when action is invoked from card', async() => {
186
+ mockClusterFind.mockReturnValue(mockResource);
187
+ const wrapper = await createWrapper();
188
+
189
+ await wrapper.find('.display').trigger('mouseenter');
190
+ await wrapper.vm.$nextTick();
191
+
192
+ const card = wrapper.findComponent({ name: 'ResourcePopoverCard' });
193
+
194
+ await card.vm.$emit('action-invoked');
195
+ expect(wrapper.vm.showPopover).toBe(false);
196
+ await wrapper.vm.$nextTick();
197
+ expect(wrapper.findComponent({ name: 'ResourcePopoverCard' }).exists()).toBe(false);
198
+ });
199
+
200
+ it('should hide popover on Escape keydown', async() => {
201
+ mockClusterFind.mockReturnValue(mockResource);
202
+ const wrapper = await createWrapper();
203
+
204
+ await wrapper.find('.focus-button').trigger('click');
205
+ await wrapper.vm.$nextTick();
206
+
207
+ const card = wrapper.findComponent({ name: 'ResourcePopoverCard' });
208
+
209
+ await card.trigger('keydown.escape');
210
+ await wrapper.vm.$nextTick();
211
+
212
+ expect(wrapper.findComponent({ name: 'ResourcePopoverCard' }).exists()).toBe(false);
213
+ });
214
+ });
215
+
216
+ describe('focus Trap', () => {
217
+ it('should not setup focus trap on mouseenter', async() => {
218
+ mockClusterFind.mockReturnValue(mockResource);
219
+ const wrapper = await createWrapper();
220
+
221
+ await wrapper.find('.display').trigger('mouseenter');
222
+ await wrapper.vm.$nextTick();
223
+
224
+ expect(wrapper.vm.focusOpen).toBe(false);
225
+ expect(mockFocusTrap).not.toHaveBeenCalled();
226
+ });
227
+
228
+ it('should setup focus trap when opened via focus button', async() => {
229
+ mockClusterFind.mockReturnValue(mockResource);
230
+ const wrapper = await createWrapper();
231
+
232
+ await wrapper.find('.focus-button').trigger('click');
233
+ await wrapper.vm.$nextTick(); // Triggers the watch
234
+
235
+ expect(wrapper.vm.focusOpen).toBe(true);
236
+ expect(mockFocusTrap).toHaveBeenCalledTimes(1);
237
+
238
+ // Verify the options passed to the focus trap
239
+ const focusTrapOptions = mockFocusTrap.mock.calls[0][2];
240
+
241
+ expect(focusTrapOptions.fallbackFocus).toBe('#first-glance-item');
242
+ expect(focusTrapOptions.setReturnFocus()).toBe('.focus-button');
243
+ });
244
+ });
245
+ });
@@ -0,0 +1,226 @@
1
+ <script lang="ts">
2
+ import { useFetch } from '@shell/components/Resource/Detail/FetchLoader/composables';
3
+ import { useStore } from 'vuex';
4
+ import ResourcePopoverCard from '@shell/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue';
5
+ import RcStatusIndicator from '@components/Pill/RcStatusIndicator/RcStatusIndicator.vue';
6
+ import { useI18n } from '@shell/composables/useI18n';
7
+ import { computed, ref, watch } from 'vue';
8
+ import {
9
+ DEFAULT_FOCUS_TRAP_OPTS,
10
+ useWatcherBasedSetupFocusTrapWithDestroyIncluded
11
+ } from '@shell/composables/focusTrap';
12
+
13
+ export interface Props {
14
+ type: string;
15
+ id: string;
16
+ currentStore?: string;
17
+ detailLocation?: object;
18
+ }
19
+ </script>
20
+
21
+ <script setup lang="ts">
22
+ const store = useStore();
23
+ const i18n = useI18n(store);
24
+ const props = defineProps<Props>();
25
+ const card = ref<any>(null);
26
+ const popoverContainer = ref(null);
27
+ const showPopover = ref<boolean>(false);
28
+ const focusOpen = ref<boolean>(false);
29
+
30
+ const fetch = useFetch(async() => {
31
+ const currentStore = props.currentStore || store.getters['currentStore'](props.type);
32
+
33
+ return store.dispatch(`${ currentStore }/find`, { type: props.type, id: props.id });
34
+ });
35
+
36
+ const resourceTypeLabel = computed(() => {
37
+ if (!fetch.value.data) {
38
+ return '';
39
+ }
40
+
41
+ const resource = fetch.value.data;
42
+ const currentStore = store.getters['currentStore'](resource.type);
43
+ const schema = store.getters[`${ currentStore }/schemaFor`](resource.type);
44
+
45
+ return resource.parentNameOverride || store.getters['type-map/labelFor'](schema);
46
+ });
47
+
48
+ const actionInvoked = () => {
49
+ showPopover.value = false;
50
+ };
51
+
52
+ // Set focus trap when card opened using keyboard
53
+ watch(
54
+ () => card.value,
55
+ (neu) => {
56
+ if (neu && focusOpen.value) {
57
+ const opts = {
58
+ ...DEFAULT_FOCUS_TRAP_OPTS,
59
+ fallbackFocus: '#first-glance-item',
60
+ setReturnFocus: () => '.focus-button'
61
+ };
62
+
63
+ useWatcherBasedSetupFocusTrapWithDestroyIncluded(() => showPopover.value, '#resource-popover-card', opts);
64
+ }
65
+ }
66
+ );
67
+ </script>
68
+
69
+ <template>
70
+ <div
71
+ class="resource-popover"
72
+ @mouseleave="showPopover=false"
73
+ >
74
+ <v-dropdown
75
+ :triggers="[]"
76
+ :container="popoverContainer"
77
+ :shown="showPopover"
78
+ placement="bottom-start"
79
+ :aria-label="i18n.t('component.resource.detail.glance.ariaLabel.showDetails', { name: fetch.data?.nameDisplay, resource: resourceTypeLabel })"
80
+ >
81
+ <div class="target">
82
+ <span class="display-container">
83
+ <span
84
+ v-if="fetch.data"
85
+ class="display"
86
+ @mouseenter="showPopover=true"
87
+ >
88
+ <RcStatusIndicator
89
+ shape="disc"
90
+ :status="fetch.data?.stateBackground || 'unknown'"
91
+ />
92
+ <router-link
93
+ :to="props.detailLocation || fetch.data.detailLocation || '#'"
94
+ >
95
+ {{ fetch.data.nameDisplay }}
96
+ </router-link>
97
+ <div
98
+ ref="popoverContainer"
99
+ class="resource-popover-container"
100
+ >
101
+ <!--Empty container for mounting popper content-->
102
+ </div>
103
+ </span>
104
+ <span v-else>...</span>
105
+ <button
106
+ v-if="fetch.data"
107
+ class="focus-button role-secondary"
108
+ :aria-label="i18n.t('component.resource.detail.glance.ariaLabel.showDetails', { name: fetch.data?.nameDisplay, resource: resourceTypeLabel })"
109
+ aria-haspopup="true"
110
+ :aria-expanded="showPopover"
111
+ @click="showPopover=true; focusOpen=true;"
112
+ >
113
+ <i class="icon icon-chevron-down icon-sm" />
114
+ </button>
115
+ </span>
116
+ </div>
117
+
118
+ <template #popper>
119
+ <ResourcePopoverCard
120
+ v-if="showPopover"
121
+ id="resource-popover-card"
122
+ ref="card"
123
+ :resource="fetch.data"
124
+ @action-invoked="actionInvoked"
125
+ @keydown.escape="showPopover=false; focusOpen=false"
126
+ />
127
+ </template>
128
+ </v-dropdown>
129
+ </div>
130
+ </template>
131
+
132
+ <style lang="scss" scoped>
133
+ .resource-popover {
134
+ position: relative;
135
+ width: 100%;
136
+
137
+ .target, a {
138
+ @include clip;
139
+ }
140
+
141
+ .display-container {
142
+ position: absolute;
143
+ left: 0;
144
+ right: 0;
145
+ top: 0;
146
+ bottom: 0;
147
+ }
148
+
149
+ .display {
150
+ display: inline-flex;
151
+ max-width: 100%;
152
+ a {
153
+ flex: 1;
154
+ }
155
+ }
156
+
157
+ .target {
158
+ width: 100%;
159
+ height: 17px;
160
+ }
161
+
162
+ .focus-button {
163
+ margin-left: 4px;
164
+ padding: 0;
165
+ width: 0px;
166
+ height: initial;
167
+ min-height: initial;
168
+ overflow: hidden;
169
+ border-width: 0;
170
+
171
+ &:focus {
172
+ width: initial;
173
+ border-width: 1px;
174
+ }
175
+ }
176
+
177
+ .rc-status-indicator {
178
+ margin-right: 12px;
179
+ margin-top: 4px;
180
+ height: initial;
181
+ line-height: initial;
182
+ }
183
+
184
+ .resource-popover-card {
185
+ border: none;
186
+ }
187
+
188
+ .resource-popover-container {
189
+ position: absolute;
190
+ $size: 10px;
191
+ height: $size;
192
+ bottom: -$size;
193
+ width: 100%;
194
+ }
195
+
196
+ &:deep() {
197
+ & > .v-popper > .btn.role-link {
198
+ padding: 0;
199
+ min-height: initial;
200
+ line-height: initial;
201
+
202
+ &:hover {
203
+ background: none;
204
+ }
205
+ }
206
+
207
+ .resource-popover-container > .v-popper__popper {
208
+ border-radius: 6px;
209
+ box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.04);
210
+
211
+ & > .v-popper__wrapper {
212
+ .v-popper__arrow-container {
213
+ display: none;
214
+ }
215
+
216
+ & > .v-popper__inner {
217
+ overflow: initial;
218
+ &, & > div > .dropdownTarget {
219
+ padding: 0;
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ </style>
@@ -10,5 +10,6 @@
10
10
  grid-template-columns: repeat(3, minmax(0, 1fr));;
11
11
  grid-auto-flow: dense;
12
12
  grid-gap: 24px;
13
+ justify-content: space-evenly;
13
14
  }
14
15
  </style>
@@ -5,7 +5,6 @@ import { useRoute } from 'vue-router';
5
5
  const mockStore = {
6
6
  getters: {
7
7
  'type-map/labelFor': jest.fn(),
8
- 'type-map/hasGraph': jest.fn(),
9
8
  currentStore: jest.fn(),
10
9
  'cluster/schemaFor': jest.fn()
11
10
  }
@@ -27,7 +26,6 @@ describe('composables: TitleBar', () => {
27
26
  };
28
27
  const labelFor = 'LABEL_FOR';
29
28
  const schema = { type: 'SCHEMA' };
30
- const hasGraph = true;
31
29
 
32
30
  it('should return the appropriate values based on input', async() => {
33
31
  const route = useRoute();
@@ -35,13 +33,11 @@ describe('composables: TitleBar', () => {
35
33
  mockStore.getters['currentStore'].mockImplementation(() => 'cluster');
36
34
  mockStore.getters['cluster/schemaFor'].mockImplementation(() => schema);
37
35
  mockStore.getters['type-map/labelFor'].mockImplementation(() => labelFor);
38
- mockStore.getters['type-map/hasGraph'].mockImplementation(() => hasGraph);
39
36
 
40
37
  const props = useDefaultTitleBarProps(resource, ref(undefined));
41
38
 
42
39
  expect(props.value.resourceTypeLabel).toStrictEqual(labelFor);
43
40
  expect(mockStore.getters['type-map/labelFor']).toHaveBeenLastCalledWith(schema);
44
- expect(mockStore.getters['type-map/hasGraph']).toHaveBeenLastCalledWith(resource.type);
45
41
  expect(mockStore.getters['currentStore']).toHaveBeenLastCalledWith(resource.type);
46
42
  expect(mockStore.getters['cluster/schemaFor']).toHaveBeenLastCalledWith(resource.type);
47
43
  expect(props.value.resourceTo?.params.product).toStrictEqual('explorer');
@@ -54,7 +50,6 @@ describe('composables: TitleBar', () => {
54
50
  expect(props.value.badge?.color).toStrictEqual(resource.stateBackground);
55
51
  expect(props.value.badge?.label).toStrictEqual(resource.stateDisplay);
56
52
  expect(props.value.description).toStrictEqual(resource.description);
57
- expect(props.value.showViewOptions).toStrictEqual(hasGraph);
58
53
 
59
54
  props.value.onShowConfiguration?.('callback');
60
55
  expect(resource.showConfiguration).toHaveBeenCalledTimes(1);
@@ -9,7 +9,7 @@ describe('component: TitleBar/index', () => {
9
9
  const resourceTypeLabel = 'RESOURCE_TYPE_LABEL';
10
10
  const resourceTo = 'RESOURCE_TO';
11
11
  const resourceName = 'RESOURCE_NAME';
12
- const store = createStore({});
12
+ const store = createStore({ getters: {} });
13
13
 
14
14
  it('should render container with class title-bar', async() => {
15
15
  const wrapper = mount(TitleBar, {
@@ -23,7 +23,6 @@ export const useDefaultTitleBarProps = (resource: any, resourceSubtype?: Ref<str
23
23
  resource: resourceValue.type
24
24
  }
25
25
  };
26
- const hasGraph = !!store.getters['type-map/hasGraph'](resourceValue.type);
27
26
  const onShowConfiguration = resourceValue.disableResourceDetailDrawer ? undefined : (returnFocusSelector: string) => resourceValue.showConfiguration(returnFocusSelector);
28
27
 
29
28
  return {
@@ -36,8 +35,7 @@ export const useDefaultTitleBarProps = (resource: any, resourceSubtype?: Ref<str
36
35
  color: resourceValue.stateBackground,
37
36
  label: resourceValue.stateDisplay
38
37
  },
39
- description: resourceValue.description,
40
- showViewOptions: hasGraph,
38
+ description: resourceValue.description,
41
39
  onShowConfiguration
42
40
  };
43
41
  });
@@ -9,8 +9,7 @@ import { useI18n } from '@shell/composables/useI18n';
9
9
  import RcButton from '@components/RcButton/RcButton.vue';
10
10
  import TabTitle from '@shell/components/TabTitle';
11
11
  import { computed, ref, watch } from 'vue';
12
- import { _CONFIG, _GRAPH, AS } from '@shell/config/query-params';
13
- import ButtonGroup from '@shell/components/ButtonGroup';
12
+ import { _CONFIG, AS } from '@shell/config/query-params';
14
13
  import { ExtensionPoint, PanelLocation } from '@shell/core/types';
15
14
  import ExtensionPanel from '@shell/components/ExtensionPanel.vue';
16
15
 
@@ -31,10 +30,6 @@ export interface TitleBarProps {
31
30
  // This should be replaced with a list of menu items we want to render.
32
31
  // I don't have the time right now to swap this out though.
33
32
  actionMenuResource?: any;
34
-
35
- // Please don't expand this pattern, this was a quick fix to resolve a conflict between the new masthead and fleet.
36
- showViewOptions?: boolean;
37
-
38
33
  onShowConfiguration?: (returnFocusSelector: string) => void;
39
34
  }
40
35
 
@@ -43,7 +38,7 @@ const showConfigurationIcon = require(`@shell/assets/images/icons/document.svg`)
43
38
 
44
39
  <script setup lang="ts">
45
40
  const {
46
- resource, resourceTypeLabel, resourceTo, resourceName, description, badge, showViewOptions, onShowConfiguration,
41
+ resource, resourceTypeLabel, resourceTo, resourceName, description, badge, onShowConfiguration,
47
42
  } = defineProps<TitleBarProps>();
48
43
 
49
44
  const store = useStore();
@@ -55,22 +50,6 @@ const showConfigurationDataTestId = 'show-configuration-cta';
55
50
  const showConfigurationReturnFocusSelector = computed(() => `[data-testid="${ showConfigurationDataTestId }"]`);
56
51
 
57
52
  const currentView = ref(router?.currentRoute?.value?.query?.as || _CONFIG);
58
- const viewOptions = computed(() => {
59
- if (!showViewOptions) {
60
- return;
61
- }
62
-
63
- return [
64
- {
65
- labelKey: 'resourceDetail.masthead.config',
66
- value: _CONFIG,
67
- },
68
- {
69
- labelKey: 'resourceDetail.masthead.graph',
70
- value: _GRAPH,
71
- }
72
- ];
73
- });
74
53
 
75
54
  watch(
76
55
  () => currentView.value,
@@ -111,12 +90,6 @@ watch(
111
90
  />
112
91
  </Title>
113
92
  <div class="actions">
114
- <!-- Please don't expand this pattern, this was a quick fix to resolve a conflict between the new masthead and fleet. -->
115
- <ButtonGroup
116
- v-if="viewOptions"
117
- v-model:value="currentView"
118
- :options="viewOptions"
119
- />
120
93
  <slot name="additional-actions" />
121
94
  <RcButton
122
95
  v-if="onShowConfiguration"
@@ -0,0 +1,9 @@
1
+ import { _CONFIG, VIEW } from '@shell/config/query-params';
2
+ import { computed } from 'vue';
3
+ import { useRoute } from 'vue-router';
4
+
5
+ export const useCurrentView = () => {
6
+ const route = useRoute();
7
+
8
+ return computed(() => route.query[VIEW] || _CONFIG);
9
+ };
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import { useRouter } from 'vue-router';
3
+ import { computed, ref, watch } from 'vue';
4
+ import { _CONFIG, _GRAPH } from '@shell/config/query-params';
5
+ import ButtonGroup from '@shell/components/ButtonGroup';
6
+ import { useCurrentView } from '@shell/components/Resource/Detail/ViewOptions/composable';
7
+ </script>
8
+
9
+ <script setup lang="ts">
10
+ const router = useRouter();
11
+
12
+ const currentView = useCurrentView();
13
+ const view = ref(currentView.value);
14
+ const viewOptions = computed(() => {
15
+ return [
16
+ {
17
+ labelKey: 'resourceDetail.masthead.config',
18
+ value: _CONFIG,
19
+ },
20
+ {
21
+ labelKey: 'resourceDetail.masthead.graph',
22
+ value: _GRAPH,
23
+ }
24
+ ];
25
+ });
26
+
27
+ watch(
28
+ () => view.value,
29
+ () => {
30
+ router.push({ query: { view: view.value } });
31
+ }
32
+ );
33
+ </script>
34
+
35
+ <template>
36
+ <ButtonGroup
37
+ v-if="viewOptions"
38
+ v-model:value="view"
39
+ :options="viewOptions"
40
+ />
41
+ </template>