@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.
- package/assets/styles/base/_color.scss +4 -1
- package/assets/styles/global/_tooltip.scss +7 -4
- package/assets/styles/themes/_dark.scss +11 -0
- package/assets/styles/themes/_light.scss +13 -1
- package/assets/styles/themes/_modern.scss +22 -0
- package/assets/translations/en-us.yaml +147 -19
- package/assets/translations/zh-hans.yaml +0 -1
- package/chart/monitoring/grafana/index.vue +8 -2
- package/components/ActionMenuShell.vue +3 -1
- package/components/Cron/CronExpressionEditor.vue +299 -0
- package/components/Cron/CronExpressionEditorModal.vue +247 -0
- package/components/Cron/CronTooltip.vue +87 -0
- package/components/Cron/types.ts +13 -0
- package/components/ForceDirectedTreeChart/composable.ts +11 -0
- package/components/PodSecurityAdmission.vue +2 -0
- package/components/PromptModal.vue +1 -1
- package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +1 -0
- package/components/Resource/Detail/CopyToClipboard.vue +78 -0
- package/components/Resource/Detail/FetchLoader/__tests__/composables.test.ts +69 -0
- package/components/Resource/Detail/FetchLoader/composables.ts +27 -0
- package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/Metadata/Annotations/index.vue +1 -1
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +13 -61
- package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +33 -6
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +24 -38
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +25 -5
- package/components/Resource/Detail/Metadata/KeyValue.vue +12 -23
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +144 -0
- package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -0
- package/components/Resource/Detail/Metadata/Labels/index.vue +1 -0
- package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +30 -32
- package/components/Resource/Detail/Metadata/__tests__/KeyValueRow.test.ts +108 -0
- package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +0 -3
- package/components/Resource/Detail/Metadata/__tests__/index.test.ts +12 -5
- package/components/Resource/Detail/Metadata/composables.ts +1 -4
- package/components/Resource/Detail/Metadata/index.vue +1 -0
- package/components/Resource/Detail/Preview/Content.vue +63 -0
- package/components/Resource/Detail/Preview/Preview.vue +128 -0
- package/components/Resource/Detail/Preview/__tests__/Content.spec.ts +71 -0
- package/components/Resource/Detail/Preview/__tests__/Preview.spec.ts +121 -0
- package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +141 -0
- package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +136 -0
- package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +245 -0
- package/components/Resource/Detail/ResourcePopover/index.vue +226 -0
- package/components/Resource/Detail/SpacedRow.vue +1 -0
- package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +0 -5
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
- package/components/Resource/Detail/TitleBar/composables.ts +1 -3
- package/components/Resource/Detail/TitleBar/index.vue +2 -29
- package/components/Resource/Detail/ViewOptions/composable.ts +9 -0
- package/components/Resource/Detail/ViewOptions/index.vue +41 -0
- package/components/Resource/Detail/__tests__/CopyToClipboard.spec.ts +82 -0
- package/components/ResourceDetail/Masthead/legacy.vue +0 -19
- package/components/ResourceDetail/index.vue +1 -26
- package/components/ResourceTable.vue +24 -0
- package/components/SortableTable/index.vue +7 -1
- package/components/SortableTable/paging.js +3 -0
- package/components/Tabbed/Tab.vue +43 -1
- package/components/Tabbed/index.vue +3 -1
- package/components/__tests__/Cron/CronExpressionEditor.test.ts +151 -0
- package/components/__tests__/Cron/CronExpressionEditorModal.test.ts +81 -0
- package/components/auth/login/saml.vue +86 -0
- package/components/form/LabeledSelect.vue +8 -8
- package/components/form/ProjectMemberEditor.vue +2 -0
- package/components/form/ResourceTabs/composable.ts +54 -0
- package/components/form/ResourceTabs/index.vue +10 -7
- package/components/form/Select.vue +13 -10
- package/components/form/__tests__/LabeledSelect.test.ts +133 -0
- package/components/form/__tests__/Select.test.ts +134 -0
- package/components/nav/Header.vue +6 -5
- package/composables/useExtensionManager.ts +17 -0
- package/config/home-links.js +12 -0
- package/config/labels-annotations.js +0 -1
- package/config/page-actions.js +0 -1
- package/config/product/explorer.js +3 -1
- package/config/product/fleet.js +2 -7
- package/config/product/manager.js +0 -5
- package/config/query-params.js +1 -0
- package/config/router/navigation-guards/clusters.js +2 -1
- package/config/router/navigation-guards/products.js +1 -1
- package/config/store.js +2 -0
- package/core/extension-manager-impl.js +518 -0
- package/core/plugins.js +35 -468
- package/core/types.ts +8 -2
- package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +1 -0
- package/detail/catalog.cattle.io.app.vue +7 -4
- package/detail/fleet.cattle.io.bundle.vue +1 -5
- package/detail/fleet.cattle.io.cluster.vue +3 -2
- package/detail/fleet.cattle.io.gitrepo.vue +76 -49
- package/detail/fleet.cattle.io.helmop.vue +78 -49
- package/dialog/AddonConfigConfirmationDialog.vue +1 -1
- package/dialog/GenericPrompt.vue +1 -1
- package/dialog/ImportDialog.vue +9 -2
- package/dialog/InstallExtensionDialog.vue +18 -10
- package/dialog/SloDialog.vue +1 -1
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
- package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
- package/edit/auth/oidc.vue +106 -6
- package/edit/auth/saml.vue +5 -5
- package/edit/cloudcredential.vue +31 -17
- package/edit/constraints.gatekeeper.sh.constraint/index.vue +10 -2
- package/edit/fleet.cattle.io.cluster.vue +19 -0
- package/edit/fleet.cattle.io.gitrepo.vue +23 -16
- package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +12 -11
- package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -1
- package/edit/provisioning.cattle.io.cluster/index.vue +14 -19
- package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -3
- package/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +1 -0
- package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -0
- package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +6 -0
- package/edit/resources.cattle.io.restore.vue +5 -8
- package/initialize/install-plugins.js +1 -3
- package/list/__tests__/workload.test.ts +1 -0
- package/list/workload.vue +8 -1
- package/machine-config/components/GCEImage.vue +6 -5
- package/machine-config/google.vue +11 -6
- package/mixins/__tests__/auth-config.test.ts +4 -6
- package/mixins/__tests__/chart.test.ts +139 -1
- package/mixins/auth-config.js +33 -10
- package/mixins/chart.js +58 -18
- package/models/__tests__/namespace.test.ts +69 -0
- package/models/apps.statefulset.js +8 -10
- package/models/chart.js +5 -1
- package/models/fleet-application.js +16 -46
- package/models/fleet.cattle.io.bundle.js +1 -38
- package/models/fleet.cattle.io.gitrepo.js +4 -0
- package/models/fleet.cattle.io.helmop.js +4 -0
- package/models/management.cattle.io.cluster.js +1 -1
- package/models/management.cattle.io.project.js +12 -0
- package/models/namespace.js +30 -0
- package/models/workload.js +4 -1
- package/package.json +10 -10
- package/pages/auth/login.vue +8 -3
- package/pages/auth/logout.vue +6 -5
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +26 -11
- package/pages/c/_cluster/apps/charts/chart.vue +29 -20
- package/pages/c/_cluster/apps/charts/index.vue +1 -0
- package/pages/c/_cluster/apps/charts/install.vue +6 -5
- package/pages/c/_cluster/explorer/tools/__tests__/index.test.ts +102 -12
- package/pages/c/_cluster/explorer/tools/index.vue +145 -254
- package/pages/c/_cluster/manager/cloudCredential/index.vue +18 -1
- package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +12 -2
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
- package/pages/c/_cluster/uiplugins/__tests__/index.spec.ts +318 -0
- package/pages/c/_cluster/uiplugins/index.vue +221 -363
- package/pages/home.vue +1 -9
- package/plugins/axios.js +3 -2
- package/plugins/dashboard-store/resource-class.js +49 -0
- package/plugins/ember-cookie.js +7 -3
- package/plugins/steve/subscribe.js +4 -2
- package/public/index.html +2 -1
- package/rancher-components/Card/Card.vue +1 -1
- package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
- package/rancher-components/Form/Radio/RadioButton.vue +1 -1
- package/rancher-components/Form/Radio/RadioGroup.vue +1 -1
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -11
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.test.ts +53 -0
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +65 -0
- package/rancher-components/Pill/RcCounterBadge/index.ts +1 -0
- package/rancher-components/Pill/RcCounterBadge/types.ts +7 -0
- package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +1 -1
- package/rancher-components/Pill/RcStatusBadge/index.ts +1 -1
- package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -3
- package/rancher-components/Pill/RcStatusIndicator/types.ts +1 -1
- package/rancher-components/Pill/RcTag/RcTag.test.ts +64 -0
- package/rancher-components/Pill/RcTag/RcTag.vue +94 -0
- package/rancher-components/Pill/RcTag/index.ts +1 -0
- package/rancher-components/Pill/RcTag/types.ts +9 -0
- package/rancher-components/Pill/types.ts +1 -0
- package/rancher-components/RcItemCard/RcItemCard.vue +1 -0
- package/rancher-components/RcItemCard/RcItemCardAction.vue +12 -0
- package/scripts/test-plugins-build.sh +0 -1
- package/store/__tests__/catalog.test.ts +63 -0
- package/store/__tests__/cookies.test.ts +72 -0
- package/store/auth.js +33 -10
- package/store/catalog.js +2 -2
- package/store/cookies.ts +30 -0
- package/store/prefs.js +10 -5
- package/store/type-map.js +3 -15
- package/types/extension-manager.ts +26 -0
- package/types/shell/index.d.ts +123 -27
- package/utils/__tests__/product.test.ts +129 -0
- package/utils/__tests__/resource.test.ts +87 -0
- package/utils/alertmanagerconfig.js +2 -2
- package/utils/auth.js +4 -77
- package/utils/product.ts +39 -0
- package/utils/resource.ts +35 -0
- package/utils/select.js +0 -24
- package/utils/validators/formRules/__tests__/index.test.ts +3 -0
- package/utils/validators/formRules/index.ts +2 -1
- package/vue.config.js +1 -1
- package/components/Resource/Detail/Metadata/Rectangle.vue +0 -34
- package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +0 -24
- package/components/ResourceDetail/Masthead/__tests__/legacy.test.ts +0 -65
- package/utils/cookie-universal.js +0 -10
- /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>
|
|
@@ -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:
|
|
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,
|
|
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,
|
|
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>
|