lupine.api 1.0.41

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.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/admin/admin-about.tsx +16 -0
  4. package/admin/admin-config.tsx +44 -0
  5. package/admin/admin-css.tsx +3 -0
  6. package/admin/admin-db.tsx +74 -0
  7. package/admin/admin-frame-props.tsx +9 -0
  8. package/admin/admin-frame.tsx +466 -0
  9. package/admin/admin-index.tsx +66 -0
  10. package/admin/admin-login.tsx +99 -0
  11. package/admin/admin-menu-edit.tsx +637 -0
  12. package/admin/admin-menu-list.tsx +87 -0
  13. package/admin/admin-page-edit.tsx +564 -0
  14. package/admin/admin-page-list.tsx +83 -0
  15. package/admin/admin-performance.tsx +28 -0
  16. package/admin/admin-release.tsx +320 -0
  17. package/admin/admin-resources.tsx +385 -0
  18. package/admin/admin-shell.tsx +89 -0
  19. package/admin/admin-table-data.tsx +146 -0
  20. package/admin/admin-table-list.tsx +231 -0
  21. package/admin/admin-test-animations.tsx +379 -0
  22. package/admin/admin-test-component.tsx +808 -0
  23. package/admin/admin-test-edit.tsx +319 -0
  24. package/admin/admin-test-themes.tsx +56 -0
  25. package/admin/admin-tokens.tsx +338 -0
  26. package/admin/design/admin-design.tsx +174 -0
  27. package/admin/design/block-grid.tsx +36 -0
  28. package/admin/design/block-grid1.tsx +21 -0
  29. package/admin/design/block-paragraph.tsx +19 -0
  30. package/admin/design/block-title.tsx +19 -0
  31. package/admin/design/design-block-box.tsx +140 -0
  32. package/admin/design/drag-data.tsx +24 -0
  33. package/admin/index.ts +6 -0
  34. package/admin/package.json +15 -0
  35. package/admin/tsconfig.json +127 -0
  36. package/dev/copy-folder.js +32 -0
  37. package/dev/cp-index-html.js +69 -0
  38. package/dev/file-utils.js +12 -0
  39. package/dev/index.js +19 -0
  40. package/dev/package.json +12 -0
  41. package/dev/plugin-gen-versions.js +20 -0
  42. package/dev/plugin-ifelse.js +155 -0
  43. package/dev/plugin-ifelse.test.js +37 -0
  44. package/dev/run-cmd.js +14 -0
  45. package/dev/send-request.js +12 -0
  46. package/package.json +55 -0
  47. package/src/admin-api/admin-api.ts +59 -0
  48. package/src/admin-api/admin-auth.ts +87 -0
  49. package/src/admin-api/admin-config.ts +93 -0
  50. package/src/admin-api/admin-csv.ts +81 -0
  51. package/src/admin-api/admin-db.ts +269 -0
  52. package/src/admin-api/admin-helper.ts +111 -0
  53. package/src/admin-api/admin-menu.ts +135 -0
  54. package/src/admin-api/admin-page.ts +135 -0
  55. package/src/admin-api/admin-performance.ts +128 -0
  56. package/src/admin-api/admin-release.ts +498 -0
  57. package/src/admin-api/admin-resources.ts +318 -0
  58. package/src/admin-api/admin-token-helper.ts +79 -0
  59. package/src/admin-api/admin-tokens.ts +90 -0
  60. package/src/admin-api/index.ts +2 -0
  61. package/src/api/api-cache.ts +103 -0
  62. package/src/api/api-helper.ts +44 -0
  63. package/src/api/api-module.ts +60 -0
  64. package/src/api/api-router.ts +177 -0
  65. package/src/api/api-shared-storage.ts +64 -0
  66. package/src/api/async-storage.ts +5 -0
  67. package/src/api/debug-service.ts +56 -0
  68. package/src/api/encode-html.ts +27 -0
  69. package/src/api/handle-status.ts +71 -0
  70. package/src/api/index.ts +16 -0
  71. package/src/api/mini-web-socket.ts +270 -0
  72. package/src/api/server-content-type.ts +82 -0
  73. package/src/api/server-render.ts +216 -0
  74. package/src/api/shell-service.ts +66 -0
  75. package/src/api/simple-storage.ts +80 -0
  76. package/src/api/static-server.ts +125 -0
  77. package/src/api/to-client-delivery.ts +26 -0
  78. package/src/app/app-cache.ts +55 -0
  79. package/src/app/app-loader.ts +62 -0
  80. package/src/app/app-message.ts +60 -0
  81. package/src/app/app-shared-storage.ts +317 -0
  82. package/src/app/app-start.ts +117 -0
  83. package/src/app/cleanup-exit.ts +12 -0
  84. package/src/app/host-to-path.ts +38 -0
  85. package/src/app/index.ts +11 -0
  86. package/src/app/process-dev-requests.ts +90 -0
  87. package/src/app/web-listener.ts +230 -0
  88. package/src/app/web-processor.ts +42 -0
  89. package/src/app/web-server.ts +86 -0
  90. package/src/common-js/web-env.js +104 -0
  91. package/src/index.ts +7 -0
  92. package/src/lang/api-lang-en.ts +27 -0
  93. package/src/lang/api-lang-zh-cn.ts +28 -0
  94. package/src/lang/index.ts +2 -0
  95. package/src/lang/lang-helper.ts +76 -0
  96. package/src/lang/lang-props.ts +6 -0
  97. package/src/lib/db/db-helper.ts +23 -0
  98. package/src/lib/db/db-mysql.ts +250 -0
  99. package/src/lib/db/db-sqlite.ts +101 -0
  100. package/src/lib/db/db.spec.ts +28 -0
  101. package/src/lib/db/db.ts +304 -0
  102. package/src/lib/db/index.ts +5 -0
  103. package/src/lib/index.ts +3 -0
  104. package/src/lib/logger.spec.ts +214 -0
  105. package/src/lib/logger.ts +274 -0
  106. package/src/lib/runtime-require.ts +37 -0
  107. package/src/lib/utils/cookie-util.ts +34 -0
  108. package/src/lib/utils/crypto.ts +58 -0
  109. package/src/lib/utils/date-utils.ts +317 -0
  110. package/src/lib/utils/deep-merge.ts +37 -0
  111. package/src/lib/utils/delay.ts +12 -0
  112. package/src/lib/utils/file-setting.ts +55 -0
  113. package/src/lib/utils/format-bytes.ts +11 -0
  114. package/src/lib/utils/fs-utils.ts +144 -0
  115. package/src/lib/utils/get-env.ts +27 -0
  116. package/src/lib/utils/index.ts +12 -0
  117. package/src/lib/utils/is-type.ts +48 -0
  118. package/src/lib/utils/load-env.ts +14 -0
  119. package/src/lib/utils/pad.ts +6 -0
  120. package/src/models/api-base.ts +5 -0
  121. package/src/models/api-module-props.ts +11 -0
  122. package/src/models/api-router-props.ts +26 -0
  123. package/src/models/app-cache-props.ts +33 -0
  124. package/src/models/app-data-props.ts +10 -0
  125. package/src/models/app-loader-props.ts +6 -0
  126. package/src/models/app-shared-storage-props.ts +37 -0
  127. package/src/models/app-start-props.ts +18 -0
  128. package/src/models/async-storage-props.ts +13 -0
  129. package/src/models/db-config.ts +30 -0
  130. package/src/models/host-to-path-props.ts +12 -0
  131. package/src/models/index.ts +16 -0
  132. package/src/models/json-object.ts +8 -0
  133. package/src/models/locals-props.ts +36 -0
  134. package/src/models/logger-props.ts +84 -0
  135. package/src/models/simple-storage-props.ts +14 -0
  136. package/src/models/to-client-delivery-props.ts +6 -0
  137. package/tsconfig.json +115 -0
