@lark-apaas/client-toolkit 0.1.0-alpha.log.2 → 0.1.0-alpha.safe.1
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/lib/apis/hooks/useTheme.d.ts +1 -0
- package/lib/apis/hooks/useTheme.js +1 -0
- package/lib/components/AppContainer/LogInterceptor.js +27 -5
- package/lib/components/AppContainer/api-proxy/core.d.ts +155 -0
- package/lib/components/AppContainer/api-proxy/core.js +270 -0
- package/lib/components/AppContainer/index.js +11 -5
- package/lib/components/AppContainer/safety.d.ts +3 -0
- package/lib/components/AppContainer/safety.js +129 -0
- package/lib/components/AppContainer/utils/api-panel.d.ts +29 -0
- package/lib/components/AppContainer/utils/api-panel.js +66 -0
- package/lib/components/AppContainer/utils/childApi.js +11 -1
- package/lib/components/ErrorRender/index.js +3 -3
- package/lib/components/User/UserDisplay.d.ts +2 -1
- package/lib/components/User/UserDisplay.js +103 -26
- package/lib/components/User/UserProfile/UserProfile.css +1 -1
- package/lib/components/User/UserProfile/UserProfileContainer.d.ts +1 -1
- package/lib/components/User/UserProfile/UserProfileSkeleton.d.ts +0 -1
- package/lib/components/User/UserProfile/UserProfileSkeleton.js +21 -29
- package/lib/components/User/UserProfile/UserProfileUI.d.ts +1 -2
- package/lib/components/User/UserProfile/UserProfileUI.js +106 -92
- package/lib/components/User/UserSelect.d.ts +1 -1
- package/lib/components/User/UserSelect.js +17 -143
- package/lib/components/User/UserSelectUI/ActionButtons.d.ts +11 -0
- package/lib/components/User/UserSelectUI/ActionButtons.js +44 -0
- package/lib/components/User/UserSelectUI/Dropdown.d.ts +12 -0
- package/lib/components/User/UserSelectUI/Dropdown.js +66 -0
- package/lib/components/User/UserSelectUI/MultipleSelectionTags.d.ts +14 -0
- package/lib/components/User/UserSelectUI/MultipleSelectionTags.js +48 -0
- package/lib/components/User/UserSelectUI/SingleSelectionPreview.d.ts +9 -0
- package/lib/components/User/UserSelectUI/SingleSelectionPreview.js +37 -0
- package/lib/components/User/UserSelectUI/Spinner.d.ts +2 -0
- package/lib/components/User/UserSelectUI/Spinner.js +13 -0
- package/lib/components/User/UserSelectUI/UserSelectUI.d.ts +5 -0
- package/lib/components/User/UserSelectUI/UserSelectUI.js +230 -0
- package/lib/components/User/UserSelectUI/index.d.ts +2 -0
- package/lib/components/User/UserSelectUI/index.js +2 -0
- package/lib/components/User/UserSelectUI/types.d.ts +14 -0
- package/lib/components/User/UserSelectUI/types.js +0 -0
- package/lib/components/User/UserWithAvatar.d.ts +1 -1
- package/lib/components/User/UserWithAvatar.js +39 -22
- package/lib/components/User/type.d.ts +4 -0
- package/lib/components/index.d.ts +2 -5
- package/lib/components/index.js +2 -3
- package/lib/components/ui/avatar.d.ts +6 -0
- package/lib/components/ui/avatar.js +27 -0
- package/lib/components/ui/badge.d.ts +9 -0
- package/lib/components/ui/badge.js +29 -0
- package/lib/components/ui/button.d.ts +10 -0
- package/lib/components/ui/button.js +42 -0
- package/lib/components/ui/input.d.ts +3 -0
- package/lib/components/ui/input.js +12 -0
- package/lib/components/ui/overflow-tooltip-text.d.ts +8 -0
- package/lib/components/ui/overflow-tooltip-text.js +66 -0
- package/lib/components/ui/popover.d.ts +7 -0
- package/lib/components/ui/popover.js +35 -0
- package/lib/components/ui/skeleton.d.ts +7 -0
- package/lib/components/ui/skeleton.js +10 -0
- package/lib/components/ui/tooltip.d.ts +7 -0
- package/lib/components/ui/tooltip.js +24 -0
- package/lib/hooks/useAppInfo.js +12 -1
- package/lib/integrations/getAppInfo.js +4 -2
- package/lib/logger/__tests__/batch-logger.test.d.ts +1 -0
- package/lib/logger/__tests__/batch-logger.test.js +367 -0
- package/lib/logger/batch-logger.d.ts +78 -0
- package/lib/logger/batch-logger.js +134 -0
- package/lib/override.css +0 -16
- package/lib/types/iframe-events.d.ts +9 -0
- package/lib/types/index.d.ts +0 -29
- package/lib/utils/axiosConfig.js +7 -7
- package/lib/utils/getAppId.js +4 -3
- package/package.json +8 -1
- package/lib/apis/components/SidebarNav.d.ts +0 -1
- package/lib/apis/components/SidebarNav.js +0 -2
- package/lib/components/SidebarNav/DrawerNav.d.ts +0 -3
- package/lib/components/SidebarNav/DrawerNav.js +0 -64
- package/lib/components/SidebarNav/DropdownNav.d.ts +0 -3
- package/lib/components/SidebarNav/DropdownNav.js +0 -40
- package/lib/components/SidebarNav/Sidebar.d.ts +0 -3
- package/lib/components/SidebarNav/Sidebar.js +0 -33
- package/lib/components/SidebarNav/index.d.ts +0 -5
- package/lib/components/SidebarNav/index.js +0 -61
- package/lib/components/User/UserSelect.css +0 -11
- package/lib/components/common/LogoInfo.d.ts +0 -5
- package/lib/components/common/LogoInfo.js +0 -30
- package/lib/components/common/NavItem.d.ts +0 -20
- package/lib/components/common/NavItem.js +0 -112
- package/lib/components/common/NavMenu.d.ts +0 -9
- package/lib/components/common/NavMenu.js +0 -50
- package/lib/components/common/UserAvatarLayout.d.ts +0 -4
- package/lib/components/common/UserAvatarLayout.js +0 -41
- package/lib/components/common/UserAvatarMenu.d.ts +0 -4
- package/lib/components/common/UserAvatarMenu.js +0 -58
- package/lib/components/common/index.d.ts +0 -9
- package/lib/components/common/index.js +0 -10
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { clsxWithTw } from "../../utils/utils.js";
|
|
4
|
+
const Skeleton = /*#__PURE__*/ forwardRef(({ className, ...props }, ref)=>/*#__PURE__*/ jsx("div", {
|
|
5
|
+
ref: ref,
|
|
6
|
+
className: clsxWithTw('animate-pulse rounded-md bg-accent', className),
|
|
7
|
+
...props
|
|
8
|
+
}));
|
|
9
|
+
Skeleton.displayName = 'Skeleton';
|
|
10
|
+
export { Skeleton };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
3
|
+
declare const TooltipProvider: React.FC<TooltipPrimitive.TooltipProviderProps>;
|
|
4
|
+
declare const Tooltip: React.FC<TooltipPrimitive.TooltipProps>;
|
|
5
|
+
declare const TooltipTrigger: React.ForwardRefExoticComponent<TooltipPrimitive.TooltipTriggerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
6
|
+
declare const TooltipContent: React.ForwardRefExoticComponent<Omit<TooltipPrimitive.TooltipContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
7
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { Arrow, Content, Portal, Provider, Root, Trigger } from "@radix-ui/react-tooltip";
|
|
5
|
+
import { clsxWithTw } from "../../utils/utils.js";
|
|
6
|
+
const TooltipProvider = Provider;
|
|
7
|
+
const Tooltip = Root;
|
|
8
|
+
const TooltipTrigger = Trigger;
|
|
9
|
+
const TooltipContent = /*#__PURE__*/ forwardRef(({ className, sideOffset = 4, children, ...props }, ref)=>/*#__PURE__*/ jsx(Portal, {
|
|
10
|
+
children: /*#__PURE__*/ jsxs(Content, {
|
|
11
|
+
ref: ref,
|
|
12
|
+
sideOffset: sideOffset,
|
|
13
|
+
className: clsxWithTw('z-50 w-fit rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-lg ring-1 ring-border data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 animate-in fade-in-0 zoom-in-95', className),
|
|
14
|
+
...props,
|
|
15
|
+
children: [
|
|
16
|
+
children,
|
|
17
|
+
/*#__PURE__*/ jsx(Arrow, {
|
|
18
|
+
className: "fill-[rgb(31,35,41)] stroke-[rgb(31,35,41)]"
|
|
19
|
+
})
|
|
20
|
+
]
|
|
21
|
+
})
|
|
22
|
+
}));
|
|
23
|
+
TooltipContent.displayName = Content.displayName;
|
|
24
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
package/lib/hooks/useAppInfo.js
CHANGED
|
@@ -4,7 +4,18 @@ const useAppInfo = ()=>{
|
|
|
4
4
|
const [appInfo, setAppInfo] = useState({});
|
|
5
5
|
useEffect(()=>{
|
|
6
6
|
const handleMetaInfoChanged = async ()=>{
|
|
7
|
-
|
|
7
|
+
const info = await getAppInfo();
|
|
8
|
+
if (info.name) document.title = info.name;
|
|
9
|
+
if (info.avatar) {
|
|
10
|
+
let link = document.querySelector("link[rel~='icon']");
|
|
11
|
+
if (!link) {
|
|
12
|
+
link = document.createElement('link');
|
|
13
|
+
link.rel = 'icon';
|
|
14
|
+
document.head.appendChild(link);
|
|
15
|
+
}
|
|
16
|
+
link.href = info.avatar;
|
|
17
|
+
}
|
|
18
|
+
setAppInfo(info);
|
|
8
19
|
};
|
|
9
20
|
handleMetaInfoChanged();
|
|
10
21
|
window.addEventListener('MiaoDaMetaInfoChanged', handleMetaInfoChanged);
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { getInitialInfo } from "../utils/getInitialInfo.js";
|
|
2
2
|
import { isSparkRuntime } from "../utils/utils.js";
|
|
3
3
|
async function getAppInfo() {
|
|
4
|
-
let appInfo = window._appInfo;
|
|
4
|
+
let appInfo = 'undefined' != typeof window ? window._appInfo : void 0;
|
|
5
5
|
if (!appInfo && isSparkRuntime()) {
|
|
6
|
-
const info = (await getInitialInfo())
|
|
6
|
+
const info = (await getInitialInfo())?.app_info;
|
|
7
7
|
appInfo = {
|
|
8
8
|
name: info?.app_name || '',
|
|
9
9
|
avatar: info?.app_avatar || ''
|
|
10
10
|
};
|
|
11
|
+
if (appInfo.name) appInfo.name = appInfo.name.trim();
|
|
12
|
+
if ('undefined' != typeof window) window._appInfo = appInfo;
|
|
11
13
|
}
|
|
12
14
|
return appInfo ?? {};
|
|
13
15
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { BatchLogger, batchLogInfo, initBatchLogger } from "../batch-logger.js";
|
|
3
|
+
var __webpack_require__ = {};
|
|
4
|
+
(()=>{
|
|
5
|
+
__webpack_require__.g = (()=>{
|
|
6
|
+
if ('object' == typeof globalThis) return globalThis;
|
|
7
|
+
try {
|
|
8
|
+
return this || new Function('return this')();
|
|
9
|
+
} catch (e) {
|
|
10
|
+
if ('object' == typeof window) return window;
|
|
11
|
+
}
|
|
12
|
+
})();
|
|
13
|
+
})();
|
|
14
|
+
vi.mock('node-fetch', ()=>({
|
|
15
|
+
default: vi.fn()
|
|
16
|
+
}));
|
|
17
|
+
const mockConsole = {
|
|
18
|
+
debug: vi.fn(),
|
|
19
|
+
info: vi.fn(),
|
|
20
|
+
warn: vi.fn(),
|
|
21
|
+
error: vi.fn(),
|
|
22
|
+
log: vi.fn()
|
|
23
|
+
};
|
|
24
|
+
describe('BatchLogger', ()=>{
|
|
25
|
+
let batchLogger;
|
|
26
|
+
let mockFetch;
|
|
27
|
+
beforeEach(()=>{
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
mockFetch = vi.fn();
|
|
30
|
+
__webpack_require__.g.fetch = mockFetch;
|
|
31
|
+
const mockResponse = new Response(JSON.stringify({
|
|
32
|
+
success: true
|
|
33
|
+
}), {
|
|
34
|
+
status: 200,
|
|
35
|
+
statusText: 'OK',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json'
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
mockFetch.mockResolvedValue(mockResponse);
|
|
41
|
+
if ('undefined' == typeof window) __webpack_require__.g.window = void 0;
|
|
42
|
+
});
|
|
43
|
+
afterEach(async ()=>{
|
|
44
|
+
if (batchLogger) await batchLogger.destroy();
|
|
45
|
+
});
|
|
46
|
+
afterAll(()=>{
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
describe('Basic Functionality', ()=>{
|
|
50
|
+
it('should create a BatchLogger instance', ()=>{
|
|
51
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
52
|
+
userId: 'user123',
|
|
53
|
+
tenantId: 'tenant456',
|
|
54
|
+
appId: 'app789'
|
|
55
|
+
});
|
|
56
|
+
expect(batchLogger).toBeInstanceOf(BatchLogger);
|
|
57
|
+
});
|
|
58
|
+
it('should add logs to queue', ()=>{
|
|
59
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
60
|
+
userId: 'user123',
|
|
61
|
+
tenantId: 'tenant456',
|
|
62
|
+
appId: 'app789'
|
|
63
|
+
});
|
|
64
|
+
batchLogger.batchLog('info', 'Test message', 'test-source');
|
|
65
|
+
expect(batchLogger.getQueueSize()).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
it('should respect batch size limit', async ()=>{
|
|
68
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
69
|
+
userId: 'user123',
|
|
70
|
+
tenantId: 'tenant456',
|
|
71
|
+
appId: 'app789',
|
|
72
|
+
sizeThreshold: 3,
|
|
73
|
+
flushInterval: 10000
|
|
74
|
+
});
|
|
75
|
+
for(let i = 0; i < 5; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
76
|
+
await new Promise((resolve)=>setTimeout(resolve, 200));
|
|
77
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
78
|
+
expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
79
|
+
expect(batchLogger.getQueueSize()).toBeLessThanOrEqual(2);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('Log Preservation', ()=>{
|
|
83
|
+
it('should preserve all logs without merging', ()=>{
|
|
84
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
85
|
+
userId: 'user123',
|
|
86
|
+
tenantId: 'tenant456',
|
|
87
|
+
appId: 'app789'
|
|
88
|
+
});
|
|
89
|
+
for(let i = 0; i < 5; i++)batchLogger.batchLog('error', 'Same error message');
|
|
90
|
+
expect(batchLogger.getQueueSize()).toBe(5);
|
|
91
|
+
});
|
|
92
|
+
it('should handle different log levels independently', ()=>{
|
|
93
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
94
|
+
userId: 'user123',
|
|
95
|
+
tenantId: 'tenant456',
|
|
96
|
+
appId: 'app789'
|
|
97
|
+
});
|
|
98
|
+
batchLogger.batchLog('info', 'Test message');
|
|
99
|
+
batchLogger.batchLog('info', 'Test message');
|
|
100
|
+
batchLogger.batchLog('warn', 'Test message');
|
|
101
|
+
batchLogger.batchLog('warn', 'Test message');
|
|
102
|
+
expect(batchLogger.getQueueSize()).toBe(4);
|
|
103
|
+
});
|
|
104
|
+
it('should handle large volumes of logs efficiently', ()=>{
|
|
105
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
106
|
+
userId: 'user123',
|
|
107
|
+
tenantId: 'tenant456',
|
|
108
|
+
appId: 'app789',
|
|
109
|
+
sizeThreshold: 1000,
|
|
110
|
+
flushInterval: 10000
|
|
111
|
+
});
|
|
112
|
+
for(let i = 0; i < 200; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
113
|
+
expect(batchLogger.getQueueSize()).toBe(200);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('Retry Mechanism', ()=>{
|
|
117
|
+
it('should retry failed requests', async ()=>{
|
|
118
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
119
|
+
userId: 'user123',
|
|
120
|
+
tenantId: 'tenant456',
|
|
121
|
+
appId: 'app789',
|
|
122
|
+
maxRetries: 2,
|
|
123
|
+
retryDelay: 100,
|
|
124
|
+
flushInterval: 10000
|
|
125
|
+
});
|
|
126
|
+
const successResponse = new Response(JSON.stringify({
|
|
127
|
+
success: true
|
|
128
|
+
}), {
|
|
129
|
+
status: 200,
|
|
130
|
+
statusText: 'OK',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json'
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
mockFetch.mockRejectedValueOnce(new Error('Network error')).mockRejectedValueOnce(new Error('Network error')).mockResolvedValueOnce(successResponse);
|
|
136
|
+
batchLogger.batchLog('info', 'Test message');
|
|
137
|
+
await batchLogger.flush();
|
|
138
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
139
|
+
expect(mockFetch.mock.calls.length).toBeGreaterThanOrEqual(3);
|
|
140
|
+
});
|
|
141
|
+
it('should handle permanent failures', async ()=>{
|
|
142
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
143
|
+
userId: 'user123',
|
|
144
|
+
tenantId: 'tenant456',
|
|
145
|
+
appId: 'app789',
|
|
146
|
+
maxRetries: 1,
|
|
147
|
+
retryDelay: 100,
|
|
148
|
+
flushInterval: 10000
|
|
149
|
+
});
|
|
150
|
+
mockFetch.mockRejectedValue(new Error('Permanent error'));
|
|
151
|
+
batchLogger.batchLog('info', 'Test message');
|
|
152
|
+
await batchLogger.flush();
|
|
153
|
+
expect(mockConsole.warn).toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('Auto Flush', ()=>{
|
|
157
|
+
it('should auto flush based on interval', async ()=>{
|
|
158
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
159
|
+
userId: 'user123',
|
|
160
|
+
tenantId: 'tenant456',
|
|
161
|
+
appId: 'app789',
|
|
162
|
+
flushInterval: 1000,
|
|
163
|
+
sizeThreshold: 100
|
|
164
|
+
});
|
|
165
|
+
for(let i = 0; i < 3; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
166
|
+
await new Promise((resolve)=>setTimeout(resolve, 1200));
|
|
167
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
168
|
+
expect(batchLogger.getQueueSize()).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe('Request Configuration', ()=>{
|
|
172
|
+
it('should include custom headers', async ()=>{
|
|
173
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
174
|
+
userId: 'user123',
|
|
175
|
+
tenantId: 'tenant456',
|
|
176
|
+
appId: 'app789',
|
|
177
|
+
endpoint: 'https://api.example.com/logs',
|
|
178
|
+
headers: {
|
|
179
|
+
Authorization: 'Bearer token123',
|
|
180
|
+
'X-Custom-Header': 'custom-value'
|
|
181
|
+
},
|
|
182
|
+
flushInterval: 1000
|
|
183
|
+
});
|
|
184
|
+
batchLogger.batchLog('info', 'Test message');
|
|
185
|
+
await batchLogger.flush();
|
|
186
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/logs', expect.objectContaining({
|
|
187
|
+
headers: expect.objectContaining({
|
|
188
|
+
Authorization: 'Bearer token123',
|
|
189
|
+
'X-Custom-Header': 'custom-value'
|
|
190
|
+
})
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
193
|
+
it('should send correct batch format', async ()=>{
|
|
194
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
195
|
+
userId: 'user123',
|
|
196
|
+
tenantId: 'tenant456',
|
|
197
|
+
appId: 'app789',
|
|
198
|
+
flushInterval: 10000
|
|
199
|
+
});
|
|
200
|
+
batchLogger.batchLog('info', 'Test message', 'test-source');
|
|
201
|
+
await batchLogger.flush();
|
|
202
|
+
expect(mockFetch).toHaveBeenCalledWith('/dev/logs/collect-batch', expect.objectContaining({
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: {
|
|
205
|
+
'Content-Type': 'application/json'
|
|
206
|
+
}
|
|
207
|
+
}));
|
|
208
|
+
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
209
|
+
expect(Array.isArray(callBody)).toBe(true);
|
|
210
|
+
expect(callBody).toHaveLength(1);
|
|
211
|
+
expect(callBody[0]).toMatchObject({
|
|
212
|
+
level: 'info',
|
|
213
|
+
message: 'Test message',
|
|
214
|
+
source: 'test-source',
|
|
215
|
+
user_id: 'user123',
|
|
216
|
+
tenant_id: 'tenant456',
|
|
217
|
+
app_id: 'app789'
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('Global Instance Management', ()=>{
|
|
222
|
+
it('should initialize global batch logger instance', ()=>{
|
|
223
|
+
initBatchLogger(mockConsole, {
|
|
224
|
+
userId: 'user123',
|
|
225
|
+
tenantId: 'tenant456',
|
|
226
|
+
appId: 'app789'
|
|
227
|
+
});
|
|
228
|
+
expect(()=>batchLogInfo('info', 'Test message')).not.toThrow();
|
|
229
|
+
});
|
|
230
|
+
it('should handle batchLogInfo when no instance exists', ()=>{
|
|
231
|
+
expect(()=>batchLogInfo('info', 'Test message')).not.toThrow();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('Configuration Updates', ()=>{
|
|
235
|
+
it('should update configuration', ()=>{
|
|
236
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
237
|
+
userId: 'user123',
|
|
238
|
+
tenantId: 'tenant456',
|
|
239
|
+
appId: 'app789',
|
|
240
|
+
sizeThreshold: 10
|
|
241
|
+
});
|
|
242
|
+
batchLogger.updateConfig({
|
|
243
|
+
sizeThreshold: 20,
|
|
244
|
+
maxRetries: 5
|
|
245
|
+
});
|
|
246
|
+
for(let i = 0; i < 21; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
247
|
+
expect(batchLogger.getQueueSize()).toBe(1);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
describe('High Volume Log Processing', ()=>{
|
|
251
|
+
it('should handle large batch of logs without crashing', async ()=>{
|
|
252
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
253
|
+
userId: 'user123',
|
|
254
|
+
tenantId: 'tenant456',
|
|
255
|
+
appId: 'app789',
|
|
256
|
+
sizeThreshold: 50,
|
|
257
|
+
flushInterval: 1000
|
|
258
|
+
});
|
|
259
|
+
const largeBatchSize = 1000;
|
|
260
|
+
const startTime = Date.now();
|
|
261
|
+
for(let i = 0; i < largeBatchSize; i++)batchLogger.batchLog('info', `High volume log message ${i % 100}`, 'stress-test');
|
|
262
|
+
const endTime = Date.now();
|
|
263
|
+
expect(endTime - startTime).toBeLessThan(5000);
|
|
264
|
+
await new Promise((resolve)=>setTimeout(resolve, 2000));
|
|
265
|
+
expect(batchLogger.getQueueSize()).toBe(0);
|
|
266
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
267
|
+
const callCount = mockFetch.mock.calls.length;
|
|
268
|
+
expect(callCount).toBeGreaterThan(0);
|
|
269
|
+
expect(callCount).toBeLessThanOrEqual(25);
|
|
270
|
+
});
|
|
271
|
+
it('should control request frequency under high load', async ()=>{
|
|
272
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
273
|
+
userId: 'user123',
|
|
274
|
+
tenantId: 'tenant456',
|
|
275
|
+
appId: 'app789',
|
|
276
|
+
sizeThreshold: 30,
|
|
277
|
+
flushInterval: 500
|
|
278
|
+
});
|
|
279
|
+
const requestsBefore = mockFetch.mock.calls.length;
|
|
280
|
+
for(let batch = 0; batch < 5; batch++){
|
|
281
|
+
for(let i = 0; i < 25; i++)batchLogger.batchLog('error', `Error message ${batch}`, 'high-frequency-test');
|
|
282
|
+
await new Promise((resolve)=>setTimeout(resolve, 50));
|
|
283
|
+
}
|
|
284
|
+
await new Promise((resolve)=>setTimeout(resolve, 1000));
|
|
285
|
+
const totalRequests = mockFetch.mock.calls.length - requestsBefore;
|
|
286
|
+
expect(totalRequests).toBeGreaterThan(0);
|
|
287
|
+
expect(totalRequests).toBeLessThanOrEqual(10);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
describe('Edge Cases', ()=>{
|
|
291
|
+
it('should handle empty flush', async ()=>{
|
|
292
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
293
|
+
userId: 'user123',
|
|
294
|
+
tenantId: 'tenant456',
|
|
295
|
+
appId: 'app789'
|
|
296
|
+
});
|
|
297
|
+
await batchLogger.flush();
|
|
298
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
it('should handle concurrent flush calls safely', async ()=>{
|
|
301
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
302
|
+
userId: 'user123',
|
|
303
|
+
tenantId: 'tenant456',
|
|
304
|
+
appId: 'app789',
|
|
305
|
+
flushInterval: 10000
|
|
306
|
+
});
|
|
307
|
+
for(let i = 0; i < 20; i++)batchLogger.batchLog('info', `Message ${i}`);
|
|
308
|
+
const flushPromises = [];
|
|
309
|
+
for(let i = 0; i < 10; i++)flushPromises.push(batchLogger.flush());
|
|
310
|
+
await Promise.all(flushPromises);
|
|
311
|
+
expect(batchLogger.getQueueSize()).toBe(0);
|
|
312
|
+
expect(mockFetch.mock.calls.length).toBeGreaterThan(0);
|
|
313
|
+
expect(mockFetch.mock.calls.length).toBeLessThanOrEqual(3);
|
|
314
|
+
});
|
|
315
|
+
it('should ensure no logs remain after destroy', async ()=>{
|
|
316
|
+
mockFetch.mockResolvedValueOnce(new Response(JSON.stringify({
|
|
317
|
+
success: true
|
|
318
|
+
}), {
|
|
319
|
+
status: 200,
|
|
320
|
+
statusText: 'OK',
|
|
321
|
+
headers: {
|
|
322
|
+
'Content-Type': 'application/json'
|
|
323
|
+
}
|
|
324
|
+
}));
|
|
325
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
326
|
+
userId: 'user123',
|
|
327
|
+
tenantId: 'tenant456',
|
|
328
|
+
appId: 'app789',
|
|
329
|
+
flushInterval: 10000,
|
|
330
|
+
sizeThreshold: 1000
|
|
331
|
+
});
|
|
332
|
+
for(let i = 0; i < 50; i++){
|
|
333
|
+
batchLogger.batchLog('info', `Test message info ${i}`);
|
|
334
|
+
batchLogger.batchLog('warn', `Warning message warn ${i}`);
|
|
335
|
+
}
|
|
336
|
+
const queueSize = batchLogger.getQueueSize();
|
|
337
|
+
expect(queueSize).toBe(100);
|
|
338
|
+
const fetchCallsBefore = mockFetch.mock.calls.length;
|
|
339
|
+
await batchLogger.destroy();
|
|
340
|
+
const queueSizeAfter = batchLogger.getQueueSize();
|
|
341
|
+
const fetchCallsAfter = mockFetch.mock.calls.length;
|
|
342
|
+
expect(queueSizeAfter).toBe(0);
|
|
343
|
+
expect(fetchCallsAfter).toBeGreaterThan(fetchCallsBefore);
|
|
344
|
+
});
|
|
345
|
+
it('should handle environment detection without browser APIs', ()=>{
|
|
346
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
347
|
+
userId: 'user123',
|
|
348
|
+
tenantId: 'tenant456',
|
|
349
|
+
appId: 'app789'
|
|
350
|
+
});
|
|
351
|
+
batchLogger.batchLog('info', 'Test message');
|
|
352
|
+
expect(batchLogger.getQueueSize()).toBe(1);
|
|
353
|
+
});
|
|
354
|
+
it('should handle destroy method with pending logs', async ()=>{
|
|
355
|
+
batchLogger = new BatchLogger(mockConsole, {
|
|
356
|
+
userId: 'user123',
|
|
357
|
+
tenantId: 'tenant456',
|
|
358
|
+
appId: 'app789'
|
|
359
|
+
});
|
|
360
|
+
batchLogger.batchLog('info', 'Test message');
|
|
361
|
+
batchLogger.batchLog('warn', 'Warning message');
|
|
362
|
+
await batchLogger.destroy();
|
|
363
|
+
expect(batchLogger.getQueueSize()).toBe(0);
|
|
364
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
interface BatchLoggerConfig {
|
|
2
|
+
/** 用户ID */
|
|
3
|
+
userId: string;
|
|
4
|
+
/** 租户ID */
|
|
5
|
+
tenantId: string;
|
|
6
|
+
/** 应用ID */
|
|
7
|
+
appId: string;
|
|
8
|
+
/** 后端接收日志的接口地址,默认为/dev/logs/collect */
|
|
9
|
+
endpoint?: string;
|
|
10
|
+
/** 达到此数量时触发刷新 */
|
|
11
|
+
sizeThreshold?: number;
|
|
12
|
+
/** 自动刷新的时间间隔(毫秒) */
|
|
13
|
+
flushInterval?: number;
|
|
14
|
+
/** 最大重试次数 */
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
/** 重试延迟(毫秒) */
|
|
17
|
+
retryDelay?: number;
|
|
18
|
+
/** 自定义请求头 */
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
export declare class BatchLogger {
|
|
22
|
+
private config;
|
|
23
|
+
private logQueue;
|
|
24
|
+
private flushTimer;
|
|
25
|
+
private isProcessing;
|
|
26
|
+
private originConsole;
|
|
27
|
+
constructor(console: Console, config?: BatchLoggerConfig);
|
|
28
|
+
/**
|
|
29
|
+
* 批量记录日志(对外暴露的唯一方法)
|
|
30
|
+
*/
|
|
31
|
+
batchLog(level: string, message: string, source?: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* 刷新日志队列,全部发送
|
|
34
|
+
*/
|
|
35
|
+
flush(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* 发送日志批次到后端
|
|
38
|
+
*/
|
|
39
|
+
private sendBatch;
|
|
40
|
+
/**
|
|
41
|
+
* 执行实际的fetch请求
|
|
42
|
+
*/
|
|
43
|
+
private execFetch;
|
|
44
|
+
/**
|
|
45
|
+
* 启动自动刷新定时器
|
|
46
|
+
*/
|
|
47
|
+
private startFlushTimer;
|
|
48
|
+
/**
|
|
49
|
+
* 设置页面卸载时的处理
|
|
50
|
+
*/
|
|
51
|
+
private setupBeforeUnloadHandler;
|
|
52
|
+
/**
|
|
53
|
+
* 延迟函数
|
|
54
|
+
*/
|
|
55
|
+
private delay;
|
|
56
|
+
/**
|
|
57
|
+
* 生成唯一ID
|
|
58
|
+
*/
|
|
59
|
+
private generateId;
|
|
60
|
+
/**
|
|
61
|
+
* 销毁资源
|
|
62
|
+
*/
|
|
63
|
+
destroy(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* 获取队列大小
|
|
66
|
+
*/
|
|
67
|
+
getQueueSize(): number;
|
|
68
|
+
/**
|
|
69
|
+
* 更新配置
|
|
70
|
+
*/
|
|
71
|
+
updateConfig(newConfig: Partial<BatchLoggerConfig>): void;
|
|
72
|
+
}
|
|
73
|
+
export declare function getBatchLogger(): BatchLogger;
|
|
74
|
+
export declare function initBatchLogger(oldConsole: Console, config?: BatchLoggerConfig): void;
|
|
75
|
+
/** 记录日志进行批量同步 */
|
|
76
|
+
export declare function batchLogInfo(level: string, message: string, source?: string): void;
|
|
77
|
+
export declare function destroyBatchLogger(): void;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
class BatchLogger {
|
|
2
|
+
config;
|
|
3
|
+
logQueue = [];
|
|
4
|
+
flushTimer = null;
|
|
5
|
+
isProcessing = false;
|
|
6
|
+
originConsole;
|
|
7
|
+
constructor(console, config){
|
|
8
|
+
this.originConsole = {
|
|
9
|
+
...console
|
|
10
|
+
};
|
|
11
|
+
const { userId = '', tenantId = '', appId = '' } = window || {};
|
|
12
|
+
this.config = {
|
|
13
|
+
userId,
|
|
14
|
+
tenantId,
|
|
15
|
+
appId,
|
|
16
|
+
endpoint: (process.env.CLIENT_BASE_PATH || '') + '/dev/logs/collect-batch',
|
|
17
|
+
sizeThreshold: 20,
|
|
18
|
+
flushInterval: 1000,
|
|
19
|
+
maxRetries: 3,
|
|
20
|
+
retryDelay: 500,
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json'
|
|
23
|
+
},
|
|
24
|
+
...config || {}
|
|
25
|
+
};
|
|
26
|
+
this.startFlushTimer();
|
|
27
|
+
this.setupBeforeUnloadHandler();
|
|
28
|
+
}
|
|
29
|
+
batchLog(level, message, source) {
|
|
30
|
+
const logEntry = {
|
|
31
|
+
id: this.generateId(),
|
|
32
|
+
level,
|
|
33
|
+
message,
|
|
34
|
+
source,
|
|
35
|
+
timestamp: Date.now()
|
|
36
|
+
};
|
|
37
|
+
this.logQueue.push(logEntry);
|
|
38
|
+
if (this.logQueue.length >= this.config.sizeThreshold) this.flush();
|
|
39
|
+
}
|
|
40
|
+
async flush() {
|
|
41
|
+
if (this.isProcessing || 0 === this.logQueue.length) return;
|
|
42
|
+
this.isProcessing = true;
|
|
43
|
+
const logsToSend = this.logQueue.splice(0, this.logQueue.length);
|
|
44
|
+
try {
|
|
45
|
+
await this.sendBatch(logsToSend);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.logQueue.unshift(...logsToSend);
|
|
48
|
+
} finally{
|
|
49
|
+
this.isProcessing = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async sendBatch(logs) {
|
|
53
|
+
const collectLogs = logs.map((log)=>({
|
|
54
|
+
level: log.level,
|
|
55
|
+
message: log.message,
|
|
56
|
+
time: new Date(log.timestamp).toISOString(),
|
|
57
|
+
source: log.source,
|
|
58
|
+
user_id: this.config.userId,
|
|
59
|
+
tenant_id: this.config.tenantId,
|
|
60
|
+
app_id: this.config.appId
|
|
61
|
+
}));
|
|
62
|
+
let retries = 0;
|
|
63
|
+
while(retries <= this.config.maxRetries)try {
|
|
64
|
+
await this.execFetch(this.config.endpoint, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: this.config.headers,
|
|
67
|
+
body: JSON.stringify(collectLogs)
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
retries++;
|
|
72
|
+
if (retries > this.config.maxRetries) this.originConsole.error(`Failed to send logs (attempt ${retries}), retrying in ${this.config.retryDelay}ms...`);
|
|
73
|
+
else this.originConsole.warn(`Failed to send logs (attempt ${retries}), retrying in ${this.config.retryDelay}ms...`);
|
|
74
|
+
await this.delay(this.config.retryDelay * retries);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async execFetch(url, options) {
|
|
78
|
+
return fetch(url, options);
|
|
79
|
+
}
|
|
80
|
+
startFlushTimer() {
|
|
81
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
82
|
+
this.flushTimer = setInterval(()=>{
|
|
83
|
+
if (this.logQueue.length > 0) this.flush();
|
|
84
|
+
}, this.config.flushInterval);
|
|
85
|
+
}
|
|
86
|
+
setupBeforeUnloadHandler() {
|
|
87
|
+
if ('undefined' != typeof window) window.addEventListener('beforeunload', ()=>{
|
|
88
|
+
this.flush().finally(()=>{
|
|
89
|
+
this.destroy();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
delay(ms) {
|
|
94
|
+
return new Promise((resolve)=>setTimeout(resolve, ms));
|
|
95
|
+
}
|
|
96
|
+
generateId() {
|
|
97
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
98
|
+
}
|
|
99
|
+
async destroy() {
|
|
100
|
+
if (this.flushTimer) {
|
|
101
|
+
clearInterval(this.flushTimer);
|
|
102
|
+
this.flushTimer = null;
|
|
103
|
+
}
|
|
104
|
+
if (this.logQueue.length > 0) await this.flush();
|
|
105
|
+
}
|
|
106
|
+
getQueueSize() {
|
|
107
|
+
return this.logQueue.length;
|
|
108
|
+
}
|
|
109
|
+
updateConfig(newConfig) {
|
|
110
|
+
this.config = {
|
|
111
|
+
...this.config,
|
|
112
|
+
...newConfig
|
|
113
|
+
};
|
|
114
|
+
this.startFlushTimer();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let defaultBatchLogger = null;
|
|
118
|
+
function getBatchLogger() {
|
|
119
|
+
return defaultBatchLogger;
|
|
120
|
+
}
|
|
121
|
+
function initBatchLogger(oldConsole, config) {
|
|
122
|
+
defaultBatchLogger = new BatchLogger(oldConsole, config);
|
|
123
|
+
}
|
|
124
|
+
function batchLogInfo(level, message, source) {
|
|
125
|
+
if (!defaultBatchLogger) return;
|
|
126
|
+
defaultBatchLogger.batchLog(level, message, source);
|
|
127
|
+
}
|
|
128
|
+
function destroyBatchLogger() {
|
|
129
|
+
if (defaultBatchLogger) {
|
|
130
|
+
defaultBatchLogger.destroy();
|
|
131
|
+
defaultBatchLogger = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export { BatchLogger, batchLogInfo, destroyBatchLogger, getBatchLogger, initBatchLogger };
|
package/lib/override.css
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
.miao-top-nav {
|
|
2
|
-
& .ant-menu-overflow.ant-menu-horizontal > .ant-menu-item:after, & .ant-menu-overflow > .ant-menu.ant-menu-horizontal > .ant-menu-item:after, & .ant-menu-overflow.ant-menu-horizontal > .ant-menu-submenu:after, & .ant-menu-overflow > .ant-menu.ant-menu-horizontal > .ant-menu-submenu:after {
|
|
3
|
-
bottom: 1px !important;
|
|
4
|
-
}
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.miao-nav {
|
|
8
|
-
& .ant-drawer-content-wrapper {
|
|
9
|
-
width: inherit !important;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
&.ant-btn-color-default.ant-btn-variant-link:not(:disabled):not(.ant-btn-disabled):hover {
|
|
13
|
-
color: inherit;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
1
|
.ant-btn .ant-btn-icon {
|
|
18
2
|
align-items: center;
|
|
19
3
|
display: inline-flex;
|