@rancher/shell 3.0.8-rc.12 → 3.0.8-rc.13
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 +34 -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/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/ModalManager.vue +11 -1
- package/components/ResourceDetail/index.vue +3 -0
- package/components/ResourceTable.vue +53 -20
- package/components/SlideInPanelManager.vue +16 -11
- package/components/SortableTable/index.vue +19 -1
- package/components/Tabbed/index.vue +37 -2
- package/components/form/ResourceTabs/composable.ts +2 -2
- package/composables/cruResource.ts +27 -0
- package/composables/focusTrap.ts +3 -1
- package/composables/resourceDetail.ts +15 -0
- package/core/__tests__/extension-manager-impl.test.js +437 -0
- package/core/extension-manager-impl.js +1 -22
- package/core/plugin.ts +9 -1
- package/core/types.ts +35 -0
- package/detail/provisioning.cattle.io.cluster.vue +2 -0
- package/edit/workload/index.vue +1 -1
- package/initialize/install-plugins.js +4 -5
- package/package.json +1 -1
- package/pages/c/_cluster/apps/charts/install.vue +33 -0
- package/pages/c/_cluster/fleet/index.vue +4 -7
- package/plugins/steve/__tests__/steve-pagination-utils.test.ts +301 -128
- package/plugins/steve/steve-pagination-utils.ts +108 -43
- package/scripts/publish-shell.sh +25 -0
- package/store/__tests__/type-map.test.ts +164 -2
- package/store/notifications.ts +2 -0
- package/store/type-map.js +10 -1
- 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/vue-shim.d.ts +5 -4
- package/composables/useExtensionManager.ts +0 -17
- package/core/__test__/extension-manager-impl.test.js +0 -236
- package/core/plugins.js +0 -38
- 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
|
@@ -4,36 +4,102 @@ import { RouteLocationRaw } from 'vue-router';
|
|
|
4
4
|
* Type definitions for the Notification Center
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Notification Level for a notification in the Notification Center
|
|
9
|
-
*/
|
|
10
7
|
export enum NotificationLevel {
|
|
8
|
+
/**
|
|
9
|
+
* An announcement. To be used when we want to inform on high-interest topics - news, updates, changes, scheduled maintenance, etc. E.g. “New version available!” <img class="svg-blue" src="https://raw.githubusercontent.com/rancher/icons/refs/heads/master/svg/notify-announcement.svg" width="20" />
|
|
10
|
+
*/
|
|
11
11
|
Announcement = 0, // eslint-disable-line no-unused-vars
|
|
12
|
+
/**
|
|
13
|
+
* A task that is underway. To be used when we want to inform on a process taking place - on-going actions that might take a while. E.g. “Cluster provisioning in progress”. The progress bar will also be shown if the `progress` field is set <img class="svg-blue" src="https://raw.githubusercontent.com/rancher/icons/refs/heads/master/svg/notify-busy.svg" width="20" />
|
|
14
|
+
*/
|
|
12
15
|
Task, // eslint-disable-line no-unused-vars
|
|
16
|
+
/**
|
|
17
|
+
* Information notification. To be used when we want to inform on low-interest topics. E.g. “Welcome to Rancher v2.8" <img class="svg-blue" src="https://raw.githubusercontent.com/rancher/icons/refs/heads/master/svg/notify-info.svg"/>
|
|
18
|
+
*/
|
|
13
19
|
Info, // eslint-disable-line no-unused-vars
|
|
20
|
+
/**
|
|
21
|
+
* Notification that something has completed successfully. To be used when we want to confirm a successful action was completed. E.g. “Cluster provisioning completed” <img class="svg-green" src="https://raw.githubusercontent.com/rancher/icons/refs/heads/master/svg/notify-tick.svg"/>
|
|
22
|
+
*/
|
|
14
23
|
Success, // eslint-disable-line no-unused-vars
|
|
24
|
+
/**
|
|
25
|
+
* Notification of a warning. To be used when we want to warn about a potential risk. E.g. “Nodes limitation warning” <img class="svg-orange" src="https://raw.githubusercontent.com/rancher/icons/refs/heads/master/svg/notify-warning.svg"/>
|
|
26
|
+
*/
|
|
15
27
|
Warning, // eslint-disable-line no-unused-vars
|
|
28
|
+
/**
|
|
29
|
+
* Notification of an error. To be used when we want to alert on a confirmed risk. E.g. “Extension failed to load” <img class="svg-red" src="https://raw.githubusercontent.com/rancher/icons/refs/heads/master/svg/notify-error.svg"/>
|
|
30
|
+
*/
|
|
16
31
|
Error, // eslint-disable-line no-unused-vars
|
|
17
32
|
Hidden, // eslint-disable-line no-unused-vars
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
/**
|
|
21
|
-
*
|
|
36
|
+
* Notification Action definition
|
|
22
37
|
*/
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
export interface NotificationAction {
|
|
39
|
+
/**
|
|
40
|
+
* Button label for the action
|
|
41
|
+
*/
|
|
42
|
+
label: string;
|
|
43
|
+
/**
|
|
44
|
+
* Href target when the button is clicked
|
|
45
|
+
*/
|
|
46
|
+
target?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Vue Route to navigate to when the button is clicked
|
|
49
|
+
*/
|
|
50
|
+
route?: RouteLocationRaw;
|
|
51
|
+
}
|
|
28
52
|
|
|
29
53
|
/**
|
|
30
|
-
*
|
|
54
|
+
* Notification Preference definition
|
|
31
55
|
*/
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
56
|
+
export interface NotificationPreference {
|
|
57
|
+
/**
|
|
58
|
+
* User preference key to use when setting the preference when the notification is marked as read
|
|
59
|
+
*/
|
|
60
|
+
key: string;
|
|
61
|
+
/**
|
|
62
|
+
* User preference value to use when setting the preference when the notification is marked as read
|
|
63
|
+
*/
|
|
64
|
+
value: string;
|
|
65
|
+
/**
|
|
66
|
+
* User preference value to use when setting the preference when the notification is marked as unread - defaults to empty string
|
|
67
|
+
*/
|
|
68
|
+
unsetValue?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Configuration object for the Notification Center
|
|
73
|
+
*
|
|
74
|
+
*/
|
|
75
|
+
export interface NotificationConfig {
|
|
76
|
+
/**
|
|
77
|
+
* - **{@link NotificationAction}**
|
|
78
|
+
*
|
|
79
|
+
* Primary action to be shown in the notification
|
|
80
|
+
*/
|
|
81
|
+
primaryAction?: NotificationAction;
|
|
82
|
+
/**
|
|
83
|
+
* - **{@link NotificationAction}**
|
|
84
|
+
*
|
|
85
|
+
* Secondary to be shown in the notification
|
|
86
|
+
*/
|
|
87
|
+
secondaryAction?: NotificationAction;
|
|
88
|
+
/**
|
|
89
|
+
* Unique ID for the notification
|
|
90
|
+
*/
|
|
91
|
+
id?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Progress (0-100) for notifications of type `Task`
|
|
94
|
+
*/
|
|
95
|
+
progress?: number;
|
|
96
|
+
/**
|
|
97
|
+
* - **{@link NotificationPreference}**
|
|
98
|
+
*
|
|
99
|
+
* User Preference tied to the notification (the preference will be updated when the notification is marked read)
|
|
100
|
+
*/
|
|
101
|
+
preference?: NotificationPreference;
|
|
102
|
+
}
|
|
37
103
|
|
|
38
104
|
/**
|
|
39
105
|
* Type for Encrypted Notification data that is stored in local storage
|
|
@@ -96,3 +162,48 @@ export interface NotificationHandler {
|
|
|
96
162
|
*/
|
|
97
163
|
onReadUpdated(notification: Notification, read: boolean): void;
|
|
98
164
|
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* API for notifications in the Rancher UI Notification Center
|
|
168
|
+
* * 
|
|
169
|
+
*/
|
|
170
|
+
export interface NotificationApi {
|
|
171
|
+
/**
|
|
172
|
+
* Sends a notification to the Rancher UI Notification Center
|
|
173
|
+
*
|
|
174
|
+
* Example:
|
|
175
|
+
* ```ts
|
|
176
|
+
* import { NotificationLevel } from '@shell/types/notifications';
|
|
177
|
+
*
|
|
178
|
+
* this.$shell.notification.send(NotificationLevel.Success, 'Some notification title', 'Hello world! Success!', {})
|
|
179
|
+
* ```
|
|
180
|
+
*
|
|
181
|
+
* For usage with the Composition API check usage guide [here](../../shell-api#using-composition-api-in-vue).
|
|
182
|
+
*
|
|
183
|
+
* @param level The `level` specifies the importance of the notification and determines the icon that is shown in the notification
|
|
184
|
+
* @param title The notification title
|
|
185
|
+
* @param message The notification message to be displayed
|
|
186
|
+
* @param config Notifications configuration object
|
|
187
|
+
*
|
|
188
|
+
* @returns notification ID
|
|
189
|
+
*
|
|
190
|
+
*/
|
|
191
|
+
send(level: NotificationLevel | NotificationLevel, title: string, message?:string, config?: NotificationConfig): Promise<string>;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Update notification progress (Only valid for notifications of type `Task`)
|
|
195
|
+
*
|
|
196
|
+
* Example:
|
|
197
|
+
* ```ts
|
|
198
|
+
* this.$shell.notification.updateProgress('some-notification-id', 80)
|
|
199
|
+
* ```
|
|
200
|
+
*
|
|
201
|
+
* For usage with the Composition API check usage guide [here](../../shell-api#using-composition-api-in-vue).
|
|
202
|
+
*
|
|
203
|
+
* @param notificationId Unique ID for the notification
|
|
204
|
+
* @param progress Progress (0-100) for notifications of type `Task`
|
|
205
|
+
*
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
updateProgress(notificationId: string, progress: number): void;
|
|
209
|
+
}
|
package/types/rancher/index.d.ts
CHANGED
|
@@ -8,3 +8,12 @@ declare module '@shell/store/type-map' {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
declare module '@shell/plugins/dashboard-store';
|
|
11
|
+
|
|
12
|
+
declare module '@shell/config/query-params' {
|
|
13
|
+
export const _DETAIL: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module '@shell/config/version' {
|
|
17
|
+
export const CURRENT_RANCHER_VERSION: string;
|
|
18
|
+
export function getVersionData(): any;
|
|
19
|
+
}
|
package/types/vue-shim.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
import type ShellApi from '@shell/plugins/internal-api/shell/shell.api';
|
|
3
2
|
import { VuexStore } from '@shell/types/store/vuex';
|
|
4
3
|
|
|
4
|
+
// Include the types for the APIs
|
|
5
|
+
/// <reference path="../apis/vue-shim.d.ts" />
|
|
6
|
+
|
|
5
7
|
export {};
|
|
6
8
|
|
|
7
9
|
declare module 'vue' {
|
|
@@ -14,7 +16,6 @@ declare module 'vue' {
|
|
|
14
16
|
(key: string, args?: Record<string, any>, raw?: boolean): string;
|
|
15
17
|
(options: { k: string; raw?: boolean; tag?: string | Record<string, any>; escapehtml?: boolean }): string;
|
|
16
18
|
},
|
|
17
|
-
$store: VuexStore
|
|
18
|
-
$shell: ShellApi,
|
|
19
|
+
$store: VuexStore
|
|
19
20
|
}
|
|
20
|
-
}
|
|
21
|
+
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { ExtensionManager } from '@shell/types/extension-manager';
|
|
2
|
-
import { getExtensionManager } from '@shell/core/extension-manager-impl';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Provides access to the registered extension manager instance. Used within Vue
|
|
6
|
-
* components or other composables that require extension functionality.
|
|
7
|
-
* @returns The extension manager instance
|
|
8
|
-
*/
|
|
9
|
-
export const useExtensionManager = (): ExtensionManager => {
|
|
10
|
-
const extension = getExtensionManager();
|
|
11
|
-
|
|
12
|
-
if (!extension) {
|
|
13
|
-
throw new Error('useExtensionManager must be called after the extensionManager has been initialized');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return extension;
|
|
17
|
-
};
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { DEVELOPER_LOAD_NAME_SUFFIX } from '@shell/core/extension-manager-impl';
|
|
2
|
-
|
|
3
|
-
// Mock external dependencies
|
|
4
|
-
jest.mock('@shell/store/type-map', () => ({ productsLoaded: jest.fn().mockReturnValue(true) }));
|
|
5
|
-
|
|
6
|
-
jest.mock('@shell/plugins/dashboard-store/model-loader', () => ({ clearModelCache: jest.fn() }));
|
|
7
|
-
|
|
8
|
-
jest.mock('@shell/config/uiplugins', () => ({ UI_PLUGIN_BASE_URL: '/api/v1/uiplugins' }));
|
|
9
|
-
|
|
10
|
-
jest.mock('@shell/plugins/clean-html', () => ({
|
|
11
|
-
addLinkInterceptor: jest.fn(),
|
|
12
|
-
removeLinkInterceptor: jest.fn(),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
// Mock the Plugin class
|
|
16
|
-
jest.mock('@shell/core/plugin', () => {
|
|
17
|
-
return {
|
|
18
|
-
Plugin: jest.fn().mockImplementation((id) => ({
|
|
19
|
-
id,
|
|
20
|
-
name: id,
|
|
21
|
-
types: {},
|
|
22
|
-
uiConfig: {},
|
|
23
|
-
l10n: {},
|
|
24
|
-
modelExtensions: {},
|
|
25
|
-
stores: [],
|
|
26
|
-
locales: [],
|
|
27
|
-
routes: [],
|
|
28
|
-
validators: {},
|
|
29
|
-
uninstallHooks: [],
|
|
30
|
-
productNames: [],
|
|
31
|
-
})),
|
|
32
|
-
EXT_IDS: {
|
|
33
|
-
MODELS: 'models',
|
|
34
|
-
MODEL_EXTENSION: 'model-extension'
|
|
35
|
-
},
|
|
36
|
-
ExtensionPoint: { EDIT_YAML: 'edit-yaml' }
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Mock PluginRoutes
|
|
41
|
-
jest.mock('@shell/core/plugin-routes', () => {
|
|
42
|
-
return { PluginRoutes: jest.fn().mockImplementation(() => ({ addRoutes: jest.fn() })) };
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('extension Manager', () => {
|
|
46
|
-
let mockStore;
|
|
47
|
-
let mockApp;
|
|
48
|
-
let context;
|
|
49
|
-
|
|
50
|
-
// These variables will be assigned the fresh functions inside beforeEach
|
|
51
|
-
let initExtensionManager;
|
|
52
|
-
let getExtensionManager;
|
|
53
|
-
|
|
54
|
-
beforeEach(() => {
|
|
55
|
-
// singleton instance for every test run, preventing mock store leaks.
|
|
56
|
-
jest.resetModules();
|
|
57
|
-
|
|
58
|
-
// Re-require the System Under Test (SUT)
|
|
59
|
-
const extensionManagerModule = require('../extension-manager-impl');
|
|
60
|
-
|
|
61
|
-
initExtensionManager = extensionManagerModule.initExtensionManager;
|
|
62
|
-
getExtensionManager = extensionManagerModule.getExtensionManager;
|
|
63
|
-
|
|
64
|
-
jest.clearAllMocks();
|
|
65
|
-
|
|
66
|
-
// Setup Mock Context
|
|
67
|
-
mockStore = {
|
|
68
|
-
getters: { 'i18n/t': jest.fn() },
|
|
69
|
-
dispatch: jest.fn(),
|
|
70
|
-
commit: jest.fn(),
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
mockApp = { router: {} };
|
|
74
|
-
|
|
75
|
-
context = {
|
|
76
|
-
app: mockApp,
|
|
77
|
-
store: mockStore,
|
|
78
|
-
$axios: {},
|
|
79
|
-
redirect: jest.fn(),
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Clean up DOM from previous tests
|
|
83
|
-
document.head.innerHTML = '';
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('singleton Pattern', () => {
|
|
87
|
-
it('initializes and returns the same instance', () => {
|
|
88
|
-
const instance1 = initExtensionManager(context);
|
|
89
|
-
const instance2 = getExtensionManager();
|
|
90
|
-
const instance3 = initExtensionManager(context);
|
|
91
|
-
|
|
92
|
-
expect(instance1).toBeDefined();
|
|
93
|
-
expect(instance1).toBe(instance2);
|
|
94
|
-
expect(instance1).toBe(instance3);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('registration (Dynamic)', () => {
|
|
99
|
-
it('registers and retrieves a dynamic component', () => {
|
|
100
|
-
const manager = initExtensionManager(context);
|
|
101
|
-
const mockFn = jest.fn();
|
|
102
|
-
|
|
103
|
-
manager.register('component', 'my-component', mockFn);
|
|
104
|
-
|
|
105
|
-
const retrieved = manager.getDynamic('component', 'my-component');
|
|
106
|
-
|
|
107
|
-
expect(retrieved).toBe(mockFn);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('unregisters a dynamic component', () => {
|
|
111
|
-
const manager = initExtensionManager(context);
|
|
112
|
-
const mockFn = jest.fn();
|
|
113
|
-
|
|
114
|
-
manager.register('component', 'my-component', mockFn);
|
|
115
|
-
manager.unregister('component', 'my-component');
|
|
116
|
-
|
|
117
|
-
const retrieved = manager.getDynamic('component', 'my-component');
|
|
118
|
-
|
|
119
|
-
expect(retrieved).toBeUndefined();
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('loadPluginAsync (URL Generation)', () => {
|
|
124
|
-
let manager;
|
|
125
|
-
|
|
126
|
-
beforeEach(() => {
|
|
127
|
-
manager = initExtensionManager(context);
|
|
128
|
-
// Mock the internal loadAsync so we only test URL generation here
|
|
129
|
-
jest.spyOn(manager, 'loadAsync').mockImplementation().mockResolvedValue();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('generates correct URL for standard plugin', async() => {
|
|
133
|
-
const pluginData = { name: 'elemental', version: '1.0.0' };
|
|
134
|
-
const expectedId = 'elemental-1.0.0';
|
|
135
|
-
const expectedUrl = `/api/v1/uiplugins/elemental/1.0.0/plugin/elemental-1.0.0.umd.min.js`;
|
|
136
|
-
|
|
137
|
-
await manager.loadPluginAsync(pluginData);
|
|
138
|
-
|
|
139
|
-
expect(manager.loadAsync).toHaveBeenCalledWith(expectedId, expectedUrl);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('handles "direct" metadata plugins', async() => {
|
|
143
|
-
const pluginData = {
|
|
144
|
-
name: 'direct-plugin',
|
|
145
|
-
version: '1.0.0',
|
|
146
|
-
endpoint: 'http://localhost:8000/plugin.js',
|
|
147
|
-
metadata: { direct: 'true' }
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
await manager.loadPluginAsync(pluginData);
|
|
151
|
-
|
|
152
|
-
expect(manager.loadAsync).toHaveBeenCalledWith('direct-plugin-1.0.0', 'http://localhost:8000/plugin.js');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('removes developer suffix from ID but keeps it for internal logic', async() => {
|
|
156
|
-
const pluginData = {
|
|
157
|
-
name: `my-plugin${ DEVELOPER_LOAD_NAME_SUFFIX }`,
|
|
158
|
-
version: `1.0.0`
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
await manager.loadPluginAsync(pluginData);
|
|
162
|
-
|
|
163
|
-
// Expected ID passed to loadAsync should NOT have the suffix
|
|
164
|
-
const expectedIdWithoutSuffix = 'my-plugin-1.0.0';
|
|
165
|
-
|
|
166
|
-
expect(manager.loadAsync).toHaveBeenCalledWith(
|
|
167
|
-
expectedIdWithoutSuffix,
|
|
168
|
-
expect.any(String)
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('loadAsync (Script Injection)', () => {
|
|
174
|
-
let manager;
|
|
175
|
-
|
|
176
|
-
beforeEach(() => {
|
|
177
|
-
manager = initExtensionManager(context);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('resolves immediately if element already exists', async() => {
|
|
181
|
-
const id = 'existing-plugin';
|
|
182
|
-
const script = document.createElement('script');
|
|
183
|
-
|
|
184
|
-
script.id = id;
|
|
185
|
-
document.body.appendChild(script);
|
|
186
|
-
|
|
187
|
-
await expect(manager.loadAsync(id, 'url.js')).resolves.toBeUndefined();
|
|
188
|
-
|
|
189
|
-
document.body.removeChild(script);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('injects script tag and initializes plugin on load', async() => {
|
|
193
|
-
const pluginId = 'test-plugin';
|
|
194
|
-
const pluginUrl = 'http://test.com/plugin.js';
|
|
195
|
-
|
|
196
|
-
// Mock the window object to simulate the plugin loading into global scope
|
|
197
|
-
const mockPluginInit = jest.fn();
|
|
198
|
-
|
|
199
|
-
window[pluginId] = { default: mockPluginInit };
|
|
200
|
-
|
|
201
|
-
// Start the load
|
|
202
|
-
const loadPromise = manager.loadAsync(pluginId, pluginUrl);
|
|
203
|
-
|
|
204
|
-
// Find the injected script tag in the DOM
|
|
205
|
-
const script = document.head.querySelector(`script[id="${ pluginId }"]`);
|
|
206
|
-
|
|
207
|
-
expect(script).toBeTruthy();
|
|
208
|
-
expect(script.src).toBe(pluginUrl);
|
|
209
|
-
|
|
210
|
-
// Manually trigger the onload event
|
|
211
|
-
script.onload();
|
|
212
|
-
|
|
213
|
-
// Await the promise
|
|
214
|
-
await loadPromise;
|
|
215
|
-
|
|
216
|
-
// Assertions
|
|
217
|
-
expect(mockPluginInit).toHaveBeenCalledWith(expect.objectContaining({ id: pluginId }), expect.objectContaining({ ...context }));
|
|
218
|
-
expect(mockStore.dispatch).toHaveBeenCalledWith('uiplugins/addPlugin', expect.objectContaining({ id: pluginId }));
|
|
219
|
-
|
|
220
|
-
// Cleanup
|
|
221
|
-
delete window[pluginId];
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('rejects if script load fails', async() => {
|
|
225
|
-
const pluginId = 'fail-plugin';
|
|
226
|
-
const loadPromise = manager.loadAsync(pluginId, 'bad-url.js');
|
|
227
|
-
|
|
228
|
-
const script = document.head.querySelector(`script[id="${ pluginId }"]`);
|
|
229
|
-
|
|
230
|
-
// Trigger error
|
|
231
|
-
script.onerror({ target: { src: 'bad-url.js' } });
|
|
232
|
-
|
|
233
|
-
await expect(loadPromise).rejects.toThrow('Failed to load script');
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
});
|
package/core/plugins.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { throttle } from 'lodash';
|
|
2
|
-
import { initExtensionManager } from './extension-manager-impl';
|
|
3
|
-
|
|
4
|
-
export default function(context, inject) {
|
|
5
|
-
const extensionManager = initExtensionManager(context);
|
|
6
|
-
const deprecationMessage = '[DEPRECATED] `this.$plugin` is deprecated and will be removed in a future version. Use `this.$extension` instead.';
|
|
7
|
-
|
|
8
|
-
inject('plugin', deprecationProxy(extensionManager, deprecationMessage));
|
|
9
|
-
inject('extension', extensionManager);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Proxy to log a deprecation warning when target is accessed. Only prints
|
|
14
|
-
* deprecation warnings in dev builds.
|
|
15
|
-
* @param {*} target the object to proxy
|
|
16
|
-
* @param {*} message the deprecation warning to print to the console
|
|
17
|
-
* @returns The proxied target that prints a deprecation warning when target is
|
|
18
|
-
* accessed
|
|
19
|
-
*/
|
|
20
|
-
const deprecationProxy = (target, message) => {
|
|
21
|
-
const logWarning = throttle(() => {
|
|
22
|
-
// eslint-disable-next-line no-console
|
|
23
|
-
console.warn(message);
|
|
24
|
-
}, 150);
|
|
25
|
-
|
|
26
|
-
const deprecationHandler = {
|
|
27
|
-
get(target, prop) {
|
|
28
|
-
logWarning();
|
|
29
|
-
|
|
30
|
-
return Reflect.get(target, prop);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// an empty handler allows the proxy to behave just like the original target
|
|
35
|
-
const proxyHandler = !!process.env.dev ? deprecationHandler : {};
|
|
36
|
-
|
|
37
|
-
return new Proxy(target, proxyHandler);
|
|
38
|
-
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { Store } from 'vuex';
|
|
2
|
-
|
|
3
|
-
interface PluginContext {
|
|
4
|
-
store: Store<any>;
|
|
5
|
-
[key: string]: any;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export default function(context: PluginContext, inject: (key: string, value: any) => void) {
|
|
9
|
-
const { store } = context;
|
|
10
|
-
|
|
11
|
-
// Load all API modules
|
|
12
|
-
const apiContext = (require as any).context(
|
|
13
|
-
'@shell/plugins/internal-api', // the base directory
|
|
14
|
-
true, // whether to search subdirectories
|
|
15
|
-
/\.api\.ts$/ // only .api.ts files
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
apiContext.keys().forEach((relativePath: string) => {
|
|
19
|
-
const mod = apiContext(relativePath);
|
|
20
|
-
const ApiClass = mod.default;
|
|
21
|
-
|
|
22
|
-
if (typeof ApiClass === 'function') {
|
|
23
|
-
// Check for a static `apiName` property, or fallback to filename
|
|
24
|
-
let apiName: string = ApiClass.apiName();
|
|
25
|
-
|
|
26
|
-
if (!apiName) {
|
|
27
|
-
// fallback to filename (strip leading ‘./’ and extension)
|
|
28
|
-
apiName = `$${ relativePath.replace(/^\.\//, '').replace(/\.\w+$/, '') }`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const instance = new ApiClass(store);
|
|
32
|
-
|
|
33
|
-
// The inject() method automatically adds the `$` prefix
|
|
34
|
-
inject(apiName, instance);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export abstract class BaseApi {
|
|
2
|
-
// The Vuex store, available to all API classes
|
|
3
|
-
protected $store: any;
|
|
4
|
-
|
|
5
|
-
// Documented requirement: each API should define its static apiName.
|
|
6
|
-
static apiName(): string {
|
|
7
|
-
throw new Error('apiName() static method has not been implemented');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
constructor(store: any) {
|
|
11
|
-
this.$store = store;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { GrowlConfig } from '@shell/types/internal-api/shell/growl';
|
|
2
|
-
import { ModalConfig } from '@shell/types/internal-api/shell/modal';
|
|
3
|
-
import { SlideInConfig } from '@shell/types/internal-api/shell/slideIn';
|
|
4
|
-
|
|
5
|
-
import { BaseApi } from '@shell/plugins/internal-api/shared/base-api';
|
|
6
|
-
|
|
7
|
-
export default class ShellApi extends BaseApi {
|
|
8
|
-
static apiName() {
|
|
9
|
-
return 'shell';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Dispatches a growl notification.
|
|
14
|
-
*
|
|
15
|
-
* @param config - Configuration for the growl notification.
|
|
16
|
-
* - If `message` is a string, it is treated as the main content of the notification.
|
|
17
|
-
* - If `message` is a `DetailedMessage` object, `title` and `description` are extracted.
|
|
18
|
-
*
|
|
19
|
-
* Example:
|
|
20
|
-
* ```ts
|
|
21
|
-
* this.$shell.growl({ message: 'Operation successful!', type: 'success' });
|
|
22
|
-
* this.$shell.growl({ message: { title: 'Warning', description: 'Check your input.' }, type: 'warning' });
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
protected growl(config: GrowlConfig): void {
|
|
26
|
-
const { type = 'error', timeout = 5000 } = config;
|
|
27
|
-
|
|
28
|
-
let title = '';
|
|
29
|
-
let description = '';
|
|
30
|
-
|
|
31
|
-
if (typeof config.message === 'string') {
|
|
32
|
-
description = config.message;
|
|
33
|
-
} else {
|
|
34
|
-
title = config.message.title || '';
|
|
35
|
-
description = config.message.description;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
this.$store.dispatch(
|
|
39
|
-
`growl/${ type }`,
|
|
40
|
-
{
|
|
41
|
-
title,
|
|
42
|
-
message: description,
|
|
43
|
-
timeout,
|
|
44
|
-
},
|
|
45
|
-
{ root: true }
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Opens a modal by committing to the Vuex store.
|
|
51
|
-
*
|
|
52
|
-
* This method updates the store's `modal` module to show a modal with the
|
|
53
|
-
* specified configuration. The modal is rendered using the `ModalManager` component,
|
|
54
|
-
* and its content is dynamically loaded based on the `component` field in the configuration.
|
|
55
|
-
*
|
|
56
|
-
* @param config A `ModalConfig` object defining the modal’s content and behavior.
|
|
57
|
-
*
|
|
58
|
-
* Example:
|
|
59
|
-
* ```ts
|
|
60
|
-
* this.$shell.modal({
|
|
61
|
-
* component: MyCustomModal,
|
|
62
|
-
* componentProps: { title: 'Hello Modal' },
|
|
63
|
-
* resources: [someResource],
|
|
64
|
-
* modalWidth: '800px',
|
|
65
|
-
* closeOnClickOutside: false
|
|
66
|
-
* });
|
|
67
|
-
* ```
|
|
68
|
-
*/
|
|
69
|
-
protected modal(config: ModalConfig): void {
|
|
70
|
-
this.$store.commit('modal/openModal', {
|
|
71
|
-
component: config.component,
|
|
72
|
-
componentProps: config.componentProps || {},
|
|
73
|
-
resources: config.resources || [],
|
|
74
|
-
modalWidth: config.modalWidth || '600px',
|
|
75
|
-
closeOnClickOutside: config.closeOnClickOutside ?? true,
|
|
76
|
-
// modalSticky: config.modalSticky ?? false // Not implemented yet
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Opens the slide-in panel with the specified component and props.
|
|
82
|
-
*
|
|
83
|
-
* This method commits the `open` mutation to the `slideInPanel` Vuex module,
|
|
84
|
-
* which sets the current component to be rendered and its associated props.
|
|
85
|
-
* The slide-in panel becomes visible after the mutation.
|
|
86
|
-
*
|
|
87
|
-
* @param config - The configuration object for the slide-in panel.
|
|
88
|
-
*
|
|
89
|
-
* Example Usage:
|
|
90
|
-
* ```ts
|
|
91
|
-
* import MyComponent from '@/components/MyComponent.vue';
|
|
92
|
-
*
|
|
93
|
-
* this.$shell.slideInPanel({
|
|
94
|
-
* component: MyComponent,
|
|
95
|
-
* componentProps: { foo: 'bar' }
|
|
96
|
-
* });
|
|
97
|
-
* ```
|
|
98
|
-
*
|
|
99
|
-
* @param config.component - A Vue component (imported SFC, functional component, etc.) to be rendered in the panel.
|
|
100
|
-
* @param config.componentProps - (Optional) Props to pass to the component. These should align with the component's defined props.
|
|
101
|
-
*/
|
|
102
|
-
protected slideInPanel(config: SlideInConfig): void {
|
|
103
|
-
this.$store.commit('slideInPanel/open', {
|
|
104
|
-
component: config.component,
|
|
105
|
-
componentProps: config.componentProps || {}
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export interface DetailedMessage {
|
|
2
|
-
title?: string;
|
|
3
|
-
description: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface GrowlConfig {
|
|
7
|
-
/**
|
|
8
|
-
* The content of the notification message.
|
|
9
|
-
* Either a simple string or an object with `title` and `description` for detailed notifications.
|
|
10
|
-
*/
|
|
11
|
-
message: string | DetailedMessage;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Optional type of the growl notification.
|
|
15
|
-
* Determines the visual style of the notification.
|
|
16
|
-
* Defaults to `'error'` if not provided.
|
|
17
|
-
*/
|
|
18
|
-
type?: 'success' | 'info' | 'warning' | 'error';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Optional duration (in milliseconds) for which the notification should be displayed.
|
|
22
|
-
* Defaults to `5000` milliseconds. A value of `0` keeps the notification indefinitely.
|
|
23
|
-
*/
|
|
24
|
-
timeout?: number;
|
|
25
|
-
}
|