@rancher/shell 3.0.8-rc.9 → 3.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apis/impl/apis.ts +61 -0
- package/apis/index.ts +40 -0
- package/apis/intf/modal.ts +90 -0
- package/apis/intf/shell.ts +36 -0
- package/apis/intf/slide-in.ts +98 -0
- package/apis/intf/system.ts +41 -0
- package/apis/shell/__tests__/modal.test.ts +80 -0
- package/apis/shell/__tests__/notifications.test.ts +71 -0
- package/apis/shell/__tests__/slide-in.test.ts +54 -0
- package/apis/shell/__tests__/system.test.ts +129 -0
- package/apis/shell/index.ts +38 -0
- package/apis/shell/modal.ts +41 -0
- package/apis/shell/notifications.ts +65 -0
- package/apis/shell/slide-in.ts +33 -0
- package/apis/shell/system.ts +65 -0
- package/apis/vue-shim.d.ts +11 -0
- package/assets/styles/global/_tooltip.scss +6 -1
- package/assets/translations/en-us.yaml +5 -0
- package/components/ActionMenuShell.vue +3 -1
- package/components/CruResource.vue +8 -1
- package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +50 -1
- package/components/Drawer/ResourceDetailDrawer/composables.ts +19 -0
- package/components/Drawer/ResourceDetailDrawer/index.vue +3 -1
- package/components/LocaleSelector.vue +2 -2
- package/components/ModalManager.vue +11 -1
- package/components/Questions/__tests__/Yaml.test.ts +1 -1
- package/components/RelatedResources.vue +5 -0
- package/components/Resource/Detail/ResourcePopover/index.vue +5 -1
- package/components/ResourceDetail/Masthead/latest.vue +23 -21
- package/components/ResourceDetail/index.vue +3 -0
- package/components/ResourceTable.vue +54 -21
- package/components/SlideInPanelManager.vue +16 -11
- package/components/SortableTable/THead.vue +2 -1
- package/components/SortableTable/index.vue +20 -2
- package/components/Tabbed/index.vue +37 -2
- package/components/__tests__/NamespaceFilter.test.ts +49 -0
- package/components/auth/SelectPrincipal.vue +4 -0
- package/components/auth/login/ldap.vue +3 -3
- package/components/fleet/FleetSecretSelector.vue +1 -1
- package/components/form/KeyValue.vue +1 -1
- package/components/form/NameNsDescription.vue +1 -1
- package/components/form/NodeScheduling.vue +2 -2
- package/components/form/ResourceTabs/composable.ts +2 -2
- package/components/form/ResourceTabs/index.vue +0 -2
- package/components/form/__tests__/NameNsDescription.test.ts +42 -0
- package/components/formatter/LinkName.vue +5 -0
- package/components/nav/Group.vue +25 -7
- package/components/nav/Header.vue +1 -1
- package/components/nav/NamespaceFilter.vue +1 -0
- package/components/nav/Type.vue +17 -6
- package/components/nav/WindowManager/panels/TabBodyContainer.vue +1 -1
- package/components/nav/__tests__/Type.test.ts +59 -0
- package/composables/cruResource.ts +27 -0
- package/composables/focusTrap.ts +3 -1
- package/composables/resourceDetail.ts +15 -0
- package/composables/useLabeledFormElement.ts +3 -4
- package/config/product/fleet.js +1 -1
- package/config/router/navigation-guards/clusters.js +3 -3
- package/config/router/navigation-guards/products.js +1 -1
- package/config/router/routes.js +1 -5
- package/core/__tests__/extension-manager-impl.test.js +437 -0
- package/core/extension-manager-impl.js +6 -27
- package/core/plugin-helpers.ts +2 -2
- package/core/plugin.ts +9 -1
- package/core/plugins-loader.js +2 -2
- package/core/types-provisioning.ts +4 -0
- package/core/types.ts +35 -0
- package/detail/provisioning.cattle.io.cluster.vue +8 -6
- package/dialog/DeveloperLoadExtensionDialog.vue +1 -1
- package/dialog/MoveNamespaceDialog.vue +20 -4
- package/dialog/SearchDialog.vue +1 -0
- package/dialog/__tests__/MoveNamespaceDialog.test.ts +249 -0
- package/directives/__tests__/clean-tooltip.test.ts +298 -0
- package/directives/clean-tooltip.ts +234 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -2
- package/edit/__tests__/fleet.cattle.io.helmop.test.ts +98 -1
- package/edit/fleet.cattle.io.helmop.vue +5 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +21 -21
- package/edit/provisioning.cattle.io.cluster/index.vue +5 -5
- package/edit/provisioning.cattle.io.cluster/rke2.vue +8 -8
- package/edit/resources.cattle.io.restore.vue +1 -1
- package/edit/workload/Job.vue +2 -2
- package/edit/workload/index.vue +1 -1
- package/initialize/install-plugins.js +4 -5
- package/machine-config/azure.vue +1 -1
- package/machine-config/components/GCEImage.vue +1 -1
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +16 -0
- package/models/chart.js +70 -74
- package/models/management.cattle.io.cluster.js +1 -1
- package/models/provisioning.cattle.io.cluster.js +11 -3
- package/package.json +7 -7
- package/pages/auth/login.vue +3 -3
- package/pages/auth/setup.vue +1 -1
- package/pages/auth/verify.vue +3 -3
- package/pages/c/_cluster/apps/charts/index.vue +122 -24
- package/pages/c/_cluster/apps/charts/install.vue +33 -0
- package/pages/c/_cluster/explorer/__tests__/index.test.ts +1 -1
- package/pages/c/_cluster/fleet/index.vue +4 -7
- package/pages/c/_cluster/settings/index.vue +5 -0
- package/pkg/auto-import.js +3 -3
- package/pkg/dynamic-importer.lib.js +1 -1
- package/pkg/import.js +1 -1
- package/plugins/__tests__/mutations.tests.ts +179 -0
- package/plugins/dashboard-store/getters.js +1 -1
- package/plugins/dashboard-store/model-loader.js +1 -1
- package/plugins/dashboard-store/mutations.js +23 -2
- package/plugins/dashboard-store/resource-class.js +8 -3
- package/plugins/plugin.js +2 -2
- package/plugins/steve/__tests__/steve-pagination-utils.test.ts +301 -128
- package/plugins/steve/steve-class.js +1 -1
- package/plugins/steve/steve-pagination-utils.ts +108 -43
- package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -1
- package/rancher-components/RcDropdown/useDropdownContext.ts +2 -4
- package/rancher-components/RcItemCard/RcItemCard.vue +1 -1
- package/scripts/publish-shell.sh +25 -0
- package/store/__tests__/catalog.test.ts +1 -1
- package/store/__tests__/type-map.test.ts +164 -2
- package/store/auth.js +23 -11
- package/store/i18n.js +3 -3
- package/store/index.js +5 -3
- package/store/notifications.ts +2 -0
- package/store/prefs.js +2 -2
- package/store/type-map.js +17 -7
- package/types/internal-api/shell/modal.d.ts +6 -6
- package/types/notifications/index.ts +126 -15
- package/types/rancher/index.d.ts +9 -0
- package/types/shell/index.d.ts +16 -1
- package/types/vue-shim.d.ts +5 -4
- package/utils/__tests__/router.test.js +238 -0
- package/utils/cluster.js +4 -1
- package/utils/fleet.ts +8 -1
- package/utils/pagination-utils.ts +2 -2
- package/utils/pagination-wrapper.ts +1 -1
- package/utils/router.js +50 -0
- package/utils/unit-tests/pagination-utils.spec.ts +8 -8
- package/vue.config.js +3 -3
- package/composables/useExtensionManager.ts +0 -17
- package/core/__test__/extension-manager-impl.test.js +0 -236
- package/core/plugins.js +0 -38
- package/directives/clean-tooltip.js +0 -32
- package/plugins/internal-api/index.ts +0 -37
- package/plugins/internal-api/shared/base-api.ts +0 -13
- package/plugins/internal-api/shell/shell.api.ts +0 -108
- package/types/internal-api/shell/growl.d.ts +0 -25
- package/types/internal-api/shell/slideIn.d.ts +0 -15
|
@@ -6,6 +6,8 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
|
6
6
|
import { MANAGEMENT } from '@shell/config/types';
|
|
7
7
|
import { PROJECT } from '@shell/config/labels-annotations';
|
|
8
8
|
|
|
9
|
+
const NONE_VALUE = ' ';
|
|
10
|
+
|
|
9
11
|
export default {
|
|
10
12
|
emits: ['close'],
|
|
11
13
|
|
|
@@ -48,8 +50,12 @@ export default {
|
|
|
48
50
|
return this.toMove.filter((namespace) => !!namespace.project).map((namespace) => namespace.project.shortId);
|
|
49
51
|
},
|
|
50
52
|
|
|
53
|
+
isAllInProject() {
|
|
54
|
+
return this.toMove.every((namespace) => !!namespace.project);
|
|
55
|
+
},
|
|
56
|
+
|
|
51
57
|
projectOptions() {
|
|
52
|
-
|
|
58
|
+
const options = this.projects.reduce((inCluster, project) => {
|
|
53
59
|
if (!this.excludedProjects.includes(project.shortId) && project.spec?.clusterName === this.currentCluster.id) {
|
|
54
60
|
inCluster.push({
|
|
55
61
|
value: project.shortId,
|
|
@@ -59,6 +65,16 @@ export default {
|
|
|
59
65
|
|
|
60
66
|
return inCluster;
|
|
61
67
|
}, []);
|
|
68
|
+
|
|
69
|
+
// To be consistent with listed projects we should only provide the option if it applies too all of the namespaces
|
|
70
|
+
if (this.isAllInProject) {
|
|
71
|
+
options.unshift({
|
|
72
|
+
value: NONE_VALUE,
|
|
73
|
+
label: this.t('moveModal.noProject')
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return options;
|
|
62
78
|
}
|
|
63
79
|
},
|
|
64
80
|
|
|
@@ -69,10 +85,10 @@ export default {
|
|
|
69
85
|
|
|
70
86
|
async move(finish) {
|
|
71
87
|
const cluster = this.$store.getters['currentCluster'];
|
|
72
|
-
const clusterWithProjectId = `${ cluster.id }:${ this.targetProject }
|
|
88
|
+
const clusterWithProjectId = this.targetProject && this.targetProject !== NONE_VALUE ? `${ cluster.id }:${ this.targetProject }` : null;
|
|
73
89
|
|
|
74
90
|
const promises = this.toMove.map((namespace) => {
|
|
75
|
-
namespace.setLabel(PROJECT, this.targetProject);
|
|
91
|
+
namespace.setLabel(PROJECT, this.targetProject && this.targetProject !== NONE_VALUE ? this.targetProject : null);
|
|
76
92
|
namespace.setAnnotation(PROJECT, clusterWithProjectId);
|
|
77
93
|
|
|
78
94
|
return namespace.save();
|
|
@@ -128,7 +144,7 @@ export default {
|
|
|
128
144
|
<AsyncButton
|
|
129
145
|
:action-label="t('moveModal.moveButtonLabel')"
|
|
130
146
|
class="btn bg-primary ml-10"
|
|
131
|
-
:disabled="
|
|
147
|
+
:disabled="targetProject === null"
|
|
132
148
|
@click="move"
|
|
133
149
|
/>
|
|
134
150
|
</template>
|
package/dialog/SearchDialog.vue
CHANGED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { shallowMount, VueWrapper } from '@vue/test-utils';
|
|
2
|
+
import MoveNamespaceDialog from '@shell/dialog/MoveNamespaceDialog.vue';
|
|
3
|
+
|
|
4
|
+
const t = (key: string): string => key;
|
|
5
|
+
const NONE_VALUE = ' ';
|
|
6
|
+
|
|
7
|
+
describe('component: MoveNamespaceDialog', () => {
|
|
8
|
+
let wrapper: VueWrapper<any>;
|
|
9
|
+
|
|
10
|
+
const mockProjects = [
|
|
11
|
+
{
|
|
12
|
+
shortId: 'p-abc123',
|
|
13
|
+
nameDisplay: 'Project A',
|
|
14
|
+
spec: { clusterName: 'local' }
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
shortId: 'p-def456',
|
|
18
|
+
nameDisplay: 'Project B',
|
|
19
|
+
spec: { clusterName: 'local' }
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
shortId: 'p-other',
|
|
23
|
+
nameDisplay: 'Other Cluster Project',
|
|
24
|
+
spec: { clusterName: 'other-cluster' }
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const createMockNamespace = (projectId: string | null = null) => {
|
|
29
|
+
const namespace: any = {
|
|
30
|
+
nameDisplay: 'test-namespace',
|
|
31
|
+
projectId,
|
|
32
|
+
project: projectId ? { shortId: projectId } : null,
|
|
33
|
+
setLabel: jest.fn(),
|
|
34
|
+
setAnnotation: jest.fn(),
|
|
35
|
+
save: jest.fn().mockResolvedValue({}),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return namespace;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const mountComponent = (propsData = {}, options = {}) => {
|
|
42
|
+
const store = {
|
|
43
|
+
dispatch: jest.fn().mockResolvedValue(mockProjects),
|
|
44
|
+
getters: { currentCluster: { id: 'local' } }
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const defaultProps = {
|
|
48
|
+
resources: [createMockNamespace('p-abc123')],
|
|
49
|
+
movingCb: jest.fn(),
|
|
50
|
+
registerBackgroundClosing: jest.fn(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return shallowMount(MoveNamespaceDialog, {
|
|
54
|
+
propsData: {
|
|
55
|
+
...defaultProps,
|
|
56
|
+
...propsData,
|
|
57
|
+
},
|
|
58
|
+
global: {
|
|
59
|
+
mocks: {
|
|
60
|
+
$store: store,
|
|
61
|
+
t,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
...options,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
if (wrapper) {
|
|
70
|
+
wrapper.unmount();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('projectOptions', () => {
|
|
75
|
+
it('should include "None" option as first item', async() => {
|
|
76
|
+
wrapper = mountComponent();
|
|
77
|
+
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
|
78
|
+
|
|
79
|
+
const options = wrapper.vm.projectOptions;
|
|
80
|
+
|
|
81
|
+
expect(options[0]).toStrictEqual({
|
|
82
|
+
value: NONE_VALUE,
|
|
83
|
+
label: 'moveModal.noProject'
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should include projects from current cluster', async() => {
|
|
88
|
+
// Use a namespace not in any project so no projects get excluded
|
|
89
|
+
const namespace = createMockNamespace(null);
|
|
90
|
+
|
|
91
|
+
wrapper = mountComponent({ resources: [namespace] });
|
|
92
|
+
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
|
93
|
+
|
|
94
|
+
const options = wrapper.vm.projectOptions;
|
|
95
|
+
const projectLabels = options.map((o: any) => o.label);
|
|
96
|
+
|
|
97
|
+
expect(projectLabels).toContain('Project A');
|
|
98
|
+
expect(projectLabels).toContain('Project B');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should exclude projects from other clusters', async() => {
|
|
102
|
+
wrapper = mountComponent();
|
|
103
|
+
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
|
104
|
+
|
|
105
|
+
const options = wrapper.vm.projectOptions;
|
|
106
|
+
const projectLabels = options.map((o: any) => o.label);
|
|
107
|
+
|
|
108
|
+
expect(projectLabels).not.toContain('Other Cluster Project');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should exclude current project of namespaces being moved', async() => {
|
|
112
|
+
const namespace = createMockNamespace('p-abc123');
|
|
113
|
+
|
|
114
|
+
wrapper = mountComponent({ resources: [namespace] });
|
|
115
|
+
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
|
116
|
+
|
|
117
|
+
const options = wrapper.vm.projectOptions;
|
|
118
|
+
const projectValues = options.map((o: any) => o.value);
|
|
119
|
+
|
|
120
|
+
expect(projectValues).not.toContain('p-abc123');
|
|
121
|
+
expect(projectValues).toContain('p-def456');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should NOT include "None" option when some namespaces are not in a project', async() => {
|
|
125
|
+
const namespaceInProject = createMockNamespace('p-abc123');
|
|
126
|
+
const namespaceNotInProject = createMockNamespace(null);
|
|
127
|
+
|
|
128
|
+
wrapper = mountComponent({ resources: [namespaceInProject, namespaceNotInProject] });
|
|
129
|
+
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
|
130
|
+
|
|
131
|
+
const options = wrapper.vm.projectOptions;
|
|
132
|
+
const optionValues = options.map((o: any) => o.value);
|
|
133
|
+
|
|
134
|
+
expect(optionValues).not.toContain(NONE_VALUE);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should NOT include "None" option when no namespaces are in a project', async() => {
|
|
138
|
+
const namespace = createMockNamespace(null);
|
|
139
|
+
|
|
140
|
+
wrapper = mountComponent({ resources: [namespace] });
|
|
141
|
+
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
|
142
|
+
|
|
143
|
+
const options = wrapper.vm.projectOptions;
|
|
144
|
+
const optionValues = options.map((o: any) => o.value);
|
|
145
|
+
|
|
146
|
+
expect(optionValues).not.toContain(NONE_VALUE);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('targetProject default value', () => {
|
|
151
|
+
it('should default to empty string (None option)', () => {
|
|
152
|
+
wrapper = mountComponent();
|
|
153
|
+
|
|
154
|
+
expect(wrapper.vm.targetProject).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('move button disabled state', () => {
|
|
159
|
+
it('should be enabled when targetProject is NONE_VALUE (None)', () => {
|
|
160
|
+
wrapper = mountComponent();
|
|
161
|
+
wrapper.vm.targetProject = NONE_VALUE;
|
|
162
|
+
|
|
163
|
+
// The button should be enabled when targetProject !== null
|
|
164
|
+
expect(wrapper.vm.targetProject === null).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should be enabled when targetProject is a project id', () => {
|
|
168
|
+
wrapper = mountComponent();
|
|
169
|
+
wrapper.vm.targetProject = 'p-def456';
|
|
170
|
+
|
|
171
|
+
expect(wrapper.vm.targetProject === null).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('move method', () => {
|
|
176
|
+
it('should clear labels and annotations when targetProject is NONE_VALUE (None)', async() => {
|
|
177
|
+
const namespace = createMockNamespace('p-abc123');
|
|
178
|
+
|
|
179
|
+
wrapper = mountComponent({ resources: [namespace] });
|
|
180
|
+
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
|
181
|
+
|
|
182
|
+
wrapper.vm.targetProject = NONE_VALUE;
|
|
183
|
+
|
|
184
|
+
const finish = jest.fn();
|
|
185
|
+
|
|
186
|
+
await wrapper.vm.move(finish);
|
|
187
|
+
|
|
188
|
+
expect(namespace.setLabel).toHaveBeenCalledWith('field.cattle.io/projectId', null);
|
|
189
|
+
expect(namespace.setAnnotation).toHaveBeenCalledWith('field.cattle.io/projectId', null);
|
|
190
|
+
expect(namespace.save).toHaveBeenCalledWith();
|
|
191
|
+
expect(finish).toHaveBeenCalledWith(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should set labels and annotations when moving to a project', async() => {
|
|
195
|
+
const namespace = createMockNamespace('p-abc123');
|
|
196
|
+
|
|
197
|
+
wrapper = mountComponent({ resources: [namespace] });
|
|
198
|
+
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
|
199
|
+
|
|
200
|
+
wrapper.vm.targetProject = 'p-def456';
|
|
201
|
+
|
|
202
|
+
const finish = jest.fn();
|
|
203
|
+
|
|
204
|
+
await wrapper.vm.move(finish);
|
|
205
|
+
|
|
206
|
+
expect(namespace.setLabel).toHaveBeenCalledWith('field.cattle.io/projectId', 'p-def456');
|
|
207
|
+
expect(namespace.setAnnotation).toHaveBeenCalledWith('field.cattle.io/projectId', 'local:p-def456');
|
|
208
|
+
expect(namespace.save).toHaveBeenCalledWith();
|
|
209
|
+
expect(finish).toHaveBeenCalledWith(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle multiple namespaces', async() => {
|
|
213
|
+
const namespace1 = createMockNamespace('p-abc123');
|
|
214
|
+
const namespace2 = createMockNamespace('p-abc123');
|
|
215
|
+
|
|
216
|
+
wrapper = mountComponent({ resources: [namespace1, namespace2] });
|
|
217
|
+
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
|
218
|
+
|
|
219
|
+
wrapper.vm.targetProject = NONE_VALUE;
|
|
220
|
+
|
|
221
|
+
const finish = jest.fn();
|
|
222
|
+
|
|
223
|
+
await wrapper.vm.move(finish);
|
|
224
|
+
|
|
225
|
+
expect(namespace1.setLabel).toHaveBeenCalledWith('field.cattle.io/projectId', null);
|
|
226
|
+
expect(namespace1.setAnnotation).toHaveBeenCalledWith('field.cattle.io/projectId', null);
|
|
227
|
+
expect(namespace2.setLabel).toHaveBeenCalledWith('field.cattle.io/projectId', null);
|
|
228
|
+
expect(namespace2.setAnnotation).toHaveBeenCalledWith('field.cattle.io/projectId', null);
|
|
229
|
+
expect(namespace1.save).toHaveBeenCalledWith();
|
|
230
|
+
expect(namespace2.save).toHaveBeenCalledWith();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should call finish with false when save fails', async() => {
|
|
234
|
+
const namespace = createMockNamespace('p-abc123');
|
|
235
|
+
|
|
236
|
+
jest.spyOn(namespace, 'save').mockImplementation().mockRejectedValue(new Error('Save failed'));
|
|
237
|
+
wrapper = mountComponent({ resources: [namespace] });
|
|
238
|
+
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
|
239
|
+
|
|
240
|
+
wrapper.vm.targetProject = NONE_VALUE;
|
|
241
|
+
|
|
242
|
+
const finish = jest.fn();
|
|
243
|
+
|
|
244
|
+
await wrapper.vm.move(finish);
|
|
245
|
+
|
|
246
|
+
expect(finish).toHaveBeenCalledWith(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
const mockCreateTooltip = jest.fn();
|
|
2
|
+
const mockDestroyTooltip = jest.fn();
|
|
3
|
+
const mockPurifyHTML = jest.fn((content) => (content || '').trim());
|
|
4
|
+
|
|
5
|
+
jest.mock('floating-vue', () => ({
|
|
6
|
+
createTooltip: mockCreateTooltip,
|
|
7
|
+
destroyTooltip: mockDestroyTooltip,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
jest.mock('@shell/plugins/clean-html', () => ({ purifyHTML: mockPurifyHTML }));
|
|
11
|
+
|
|
12
|
+
// A simple mock for the tooltip instance returned by createTooltip
|
|
13
|
+
const mockTooltipInstance = { show: jest.fn() };
|
|
14
|
+
|
|
15
|
+
describe('clean-tooltip.ts', () => {
|
|
16
|
+
let el: any;
|
|
17
|
+
let cleanTooltipDirective: any;
|
|
18
|
+
let onMouseEnter: (e: MouseEvent | FocusEvent) => void;
|
|
19
|
+
let onMouseLeave: (e: MouseEvent | FocusEvent) => void;
|
|
20
|
+
let onMouseClick: (e: MouseEvent) => void;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
jest.isolateModules(() => {
|
|
25
|
+
const module = require('../clean-tooltip');
|
|
26
|
+
|
|
27
|
+
cleanTooltipDirective = module.default;
|
|
28
|
+
onMouseEnter = module.onMouseEnter;
|
|
29
|
+
onMouseLeave = module.onMouseLeave;
|
|
30
|
+
onMouseClick = module.onMouseClick;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
mockCreateTooltip.mockReturnValue(mockTooltipInstance);
|
|
34
|
+
|
|
35
|
+
el = document.createElement('div');
|
|
36
|
+
document.body.appendChild(el);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
if (document.body.contains(el)) {
|
|
41
|
+
document.body.removeChild(el);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('directive: cleanTooltipDirective', () => {
|
|
46
|
+
describe('mounted', () => {
|
|
47
|
+
it('should add event listeners and set initial properties for string value', () => {
|
|
48
|
+
const addEventListenerSpy = jest.spyOn(el, 'addEventListener');
|
|
49
|
+
const binding = {
|
|
50
|
+
value: 'Test Tooltip',
|
|
51
|
+
modifiers: {},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
cleanTooltipDirective.mounted(el, binding);
|
|
55
|
+
|
|
56
|
+
expect(el.classList.contains('has-clean-tooltip')).toBe(true);
|
|
57
|
+
expect(el.__tooltipOptions__).toStrictEqual({ content: 'Test Tooltip' });
|
|
58
|
+
|
|
59
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith('mouseenter', onMouseEnter);
|
|
60
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith('mouseleave', onMouseLeave);
|
|
61
|
+
expect(addEventListenerSpy).not.toHaveBeenCalledWith('focus', onMouseEnter);
|
|
62
|
+
expect(addEventListenerSpy).not.toHaveBeenCalledWith('blur', onMouseLeave);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should parse object value and modifiers correctly', () => {
|
|
66
|
+
const binding = {
|
|
67
|
+
value: {
|
|
68
|
+
content: 'Object Tooltip',
|
|
69
|
+
placement: 'bottom',
|
|
70
|
+
popperClass: 'custom-class',
|
|
71
|
+
delay: { show: 500, hide: 200 },
|
|
72
|
+
},
|
|
73
|
+
modifiers: { right: true },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
cleanTooltipDirective.mounted(el, binding);
|
|
77
|
+
|
|
78
|
+
expect(el.__tooltipOptions__).toStrictEqual({
|
|
79
|
+
content: 'Object Tooltip',
|
|
80
|
+
placement: 'right', // Modifier should override
|
|
81
|
+
popperClass: 'custom-class',
|
|
82
|
+
delay: { show: 500, hide: 200 },
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should not add has-clean-tooltip class if content is empty', () => {
|
|
87
|
+
const binding = { value: '', modifiers: {} };
|
|
88
|
+
|
|
89
|
+
cleanTooltipDirective.mounted(el, binding);
|
|
90
|
+
expect(el.classList.contains('has-clean-tooltip')).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('updated', () => {
|
|
95
|
+
it('should update the stored tooltip options', () => {
|
|
96
|
+
const initialBinding = { value: 'Initial', modifiers: {} };
|
|
97
|
+
|
|
98
|
+
cleanTooltipDirective.mounted(el, initialBinding);
|
|
99
|
+
|
|
100
|
+
const updatedBinding = { value: 'Updated', modifiers: { bottom: true } };
|
|
101
|
+
|
|
102
|
+
cleanTooltipDirective.updated(el, updatedBinding);
|
|
103
|
+
|
|
104
|
+
expect(el.__tooltipOptions__).toStrictEqual({ content: 'Updated', placement: 'bottom' });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should re-show the tooltip if it is currently active on the element', () => {
|
|
108
|
+
const binding = { value: { content: 'Test' }, modifiers: {} };
|
|
109
|
+
|
|
110
|
+
cleanTooltipDirective.mounted(el, binding);
|
|
111
|
+
|
|
112
|
+
const mouseEnterEvent = new MouseEvent('mouseenter');
|
|
113
|
+
|
|
114
|
+
Object.defineProperty(mouseEnterEvent, 'currentTarget', { value: el });
|
|
115
|
+
onMouseEnter(mouseEnterEvent);
|
|
116
|
+
|
|
117
|
+
expect(mockCreateTooltip).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(mockTooltipInstance.show).toHaveBeenCalledTimes(1);
|
|
119
|
+
|
|
120
|
+
const updatedBinding = { value: { content: 'Updated Content' }, modifiers: {} };
|
|
121
|
+
|
|
122
|
+
cleanTooltipDirective.updated(el, updatedBinding);
|
|
123
|
+
|
|
124
|
+
expect(mockDestroyTooltip).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(mockCreateTooltip).toHaveBeenCalledTimes(2);
|
|
126
|
+
expect(mockTooltipInstance.show).toHaveBeenCalledTimes(2);
|
|
127
|
+
expect(mockCreateTooltip).toHaveBeenCalledWith(el, expect.objectContaining({ content: 'Updated Content' }), {});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('unmounted', () => {
|
|
132
|
+
it('should remove event listeners and class', () => {
|
|
133
|
+
const removeEventListenerSpy = jest.spyOn(el, 'removeEventListener');
|
|
134
|
+
const binding = { value: 'Test', modifiers: {} };
|
|
135
|
+
|
|
136
|
+
cleanTooltipDirective.mounted(el, binding);
|
|
137
|
+
el.classList.add('has-clean-tooltip');
|
|
138
|
+
|
|
139
|
+
cleanTooltipDirective.unmounted(el, binding);
|
|
140
|
+
|
|
141
|
+
expect(el.classList.contains('has-clean-tooltip')).toBe(false);
|
|
142
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith('mouseenter', onMouseEnter);
|
|
143
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith('mouseleave', onMouseLeave);
|
|
144
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith('focus', onMouseEnter);
|
|
145
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith('blur', onMouseLeave);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should hide the tooltip if it is active on the element', () => {
|
|
149
|
+
const binding = { value: { content: 'Test' }, modifiers: {} };
|
|
150
|
+
|
|
151
|
+
cleanTooltipDirective.mounted(el, binding);
|
|
152
|
+
|
|
153
|
+
const mouseEnterEvent = new MouseEvent('mouseenter');
|
|
154
|
+
|
|
155
|
+
Object.defineProperty(mouseEnterEvent, 'currentTarget', { value: el });
|
|
156
|
+
onMouseEnter(mouseEnterEvent);
|
|
157
|
+
|
|
158
|
+
expect(mockCreateTooltip).toHaveBeenCalledTimes(1);
|
|
159
|
+
|
|
160
|
+
cleanTooltipDirective.unmounted(el, binding);
|
|
161
|
+
|
|
162
|
+
expect(mockDestroyTooltip).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(mockDestroyTooltip).toHaveBeenCalledWith(el);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('event handlers', () => {
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
el.__tooltipOptions__ = {
|
|
171
|
+
content: 'Handler Test',
|
|
172
|
+
delay: { show: 1, hide: 1 },
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('onMouseEnter should show the tooltip', () => {
|
|
177
|
+
const event = new MouseEvent('mouseenter');
|
|
178
|
+
|
|
179
|
+
Object.defineProperty(event, 'currentTarget', { value: el });
|
|
180
|
+
onMouseEnter(event);
|
|
181
|
+
|
|
182
|
+
expect(mockCreateTooltip).toHaveBeenCalledTimes(1);
|
|
183
|
+
expect(mockCreateTooltip).toHaveBeenCalledWith(el, {
|
|
184
|
+
content: 'Handler Test',
|
|
185
|
+
delay: { show: 1, hide: 1 },
|
|
186
|
+
}, {});
|
|
187
|
+
expect(mockTooltipInstance.show).toHaveBeenCalledTimes(1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('onMouseLeave should hide the tooltip', () => {
|
|
191
|
+
const enterEvent = new MouseEvent('mouseenter');
|
|
192
|
+
|
|
193
|
+
Object.defineProperty(enterEvent, 'currentTarget', { value: el });
|
|
194
|
+
onMouseEnter(enterEvent);
|
|
195
|
+
|
|
196
|
+
const leaveEvent = new MouseEvent('mouseleave');
|
|
197
|
+
|
|
198
|
+
Object.defineProperty(leaveEvent, 'currentTarget', { value: el });
|
|
199
|
+
onMouseLeave(leaveEvent);
|
|
200
|
+
|
|
201
|
+
expect(mockDestroyTooltip).toHaveBeenCalledTimes(1);
|
|
202
|
+
expect(mockDestroyTooltip).toHaveBeenCalledWith(el);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('onMouseClick should toggle the tooltip', () => {
|
|
206
|
+
const event = new MouseEvent('click');
|
|
207
|
+
|
|
208
|
+
el.__tooltipOptions__.triggers = ['click'];
|
|
209
|
+
Object.defineProperty(event, 'currentTarget', { value: el });
|
|
210
|
+
|
|
211
|
+
// First click shows tooltip
|
|
212
|
+
onMouseClick(event);
|
|
213
|
+
expect(mockCreateTooltip).toHaveBeenCalledTimes(1);
|
|
214
|
+
expect(mockTooltipInstance.show).toHaveBeenCalledTimes(1);
|
|
215
|
+
|
|
216
|
+
// To simulate it's open, we need to set the internal currentTarget.
|
|
217
|
+
// We can do this by calling onMouseEnter.
|
|
218
|
+
const enterEvent = new MouseEvent('mouseenter');
|
|
219
|
+
|
|
220
|
+
Object.defineProperty(enterEvent, 'currentTarget', { value: el });
|
|
221
|
+
onMouseEnter(enterEvent);
|
|
222
|
+
|
|
223
|
+
// onMouseEnter destroys the previous tooltip and creates a new one.
|
|
224
|
+
expect(mockDestroyTooltip).toHaveBeenCalledTimes(1);
|
|
225
|
+
expect(mockCreateTooltip).toHaveBeenCalledTimes(2);
|
|
226
|
+
|
|
227
|
+
// Now that the tooltip for `el` is considered active, a click should hide it.
|
|
228
|
+
onMouseClick(event);
|
|
229
|
+
|
|
230
|
+
expect(mockDestroyTooltip).toHaveBeenCalledTimes(2);
|
|
231
|
+
expect(mockDestroyTooltip).toHaveBeenLastCalledWith(el);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('content', () => {
|
|
236
|
+
it('should not show tooltip for empty content', () => {
|
|
237
|
+
const binding = { value: ' ', modifiers: {} };
|
|
238
|
+
|
|
239
|
+
cleanTooltipDirective.mounted(el, binding);
|
|
240
|
+
|
|
241
|
+
const enterEvent = new MouseEvent('mouseenter');
|
|
242
|
+
|
|
243
|
+
Object.defineProperty(enterEvent, 'currentTarget', { value: el });
|
|
244
|
+
onMouseEnter(enterEvent);
|
|
245
|
+
|
|
246
|
+
expect(mockCreateTooltip).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should purify string content', () => {
|
|
250
|
+
const binding = { value: '<h1>Hello</h1>', modifiers: {} };
|
|
251
|
+
|
|
252
|
+
cleanTooltipDirective.mounted(el, binding);
|
|
253
|
+
|
|
254
|
+
const enterEvent = new MouseEvent('mouseenter');
|
|
255
|
+
|
|
256
|
+
Object.defineProperty(enterEvent, 'currentTarget', { value: el });
|
|
257
|
+
onMouseEnter(enterEvent);
|
|
258
|
+
|
|
259
|
+
expect(mockPurifyHTML).toHaveBeenCalledWith('<h1>Hello</h1>');
|
|
260
|
+
expect(mockCreateTooltip).toHaveBeenCalledWith(el, expect.objectContaining({ content: '<h1>Hello</h1>' }), {});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should purify content within an object value', () => {
|
|
264
|
+
const binding = { value: { content: '<p>World</p>' }, modifiers: {} };
|
|
265
|
+
|
|
266
|
+
cleanTooltipDirective.mounted(el, binding);
|
|
267
|
+
|
|
268
|
+
const enterEvent = new MouseEvent('mouseenter');
|
|
269
|
+
|
|
270
|
+
Object.defineProperty(enterEvent, 'currentTarget', { value: el });
|
|
271
|
+
onMouseEnter(enterEvent);
|
|
272
|
+
|
|
273
|
+
expect(mockPurifyHTML).toHaveBeenCalledWith('<p>World</p>');
|
|
274
|
+
expect(mockCreateTooltip).toHaveBeenCalledWith(el, expect.objectContaining({ content: '<p>World</p>' }), {});
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('triggers', () => {
|
|
279
|
+
it('should only add click listeners if triggers: [\'click\'] is provided', () => {
|
|
280
|
+
const addEventListenerSpy = jest.spyOn(el, 'addEventListener');
|
|
281
|
+
const binding = {
|
|
282
|
+
value: {
|
|
283
|
+
content: 'Click Tooltip',
|
|
284
|
+
triggers: ['click'],
|
|
285
|
+
},
|
|
286
|
+
modifiers: {},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
cleanTooltipDirective.mounted(el, binding);
|
|
290
|
+
|
|
291
|
+
expect(addEventListenerSpy).not.toHaveBeenCalledWith('mouseenter', onMouseEnter);
|
|
292
|
+
expect(addEventListenerSpy).not.toHaveBeenCalledWith('mouseleave', onMouseLeave);
|
|
293
|
+
expect(addEventListenerSpy).not.toHaveBeenCalledWith('focus', onMouseEnter);
|
|
294
|
+
expect(addEventListenerSpy).not.toHaveBeenCalledWith('blur', onMouseLeave);
|
|
295
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith('click', onMouseClick);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|