@rancher/shell 3.0.8-rc.2 → 3.0.8-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/brand/suse/dark/rancher-logo.svg +64 -1
- package/assets/brand/suse/rancher-logo.svg +1 -1
- package/assets/styles/global/_cards.scss +0 -3
- package/assets/styles/themes/_modern.scss +9 -1
- package/assets/styles/themes/_suse.scss +81 -24
- package/assets/translations/en-us.yaml +68 -3
- package/components/AutoscalerCard.vue +113 -0
- package/components/AutoscalerTab.vue +94 -0
- package/components/ClusterIconMenu.vue +1 -1
- package/components/ClusterProviderIcon.vue +1 -1
- package/components/IconOrSvg.vue +2 -2
- package/components/PopoverCard.vue +192 -0
- package/components/Resource/Detail/FetchLoader/composables.ts +18 -4
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +1 -1
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +4 -0
- package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +2 -19
- package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +0 -29
- package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +132 -150
- package/components/Resource/Detail/ResourcePopover/index.vue +54 -159
- package/components/ResourceDetail/Masthead/latest.vue +29 -0
- package/components/ResourceList/Masthead.vue +1 -1
- package/components/__tests__/AutoscalerCard.test.ts +154 -0
- package/components/__tests__/AutoscalerTab.test.ts +125 -0
- package/components/__tests__/PopoverCard.test.ts +204 -0
- package/components/formatter/Autoscaler.vue +97 -0
- package/components/formatter/InternalExternalIP.vue +195 -24
- package/components/formatter/__tests__/Autoscaler.test.ts +156 -0
- package/components/formatter/__tests__/InternalExternalIP.test.ts +133 -0
- package/components/nav/Group.vue +12 -3
- package/components/nav/TopLevelMenu.vue +2 -2
- package/composables/useInterval.ts +15 -0
- package/config/labels-annotations.js +8 -1
- package/config/product/manager.js +20 -9
- package/config/router/routes.js +4 -0
- package/config/settings.ts +2 -1
- package/config/table-headers.js +8 -0
- package/config/types.js +2 -0
- package/core/types-provisioning.ts +3 -0
- package/detail/provisioning.cattle.io.cluster.vue +12 -1
- package/directives/ui-context.ts +8 -2
- package/edit/auth/github.vue +5 -0
- package/edit/cloudcredential.vue +1 -1
- package/edit/fleet.cattle.io.gitrepo.vue +0 -10
- package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +32 -5
- package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.test.ts +35 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Networking.test.ts +132 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +18 -12
- package/edit/provisioning.cattle.io.cluster/rke2.vue +39 -8
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +107 -5
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +90 -3
- package/initialize/install-plugins.js +3 -1
- package/list/provisioning.cattle.io.cluster.vue +15 -2
- package/machine-config/amazonec2.vue +36 -135
- package/machine-config/components/EC2Networking.vue +474 -0
- package/machine-config/components/__tests__/EC2Networking.test.ts +94 -0
- package/machine-config/components/__tests__/utils/vpcSubnetMockData.js +294 -0
- package/machine-config/digitalocean.vue +11 -0
- package/models/cluster/node.js +13 -6
- package/models/cluster.x-k8s.io.machine.js +10 -20
- package/models/cluster.x-k8s.io.machinedeployment.js +5 -1
- package/models/management.cattle.io.kontainerdriver.js +1 -0
- package/models/provisioning.cattle.io.cluster.js +223 -2
- package/package.json +1 -1
- package/pages/c/_cluster/apps/charts/install.vue +1 -1
- package/pages/c/_cluster/manager/hostedprovider/index.vue +209 -0
- package/plugins/dynamic-content.js +13 -0
- package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +8 -0
- package/store/features.js +1 -0
- package/store/notifications.ts +32 -1
- package/store/plugins.js +7 -3
- package/store/prefs.js +1 -0
- package/types/notifications/index.ts +24 -3
- package/types/shell/index.d.ts +26 -1
- package/utils/__tests__/object.test.ts +19 -0
- package/utils/autoscaler-utils.ts +7 -0
- package/utils/dynamic-content/__tests__/announcement.test.ts +498 -0
- package/utils/dynamic-content/announcement.ts +112 -0
- package/utils/dynamic-content/example.json +40 -0
- package/utils/dynamic-content/index.ts +6 -2
- package/utils/dynamic-content/new-release.ts +1 -1
- package/utils/dynamic-content/notification-handler.ts +48 -0
- package/utils/dynamic-content/types.d.ts +33 -1
- package/utils/object.js +20 -2
- package/utils/scroll.js +7 -0
- package/utils/settings.ts +15 -0
- package/utils/validators/machine-pool.ts +13 -3
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { mount, RouterLinkStub } from '@vue/test-utils';
|
|
2
2
|
import { createStore } from 'vuex';
|
|
3
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) }));
|
|
4
|
+
import PopoverCard from '@shell/components/PopoverCard.vue';
|
|
10
5
|
|
|
11
6
|
const mockResource = {
|
|
12
7
|
id: 'test-ns/test-pod',
|
|
@@ -14,7 +9,6 @@ const mockResource = {
|
|
|
14
9
|
nameDisplay: 'My Test Pod',
|
|
15
10
|
stateBackground: 'bg-success',
|
|
16
11
|
detailLocation: { name: 'pod-detail', params: { id: 'test-pod' } },
|
|
17
|
-
glance: [{ label: 'Status', content: 'Active' }],
|
|
18
12
|
parentNameOverride: 'Overridden Pod',
|
|
19
13
|
};
|
|
20
14
|
|
|
@@ -23,223 +17,211 @@ describe('component: ResourcePopover/index.vue', () => {
|
|
|
23
17
|
const mockClusterFind = jest.fn();
|
|
24
18
|
const mockSomethingFind = jest.fn();
|
|
25
19
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
20
|
+
const defaultStore = {
|
|
21
|
+
getters: {
|
|
22
|
+
'i18n/t': () => (key: string) => key,
|
|
23
|
+
currentStore: () => () => 'cluster',
|
|
24
|
+
'cluster/schemaFor': () => () => ({ id: 'pod' }),
|
|
25
|
+
'type-map/labelFor': () => () => 'Pod',
|
|
26
|
+
},
|
|
27
|
+
actions: { 'cluster/find': mockClusterFind, 'something/find': mockSomethingFind }
|
|
28
|
+
};
|
|
36
29
|
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
const PopoverCardStub = {
|
|
31
|
+
PopoverCard: {
|
|
32
|
+
template: `
|
|
33
|
+
<div>
|
|
34
|
+
<slot />
|
|
35
|
+
<slot name="heading-action" :close="() => {}" />
|
|
36
|
+
<slot name="card-body" />
|
|
37
|
+
</div>
|
|
38
|
+
`,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const createWrapper = (props: any = {}, storeConfig: any = defaultStore, stubs: any = {}) => {
|
|
43
|
+
store = createStore(storeConfig);
|
|
44
|
+
|
|
45
|
+
return mount(ResourcePopover, {
|
|
46
|
+
props: {
|
|
47
|
+
type: 'pod',
|
|
48
|
+
id: 'test-ns/test-pod',
|
|
49
|
+
...props,
|
|
50
|
+
},
|
|
39
51
|
global: {
|
|
40
52
|
plugins: [store],
|
|
41
53
|
stubs: {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
RcStatusIndicator: true,
|
|
54
|
+
RouterLink: RouterLinkStub,
|
|
55
|
+
RcStatusIndicator: true,
|
|
56
|
+
ActionMenu: true,
|
|
57
|
+
ResourcePopoverCard: true,
|
|
58
|
+
VDropdown: true,
|
|
59
|
+
...stubs
|
|
49
60
|
},
|
|
50
61
|
},
|
|
51
62
|
});
|
|
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
63
|
};
|
|
59
64
|
|
|
60
|
-
// Reset mocks before each test
|
|
61
65
|
beforeEach(() => {
|
|
62
66
|
jest.clearAllMocks();
|
|
63
67
|
});
|
|
64
68
|
|
|
65
|
-
describe('data Fetching and
|
|
66
|
-
it('should
|
|
67
|
-
|
|
69
|
+
describe('data Fetching and Rendering', () => {
|
|
70
|
+
it('should display a loading indicator while fetching data', async() => {
|
|
71
|
+
mockClusterFind.mockImplementation(() => new Promise(() => { }));
|
|
72
|
+
const wrapper = createWrapper(undefined, undefined, PopoverCardStub);
|
|
73
|
+
|
|
74
|
+
await wrapper.vm.$nextTick();
|
|
75
|
+
await wrapper.vm.$nextTick();
|
|
76
|
+
await wrapper.vm.$nextTick();
|
|
68
77
|
|
|
69
|
-
expect(wrapper.find('.display').exists()).toBe(false);
|
|
70
78
|
expect(wrapper.text()).toContain('...');
|
|
79
|
+
expect(wrapper.find('.display').exists()).toBe(false);
|
|
71
80
|
});
|
|
72
81
|
|
|
73
|
-
it('should
|
|
74
|
-
|
|
82
|
+
it('should fetch data using the default store', async() => {
|
|
83
|
+
const wrapper = createWrapper();
|
|
84
|
+
|
|
85
|
+
await wrapper.vm.$nextTick();
|
|
86
|
+
await wrapper.vm.$nextTick();
|
|
75
87
|
|
|
76
|
-
expect(mockClusterFind).toHaveBeenCalledWith(expect.
|
|
88
|
+
expect(mockClusterFind).toHaveBeenCalledWith(expect.any(Object), { type: 'pod', id: 'test-ns/test-pod' });
|
|
77
89
|
});
|
|
78
90
|
|
|
79
|
-
it('should
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
91
|
+
it('should fetch data using the store specified in props', async() => {
|
|
92
|
+
mockClusterFind.mockReturnValue(mockResource);
|
|
93
|
+
const wrapper = createWrapper({ currentStore: 'something' });
|
|
83
94
|
|
|
84
|
-
|
|
95
|
+
await wrapper.vm.$nextTick();
|
|
96
|
+
await wrapper.vm.$nextTick();
|
|
97
|
+
|
|
98
|
+
expect(mockSomethingFind).toHaveBeenCalledWith(expect.any(Object), { type: 'pod', id: 'test-ns/test-pod' });
|
|
99
|
+
expect(mockClusterFind).not.toHaveBeenCalled();
|
|
85
100
|
});
|
|
86
101
|
|
|
87
|
-
it('should
|
|
102
|
+
it('should display resource details after data is fetched', async() => {
|
|
88
103
|
mockClusterFind.mockReturnValue(mockResource);
|
|
89
|
-
const wrapper =
|
|
104
|
+
const wrapper = createWrapper(undefined, undefined, PopoverCardStub);
|
|
105
|
+
|
|
106
|
+
await wrapper.vm.$nextTick();
|
|
107
|
+
await wrapper.vm.$nextTick();
|
|
108
|
+
await wrapper.vm.$nextTick();
|
|
109
|
+
|
|
90
110
|
const link = wrapper.findComponent(RouterLinkStub);
|
|
91
111
|
|
|
92
|
-
expect(link.exists()).toBe(true);
|
|
93
112
|
expect(link.text()).toBe(mockResource.nameDisplay);
|
|
94
113
|
expect(link.props('to')).toStrictEqual(mockResource.detailLocation);
|
|
95
114
|
});
|
|
96
115
|
|
|
97
|
-
it('should use
|
|
98
|
-
const
|
|
116
|
+
it('should use detailLocation prop for the link if provided', async() => {
|
|
117
|
+
const customLocation = { name: 'custom-route' };
|
|
99
118
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
mockClusterFind.mockReturnValue(mockResource);
|
|
120
|
+
const wrapper = createWrapper({ detailLocation: customLocation }, undefined, PopoverCardStub);
|
|
121
|
+
|
|
122
|
+
await wrapper.vm.$nextTick();
|
|
123
|
+
await wrapper.vm.$nextTick();
|
|
124
|
+
await wrapper.vm.$nextTick();
|
|
105
125
|
|
|
106
126
|
const link = wrapper.findComponent(RouterLinkStub);
|
|
107
127
|
|
|
108
|
-
expect(link.props('to')).toStrictEqual(
|
|
128
|
+
expect(link.props('to')).toStrictEqual(customLocation);
|
|
109
129
|
});
|
|
110
130
|
});
|
|
111
131
|
|
|
112
132
|
describe('computed Properties', () => {
|
|
113
|
-
it('should
|
|
114
|
-
mockClusterFind.mockReturnValue(
|
|
115
|
-
|
|
116
|
-
const wrapper = await createWrapper();
|
|
133
|
+
it('resourceTypeLabel: should be empty if data is not loaded', async() => {
|
|
134
|
+
mockClusterFind.mockReturnValue(undefined);
|
|
135
|
+
const wrapper = createWrapper();
|
|
117
136
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
it('should return resourceTypeLabel from type-map getter', async() => {
|
|
122
|
-
const resourceWithoutOverride = { ...mockResource, parentNameOverride: undefined };
|
|
137
|
+
await wrapper.vm.$nextTick();
|
|
138
|
+
await wrapper.vm.$nextTick();
|
|
139
|
+
await wrapper.vm.$nextTick();
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
const
|
|
141
|
+
const popoverCard = wrapper.findComponent(PopoverCard);
|
|
142
|
+
const ariaLabel = popoverCard.props('showPopoverAriaLabel');
|
|
126
143
|
|
|
127
|
-
expect(
|
|
144
|
+
expect(ariaLabel).toBe('component.resource.detail.glance.ariaLabel.showDetails-{\"name\":\"\",\"resource\":\"\"}');
|
|
128
145
|
});
|
|
129
146
|
|
|
130
|
-
it('should
|
|
131
|
-
mockClusterFind.
|
|
132
|
-
const wrapper =
|
|
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');
|
|
147
|
+
it('resourceTypeLabel: should use parentNameOverride when available', async() => {
|
|
148
|
+
mockClusterFind.mockResolvedValue(mockResource);
|
|
149
|
+
const wrapper = createWrapper();
|
|
137
150
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
151
|
+
await wrapper.vm.$nextTick();
|
|
152
|
+
await wrapper.vm.$nextTick();
|
|
153
|
+
await wrapper.vm.$nextTick();
|
|
142
154
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
mockClusterFind.mockReturnValue(mockResource);
|
|
146
|
-
const wrapper = await createWrapper();
|
|
155
|
+
const popoverCard = wrapper.findComponent(PopoverCard);
|
|
156
|
+
const ariaLabel = popoverCard.props('showPopoverAriaLabel');
|
|
147
157
|
|
|
148
|
-
expect(
|
|
158
|
+
expect(ariaLabel).toBe('component.resource.detail.glance.ariaLabel.showDetails-{\"name\":\"My Test Pod\",\"resource\":\"Overridden Pod\"}');
|
|
149
159
|
});
|
|
150
160
|
|
|
151
|
-
it('should
|
|
152
|
-
|
|
153
|
-
const wrapper = await createWrapper();
|
|
161
|
+
it('resourceTypeLabel: should use type-map label as a fallback', async() => {
|
|
162
|
+
const resourceWithoutOverride = { ...mockResource, parentNameOverride: null };
|
|
154
163
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await wrapper.vm.$nextTick();
|
|
158
|
-
expect(wrapper.findComponent({ name: 'ResourcePopoverCard' }).exists()).toBe(true);
|
|
159
|
-
});
|
|
164
|
+
mockClusterFind.mockResolvedValue(resourceWithoutOverride);
|
|
165
|
+
const wrapper = createWrapper();
|
|
160
166
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
await wrapper.vm.$nextTick();
|
|
168
|
+
await wrapper.vm.$nextTick();
|
|
169
|
+
await wrapper.vm.$nextTick();
|
|
164
170
|
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
const popoverCard = wrapper.findComponent(PopoverCard);
|
|
172
|
+
const ariaLabel = popoverCard.props('showPopoverAriaLabel');
|
|
167
173
|
|
|
168
|
-
|
|
169
|
-
expect(wrapper.vm.showPopover).toBe(false);
|
|
170
|
-
await wrapper.vm.$nextTick();
|
|
171
|
-
expect(wrapper.findComponent({ name: 'ResourcePopoverCard' }).exists()).toBe(false);
|
|
174
|
+
expect(ariaLabel).toBe('component.resource.detail.glance.ariaLabel.showDetails-{\"name\":\"My Test Pod\",\"resource\":\"Pod\"}');
|
|
172
175
|
});
|
|
176
|
+
});
|
|
173
177
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
describe('props passed to child components', () => {
|
|
179
|
+
it('should pass correct props to PopoverCard', async() => {
|
|
180
|
+
mockClusterFind.mockResolvedValue(mockResource);
|
|
181
|
+
const wrapper = createWrapper();
|
|
177
182
|
|
|
178
|
-
await wrapper.find('.focus-button').trigger('click');
|
|
179
|
-
expect(wrapper.vm.showPopover).toBe(true);
|
|
180
|
-
expect(wrapper.vm.focusOpen).toBe(true);
|
|
181
183
|
await wrapper.vm.$nextTick();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
await wrapper.vm.$nextTick();
|
|
185
|
+
await wrapper.vm.$nextTick();
|
|
184
186
|
|
|
185
|
-
|
|
186
|
-
mockClusterFind.mockReturnValue(mockResource);
|
|
187
|
-
const wrapper = await createWrapper();
|
|
187
|
+
const popoverCard = wrapper.findComponent(PopoverCard);
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
expect(popoverCard.props('cardTitle')).toBe(mockResource.nameDisplay);
|
|
190
|
+
expect(popoverCard.props('fallbackFocus')).toBe("[data-testid='resource-popover-action-menu']");
|
|
191
191
|
|
|
192
|
-
const
|
|
192
|
+
const expectedAriaLabel = 'component.resource.detail.glance.ariaLabel.showDetails-{\"name\":\"My Test Pod\",\"resource\":\"Overridden Pod\"}';
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
expect(wrapper.vm.showPopover).toBe(false);
|
|
196
|
-
await wrapper.vm.$nextTick();
|
|
197
|
-
expect(wrapper.findComponent({ name: 'ResourcePopoverCard' }).exists()).toBe(false);
|
|
194
|
+
expect(popoverCard.props('showPopoverAriaLabel')).toBe(expectedAriaLabel);
|
|
198
195
|
});
|
|
199
196
|
|
|
200
|
-
it('should
|
|
201
|
-
mockClusterFind.
|
|
202
|
-
const wrapper =
|
|
197
|
+
it('should pass correct props to ActionMenu', async() => {
|
|
198
|
+
mockClusterFind.mockResolvedValue(mockResource);
|
|
199
|
+
const wrapper = createWrapper(undefined, undefined, PopoverCardStub);
|
|
203
200
|
|
|
204
|
-
await wrapper.find('.focus-button').trigger('click');
|
|
205
201
|
await wrapper.vm.$nextTick();
|
|
206
|
-
|
|
207
|
-
const card = wrapper.findComponent({ name: 'ResourcePopoverCard' });
|
|
208
|
-
|
|
209
|
-
await card.trigger('keydown.escape');
|
|
202
|
+
await wrapper.vm.$nextTick();
|
|
210
203
|
await wrapper.vm.$nextTick();
|
|
211
204
|
|
|
212
|
-
|
|
213
|
-
});
|
|
214
|
-
});
|
|
205
|
+
const actionMenu = wrapper.findComponent({ name: 'ActionMenu' });
|
|
215
206
|
|
|
216
|
-
|
|
217
|
-
it('should not setup focus trap on mouseenter', async() => {
|
|
218
|
-
mockClusterFind.mockReturnValue(mockResource);
|
|
219
|
-
const wrapper = await createWrapper();
|
|
207
|
+
expect(actionMenu.props('resource')).toStrictEqual(mockResource);
|
|
220
208
|
|
|
221
|
-
|
|
222
|
-
await wrapper.vm.$nextTick();
|
|
209
|
+
const expectedAriaLabel = 'component.resource.detail.glance.ariaLabel.actionMenu-{\"resource\":\"My Test Pod\"}';
|
|
223
210
|
|
|
224
|
-
expect(
|
|
225
|
-
expect(mockFocusTrap).not.toHaveBeenCalled();
|
|
211
|
+
expect(actionMenu.props('buttonAriaLabel')).toBe(expectedAriaLabel);
|
|
226
212
|
});
|
|
227
213
|
|
|
228
|
-
it('should
|
|
229
|
-
mockClusterFind.
|
|
230
|
-
const wrapper =
|
|
214
|
+
it('should pass correct props to ResourcePopoverCard', async() => {
|
|
215
|
+
mockClusterFind.mockResolvedValue(mockResource);
|
|
216
|
+
const wrapper = createWrapper(undefined, undefined, PopoverCardStub);
|
|
231
217
|
|
|
232
|
-
await wrapper.
|
|
233
|
-
await wrapper.vm.$nextTick();
|
|
234
|
-
|
|
235
|
-
expect(wrapper.vm.focusOpen).toBe(true);
|
|
236
|
-
expect(mockFocusTrap).toHaveBeenCalledTimes(1);
|
|
218
|
+
await wrapper.vm.$nextTick();
|
|
219
|
+
await wrapper.vm.$nextTick();
|
|
220
|
+
await wrapper.vm.$nextTick();
|
|
237
221
|
|
|
238
|
-
|
|
239
|
-
const focusTrapOptions = mockFocusTrap.mock.calls[0][2];
|
|
222
|
+
const resourceCard = wrapper.findComponent({ name: 'ResourcePopoverCard' });
|
|
240
223
|
|
|
241
|
-
expect(
|
|
242
|
-
expect(focusTrapOptions.setReturnFocus()).toBe('.focus-button');
|
|
224
|
+
expect(resourceCard.props('resource')).toStrictEqual(mockResource);
|
|
243
225
|
});
|
|
244
226
|
});
|
|
245
227
|
});
|
|
@@ -4,11 +4,9 @@ import { useStore } from 'vuex';
|
|
|
4
4
|
import ResourcePopoverCard from '@shell/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue';
|
|
5
5
|
import RcStatusIndicator from '@components/Pill/RcStatusIndicator/RcStatusIndicator.vue';
|
|
6
6
|
import { useI18n } from '@shell/composables/useI18n';
|
|
7
|
-
import { computed, ref
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
useWatcherBasedSetupFocusTrapWithDestroyIncluded
|
|
11
|
-
} from '@shell/composables/focusTrap';
|
|
7
|
+
import { computed, ref } from 'vue';
|
|
8
|
+
import PopoverCard from '@shell/components/PopoverCard.vue';
|
|
9
|
+
import ActionMenu from '@shell/components/ActionMenuShell.vue';
|
|
12
10
|
|
|
13
11
|
export interface Props {
|
|
14
12
|
type: string;
|
|
@@ -23,14 +21,14 @@ const store = useStore();
|
|
|
23
21
|
const i18n = useI18n(store);
|
|
24
22
|
const props = defineProps<Props>();
|
|
25
23
|
const card = ref<any>(null);
|
|
26
|
-
const popoverContainer = ref(null);
|
|
27
24
|
const showPopover = ref<boolean>(false);
|
|
28
|
-
const focusOpen = ref<boolean>(false);
|
|
29
25
|
|
|
30
26
|
const fetch = useFetch(async() => {
|
|
31
27
|
const currentStore = props.currentStore || store.getters['currentStore'](props.type);
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
const r = await store.dispatch(`${ currentStore }/find`, { type: props.type, id: props.id });
|
|
30
|
+
|
|
31
|
+
return r;
|
|
34
32
|
});
|
|
35
33
|
|
|
36
34
|
const resourceTypeLabel = computed(() => {
|
|
@@ -45,88 +43,63 @@ const resourceTypeLabel = computed(() => {
|
|
|
45
43
|
return resource.parentNameOverride || store.getters['type-map/labelFor'](schema);
|
|
46
44
|
});
|
|
47
45
|
|
|
46
|
+
const nameDisplay = computed(() => {
|
|
47
|
+
return fetch.value.data?.nameDisplay || '';
|
|
48
|
+
});
|
|
49
|
+
|
|
48
50
|
const actionInvoked = () => {
|
|
49
51
|
showPopover.value = false;
|
|
50
52
|
};
|
|
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
53
|
</script>
|
|
68
54
|
|
|
69
55
|
<template>
|
|
70
|
-
<
|
|
56
|
+
<PopoverCard
|
|
71
57
|
class="resource-popover"
|
|
72
|
-
|
|
58
|
+
:card-title="nameDisplay"
|
|
59
|
+
fallback-focus="[data-testid='resource-popover-action-menu']"
|
|
60
|
+
:show-popover-aria-label="i18n.t('component.resource.detail.glance.ariaLabel.showDetails', { name: nameDisplay, resource: resourceTypeLabel })"
|
|
73
61
|
>
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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"
|
|
62
|
+
<span>
|
|
63
|
+
<span
|
|
64
|
+
v-if="fetch.data"
|
|
65
|
+
class="display"
|
|
66
|
+
@mouseenter="showPopover=true"
|
|
67
|
+
>
|
|
68
|
+
<RcStatusIndicator
|
|
69
|
+
shape="disc"
|
|
70
|
+
:status="fetch.data?.stateBackground || 'unknown'"
|
|
126
71
|
/>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
72
|
+
<router-link
|
|
73
|
+
:to="props.detailLocation || fetch.data.detailLocation || '#'"
|
|
74
|
+
>
|
|
75
|
+
{{ nameDisplay }}
|
|
76
|
+
</router-link>
|
|
77
|
+
</span>
|
|
78
|
+
<span v-else>{{ fetch.loading }}...</span>
|
|
79
|
+
</span>
|
|
80
|
+
<template
|
|
81
|
+
v-if="fetch.data"
|
|
82
|
+
#heading-action="{close}"
|
|
83
|
+
>
|
|
84
|
+
<ActionMenu
|
|
85
|
+
:resource="fetch.data"
|
|
86
|
+
:button-aria-label="i18n.t('component.resource.detail.glance.ariaLabel.actionMenu', { resource: nameDisplay })"
|
|
87
|
+
data-testid="resource-popover-action-menu"
|
|
88
|
+
@action-invoked="close"
|
|
89
|
+
/>
|
|
90
|
+
</template>
|
|
91
|
+
<template
|
|
92
|
+
v-if="fetch.data"
|
|
93
|
+
#card-body
|
|
94
|
+
>
|
|
95
|
+
<ResourcePopoverCard
|
|
96
|
+
id="resource-popover-card"
|
|
97
|
+
ref="card"
|
|
98
|
+
:resource="fetch.data"
|
|
99
|
+
@action-invoked="actionInvoked"
|
|
100
|
+
/>
|
|
101
|
+
</template>
|
|
102
|
+
</PopoverCard>
|
|
130
103
|
</template>
|
|
131
104
|
|
|
132
105
|
<style lang="scss" scoped>
|
|
@@ -134,44 +107,8 @@ watch(
|
|
|
134
107
|
position: relative;
|
|
135
108
|
width: 100%;
|
|
136
109
|
|
|
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
110
|
.display {
|
|
150
111
|
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
112
|
}
|
|
176
113
|
|
|
177
114
|
.rc-status-indicator {
|
|
@@ -180,47 +117,5 @@ watch(
|
|
|
180
117
|
height: initial;
|
|
181
118
|
line-height: initial;
|
|
182
119
|
}
|
|
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
120
|
}
|
|
226
121
|
</style>
|