@@ -0,0 +1,319 @@
1
+ import {
2
+ CssProps,
3
+ RefProps,
4
+ EditableLabel,
5
+ HtmlVar,
6
+ ModalWindow,
7
+ NotificationColor,
8
+ NotificationMessage,
9
+ PagingLink,
10
+ ToggleBaseHookProps,
11
+ ToggleSwitch,
12
+ ToggleSwitchSize,
13
+ } from 'lupine.components';
14
+
15
+ type SampleDataProps = {
16
+ id: number;
17
+ name: string;
18
+ info: string;
19
+ checked: boolean;
20
+ };
21
+ const _DEFAULT_PAGE_LIMIT = 10;
22
+ const _DEFAULT_ITEM_COUNT = 101;
23
+ const sampleData: SampleDataProps[] = Array.from({ length: _DEFAULT_ITEM_COUNT }, (_, i) => ({
24
+ id: i + 1,
25
+ name: `Book Name ${i + 1}`,
26
+ info: `This is the info field ${i + 1}`,
27
+ checked: true,
28
+ }));
29
+ const getSampleData = (pageIndex = 0, searchTexts: string[] = []) => {
30
+ const filterItems = sampleData.filter((item) =>
31
+ searchTexts.every(
32
+ (text) =>
33
+ item.name.toLowerCase().includes(text.toLowerCase()) || item.info.toLowerCase().includes(text.toLowerCase())
34
+ )
35
+ );
36
+ const pageLimit = _DEFAULT_PAGE_LIMIT;
37
+ const itemsCount = filterItems.length;
38
+ let maxPages = Math.floor(itemsCount / pageLimit);
39
+ if (itemsCount % pageLimit !== 0) {
40
+ maxPages++;
41
+ }
42
+ if (pageIndex > maxPages) {
43
+ pageIndex = maxPages - 1;
44
+ }
45
+ const offset = pageIndex * pageLimit;
46
+ return {
47
+ status: 'ok',
48
+ itemsCount,
49
+ pageIndex,
50
+ result: filterItems.slice(offset, offset + pageLimit),
51
+ };
52
+ };
53
+ const removeSampleData = (itemId: number) => {
54
+ const index = sampleData.findIndex((item) => item.id === itemId);
55
+ if (index !== -1) {
56
+ sampleData.splice(index, 1);
57
+ }
58
+ };
59
+ const updateSampleData = (item: SampleDataProps) => {
60
+ const index = sampleData.findIndex((i) => i.id === item.id);
61
+ if (index !== -1) {
62
+ sampleData[index] = { ...item };
63
+ } else {
64
+ // it's a new record
65
+ let newId = sampleData.length + 1;
66
+ while (sampleData.some((item) => item.id === newId)) {
67
+ newId++;
68
+ }
69
+ item.id = newId;
70
+ sampleData.push({ ...item });
71
+ }
72
+ return item;
73
+ };
74
+
75
+ type SampleDataUpdateProps = {
76
+ save?: () => SampleDataProps | null;
77
+ cancel?: () => void;
78
+ };
79
+ // show dialog to edit one item, and call update when save the result
80
+ const showBookEditItem = async (item: SampleDataProps, update: (item: SampleDataProps) => void) => {
81
+ const handleClicked = async (index: number) => {
82
+ if (index === 1) {
83
+ updateFn.cancel?.();
84
+ modalClose();
85
+ }
86
+ if (index === 0) {
87
+ const newItem = updateFn.save?.();
88
+ if (newItem) {
89
+ update(newItem);
90
+ modalClose();
91
+ }
92
+ }
93
+ };
94
+ const updateFn: SampleDataUpdateProps = {};
95
+ const modalClose = await ModalWindow.show({
96
+ title: 'Edit Sample Data',
97
+ buttons: ['Save', 'Cancel'],
98
+ // contentMaxHeight: '400px',
99
+ handleClicked,
100
+ children: <BookEditItem item={item} update={updateFn}></BookEditItem>,
101
+ closeWhenClickOutside: false,
102
+ });
103
+ };
104
+
105
+ // edit one item
106
+ export const BookEditItem = (props: { item: SampleDataProps; update: SampleDataUpdateProps }) => {
107
+ const ref: RefProps = { id: '' };
108
+ const css: CssProps = {
109
+ padding: '10px',
110
+ border: 'solid 1px gray',
111
+ margin: '1px',
112
+ position: 'relative',
113
+ '.lable': {
114
+ width: '70px',
115
+ },
116
+ };
117
+ props.update.save = () => {
118
+ const name = ref.$('input.name').value;
119
+ const info = ref.$('input.info').value;
120
+ if (name && info) {
121
+ const newItem = updateSampleData({ id: props.item.id, name, info, checked: switchUpdate.getChecked!() });
122
+ props.item.name = newItem.name;
123
+ props.item.info = newItem.info;
124
+ props.item.checked = newItem.checked;
125
+ props.item.id = newItem.id;
126
+ return newItem;
127
+ }
128
+ NotificationMessage.sendMessage('Please input name and info', NotificationColor.Error);
129
+ return null;
130
+ };
131
+ const switchUpdate: ToggleBaseHookProps = {};
132
+ return (
133
+ <div ref={ref} css={css} class='sample-data'>
134
+ <div class='row-box'>
135
+ <div class='lable'>Name: </div>
136
+ <div>
137
+ <input type='text' class='input-base name' value={props.item.name} />
138
+ </div>
139
+ </div>
140
+ <div class='row-box mt-m'>
141
+ <div class='lable'>Info: </div>
142
+ <div>
143
+ <input type='text' class='input-base info' value={props.item.info} />
144
+ </div>
145
+ </div>
146
+ <div class='row-box mt-m'>
147
+ <div class='lable'>Checked: </div>
148
+ <ToggleSwitch size={ToggleSwitchSize.Small} hook={switchUpdate} checked={props.item.checked} />
149
+ </div>
150
+ </div>
151
+ );
152
+ };
153
+
154
+ // show one item
155
+ export const BookShowItem = (props: { item: SampleDataProps }) => {
156
+ const ref: RefProps = { id: '' };
157
+ const css: CssProps = {
158
+ padding: '10px',
159
+ border: 'solid 1px gray',
160
+ margin: '1px',
161
+ position: 'relative',
162
+ '.control-box': {
163
+ display: 'none',
164
+ position: 'absolute',
165
+ right: '10px',
166
+ top: '10px',
167
+ },
168
+ '&:hover .control-box': {
169
+ display: 'block',
170
+ },
171
+ '.lable': {
172
+ width: '70px',
173
+ },
174
+ };
175
+ const onEdit = (ev: any) => {
176
+ const update = (item: SampleDataProps) => {
177
+ dom.value = makeDom(item);
178
+ };
179
+ showBookEditItem(props.item, update);
180
+ };
181
+ const onRemove = (ev: any) => {
182
+ removeSampleData(props.item.id);
183
+ ref.current.remove();
184
+ };
185
+
186
+ const makeDom = (item: SampleDataProps) => {
187
+ const saveText = (text: string) => {
188
+ item.name = text;
189
+ updateSampleData(item);
190
+ dom.value = makeDom(item);
191
+ };
192
+ return (
193
+ <>
194
+ <div class='control-box'>
195
+ <button class='button-base button-ss' onClick={onEdit}>
196
+ Edit
197
+ </button>
198
+ <button class='button-base button-ss' onClick={onRemove}>
199
+ Delete
200
+ </button>
201
+ </div>
202
+ <div class='row-box'>
203
+ <div class='lable'>Name: </div>
204
+ <div>{item.name}</div>
205
+ <div class='px-m'>Double Click to edit: </div>
206
+ <EditableLabel text={item.name} save={saveText} type='text' />
207
+ </div>
208
+ <div class='row-box'>
209
+ <div class='lable'>Info: </div>
210
+ <div>{item.info}</div>
211
+ </div>
212
+ <div class='row-box'>
213
+ <div class='lable'>Checked: </div>
214
+ <div>{item.checked ? 'Yes' : 'No'}</div>
215
+ </div>
216
+ </>
217
+ );
218
+ };
219
+
220
+ const dom = HtmlVar(makeDom(props.item));
221
+ return (
222
+ <div ref={ref} css={css} class='sample-data'>
223
+ {dom.node}
224
+ </div>
225
+ );
226
+ };
227
+
228
+ // show the list
229
+ export const BookList = () => {
230
+ let currentIndex = 0;
231
+ let searchTexts: string[] = [];
232
+ let items = getSampleData(currentIndex, searchTexts);
233
+ const ref: RefProps = { onLoad: async (self: Element) => {} };
234
+ const onAdd = async () => {
235
+ const update = (item: SampleDataProps) => {
236
+ // new item is added at the list end, so update the last page
237
+ currentIndex = Math.floor((items.itemsCount + 1) / _DEFAULT_PAGE_LIMIT);
238
+ listDom.value = makeList(currentIndex);
239
+ };
240
+ showBookEditItem(
241
+ {
242
+ id: -1,
243
+ name: '',
244
+ info: '',
245
+ checked: false,
246
+ },
247
+ update
248
+ );
249
+ };
250
+ const onSearch = () => {
251
+ searchTexts = ref.$('input.search').value.trim().split(' ');
252
+ items = getSampleData(currentIndex, searchTexts);
253
+ listDom.value = makeList(currentIndex);
254
+ };
255
+ const makeList = (pageIndex: number) => {
256
+ const onLinkClick = (index: number) => {
257
+ currentIndex = index;
258
+ listDom.value = makeList(currentIndex);
259
+ };
260
+ const items = getSampleData(pageIndex, searchTexts);
261
+ return (
262
+ <div>
263
+ <PagingLink
264
+ itemsCount={items.itemsCount}
265
+ pageIndex={pageIndex}
266
+ pageLimit={_DEFAULT_PAGE_LIMIT}
267
+ onClick={onLinkClick}
268
+ baseLink=''
269
+ ></PagingLink>
270
+ {items.result.map((item) => (
271
+ <BookShowItem item={item}></BookShowItem>
272
+ ))}
273
+ <PagingLink
274
+ itemsCount={items.itemsCount}
275
+ pageIndex={pageIndex}
276
+ pageLimit={_DEFAULT_PAGE_LIMIT}
277
+ onClick={onLinkClick}
278
+ baseLink=''
279
+ ></PagingLink>
280
+ </div>
281
+ );
282
+ };
283
+ const listDom = HtmlVar(makeList(currentIndex));
284
+ const css: CssProps = {
285
+ display: 'flex',
286
+ flexDirection: 'column',
287
+ '.label': {
288
+ width: '70px',
289
+ },
290
+ };
291
+ return (
292
+ <div ref={ref} css={css}>
293
+ <div>
294
+ <div class='row-box'>
295
+ <div class='label'>Search: </div>
296
+ <input type='text' class='input-base search' />
297
+ <button class='button-base mr-s' onClick={onSearch}>
298
+ Search
299
+ </button>
300
+ <button class='button-base' onClick={onAdd}>
301
+ Add
302
+ </button>
303
+ </div>
304
+ </div>
305
+ <div class='list'>{listDom.node}</div>
306
+ </div>
307
+ );
308
+ };
309
+
310
+ export const AdminTestEditPage = () => {
311
+ return (
312
+ <div>
313
+ <div>
314
+ <div>Test editing.</div>
315
+ <BookList />
316
+ </div>
317
+ </div>
318
+ );
319
+ };
@@ -0,0 +1,56 @@
1
+ import { CssProps, darkThemes, lightThemes } from 'lupine.components';
2
+
3
+ const TestThemes = () => {
4
+ const lightNames = Object.keys(lightThemes);
5
+ const css: CssProps = {
6
+ '.theme-title': {
7
+ width: '200px',
8
+ },
9
+ '.theme-item1': {
10
+ width: '10%',
11
+ },
12
+ '.theme-item2': {
13
+ width: '10%',
14
+ },
15
+ '.theme-item3': {
16
+ width: '10%',
17
+ },
18
+ '.theme-item4': {
19
+ width: '10%',
20
+ },
21
+ };
22
+ return (
23
+ <div css={css}>
24
+ <div class='row-box mb-s'>
25
+ <div class='theme-title bold mb-s'>Theme attributes</div>
26
+ <div class='theme-item1 mb-s'>Light</div>
27
+ <div class='theme-item2 mb-s'></div>
28
+ <div class='theme-item3 mb-s'>Dark</div>
29
+ <div class='theme-item4 mb-s'></div>
30
+ </div>
31
+
32
+ {lightNames.map((themeName) => (
33
+ <div key={themeName} class='row-box mb-s'>
34
+ <div class='theme-title bold mb-s'>{themeName}</div>
35
+ <div class='theme-item1 mb-s' style={{ color: lightThemes[themeName] }}>
36
+ {lightThemes[themeName]}
37
+ </div>
38
+ <div class='theme-item2 mb-s' style={{ background: lightThemes[themeName] }}>
39
+ {lightThemes[themeName]}
40
+ </div>
41
+
42
+ <div class='theme-item3 mb-s' style={{ color: darkThemes[themeName] }}>
43
+ {darkThemes[themeName]}
44
+ </div>
45
+ <div class='theme-item4 mb-s' style={{ background: darkThemes[themeName] }}>
46
+ {darkThemes[themeName]}
47
+ </div>
48
+ </div>
49
+ ))}
50
+ </div>
51
+ );
52
+ };
53
+
54
+ export const TestThemesPage = () => {
55
+ return <TestThemes />;
56
+ };
@@ -0,0 +1,338 @@
1
+ import {
2
+ CssProps,
3
+ getRenderPageProps,
4
+ RefProps,
5
+ EditableLabel,
6
+ HtmlVar,
7
+ ModalWindow,
8
+ NotificationColor,
9
+ NotificationMessage,
10
+ PagingLink,
11
+ } from 'lupine.components';
12
+
13
+ export type TokenProps = {
14
+ token: string;
15
+ description: string;
16
+ timestamp?: number;
17
+ };
18
+
19
+ const getTokenData = async (pageIndex = 0, searchText: string = '') => {
20
+ const response = await getRenderPageProps().renderPageFunctions.fetchData('/api/admin/tokens/list', {
21
+ q: searchText,
22
+ });
23
+ const dataResponse = await response.json;
24
+ let pageLimit = 15;
25
+ let tokenList: TokenProps[] = [];
26
+ if (dataResponse && dataResponse.status === 'ok') {
27
+ tokenList = dataResponse.result;
28
+ pageLimit = dataResponse.pageLimit;
29
+ tokenList;
30
+ }
31
+
32
+ const itemsCount = tokenList.length;
33
+ let maxPages = Math.floor(itemsCount / pageLimit);
34
+ if (itemsCount % pageLimit !== 0) {
35
+ maxPages++;
36
+ }
37
+ if (pageIndex > maxPages - 1) {
38
+ pageIndex = maxPages - 1;
39
+ }
40
+ const offset = pageIndex * pageLimit;
41
+ return {
42
+ status: 'ok',
43
+ itemsCount,
44
+ pageIndex,
45
+ pageLimit,
46
+ result: tokenList.slice(offset, offset + pageLimit),
47
+ };
48
+ };
49
+ const generateToken = async () => {
50
+ const response = await getRenderPageProps().renderPageFunctions.fetchData('/api/admin/tokens/generate');
51
+ const dataResponse = await response.json;
52
+ if (dataResponse && dataResponse.status === 'ok') {
53
+ NotificationMessage.sendMessage(dataResponse.message || 'Token generated successfully', NotificationColor.Success);
54
+ } else {
55
+ NotificationMessage.sendMessage(dataResponse.message || 'Failed to generate token', NotificationColor.Error);
56
+ }
57
+ return dataResponse.result;
58
+ };
59
+ const addTokenData = async (item: TokenProps) => {
60
+ const response = await getRenderPageProps().renderPageFunctions.fetchData('/api/admin/tokens/add', { ...item });
61
+ const dataResponse = await response.json;
62
+ if (dataResponse && dataResponse.status === 'ok') {
63
+ NotificationMessage.sendMessage(dataResponse.message || 'Token added successfully', NotificationColor.Success);
64
+ } else {
65
+ NotificationMessage.sendMessage(dataResponse.message || 'Failed to add token', NotificationColor.Error);
66
+ }
67
+ return { ...item };
68
+ };
69
+
70
+ const removeTokenData = async (token: string) => {
71
+ const response = await getRenderPageProps().renderPageFunctions.fetchData('/api/admin/tokens/remove', { token });
72
+ const dataResponse = await response.json;
73
+ if (dataResponse && dataResponse.status === 'ok') {
74
+ NotificationMessage.sendMessage(dataResponse.message || 'Token removed successfully', NotificationColor.Success);
75
+ } else {
76
+ NotificationMessage.sendMessage(dataResponse.message || 'Failed to remove token', NotificationColor.Error);
77
+ }
78
+ };
79
+ const updateTokenData = async (item: TokenProps) => {
80
+ const response = await getRenderPageProps().renderPageFunctions.fetchData('/api/admin/tokens/update', { ...item });
81
+ const dataResponse = await response.json;
82
+ if (dataResponse && dataResponse.status === 'ok') {
83
+ NotificationMessage.sendMessage(dataResponse.message || 'Token updated successfully', NotificationColor.Success);
84
+ } else {
85
+ NotificationMessage.sendMessage(dataResponse.message || 'Failed to update token', NotificationColor.Error);
86
+ }
87
+ return { ...item };
88
+ };
89
+
90
+ type TokenDataUpdateProps = {
91
+ save?: (isNew: boolean) => Promise<TokenProps | null>;
92
+ cancel?: () => void;
93
+ };
94
+ // show dialog to edit one item, and call update when save the result
95
+ const showTokenEditItem = async (item: TokenProps, update: (item: TokenProps) => void, isNew: boolean) => {
96
+ const handleClicked = async (index: number) => {
97
+ if (index === 1) {
98
+ updateFn.cancel?.();
99
+ modalClose();
100
+ }
101
+ if (index === 0) {
102
+ const newItem = await updateFn.save?.(isNew);
103
+ if (newItem) {
104
+ update(newItem);
105
+ modalClose();
106
+ }
107
+ }
108
+ };
109
+ const updateFn: TokenDataUpdateProps = {};
110
+ const modalClose = await ModalWindow.show({
111
+ title: 'Edit Token Data',
112
+ buttons: ['Save', 'Cancel'],
113
+ // contentMaxHeight: '400px',
114
+ handleClicked,
115
+ children: <TokenEditItem item={item} update={updateFn}></TokenEditItem>,
116
+ closeWhenClickOutside: false,
117
+ });
118
+ };
119
+
120
+ // edit one item
121
+ export const TokenEditItem = (props: { item: TokenProps; update: TokenDataUpdateProps }) => {
122
+ const ref: RefProps = { id: '' };
123
+ const css: CssProps = {
124
+ padding: '10px',
125
+ border: 'solid 1px gray',
126
+ margin: '1px',
127
+ position: 'relative',
128
+ '.lable': {
129
+ width: '90px',
130
+ },
131
+ };
132
+ props.update.save = async (isNew: boolean) => {
133
+ const token = ref.$('input.token').value;
134
+ const description = ref.$('input.description').value;
135
+ if (token && description) {
136
+ const newItem = isNew
137
+ ? await addTokenData({ token, description })
138
+ : await updateTokenData({ token, description });
139
+ props.item.token = newItem.token;
140
+ props.item.description = newItem.description;
141
+ return newItem;
142
+ }
143
+ NotificationMessage.sendMessage('Please input token and description', NotificationColor.Error);
144
+ return null;
145
+ };
146
+ return (
147
+ <div ref={ref} css={css} class='sample-data'>
148
+ <div class='row-box mt-m'>
149
+ <div class='lable'>Token: </div>
150
+ <div>
151
+ <input type='text' class='input-base token' value={props.item.token} readonly={true} />
152
+ </div>
153
+ </div>
154
+ <div class='row-box'>
155
+ <div class='lable'>Description: </div>
156
+ <div>
157
+ <input type='text' class='input-base description' value={props.item.description} />
158
+ </div>
159
+ </div>
160
+ </div>
161
+ );
162
+ };
163
+
164
+ // show one item
165
+ export const TokenShowItem = (props: { item: TokenProps }) => {
166
+ const ref: RefProps = {};
167
+ const css: CssProps = {
168
+ padding: '10px',
169
+ border: 'solid 1px gray',
170
+ margin: '1px',
171
+ position: 'relative',
172
+ '.control-box': {
173
+ display: 'none',
174
+ position: 'absolute',
175
+ right: '10px',
176
+ top: '10px',
177
+ },
178
+ '&:hover .control-box': {
179
+ display: 'block',
180
+ },
181
+ '.lable': {
182
+ width: '85px',
183
+ },
184
+ '.token': {
185
+ lineBreak: 'anywhere',
186
+ wordBreak: 'break-all',
187
+ },
188
+ };
189
+ const onEdit = (ev: any) => {
190
+ const update = (item: TokenProps) => {
191
+ dom.value = makeDom(item);
192
+ };
193
+ showTokenEditItem(props.item, update, false);
194
+ };
195
+ const onRemove = async (ev: any) => {
196
+ if (!confirm('Are you sure you want to delete this token?')) {
197
+ return;
198
+ }
199
+ await removeTokenData(props.item.token);
200
+ ref.current.remove();
201
+ };
202
+
203
+ const makeDom = (item: TokenProps) => {
204
+ const saveText = (text: string) => {
205
+ item.description = text;
206
+ updateTokenData(item);
207
+ dom.value = makeDom(item);
208
+ };
209
+ return (
210
+ <div>
211
+ <div class='control-box'>
212
+ <button class='button-base button-ss' onClick={onEdit}>
213
+ Edit
214
+ </button>
215
+ <button class='button-base button-ss' onClick={onRemove}>
216
+ Delete
217
+ </button>
218
+ </div>
219
+ <div class='row-box'>
220
+ <div class='lable'>Token: </div>
221
+ <div class='token'>{item.token}</div>
222
+ </div>
223
+ <div class='row-box'>
224
+ <div class='lable'>Description: </div>
225
+ <div class='flex-1'>
226
+ <EditableLabel text={item.description} save={saveText} type='text' />
227
+ </div>
228
+ </div>
229
+ <div class='row-box'>
230
+ <div class='lable'>Timestamp: </div>
231
+ <div>{new Date(item.timestamp || 0).toLocaleString()}</div>
232
+ </div>
233
+ </div>
234
+ );
235
+ };
236
+
237
+ const dom = HtmlVar(makeDom(props.item));
238
+ return (
239
+ <div ref={ref} css={css} class='sample-data'>
240
+ {dom.node}
241
+ </div>
242
+ );
243
+ };
244
+
245
+ // show the list
246
+ const TokenList = () => {
247
+ let currentIndex = 0;
248
+ let searchText = '';
249
+ let maxPages = 0;
250
+ const ref: RefProps = {
251
+ onLoad: async (self: Element) => {
252
+ await makeList(currentIndex);
253
+ },
254
+ };
255
+ const onAdd = async () => {
256
+ const update = async (item: TokenProps) => {
257
+ // new item is added at the list end, so update the last page
258
+ await makeList(maxPages + 1);
259
+ };
260
+ showTokenEditItem(
261
+ {
262
+ token: await generateToken(),
263
+ description: '',
264
+ },
265
+ update,
266
+ true
267
+ );
268
+ };
269
+ const onSearch = async () => {
270
+ searchText = ref.$('input.search').value.trim();
271
+ await makeList(currentIndex);
272
+ };
273
+ const makeList = async (pageIndex: number) => {
274
+ const onLinkClick = async (index: number) => {
275
+ currentIndex = index;
276
+ await makeList(currentIndex);
277
+ };
278
+ const items = await getTokenData(pageIndex, searchText);
279
+ maxPages = Math.floor((items.itemsCount + 1) / items.pageLimit);
280
+ listDom.value = (
281
+ <div>
282
+ <PagingLink
283
+ itemsCount={items.itemsCount}
284
+ pageIndex={pageIndex}
285
+ pageLimit={items.pageLimit}
286
+ onClick={onLinkClick}
287
+ baseLink=''
288
+ ></PagingLink>
289
+ {items.result.map((item) => (
290
+ <TokenShowItem item={item}></TokenShowItem>
291
+ ))}
292
+ <PagingLink
293
+ itemsCount={items.itemsCount}
294
+ pageIndex={pageIndex}
295
+ pageLimit={items.pageLimit}
296
+ onClick={onLinkClick}
297
+ baseLink=''
298
+ ></PagingLink>
299
+ </div>
300
+ );
301
+ };
302
+ const listDom = HtmlVar(<div></div>);
303
+ const css: CssProps = {
304
+ display: 'flex',
305
+ flexDirection: 'column',
306
+ '.label': {
307
+ width: '70px',
308
+ },
309
+ };
310
+ return (
311
+ <div ref={ref} css={css}>
312
+ <div>
313
+ <div class='row-box'>
314
+ <div class='label'>Search: </div>
315
+ <input type='text' class='input-base search' />
316
+ <button class='button-base mr-s' onClick={onSearch}>
317
+ Search
318
+ </button>
319
+ <button class='button-base' onClick={onAdd}>
320
+ Add
321
+ </button>
322
+ </div>
323
+ </div>
324
+ <div class='list'>{listDom.node}</div>
325
+ </div>
326
+ );
327
+ };
328
+
329
+ export const AdminTokensPage = () => {
330
+ return (
331
+ <div>
332
+ <div>
333
+ <div>Tokens management</div>
334
+ <TokenList />
335
+ </div>
336
+ </div>
337
+ );
338
+ };