Compare commits

...
253 Commits
Author SHA1 Message Date
jialin 2df7c17bc1 chore: remove experimental from hygon 2025-12-04 22:35:15 +08:00
jialin a274349ace chore: upgrade info 2025-12-04 16:43:33 +08:00
jialin 1c699fe307 chore: tidy code 2025-12-04 16:43:33 +08:00
jialin f0a3e84e4f fix: set default os image 2025-12-04 10:48:34 +08:00
jialin f87989d32e fix: clear os_image when it's not in new image list 2025-12-04 10:12:19 +08:00
jialin 7355be9e3a fix: init cluster id when deploy model 2025-12-03 19:18:07 +08:00
jialin d8f1e95e1a fix: use image label in inputbox 2025-12-03 16:00:59 +08:00
jialin 59eace702b fix: add worker init steps failed 2025-12-03 12:32:04 +08:00
jialin 8a24ba0252 fix: cluster option disabled 2025-12-03 11:50:35 +08:00
jialin 03999a9d2c chore: os image filter 2025-12-03 11:38:16 +08:00
jialin f335cb1775 fix: click icon do not worker on upload 2025-12-02 17:24:14 +08:00
jialin b9c034469b fix: input img url fall back image 2025-12-02 15:58:17 +08:00
jialin ce3a82ddda fix: edit message add image 2025-12-02 14:00:58 +08:00
jialin 5cbb38922c feat: add image from url 2025-12-01 19:01:33 +08:00
jialin 14f0afec08 chore: allow space in backend parameters 2025-12-01 10:06:00 +08:00
jialin 769cc09906 fix: logs are not fully loaded 2025-11-30 20:33:16 +08:00
jialin 306daf7591 chore: amd note for add worker 2025-11-27 19:35:04 +08:00
jialin d1c1132e6e style: button for empty 2025-11-27 15:53:23 +08:00
jialin 962df859f1 chore: get started for add cluster 2025-11-27 11:35:45 +08:00
jialin 97601280d1 fix: model download progress text overflow 2025-11-26 10:28:40 +08:00
jialin ede42e99de fix: do not reset the backend after selecting a gguf file 2025-11-25 17:20:09 +08:00
jialin 47cae0134c feat: display deprecated backend versions group 2025-11-25 16:04:27 +08:00
jialin 33defab7df fix: gpus link 2025-11-25 11:34:11 +08:00
EVGENY Mandjialin 6b402e944b Update Russian translations in models.ts 2025-11-24 09:38:32 +08:00
EVGENY Mandjialin f8a6f4c5e5 Translate cluster messages to Russian 2025-11-24 09:38:14 +08:00
EVGENY Mandjialin adb999d3a9 Update dashboard.ts 2025-11-24 09:37:42 +08:00
jialin f4ea23dd14 fix: set default vllm backend for gguf 2025-11-23 21:08:28 +08:00
jialin 5bd0020e49 fix: check vllm backend for gguf model 2025-11-23 19:57:41 +08:00
jialin 782e81568d fix: check the image and run_command valid when select custom backend 2025-11-23 18:51:01 +08:00
jialin 2ecf83ddae fix: warning message for gguf model 2025-11-23 17:27:42 +08:00
jialin de0982d590 fix: compatibility message for gguf 2025-11-23 15:10:46 +08:00
jialin e5755a2eec chore: adjust ui statements 2025-11-22 13:06:05 +08:00
EVGENY Mandjialin e755c34ab5 Translate health check command tips to Russian 2025-11-22 12:19:10 +08:00
EVGENY Mandjialin 85e4f1a5ed Translate common phrases to Russian 2025-11-22 12:18:40 +08:00
EVGENY Mandjialin 3eb403a2c4 Translate user status messages to Russian 2025-11-22 12:18:15 +08:00
jialin 79442a6a6c fix: apikey form value diff 2025-11-21 20:41:20 +08:00
jialin f49001b742 chore: access setting notes 2025-11-21 20:10:21 +08:00
jialin e80ecfac8d style: table header text-overflow 2025-11-21 16:56:23 +08:00
jialin ff67fba2c9 chore: reset the primary color after logout 2025-11-21 16:56:23 +08:00
jialin 3f63831488 fix: modelscope api incorrect 2025-11-21 13:43:39 +08:00
jialin 19babbc73d style: adjust some styles 2025-11-21 12:16:49 +08:00
jialin 26a4d5474d fix(style): gpu_selector position 2025-11-20 20:49:19 +08:00
jialin af3e03230a fix: rerank api prefix 2025-11-20 17:43:13 +08:00
jialin 551f04011a chore: v1 to v2 2025-11-20 17:31:15 +08:00
jialin ea39a45f31 fix: sglang enable across worker 2025-11-20 15:59:05 +08:00
jialin ebff5695ff chore: consistent UI statement 2025-11-20 14:17:08 +08:00
jialin 9b63c1a515 style: example code title 2025-11-20 12:46:06 +08:00
jialin de882dc62e fix: api info example 2025-11-20 11:47:09 +08:00
jialin 8f9562d6a4 style: adjust some ui statements 2025-11-20 11:47:09 +08:00
jialin 5d383ec39b fix: add worker token do not update 2025-11-20 10:31:27 +08:00
jialin 2687fb187d style: adjust ram prcision 2025-11-19 18:47:55 +08:00
jialin ca43c0b4b2 fix(style): fix some styles 2025-11-19 11:46:18 +08:00
jialin 71bbf59467 style: deploy modal message cover the content 2025-11-19 11:46:18 +08:00
jialin 18f861f771 style: framework options 2025-11-18 20:01:54 +08:00
jialin e85d59dfb0 style: adjust some styles 2025-11-18 13:30:58 +08:00
jialin 78c8b60121 fix: re-request catalog specs after changing cluster 2025-11-18 11:28:37 +08:00
jialin e22b5ed516 chore: small fixes and optimizations 2025-11-17 20:15:26 +08:00
jialin 906f59a566 fix: model source error in catalog 2025-11-17 15:24:22 +08:00
jialin d1c3edfd9b fix: remark field option 2025-11-17 15:24:22 +08:00
jialin e8a0f33bde fix: extra newline character 2025-11-16 11:54:27 +08:00
Christianandjialin 825687f2a8 Fix typo in example-code
Remove obsolet bracket in python-example-code
2025-11-16 09:12:59 +08:00
jialin 2852648a65 fix: run command issue 2025-11-16 09:09:04 +08:00
jialin 0f3f31ef45 refactor: add worker 2025-11-15 22:37:44 +08:00
jialin 864cf76c07 feat: worker maintenance 2025-11-13 21:00:35 +08:00
jialin 98e6c71bf8 fix: model list flicker after page active 2025-11-13 17:25:28 +08:00
jialin aee9566bbb fix: yaml editor do not update when upload same file 2025-11-13 16:53:11 +08:00
jialin b61c0d6dee fix: api access info 2025-11-13 12:22:16 +08:00
jialin f963b3a000 chore: config base url 2025-11-13 12:22:16 +08:00
jialin 03963b736a chore: create, edit image handler 2025-11-12 20:47:34 +08:00
jialin a34c375e2d fix: image response handler 2025-11-12 19:55:31 +08:00
jialin e8eea9573e fix: set response_format as b64_json 2025-11-12 19:18:10 +08:00
jialin b7656d65e7 style: large modal height responsive 2025-11-12 19:18:10 +08:00
jialin d1fa6dd03c fix: pool's label editable 2025-11-12 16:37:01 +08:00
jialin 2045bfaf78 fix: display admin and inactive when view 2025-11-12 16:11:17 +08:00
jialin 653ada5f0f fix: do not evaluate from model files 2025-11-12 15:10:00 +08:00
jialin cbc000d35a fix: playground formref is invalid 2025-11-12 14:57:39 +08:00
jialin 11a676be83 fix: download file error 2025-11-12 11:01:13 +08:00
jialin 7eedea8035 chore: generic_prox ux 2025-11-11 20:06:25 +08:00
EVGENY Mandjialin 2d4ddea915 Update menu.ts 2025-11-11 18:47:08 +08:00
EVGENY Mandjialin 76d7a50fac Update models.ts 2025-11-11 18:46:44 +08:00
jialin 84187eedc4 fix: access control, voices options 2025-11-11 15:47:32 +08:00
jialin ee5c30934b feat: generic_proxy 2025-11-11 15:47:32 +08:00
jialin b4b25d4300 fix: default enter page for user 2025-11-10 20:40:15 +08:00
jialin 9eb1dba4a1 fix: calc scrollheight dynamically for tabs change 2025-11-10 20:40:15 +08:00
jialin 285e274818 fix: display image menu 2025-11-10 10:07:25 +08:00
jialin 0077d109e7 fix: remove page_size 2025-11-07 17:31:39 +08:00
jialin 9cd508a92a fix: draft model change 2025-11-07 16:54:33 +08:00
jialin 9ffd4def64 fix: failed field auto scroll in view 2025-11-07 16:54:33 +08:00
jialin fdb6ac7575 fix: set page to -1 for all data 2025-11-07 16:54:33 +08:00
jialin 64e2cfa35a fix: auto expand the fields group 2025-11-07 11:56:12 +08:00
jialin 39efca0d4c fix: add loading on download button 2025-11-06 19:38:01 +08:00
jialin cd2942405a feat: cluster_count on dashboard 2025-11-06 19:38:01 +08:00
jialin fadff34f5f chore: update description for adding worker 2025-11-06 19:38:01 +08:00
jialin 7334bcdf83 fix: do not paste multiple lines 2025-11-06 15:37:42 +08:00
jialin e1e38f0404 fix: chat stream data too long 2025-11-06 15:37:42 +08:00
EVGENY Mandjialin 38cc179068 Update backends.ts 2025-11-06 10:25:56 +08:00
EVGENY Mandjialin 5147d0d004 Update clusters.ts 2025-11-06 10:24:55 +08:00
EVGENY Mandjialin c237c9dbde Update models.ts 2025-11-06 10:24:15 +08:00
EVGENY Mandjialin 34bd2c3031 Update resources.ts 2025-11-06 10:23:48 +08:00
jialin 6173f3b459 fix: tts request content-type 2025-11-05 17:09:16 +08:00
jialin 333b430b97 fix(style): do not display draft model after done 2025-11-05 10:02:25 +08:00
jialin 4c22d08da5 refactor: page layout 2025-11-05 10:02:25 +08:00
jialin 219f9871a7 feat: add constants config 2025-11-05 10:02:25 +08:00
jialin 80527d5985 fix: distributed_inference always display 2025-11-04 18:57:57 +08:00
jialin 7e5edd6387 fix: backend option format 2025-11-04 18:07:14 +08:00
jialin 4ca6a221e4 fix: do not show draft model downloading progress 2025-11-04 17:19:20 +08:00
jialin e5733a7f6e chore: add worker notes 2025-11-04 17:19:20 +08:00
jialin 23b7936924 fix: custom backend and versions ux 2025-11-04 13:37:53 +08:00
jialin 9be465a8cf chore: update add worker command 2025-11-04 13:37:53 +08:00
jialin 4bbae673dc fix: draft model local path do not search 2025-11-03 14:22:52 +08:00
jialin e66ee5d3f1 fix: catalog kv cache 2025-11-03 11:00:42 +08:00
jialin 20872100d1 fix: kv cache default value 2025-11-03 10:38:41 +08:00
jialin 06f33ded10 fix: algorithm options do not display 2025-11-03 10:29:32 +08:00
jialin adb5cd72b9 chore: show quant in catalog form 2025-11-03 10:14:30 +08:00
jialin 303429c52c chore: clear space onblur 2025-11-03 10:14:30 +08:00
jialin 1a92cc8543 feat: search draft models from remote 2025-11-03 10:14:30 +08:00
jialin d5cc617848 fix: draft download progress should not show 2025-11-03 10:14:30 +08:00
jialin 4af71b5b52 chore: speculative_config init in updating 2025-11-03 10:14:30 +08:00
jialin 2dab89363d chore: draft model downloading 2025-11-03 10:14:30 +08:00
jialin ab9f9a9679 chore: update reference to baseline 2025-11-03 10:14:30 +08:00
jialin c5166f07f1 style: styles 2025-11-03 10:14:30 +08:00
jialin 86c2f8eda1 fix: backend change 2025-11-03 10:14:30 +08:00
jialin 57d41e79c7 fix: fake spec data 2025-11-03 10:14:30 +08:00
jialin 5cd9b8c22f fix: default spec by mode 2025-11-03 10:14:30 +08:00
jialin 56eba0d4fc fix: default spec by mode 2025-11-03 10:14:30 +08:00
jialin 7936d0063d chore: update models locales 2025-11-03 10:14:30 +08:00
jialin 3613fb3f69 feat: add speculative decoding 2025-11-03 10:14:30 +08:00
jialin 608b2e31c7 fix: schdule type gpu_selctor issue 2025-10-31 17:14:38 +08:00
jialin 2006827968 chore: update extended_kv_cache config 2025-10-31 15:03:23 +08:00
jialin f6a3497b0c fix: do not trigger evaluation replica 0 2025-10-31 13:10:52 +08:00
jialin d7ef64746c fix: custom backend version suffix 2025-10-30 19:08:45 +08:00
jialin b733b3615d fix: remove input box for gpus_per_replica 2025-10-30 16:38:55 +08:00
jialin e9fa3e3aa3 fix: gpus_per_replica appears null 2025-10-30 10:52:48 +08:00
jialin b77f796661 feat: add sglang parameters 2025-10-29 18:56:41 +08:00
jialin c077db1129 fix: replicas trigger compatibility 2025-10-29 17:24:26 +08:00
jialin 801c88a3e2 chore: update --token 2025-10-29 12:11:25 +08:00
jialin 646d235c65 fix: adjust gpus_per_replicas ux 2025-10-28 17:21:22 +08:00
jialin a86bed294e fix: gpu selector issues 2025-10-28 11:17:29 +08:00
jialin 697f1f9c25 fix: built-in backend versions endwith -custom 2025-10-27 16:09:09 +08:00
jialin 61065ee0b7 chore: scheduling group 2025-10-27 14:13:47 +08:00
jialin df65d3e2f6 fix: kv cache support in builtin backend 2025-10-27 14:13:47 +08:00
jialin 6969cb0b9c fix: expand model after edting replicas 2025-10-27 14:13:47 +08:00
jialin 7e81871118 fix: miss cluster id in evaluation 2025-10-24 17:55:54 +08:00
jialin ba80ade643 fix: my models status 2025-10-24 16:11:12 +08:00
jialin 0401cf5fb9 fix: no backend options from model files 2025-10-24 10:18:22 +08:00
jialin 53b431f100 fix(style): add backend 2025-10-23 18:41:16 +08:00
jialin 1a082529c6 fix: skip error handler in root path 2025-10-23 17:09:30 +08:00
jialin bf57290197 fix: admin can not delete own 2025-10-23 17:09:30 +08:00
jialin a815e71bfe chore: show existing when add file 2025-10-23 11:52:49 +08:00
jialin cc0580301e fix: do not trigger evaluation after switching model 2025-10-22 14:28:18 +08:00
jialin 5853bd1eeb fix: builtin backend do not submit default_version 2025-10-22 14:28:18 +08:00
jialin ab93c53092 chore: default cluster by state 2025-10-22 12:13:01 +08:00
jialin 818c4fc9a2 style: form tabs header gap 2025-10-22 10:21:27 +08:00
jialin fc7350b7e2 style: scroll to field 2025-10-21 19:59:49 +08:00
jialin 1e4018350f chore: worker selector 2025-10-21 15:59:48 +08:00
jialin f017d609fb fix: skip user api error 2025-10-21 13:26:06 +08:00
jialin ccb6e72057 fix: textarea scroll bar, deactive count message 2025-10-21 13:24:21 +08:00
EVGENY Mandjialin 32ad9525a6 Update apikeys.ts 2025-10-21 09:30:25 +08:00
EVGENY Mandjialin afedd4337b Update backends.ts 2025-10-21 09:30:06 +08:00
EVGENY Mandjialin 1f7acc7cf6 Update clusters.ts 2025-10-21 09:29:47 +08:00
EVGENY Mandjialin e6a5c19f0b Update menu.ts 2025-10-21 09:29:24 +08:00
EVGENY Mandjialin 8e07302ac3 Update models.ts 2025-10-21 09:29:04 +08:00
EVGENY Mandjialin 0238431b96 Update no-result.ts 2025-10-21 09:28:28 +08:00
jialin 3a7a4ccf30 fix: check driver command for adding worker 2025-10-20 19:11:02 +08:00
jialin 9b843151fe feat: add gpus_per_replicas 2025-10-20 14:53:33 +08:00
jialin 19e275a17a chore: update worker locale 2025-10-17 21:11:31 +08:00
jialin 49c6bb71a1 feat: identicon 2025-10-17 10:15:55 +08:00
jialin 005295ac97 style: gguf model ux 2025-10-17 10:15:55 +08:00
jialin 37a3aa75cc chore: cluster locales 2025-10-17 10:15:55 +08:00
jialin 07243b206e feat: enable kv cache 2025-10-16 18:05:30 +08:00
jialin f732f5c390 fix: cluster issues 2025-10-16 15:05:21 +08:00
jialin 1da16a04fe fix: do not set default version 2025-10-16 15:05:21 +08:00
jialin e2da7c5f96 style: ui for no data in table list 2025-10-16 11:11:39 +08:00
jialin ca9ceda2f7 fix: update backend parameters after changing backend 2025-10-15 13:30:58 +08:00
jialin 059416bb47 fix: backend edit format 2025-10-15 12:18:50 +08:00
jialin 4196351e48 fix: backend edit format 2025-10-15 12:18:50 +08:00
jialin f02b972dc0 fix(styles): my models 2025-10-14 20:09:43 +08:00
jialin 8398ac59a1 style: update model modal type 2025-10-14 20:09:43 +08:00
jialin 83403a1a1a feat: add extended_kv_cache 2025-10-14 20:09:43 +08:00
jialin 620c113fae fix(style): cluster state do not display 2025-10-14 13:09:22 +08:00
jialin 903500dcfa chore: infinite scroller 2025-10-13 19:55:08 +08:00
jialin 68d6993acd chore: remove test dir 2025-10-13 19:55:08 +08:00
jialin 2c9f5062a9 refactor: user table list 2025-10-13 19:55:08 +08:00
jialin d3f39ae06b fix: my models 2025-10-13 15:21:17 +08:00
jialin f42f159785 chore: update locales 2025-10-13 15:21:17 +08:00
jialin bb7c8e9c85 chore: model access statement 2025-10-13 15:21:17 +08:00
jialin 663b6c7b4e style: allowed models ux 2025-10-13 15:21:17 +08:00
jialin 1fa2124efd feat: add allowed models 2025-10-13 15:21:17 +08:00
jialin bcd1a339f9 feat: add model access control 2025-10-13 15:21:17 +08:00
jialin c41e5d9c55 chore: update is_built_in 2025-10-13 14:40:26 +08:00
jialin b5fa40047a chore: pluggable locale config 2025-10-13 14:40:26 +08:00
jialin caabeb065a fix: custom versin configs 2025-10-13 14:40:26 +08:00
jialin d393f9ee9e style: versions config 2025-10-13 14:40:26 +08:00
jialin eb509166ee style: backend list styles 2025-10-13 14:40:26 +08:00
jialin 0dfe77df37 chore: built in versions 2025-10-13 14:40:26 +08:00
jialin 46ecc2467b refactor: advance collapse 2025-10-13 14:40:26 +08:00
jialin a5c1b8e15f chore: test by api 2025-10-13 14:40:26 +08:00
jialin 54703c67c1 feat: yaml editor 2025-10-13 14:40:26 +08:00
jialin e863b84016 feat: add backend pages 2025-10-13 14:40:26 +08:00
jialin 1a693f70ac feat: worker terminal 2025-10-13 14:40:26 +08:00
jialin a9de153c00 chore: deploy form context provider 2025-10-13 14:40:26 +08:00
jialin 42217cf079 feat: add awq,gptq filter 2025-10-13 14:40:26 +08:00
jialin bca1e8140e feat: dynamic backends 2025-10-13 14:40:26 +08:00
jialin ee95fabc47 fix: add worker command miss image 2025-10-09 14:31:59 +08:00
jialin 67552c14af style: user activation 2025-10-09 11:07:09 +08:00
Hugo Haldiandjialin 521bf211a2 feat: add user activation control
- Add is_active field to user model
- Create SealCheckbox component with clickable container
- Add activation control to user modal
- Display user status in users table
- Add localization strings for activation features
2025-10-09 10:11:05 +08:00
EVGENY Mandjialin 8e77e19631 UI: Update Russian language menu.ts 2025-09-24 22:22:46 +08:00
EVGENY Mandjialin 8b91a0546c UI: Update Ru common.ts 2025-09-24 22:22:32 +08:00
EVGENY Mandjialin 715c71f9a5 UI: Update ru models.ts 2025-09-24 22:22:16 +08:00
EVGENY Mandjialin 1ec4a949b8 UI: Update ru playground.ts 2025-09-24 22:22:01 +08:00
EVGENY Mandjialin fc2388093d UI: RU language Update clusters.ts 2025-09-24 22:21:46 +08:00
jialin 3a9bf17d7e chore: update worker, cluster status value 2025-09-23 11:15:43 +08:00
jialin a3002ce450 chore: worker resource info adjust 2025-09-23 11:15:43 +08:00
jialin 2515e8f598 chore: update cloud providers 2025-09-22 10:36:34 +08:00
jialin dcc0db35b5 chore: update k8s check env command 2025-09-22 10:22:23 +08:00
jialin 749adb56f8 fix(style): select gpu vendor 2025-09-19 20:07:13 +08:00
jialin f542ebecb4 fix: cluster value setting, volumes setting 2025-09-19 20:00:25 +08:00
jialin b640963f65 fix(style): my models card 2025-09-19 15:46:24 +08:00
jialin 8f5990c537 fix: register cluster modal 2025-09-18 09:45:46 +08:00
jialin bf400547e1 fix: edit gpu_selector failed 2025-09-17 19:53:40 +08:00
jialin 4d9dec8511 refactor: deploy model 2025-09-17 19:53:40 +08:00
jialin 1ac63e36df feat: add baseSelect 2025-09-17 19:53:40 +08:00
jialin f36613227e fix: os image dropdown list 2025-09-17 15:17:31 +08:00
jialin d063dfd652 chore: display available clusters 2025-09-16 20:32:26 +08:00
jialin c9c9401b0b fix: provder disabled click in k8s 2025-09-16 11:26:17 +08:00
jialin 913f2f5077 fix: menu status mismatch 2025-09-16 11:26:17 +08:00
jialin 336bafd6ce chore: cluster docker provider 2025-09-16 11:26:17 +08:00
jialin 5e1a26b743 fix: close modal after editing cluster 2025-09-16 11:26:17 +08:00
jialin d9d5ca95df style: add worker ux 2025-09-16 11:26:17 +08:00
jialin f1bb305cbc fix: add pool not work 2025-09-16 11:26:17 +08:00
jialin c467280170 fix: display instance type spec 2025-09-16 11:26:17 +08:00
jialin deeee70807 feat: add watch api in cluster 2025-09-16 11:26:17 +08:00
jialin 07ade24ea1 refactor: cluster list 2025-09-16 11:26:17 +08:00
jialin 8de272239e refactor: create cluster by steps 2025-09-16 11:26:17 +08:00
jialin 6e5a7035c8 feat: cloud options 2025-09-16 11:26:17 +08:00
jialin 632f206228 fix: cluster apis 2025-09-16 11:26:17 +08:00
jialin f04f1f828f fix: add do cluster 2025-09-16 11:26:17 +08:00
jialin 4f91443957 chore: upgrade antd lib to 5.25.4 2025-09-16 11:26:17 +08:00
jialin 0c1057d554 refactor: credential table columns 2025-09-16 11:26:17 +08:00
jialin 43936e21d9 feat: instances on worker 2025-09-16 11:26:17 +08:00
jialin 949eded4ee refactor: resource table columns 2025-09-16 11:26:17 +08:00
jialin 84859d1b7c style: cluster table list 2025-09-16 11:26:17 +08:00
jialin 553ec724c2 fix: gpu selector by cluster 2025-09-16 11:26:17 +08:00
jialin 207c90e26c feat: terminal 2025-09-16 11:26:17 +08:00
jialin 64bbba7576 feat: add cluster detail page 2025-09-16 11:26:17 +08:00
jialin 62dbd18ef8 chore: create cluster ux 2025-09-16 11:26:17 +08:00
jialin 55b0a20a4a refactor: update scroll modal 2025-09-16 11:26:17 +08:00
jialin 29b74f85c5 chore: adjust deployment modal form 2025-09-16 11:26:17 +08:00
jialin ed49e03802 feat: cluster ux 2025-09-16 11:26:17 +08:00
jialin 4711c848f8 chore: user mdoels 2025-09-16 11:26:17 +08:00
jialin 33df825b73 feat: add card list component 2025-09-16 11:26:17 +08:00
jialin 89001dbb92 feat: add cluster list 2025-09-16 11:26:17 +08:00
jialin 43e1dd0b0e feat: clusters group 2025-09-16 11:26:17 +08:00
487 changed files with 34751 additions and 18134 deletions
-2
View File
@@ -1,4 +1,2 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged npx lint-staged
+9 -15
View File
@@ -1,9 +1,9 @@
import { defineConfig } from '@umijs/max'; import { defineConfig } from '@umijs/max';
import keepAlive from './keep-alive'; import keepAlive from './keep-alive';
import { compressionPluginConfig, monacoPluginConfig } from './plugins';
import proxy from './proxy'; import proxy from './proxy';
import routes from './routes'; import routes from './routes';
import { getBranchInfo } from './utils'; import { getBranchInfo } from './utils';
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const versionInfo = getBranchInfo(); const versionInfo = getBranchInfo();
process.env.VERSION = JSON.stringify(versionInfo); process.env.VERSION = JSON.stringify(versionInfo);
@@ -29,7 +29,7 @@ export default defineConfig({
defaultSizes: 'parsed' // stat // gzip defaultSizes: 'parsed' // stat // gzip
}, },
mfsu: { mfsu: {
exclude: ['lodash', 'ml-pca'] exclude: ['lodash', 'ml-pca', 'monaco-editor']
}, },
base: process.env.npm_config_base || '/', base: process.env.npm_config_base || '/',
...(isProduction ...(isProduction
@@ -56,21 +56,15 @@ export default defineConfig({
config.output config.output
.filename(`js/[name].${t}.js`) .filename(`js/[name].${t}.js`)
.chunkFilename(`js/[name].${t}.chunk.js`); .chunkFilename(`js/[name].${t}.chunk.js`);
config compressionPluginConfig(config);
.plugin('compression-webpack-plugin') monacoPluginConfig(config);
.use(CompressionWebpackPlugin, [
{
filename: '[path][base].gz',
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8
}
]);
} }
} }
: {}), : {
chainWebpack(config) {
monacoPluginConfig(config);
}
}),
favicons: ['/static/favicon.png'], favicons: ['/static/favicon.png'],
jsMinifier: 'terser', jsMinifier: 'terser',
cssMinifier: 'cssnano', cssMinifier: 'cssnano',
+36
View File
@@ -0,0 +1,36 @@
import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin';
const CompressionWebpackPlugin = require('compression-webpack-plugin');
export function monacoPluginConfig(config: any) {
return config
.plugin('monaco-editor-webpack-plugin')
.use(MonacoWebpackPlugin, [
{
languages: ['yaml'],
customLanguages: [
{
label: 'yaml',
entry: 'monaco-yaml',
worker: {
id: 'monaco-yaml/yamlWorker',
entry: 'monaco-yaml/yaml.worker'
}
}
]
}
]);
}
export function compressionPluginConfig(config: any) {
return config
.plugin('compression-webpack-plugin')
.use(CompressionWebpackPlugin, [
{
filename: '[path][base].gz',
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8
}
]);
}
+1
View File
@@ -1,6 +1,7 @@
const proxyTableList = [ const proxyTableList = [
'cli', 'cli',
'v1', 'v1',
'v2',
'auth', 'auth',
'v1-openai', 'v1-openai',
'version', 'version',
+74 -1
View File
@@ -80,7 +80,6 @@ export default [
name: 'models', name: 'models',
path: '/models', path: '/models',
key: 'models', key: 'models',
access: 'canSeeAdmin',
routes: [ routes: [
{ {
path: '/models', path: '/models',
@@ -105,6 +104,16 @@ export default [
defaultIcon: 'icon-rocket-launch1', defaultIcon: 'icon-rocket-launch1',
access: 'canSeeAdmin', access: 'canSeeAdmin',
component: './llmodels/index' component: './llmodels/index'
},
{
name: 'userModels',
path: '/models/user-models',
key: 'userModels',
icon: 'icon-models',
selectedIcon: 'icon-models-filled',
defaultIcon: 'icon-models',
access: 'canSeeUser',
component: './llmodels/user-models'
} }
] ]
}, },
@@ -136,6 +145,16 @@ export default [
defaultIcon: 'icon-gpu1', defaultIcon: 'icon-gpu1',
component: './resources/components/gpus' component: './resources/components/gpus'
}, },
{
name: 'backendsList',
path: '/resources/backends',
key: 'backendsList',
icon: 'icon-backend',
selectedIcon: 'icon-backend-filled',
defaultIcon: 'icon-backend',
access: 'canSeeAdmin',
component: './backends/index'
},
{ {
name: 'modelfiles', name: 'modelfiles',
path: '/resources/modelfiles', path: '/resources/modelfiles',
@@ -147,6 +166,60 @@ export default [
} }
] ]
}, },
{
name: 'clusterManagement',
path: '/cluster-management',
key: 'clusterManagement',
access: 'canSeeAdmin',
routes: [
{
path: '/cluster-management',
redirect: '/cluster-management/clusters/list'
},
{
name: 'clusters',
path: '/cluster-management/clusters/list',
key: 'clusters',
icon: 'icon-cluster2-outline',
selectedIcon: 'icon-cluster2-filled',
defaultIcon: 'icon-cluster2-outline',
component: './cluster-management/clusters',
subMenu: [
'/cluster-management/clusters/detail',
'/cluster-management/clusters/create'
]
},
{
name: 'clusterDetail',
path: '/cluster-management/clusters/detail',
key: 'clusterDetail',
icon: 'icon-cluster2-outline',
selectedIcon: 'icon-cluster2-filled',
defaultIcon: 'icon-cluster2-outline',
hideInMenu: true,
component: './cluster-management/cluster-detail'
},
{
name: 'clusterCreate',
path: '/cluster-management/clusters/create',
key: 'clusterCreate',
icon: 'icon-cluster2-outline',
selectedIcon: 'icon-cluster2-filled',
defaultIcon: 'icon-cluster2-outline',
hideInMenu: true,
component: './cluster-management/cluster-create'
},
{
name: 'credentials',
path: '/cluster-management/credentials',
key: 'credentials',
icon: 'icon-credential-outline',
selectedIcon: 'icon-credential-filled',
defaultIcon: 'icon-credential-outline',
component: './cluster-management/credentials'
}
]
},
{ {
name: 'accessControl', name: 'accessControl',
path: '/access-control', path: '/access-control',
+10 -3
View File
@@ -31,7 +31,7 @@
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"ahooks": "^3.8.5", "ahooks": "^3.8.5",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"antd": "^5.21.6", "antd": "^5.25.4",
"antd-style": "^3.6.2", "antd-style": "^3.6.2",
"axios": "^1.8.2", "axios": "^1.8.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@@ -45,7 +45,9 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"has-ansi": "^5.0.1", "has-ansi": "^5.0.1",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"jdenticon": "^3.3.0",
"jotai": "^2.8.4", "jotai": "^2.8.4",
"js-yaml": "^4.1.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"katex": "^0.16.21", "katex": "^0.16.21",
"lamejs": "github:zhuker/lamejs", "lamejs": "github:zhuker/lamejs",
@@ -53,19 +55,24 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mammoth": "^1.8.0", "mammoth": "^1.8.0",
"marked": "^14.1.0", "marked": "^14.1.0",
"minimatch": "^10.0.3",
"ml-dataset-iris": "^1.2.1", "ml-dataset-iris": "^1.2.1",
"ml-pca": "^4.1.1", "ml-pca": "^4.1.1",
"monaco-editor": "^0.53.0",
"monaco-editor-webpack-plugin": "^7.1.0",
"monaco-yaml": "^5.4.0",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"overlayscrollbars": "^2.10.0", "overlayscrollbars": "^2.10.0",
"overlayscrollbars-react": "^0.5.6", "overlayscrollbars-react": "^0.5.6",
"pdfjs-dist": "^4.7.76", "pdfjs-dist": "^4.7.76",
"query-string": "^9.0.0", "query-string": "^9.0.0",
"rc-resize-observer": "^1.4.0", "rc-resize-observer": "^1.4.3",
"rc-virtual-list": "^3.14.8", "rc-virtual-list": "^3.14.8",
"re-resizable": "^6.10.1", "re-resizable": "^6.10.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.5.0", "react-hotkeys-hook": "^4.5.0",
"react-intersection-observer": "^9.16.0",
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"remark-breaks": "^4.0.0", "remark-breaks": "^4.0.0",
@@ -73,12 +80,12 @@
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"simplebar-react": "^3.2.6", "simplebar-react": "^3.2.6",
"styled-components": "^6.1.15", "styled-components": "^6.1.15",
"tinycolor2": "^1.6.0",
"umi-presets-pro": "^2.0.3", "umi-presets-pro": "^2.0.3",
"wavesurfer.js": "^7.8.8", "wavesurfer.js": "^7.8.8",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
}, },
"devDependencies": { "devDependencies": {
"@types/marked": "^6.0.0",
"@types/react": "^18.3.1", "@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@umijs/case-sensitive-paths-webpack-plugin": "^1.0.1", "@umijs/case-sensitive-paths-webpack-plugin": "^1.0.1",
+5238 -8580
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -4,8 +4,15 @@ export default (initialState: { currentUser?: Global.UserInfo }) => {
initialState.currentUser && initialState.currentUser &&
initialState.currentUser.is_admin initialState.currentUser.is_admin
); );
const canSeeUser = !!(
initialState &&
initialState.currentUser &&
!initialState.currentUser.is_admin
);
return { return {
canSeeAdmin, canSeeAdmin,
canSeeUser,
canDelete: true, canDelete: true,
canLogin: true canLogin: true
}; };
+28 -10
View File
@@ -1,5 +1,6 @@
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user'; import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
import { setAtomStorage } from '@/atoms/utils'; import { setAtomStorage } from '@/atoms/utils';
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
import { requestConfig } from '@/request-config'; import { requestConfig } from '@/request-config';
import { import {
queryCurrentUserState, queryCurrentUserState,
@@ -13,8 +14,7 @@ import {
writeState writeState
} from '@/utils/localstore/index'; } from '@/utils/localstore/index';
import { RequestConfig, history } from '@umijs/max'; import { RequestConfig, history } from '@umijs/max';
import { message } from 'antd';
const loginPath = '/login';
// only for the first login and access from http://localhost // only for the first login and access from http://localhost
@@ -23,7 +23,7 @@ const checkDefaultPage = async (userInfo: any) => {
if (isFirstLogin === null && isOnline()) { if (isFirstLogin === null && isOnline()) {
writeState(IS_FIRST_LOGIN, true); writeState(IS_FIRST_LOGIN, true);
if (userInfo && userInfo?.is_admin) { if (userInfo && userInfo?.is_admin) {
history.push('/models/deployments'); history.push(DEFAULT_ENTER_PAGE.adminForFirst);
} }
} }
}; };
@@ -48,7 +48,9 @@ export async function getInitialState(): Promise<{
} }
}; };
const fetchUserInfo = async (): Promise<Global.UserInfo> => { const fetchUserInfo = async (config?: {
skipErrorHandler?: boolean;
}): Promise<Global.UserInfo> => {
try { try {
const data = await queryCurrentUserState({ const data = await queryCurrentUserState({
skipErrorHandler: true skipErrorHandler: true
@@ -57,8 +59,19 @@ export async function getInitialState(): Promise<{
getUpdateCheck(); getUpdateCheck();
} }
return data; return data;
} catch (error) { } catch (error: any) {
history.push(loginPath); const data = error?.response?.data;
if (data?.code === 401 && data?.message.includes('deactivate')) {
message.error({
content: (
<div>
<span>{data?.message}</span>
</div>
),
duration: 5
});
}
history.push(DEFAULT_ENTER_PAGE.login);
} }
return {} as Global.UserInfo; return {} as Global.UserInfo;
}; };
@@ -66,10 +79,15 @@ export async function getInitialState(): Promise<{
const getAppVersionInfo = async () => { const getAppVersionInfo = async () => {
try { try {
const data = await queryVersionInfo(); const data = await queryVersionInfo();
const isProduction = data.version?.indexOf('0.0.0') === -1;
const isDev = data.version?.indexOf('0.0.0') > -1;
const isRc = data.version?.indexOf('rc') > -1;
setAtomStorage(GPUStackVersionAtom, { setAtomStorage(GPUStackVersionAtom, {
...data, ...data,
isProduction isProd: !isDev && !isRc,
isDev,
isRc
}); });
} catch (error) { } catch (error) {
console.error('queryVersionInfo error', error); console.error('queryVersionInfo error', error);
@@ -78,7 +96,7 @@ export async function getInitialState(): Promise<{
getAppVersionInfo(); getAppVersionInfo();
if (![loginPath].includes(location.pathname)) { if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
const userInfo = await fetchUserInfo(); const userInfo = await fetchUserInfo();
checkDefaultPage(userInfo); checkDefaultPage(userInfo);
return { return {
@@ -92,6 +110,6 @@ export async function getInitialState(): Promise<{
} }
export const request: RequestConfig = { export const request: RequestConfig = {
baseURL: ' /v1', baseURL: `/${GPUSTACK_API_BASE_URL}`,
...requestConfig ...requestConfig
}; };
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+25
View File
@@ -104,6 +104,11 @@
justify-content: space-between; justify-content: space-between;
} }
.justify-between {
display: flex;
justify-content: space-between;
}
.justify-center { .justify-center {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -164,6 +169,10 @@
gap: 8px; gap: 8px;
} }
.gap-4 {
gap: 4px;
}
.gap-20 { .gap-20 {
gap: 20px; gap: 20px;
} }
@@ -220,6 +229,10 @@
margin-bottom: 0; margin-bottom: 0;
} }
.m-t-0 {
margin-top: 0;
}
.m-b-16 { .m-b-16 {
margin-bottom: 16px; margin-bottom: 16px;
} }
@@ -244,6 +257,10 @@
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
} }
.text-primary {
color: var(--ant-color-text);
}
.text-secondary { .text-secondary {
color: var(--ant-color-text-secondary); color: var(--ant-color-text-secondary);
} }
@@ -252,6 +269,10 @@
color: var(--ant-color-text-tertiary); color: var(--ant-color-text-tertiary);
} }
.text-quaternary {
color: var(--ant-color-text-quaternary);
}
.tips-desc-list { .tips-desc-list {
list-style-type: decimal; list-style-type: decimal;
padding-left: 16px; padding-left: 16px;
@@ -319,3 +340,7 @@ textarea:hover {
.ant-result-extra { .ant-result-extra {
color: var(--ant-color-text-secondary); color: var(--ant-color-text-secondary);
} }
.text-success {
color: var(--ant-color-success);
}
+65
View File
@@ -0,0 +1,65 @@
import { atom } from 'jotai';
// models expand keys: create, update , delete,
export const expandKeysAtom = atom<string[]>([]);
export const regionListAtom = atom<
{
datacenter: string;
label: string;
value: string;
icon: string;
sizes: string[];
}[]
>([]);
export const regionInstanceTypeListAtom = atom<
{
label: string;
value: string;
description: string;
specInfo: Record<string, any>;
vendor: string;
available: boolean;
regions: string[];
}[]
>([]);
export const regionOSImageListAtom = atom<
{
label: string;
value: string;
os_image: string;
name: string;
description: string;
vendor: string;
specInfo: Record<string, any>;
regions: string[];
}[]
>([]);
export const allRegionOSImageListAtom = atom<
{
label: string;
value: string;
regions: string[];
}[]
>([]);
export const allRegionInstanceTypeListAtom = atom<
{
label: string;
value: string;
regions: string[];
}[]
>([]);
export const fromClusterCreationAtom = atom(false);
/**
* for temporary cluster session storage during creation/editing
*/
export const clusterSessionAtom = atom<{
firstAddWorker: boolean;
firstAddCluster: boolean;
} | null>(null);
+12
View File
@@ -1,8 +1,11 @@
import { BackendOption } from '@/pages/llmodels/config/types';
import { atom, getDefaultStore } from 'jotai'; import { atom, getDefaultStore } from 'jotai';
// models expand keys: create, update , delete, // models expand keys: create, update , delete,
export const modelsExpandKeysAtom = atom<string[]>([]); export const modelsExpandKeysAtom = atom<string[]>([]);
export const modelsSessionAtom = atom<Record<string, any>>({});
export const requestIdAtom = atom<number>(0); export const requestIdAtom = atom<number>(0);
export const setRquestId = () => { export const setRquestId = () => {
@@ -16,3 +19,12 @@ export const getRequestId = () => {
const store = getDefaultStore(); const store = getDefaultStore();
return store.get(requestIdAtom); return store.get(requestIdAtom);
}; };
export const clusterListAtom = atom<
{
label: string;
value: number;
}[]
>([]);
export const backendOptionsAtom = atom<BackendOption[]>([]);
+3 -3
View File
@@ -1,4 +1,4 @@
import { colorPrimary } from '@/config/theme'; import { COLOR_PRIMARY } from '@/config/theme';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils'; import { atomWithStorage } from 'jotai/utils';
@@ -14,7 +14,7 @@ const defaultSettings: UserSettings = {
theme: 'light', theme: 'light',
mode: 'auto', mode: 'auto',
isDarkTheme: false, isDarkTheme: false,
colorPrimary: colorPrimary, colorPrimary: COLOR_PRIMARY,
collapsed: false collapsed: false
}; };
@@ -47,7 +47,7 @@ export const userSettingsHelperAtom = atom(
}; };
set(userSettingsAtom, { set(userSettingsAtom, {
...newSettings, ...newSettings,
colorPrimary: newSettings.colorPrimary || colorPrimary, colorPrimary: newSettings.colorPrimary || COLOR_PRIMARY,
isDarkTheme: newSettings.theme === 'realDark' isDarkTheme: newSettings.theme === 'realDark'
}); });
} }
+6 -2
View File
@@ -6,11 +6,15 @@ export const userAtom = atomWithStorage<any>('userInfo', null);
export const GPUStackVersionAtom = atom<{ export const GPUStackVersionAtom = atom<{
version: string; version: string;
git_commit: string; git_commit: string;
isProduction: boolean; isProd: boolean;
isDev?: boolean;
isRc?: boolean;
}>({ }>({
version: '', version: '',
git_commit: '', git_commit: '',
isProduction: false isProd: false,
isDev: false,
isRc: false
}); });
export const UpdateCheckAtom = atom<{ export const UpdateCheckAtom = atom<{
+13
View File
@@ -1,5 +1,18 @@
import { getDefaultStore } from 'jotai'; import { getDefaultStore } from 'jotai';
export const clearStorageUserSettings = () => {
const savedSettings = JSON.parse(
localStorage.getItem('userSettings') || '{}'
);
localStorage.setItem(
'userSettings',
JSON.stringify({
...savedSettings,
colorPrimary: undefined
})
);
};
export const clearAtomStorage = (atom: any) => { export const clearAtomStorage = (atom: any) => {
if (!atom) { if (!atom) {
return; return;
+2
View File
@@ -28,6 +28,8 @@ const AlertInfo: React.FC<AlertInfoProps> = (props) => {
} }
} }
style={{ style={{
fontWeight: 400,
whiteSpace: 'pre-line',
textAlign: 'center', textAlign: 'center',
padding: '2px 5px', padding: '2px 5px',
borderRadius: 'var(--border-radius-base)', borderRadius: 'var(--border-radius-base)',
+3 -2
View File
@@ -70,8 +70,9 @@ const AutoImage: React.FC<
setIsError(false); setIsError(false);
}, [props.onLoad]); }, [props.onLoad]);
const handleOnError = useCallback(() => { const handleOnError = useCallback((e: any) => {
setIsError(true); setIsError(true);
e.target.src = fallbackImg;
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -127,4 +128,4 @@ const AutoImage: React.FC<
); );
}; };
export default React.memo(AutoImage); export default AutoImage;
+15 -11
View File
@@ -1,5 +1,5 @@
import { CloseCircleOutlined } from '@ant-design/icons'; import { CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
import { Progress, ProgressProps } from 'antd'; import { Progress, ProgressProps, Spin } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { round } from 'lodash'; import { round } from 'lodash';
import ResizeObserver from 'rc-resize-observer'; import ResizeObserver from 'rc-resize-observer';
@@ -94,6 +94,17 @@ const SingleImage: React.FC<SingleImageProps> = (props) => {
const handleOnLoad = React.useCallback(async () => {}, []); const handleOnLoad = React.useCallback(async () => {}, []);
const renderProgress = () => {
<Progress
percent={round(progress, 0)}
type="dashboard"
size={loadingSize}
steps={{ count: 50, gap: 2 }}
format={() => <span className="font-size-20">{round(progress, 0)}%</span>}
trailColor="var(--ant-color-fill-secondary)"
/>;
};
return ( return (
<ResizeObserver onResize={handleResize}> <ResizeObserver onResize={handleResize}>
<div <div
@@ -135,15 +146,8 @@ const SingleImage: React.FC<SingleImageProps> = (props) => {
overflow: 'hidden' overflow: 'hidden'
}} }}
> >
<Progress <Spin
percent={round(progress, 0)} indicator={<LoadingOutlined style={{ fontSize: 32 }} spin />}
type="dashboard"
size={loadingSize}
steps={{ count: 50, gap: 2 }}
format={() => (
<span className="font-size-20">{round(progress, 0)}%</span>
)}
trailColor="var(--ant-color-fill-secondary)"
/> />
</span> </span>
) : ( ) : (
+4 -3
View File
@@ -29,6 +29,7 @@ interface AutoTooltipProps extends Omit<TagProps, 'title'> {
} }
const StyledTag = styled(Tag)` const StyledTag = styled(Tag)`
margin: 0;
&.tag-filled { &.tag-filled {
border: none; border: none;
background-color: var(--ant-color-fill-secondary); background-color: var(--ant-color-fill-secondary);
@@ -106,7 +107,7 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
<TooltipOverlayScroller <TooltipOverlayScroller
toolTipProps={{ toolTipProps={{
...tooltipProps, ...tooltipProps,
destroyTooltipOnHide: false destroyOnHidden: false
}} }}
title={isOverflowing || showTitle ? title || children : false} title={isOverflowing || showTitle ? title || children : false}
> >
@@ -120,9 +121,9 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
className={`${tagProps.className || ''} ${filled ? 'tag-filled' : ''}`} className={`${tagProps.className || ''} ${filled ? 'tag-filled' : ''}`}
ref={contentRef} ref={contentRef}
style={{ style={{
...tagStyle,
paddingInline: tagProps.closable ? '8px 22px' : 8, paddingInline: tagProps.closable ? '8px 22px' : 8,
borderRadius: radius borderRadius: radius,
...tagStyle
}} }}
closeIcon={ closeIcon={
tagProps.closable ? ( tagProps.closable ? (
+1 -1
View File
@@ -2,6 +2,6 @@
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
background-color: var(--color-white-1); background-color: var(--color-white-1);
box-shadow: none; box-shadow: none;
padding: 10px; padding: 10px 16px;
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
} }
+208
View File
@@ -0,0 +1,208 @@
import IconFont from '@/components/icon-font';
import { Card } from 'antd';
import { createStyles } from 'antd-style';
import classNames from 'classnames';
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
const CardStyled = styled(Card)`
box-shadow: none !important;
&.isOpen {
.ant-card-head {
border-bottom: 1px solid var(--ant-color-border-secondary);
border-radius: var(--ant-border-radius) var(--ant-border-radius) 0 0;
}
}
.ant-card-head {
cursor: pointer;
background-color: var(--ant-color-fill-quaternary);
border-bottom: none;
border-radius: var(--ant-border-radius);
padding: 0 16px;
&:hover {
background-color: var(--ant-color-fill-secondary);
.del-btn {
display: block;
}
}
}
`;
const useStyles = createStyles(({ css, token }) => {
return {
title: css`
font-weight: 400;
height: 56px;
font-size: var(--font-size-base);
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
`,
expandIcon: css`
display: flex;
align-items: center;
gap: 8px;
`,
subtitle: css`
font-size: 14px;
color: ${token.colorTextSecondary};
`,
content: css`
padding-top: 8px;
`,
left: css`
flex: 1;
`,
right: css`
display: flex;
align-items: center;
gap: 8px;
.del-btn {
display: none;
}
`
};
});
export interface CollapsibleContainerProps {
title?: React.ReactNode;
subtitle?: React.ReactNode;
right?: React.ReactNode;
deleteBtn?: React.ReactNode;
defaultOpen?: boolean;
open?: boolean;
collapsible?: boolean;
showExpandIcon?: boolean;
onToggle?: (open: boolean) => void;
disabled?: boolean;
variant?: 'outlined' | 'borderless' | undefined;
iconPosition?: 'left' | 'right';
className?: string;
children?: React.ReactNode;
styles?: {
body?: React.CSSProperties;
header?: React.CSSProperties;
content?: React.CSSProperties;
};
}
export default function CollapsibleContainer({
title,
subtitle,
right,
deleteBtn,
defaultOpen = true,
open,
onToggle,
disabled = false,
showExpandIcon = true,
variant = 'borderless',
className = '',
collapsible,
iconPosition = 'left',
styles: cardStyles,
children
}: CollapsibleContainerProps) {
const { styles } = useStyles();
const isControlled = typeof open === 'boolean';
const [internalOpen, setInternalOpen] = useState(defaultOpen);
const isOpen = collapsible
? isControlled
? (open as boolean)
: internalOpen
: true;
const toggle = () => {
if (disabled || !collapsible) return;
const next = !isOpen;
if (!isControlled) setInternalOpen(next);
onToggle?.(next);
};
const contentRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState(isOpen ? 'auto' : '0px');
const renderIcon = () => {
if (showExpandIcon) {
return (
<IconFont
rotate={isOpen ? 180 : 0}
type="icon-down"
style={{
cursor: disabled ? 'not-allowed' : 'pointer',
fontSize: 12
}}
/>
);
}
return null;
};
const renderTitle = () => {
if (!collapsible) {
return null;
}
return (
<div className={styles.title} onClick={toggle}>
<div className={styles.left}>
<div className={styles.expandIcon}>
{iconPosition === 'left' && renderIcon()}
{title && <div>{title}</div>}
</div>
{subtitle && <div className={styles.subtitle}>{subtitle}</div>}
</div>
<div className={styles.right}>
{right && <span>{right}</span>}
{deleteBtn && <span className="del-btn">{deleteBtn}</span>}
{iconPosition === 'right' && renderIcon()}
</div>
</div>
);
};
useEffect(() => {
if (!collapsible) {
setHeight('auto');
return;
}
if (isOpen) {
const scrollHeight = contentRef.current?.scrollHeight || 0;
setHeight(scrollHeight + 'px');
const timer = setTimeout(() => setHeight('auto'), 200);
return () => clearTimeout(timer);
} else {
const scrollHeight = contentRef.current?.scrollHeight || 0;
setHeight(scrollHeight + 'px');
requestAnimationFrame(() => setHeight('0px'));
return () => {};
}
}, [isOpen, collapsible]);
return (
<CardStyled
className={classNames(className, { collapsible, disabled, isOpen })}
variant={variant}
styles={{
body: {
padding: 0,
...cardStyles?.body
},
header: {
...cardStyles?.header
}
}}
title={renderTitle()}
>
<div
ref={contentRef}
style={{
height: height,
overflow: 'hidden'
}}
>
<div style={{ paddingTop: 8, ...cardStyles?.content }}>{children}</div>
</div>
</CardStyled>
);
}
+1 -3
View File
@@ -55,9 +55,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
}); });
clipboardRef.current.on('error', (e: any) => { clipboardRef.current.on('error', (e: any) => {
message.success( message.error(intl.formatMessage({ id: 'common.copy.fail' }) as string);
intl.formatMessage({ id: 'common.copy.fail' }) as string
);
e.clearSelection(); e.clearSelection();
}); });
} }
+2 -2
View File
@@ -138,7 +138,7 @@ const DeleteModal = forwardRef((props, ref) => {
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={handleCancel} onCancel={handleCancel}
destroyOnClose={false} destroyOnHidden={false}
closeIcon={false} closeIcon={false}
maskClosable={false} maskClosable={false}
keyboard={false} keyboard={false}
@@ -155,7 +155,7 @@ const DeleteModal = forwardRef((props, ref) => {
? intl.formatMessage({ id: config.cancelText }) ? intl.formatMessage({ id: config.cancelText })
: intl.formatMessage({ id: 'common.button.cancel' })} : intl.formatMessage({ id: 'common.button.cancel' })}
</Button> </Button>
<Button type="primary" onClick={handleOk} size="middle"> <Button type="primary" onClick={handleOk} size="middle" danger>
{config.okText {config.okText
? intl.formatMessage({ id: config.okText }) ? intl.formatMessage({ id: config.okText })
: intl.formatMessage({ id: 'common.button.delete' })} : intl.formatMessage({ id: 'common.button.delete' })}
@@ -16,6 +16,9 @@ const DropDownActions: React.FC<DropDownProps> = (props) => {
const items = useMemo(() => { const items = useMemo(() => {
return menu?.items?.map((item: any) => ({ return menu?.items?.map((item: any) => ({
..._.omit(item, 'locale'), ..._.omit(item, 'locale'),
icon: item.icon
? React.cloneElement(item.icon, { style: { fontSize: 14 } })
: null,
label: item.locale ? intl.formatMessage({ id: item.label }) : item.label label: item.locale ? intl.formatMessage({ id: item.label }) : item.label
})); }));
}, [menu?.items, intl]); }, [menu?.items, intl]);
+20 -13
View File
@@ -4,6 +4,7 @@ import { Button, Dropdown, Tooltip, type MenuProps } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import './index.less'; import './index.less';
type Trigger = 'click' | 'hover'; type Trigger = 'click' | 'hover';
@@ -19,6 +20,17 @@ interface DropdownButtonsProps {
onSelect: (val: any, item?: any) => void; onSelect: (val: any, item?: any) => void;
} }
const DropdownWrapper = styled.div`
display: flex;
flex-direction: column;
background-color: var(--ant-color-bg-elevated);
padding: 5px;
align-items: flex-start;
border-radius: var(--border-radius-base);
box-shadow: var(--ant-box-shadow-secondary);
min-width: 160px;
`;
const DropdownButtons: React.FC<DropdownButtonsProps> = ({ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
items, items,
size = 'middle', size = 'middle',
@@ -64,18 +76,9 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
disabled={disabled} disabled={disabled}
trigger={trigger} trigger={trigger}
type="primary" type="primary"
dropdownRender={(menus: any) => { popupRender={(menus: any) => {
return ( return (
<div <DropdownWrapper>
className="flex flex-column "
style={{
backgroundColor: 'var(--ant-color-bg-elevated)',
padding: 5,
alignItems: 'flex-start',
borderRadius: 'var(--border-radius-base)',
boxShadow: 'var(--ant-box-shadow-secondary)'
}}
>
{_.map(_.tail(items), (item: any) => { {_.map(_.tail(items), (item: any) => {
return ( return (
<Button <Button
@@ -86,13 +89,17 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
key={item.key} key={item.key}
disabled={item.disabled} disabled={item.disabled}
onClick={() => handleMenuClick(item)} onClick={() => handleMenuClick(item)}
style={{ width: '100%', justifyContent: 'flex-start' }} style={{
width: '100%',
justifyContent: 'flex-start',
paddingInline: 10
}}
> >
{intl.formatMessage({ id: item.label })} {intl.formatMessage({ id: item.label })}
</Button> </Button>
); );
})} })}
</div> </DropdownWrapper>
); );
}} }}
buttonsRender={([leftButton, rightButton]) => [ buttonsRender={([leftButton, rightButton]) => [
@@ -0,0 +1,19 @@
import ComponentsMap from '@/components/seal-form/config/components';
import { SealFormItemProps } from '@/components/seal-form/types';
import { Form } from 'antd';
import React from 'react';
interface FieldItemProps extends SealFormItemProps {
widget: keyof typeof ComponentsMap;
name: string;
}
const FieldItem: React.FC<FieldItemProps> = (props) => {
const { name, widget, required = [], ...rest } = props;
const Component = ComponentsMap[widget];
return <Form.Item name={name}></Form.Item>;
};
export default FieldItem;
@@ -0,0 +1,59 @@
import ComponentsMap from '@/components/seal-form/config/components';
import { FormWidgetProps } from '../config/types';
const FormWidget: React.FC<
FormWidgetProps & {
onChange?: (data: any) => void;
disabled?: boolean;
}
> = ({
widget,
title: label,
required,
placeholder,
options,
description,
enum: enumValues,
style,
value,
min,
max,
status,
checked,
isInFormItems,
disabled,
onChange
}) => {
const Component = ComponentsMap[widget];
const optionList = enumValues?.map((item: string | number) => ({
label: item,
value: item
}));
return Component ? (
<Component
{...{
label,
required,
placeholder,
description,
min,
max
}}
status={status}
isInFormItems={isInFormItems}
disabled={disabled}
options={options || optionList}
value={value}
checked={checked}
style={{
width: '100%',
...style
}}
onChange={onChange}
/>
) : null;
};
export default FormWidget;
@@ -0,0 +1,175 @@
import Wrapper from '@/components/label-selector/wrapper';
import { MinusOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import React, { useEffect, useMemo } from 'react';
import styled from 'styled-components';
import { statusType } from '../config/types';
import FormWidget from './form-widget';
const RowWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
`;
const WidgetBox = styled.div`
display: flex;
align-items: center;
gap: 8px;
width: 100%;
`;
interface ListMapProps {
minItems?: number;
dataList: any[];
label?: React.ReactNode;
btnText?: string;
requiredFields?: string[];
validateStatusList?: Record<string, statusType>[];
properties: Record<string, any>;
disabled?: boolean;
onAdd?: (data: any[]) => void;
onDelete?: (deletedItem: any, data: any[]) => void;
onChange?: (data: any) => void;
}
interface ListItemProps {
schemaList: any[];
data: Record<string, any>;
disabled?: boolean;
validateStatus?: Record<string, statusType>;
onChange?: (data: any) => void;
}
const ListItem: React.FC<ListItemProps> = ({
schemaList,
data,
onChange,
validateStatus,
disabled
}) => {
const handleValueChange = (name: string, target: any) => {
if (target?.target?.type === 'checkbox') {
const checked = target.target?.checked;
onChange?.({ [name]: checked });
} else {
const value = target?.target ? target.target.value : target;
onChange?.({ [name]: value });
}
};
return (
<>
{schemaList.map((schema: any) => (
<FormWidget
status={validateStatus?.[schema.name]}
widget={schema.type}
{...schema}
disabled={disabled || schema.readOnly}
key={schema.name}
value={data?.[schema.name]}
checked={data?.[schema.name]}
isInFormItems={false}
onChange={(target) => handleValueChange(schema.name, target)}
/>
))}
</>
);
};
const ListMap: React.FC<ListMapProps> = ({
dataList = [],
label,
btnText,
properties = {},
requiredFields = [],
minItems = 0,
validateStatusList = [],
disabled,
onAdd,
onDelete,
onChange
}) => {
const [items, setItems] = React.useState(dataList || []);
const schemaList = useMemo(() => {
const list = Object.entries(properties).map(([key, value]) => ({
...value,
required: requiredFields.includes(key),
name: key
}));
return list;
}, [properties, requiredFields]);
const handleOnAdd = () => {
const keys = Object.keys(properties);
const newItems = [
...items,
{ ...keys.reduce((acc, key) => ({ ...acc, [key]: '' }), {}) }
];
setItems(newItems);
onAdd?.(newItems);
};
const handleDelete = (index: number) => {
const deleteItem = items[index];
const newItems = items.filter((_, i) => i !== index);
setItems(newItems);
onDelete?.(deleteItem, newItems);
};
const handleItemChange = (index: number, data: { [key: string]: any }) => {
const newItems = [...items];
newItems[index] = { ...newItems[index], ...data };
setItems(newItems);
onChange?.(newItems);
};
useEffect(() => {
if (!dataList.length && minItems > 0) {
handleOnAdd();
}
}, []);
useEffect(() => {
setItems(dataList);
}, [dataList]);
return (
<Wrapper
label={label}
btnText={btnText}
onAdd={handleOnAdd}
disabled={disabled}
>
{items.map((item, index) => (
<RowWrapper key={index}>
<WidgetBox>
<ListItem
schemaList={schemaList}
data={item}
validateStatus={validateStatusList?.[index]}
disabled={disabled}
onChange={(value) => handleItemChange(index, value)}
/>
</WidgetBox>
{!disabled && (
<Button
size="small"
type="default"
shape="circle"
style={{
width: 24,
marginLeft: 10,
flex: 'none'
}}
icon={<MinusOutlined />}
onClick={() => handleDelete(index)}
/>
)}
</RowWrapper>
))}
</Wrapper>
);
};
export default ListMap;
@@ -0,0 +1,39 @@
import React from 'react';
// refer to json schema
export interface FieldSchema {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
title?: string;
name: string;
description?: string;
properties?: Record<string, FieldSchema>;
default?: any;
enum?: string[];
minItems?: number;
maxItems?: number;
items?: FieldSchema[];
widget?: string;
min?: number;
style?: React.CSSProperties;
required?: string[];
}
export type statusType = 'error' | 'warning' | '' | undefined;
export interface FormWidgetProps {
status?: statusType;
isInFormItems?: boolean;
widget: 'Input' | 'Select' | 'Checkbox' | 'InputNumber';
name: string;
title?: string;
required?: boolean;
placeholder?: string;
readOnly?: boolean;
options?: { label: string; value: string | number }[];
description?: string;
enum?: (string | number)[];
style?: React.CSSProperties;
value?: any;
checked?: boolean;
min?: number;
max?: number;
}
@@ -0,0 +1,33 @@
import { useMemo } from 'react';
import { FieldSchema } from '../config/types';
interface ParsedField {
name: (string | number)[];
schema: FieldSchema;
}
const parseSchema = (
schema: Record<string, FieldSchema>,
parentName: (string | number)[] = []
): ParsedField[] => {
const fields: ParsedField[] = [];
Object.entries(schema).forEach(([key, fieldSchema]) => {
const currentName = [...parentName, key];
if (fieldSchema.type === 'object' && fieldSchema.properties) {
fields.push(...parseSchema(fieldSchema.properties, currentName));
} else if (fieldSchema.type === 'array' && fieldSchema.items) {
fields.push({ name: currentName, schema: fieldSchema });
} else {
fields.push({ name: currentName, schema: fieldSchema });
}
});
return fields;
};
const useParsedFields = (schema: Record<string, FieldSchema>) => {
return useMemo(() => parseSchema(schema), [schema]);
};
export default useParsedFields;
@@ -0,0 +1,61 @@
import { useRef } from 'react';
import { statusType } from '../config/types';
export default function useValidateFields(params: {
requiredFields?: string[];
setValidateStatusList: (statusList: { [key: string]: statusType }[]) => void;
}) {
const { requiredFields, setValidateStatusList } = params;
const validationEnabled = useRef(false);
const isEmptyValue = (value: any, key: string) => {
return !value;
};
const validateRule = (value: any, key: string) => {
return true;
};
const listMapValidator = async (_: any, valueList: any) => {
if (!validationEnabled.current) {
return Promise.resolve();
}
const fields = new Set<string>();
const statusList: { [key: string]: statusType }[] = [];
(valueList || []).forEach((item: any, index: number) => {
const status: { [key: string]: statusType } = {};
Object.entries(item || {}).forEach(([key, value]) => {
if (isEmptyValue(value, key)) {
fields.add(key);
if (requiredFields?.includes(key)) {
status[key] = 'error';
} else {
status[key] = '';
}
} else if (validateRule(value, key)) {
status[key] = '';
}
});
statusList.push(status);
});
setValidateStatusList(statusList);
if (fields.size > 0) {
return Promise.reject(`${Array.from(fields).join(', ')} is required`);
}
return Promise.resolve();
};
const toggleValidation = (enabled: boolean) => {
validationEnabled.current = enabled;
};
return {
listMapValidator,
toggleValidation
};
}
+26
View File
@@ -0,0 +1,26 @@
import { Form } from 'antd';
import React from 'react';
import { FieldSchema } from './config/types';
interface DynamicFormProps {
schema: FieldSchema;
onSubmit: (values: any) => void;
}
const DynamicForm: React.FC<DynamicFormProps> = ({ schema, onSubmit }) => {
const form = Form.useFormInstance();
const handleFinish = (values: any) => {
onSubmit(values);
};
return (
<>
<Form form={form} onFinish={handleFinish}>
{/* Render form fields based on schema */}
</Form>
</>
);
};
export default DynamicForm;
+4 -6
View File
@@ -33,7 +33,7 @@ const Chart: React.FC<{
}; };
const setOption = (options: ECOption) => { const setOption = (options: ECOption) => {
console.log('setOption', options); if (!chart.current) return;
chart.current?.clear(); chart.current?.clear();
chart.current?.setOption(options, { chart.current?.setOption(options, {
notMerge: true, notMerge: true,
@@ -107,8 +107,8 @@ const Chart: React.FC<{
} }
return () => { return () => {
chart.current?.dispose();
chart.current?.off('finished', handleOnFinished); chart.current?.off('finished', handleOnFinished);
chart.current?.dispose();
}; };
}, []); }, []);
@@ -128,13 +128,11 @@ const Chart: React.FC<{
if (container.current) { if (container.current) {
resizeObserver.current = new ResizeObserver(handleResize); resizeObserver.current = new ResizeObserver(handleResize);
resizeObserver.current.observe(container.current); resizeObserver.current.observe(container.current);
} }
return () => { return () => {
if (container.current) { resizeObserver.current?.disconnect();
resizeObserver.current?.unobserve(container.current); resizeObserver.current = undefined;
}
}; };
}, []); }, []);
+5 -5
View File
@@ -16,14 +16,14 @@ export default function useChartConfig() {
const { useToken } = theme; const { useToken } = theme;
const { token } = useToken(); const { token } = useToken();
console.log('token:', token);
const chartColorMap = useMemo(() => { const chartColorMap = useMemo(() => {
return { return {
titleColor: token.colorText, titleColor: token.colorText,
splitLineColor: token.colorBorder, splitLineColor: token.colorBorder,
tickLineColor: token.colorSplit, tickLineColor: token.colorSplit,
axislabelColor: token.colorTextTertiary, axislabelColor: token.colorTextTertiary,
colorSecondary: token.colorTextSecondary,
colorTertiary: token.colorTextTertiary,
gaugeBgColor: token.colorFillSecondary, gaugeBgColor: token.colorFillSecondary,
gaugeSplitLineColor: isDarkTheme gaugeSplitLineColor: isDarkTheme
? 'rgba(255,255,255,.3)' ? 'rgba(255,255,255,.3)'
@@ -65,6 +65,7 @@ export default function useChartConfig() {
const legend = { const legend = {
itemWidth: 8, itemWidth: 8,
itemHeight: 8, itemHeight: 8,
itemGap: 12,
textStyle: { textStyle: {
color: chartColorMap.axislabelColor color: chartColorMap.axislabelColor
} }
@@ -123,7 +124,6 @@ export default function useChartConfig() {
type: 'bar', type: 'bar',
barMaxWidth: 20, barMaxWidth: 20,
barMinWidth: 8, barMinWidth: 8,
// stack: 'total',
barGap: '30%', barGap: '30%',
barCategoryGap: '50%' barCategoryGap: '50%'
}; };
@@ -206,13 +206,13 @@ export default function useChartConfig() {
rich: { rich: {
value: { value: {
fontSize: 16, fontSize: 16,
fontWeight: '500', fontWeight: 500,
color: chartColorMap.titleColor color: chartColorMap.titleColor
}, },
unit: { unit: {
fontSize: 14, fontSize: 14,
color: chartColorMap.titleColor, color: chartColorMap.titleColor,
fontWeight: '500', fontWeight: 500,
padding: [0, 0, 0, 2] padding: [0, 0, 0, 2]
} }
} }
+44 -12
View File
@@ -1,9 +1,19 @@
import Chart from '@/components/echarts/chart'; import Chart from '@/components/echarts/chart';
import useChartConfig from '@/components/echarts/config'; import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data'; import EmptyData from '@/components/empty-data';
import React, { memo } from 'react'; import React from 'react';
import { ChartProps } from './types'; import { ChartProps } from './types';
const strokeColorFunc = (percent: number) => {
if (percent <= 50 || percent === undefined) {
return 'rgb(84, 204, 152, 80%)';
}
if (percent <= 80) {
return 'rgba(250, 173, 20, 80%)';
}
return 'rgba(255, 77, 79, 80%)';
};
const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = ( const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
props props
) => { ) => {
@@ -12,28 +22,46 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
title: titleConfig, title: titleConfig,
chartColorMap chartColorMap
} = useChartConfig(); } = useChartConfig();
const { value, height, width, labelFormatter, title, color } = props; const { value, height, width, labelFormatter, title, color, gaugeConfig } =
props;
const titleText = typeof title === 'string' ? title : title?.text;
if (!value && value !== 0) { if (!value && value !== 0) {
return <EmptyData height={height} title={title}></EmptyData>; return <EmptyData height={height} title={titleText}></EmptyData>;
} }
const setDataOptions = () => { const setDataOptions = () => {
const colorValue = color || strokeColorFunc(value);
const combineGaugeConfig = {
...gaugeItemConfig,
...gaugeConfig
};
combineGaugeConfig.detail.rich.value.color = colorValue;
combineGaugeConfig.detail.rich.unit.color = colorValue;
return { return {
title: { title: {
...titleConfig, ...titleConfig,
text: title, text: titleText,
top: '0', textStyle: {
left: 'center' fontSize: 12,
color: chartColorMap.colorSecondary,
fontWeight: 400
},
top: 10,
left: 'center',
...(typeof title === 'object' ? title : {})
}, },
series: [ series: [
{ {
...gaugeItemConfig, ...combineGaugeConfig,
axisLine: { axisLine: {
...gaugeItemConfig.axisLine, ...combineGaugeConfig.axisLine,
lineStyle: { lineStyle: {
...gaugeItemConfig.axisLine.lineStyle, ...combineGaugeConfig.axisLine.lineStyle,
color: [ color: [
[value / 100, color], [value / 100, colorValue],
[1, chartColorMap.gaugeBgColor] [1, chartColorMap.gaugeBgColor]
] ]
} }
@@ -42,7 +70,11 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
color: 'transparent' color: 'transparent'
}, },
detail: { detail: {
...gaugeItemConfig.detail, ...combineGaugeConfig.detail,
borderColor: colorValue,
lineHeight: 20,
height: 18,
width: 50,
formatter: labelFormatter || gaugeItemConfig.detail.formatter formatter: labelFormatter || gaugeItemConfig.detail.formatter
}, },
data: [{ value }] data: [{ value }]
@@ -62,4 +94,4 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
); );
}; };
export default memo(GaugeChart); export default GaugeChart;
+47 -4
View File
@@ -1,10 +1,13 @@
import Chart from '@/components/echarts/chart'; import Chart from '@/components/echarts/chart';
import useChartConfig from '@/components/echarts/config'; import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data'; import EmptyData from '@/components/empty-data';
import { genColors } from '@/utils';
import _ from 'lodash'; import _ from 'lodash';
import React, { memo, useMemo } from 'react'; import React, { memo, useMemo } from 'react';
import echarts from '.';
import { ChartProps } from './types'; import { ChartProps } from './types';
const LinearGradient = echarts.graphic.LinearGradient;
const LineChart: React.FC<ChartProps> = (props) => { const LineChart: React.FC<ChartProps> = (props) => {
const { const {
seriesData, seriesData,
@@ -16,7 +19,11 @@ const LineChart: React.FC<ChartProps> = (props) => {
tooltipValueFormatter = null, tooltipValueFormatter = null,
legendData = [], legendData = [],
smooth, smooth,
title title,
legendOptions,
gridOptions,
titleOptions,
showArea
} = props; } = props;
const { const {
grid, grid,
@@ -42,7 +49,10 @@ const LineChart: React.FC<ChartProps> = (props) => {
title: { title: {
text: '' text: ''
}, },
grid, grid: {
...grid,
...gridOptions
},
tooltip: { tooltip: {
...tooltip, ...tooltip,
formatter(params: any) { formatter(params: any) {
@@ -61,6 +71,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
yAxis, yAxis,
legend: { legend: {
...legend, ...legend,
...legendOptions,
data: legendData.map((item: any) => { data: legendData.map((item: any) => {
return { return {
name: item, name: item,
@@ -74,6 +85,11 @@ const LineChart: React.FC<ChartProps> = (props) => {
const dataOptions = useMemo((): any => { const dataOptions = useMemo((): any => {
const data = _.map(seriesData, (item: any) => { const data = _.map(seriesData, (item: any) => {
const colors = genColors({
color: item.color,
alpha1: 0.25,
alpha2: 0.1
});
return { return {
...item, ...item,
...lineItemConfig, ...lineItemConfig,
@@ -85,7 +101,21 @@ const LineChart: React.FC<ChartProps> = (props) => {
lineStyle: { lineStyle: {
...lineItemConfig.lineStyle, ...lineItemConfig.lineStyle,
color: item.color color: item.color
},
areaStyle: showArea
? {
color: new LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: colors[0]
},
{
offset: 1,
color: colors[1]
} }
])
}
: null
}; };
}); });
return { return {
@@ -93,6 +123,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
animation: false, animation: false,
title: { title: {
...titleConfig, ...titleConfig,
...titleOptions,
text: title text: title
}, },
yAxis: { yAxis: {
@@ -109,12 +140,24 @@ const LineChart: React.FC<ChartProps> = (props) => {
}, },
series: data series: data
}; };
}, [seriesData, xAxisData, yAxisName, title, smooth, legendData, options]); }, [
seriesData,
xAxisData,
yAxisName,
title,
smooth,
titleOptions,
legendData,
options
]);
return ( return (
<> <>
{!seriesData.length ? ( {!seriesData.length ? (
<EmptyData height={height} title={title}></EmptyData> <EmptyData
height={height}
title={_.get(title, 'text', title || '')}
></EmptyData>
) : ( ) : (
<Chart <Chart
height={height} height={height}
+21
View File
@@ -1,10 +1,14 @@
import Chart from '@/components/echarts/chart'; import Chart from '@/components/echarts/chart';
import useChartConfig from '@/components/echarts/config'; import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data'; import EmptyData from '@/components/empty-data';
import { genColors } from '@/utils';
import _ from 'lodash'; import _ from 'lodash';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import echarts from '.';
import { ChartProps } from './types'; import { ChartProps } from './types';
const LinearGradient = echarts.graphic.LinearGradient;
const MixLineBarChart: React.FC< const MixLineBarChart: React.FC<
ChartProps & { ChartProps & {
chartData: { chartData: {
@@ -78,6 +82,11 @@ const MixLineBarChart: React.FC<
const dataOptions = useMemo((): any => { const dataOptions = useMemo((): any => {
const linedata = _.map(lineSeriesData, (item: any) => { const linedata = _.map(lineSeriesData, (item: any) => {
const colors = genColors({
color: item.color,
alpha1: 0.5,
alpha2: 0.1
});
return { return {
...item, ...item,
...lineItemConfig, ...lineItemConfig,
@@ -91,6 +100,18 @@ const MixLineBarChart: React.FC<
...lineItemConfig.lineStyle, ...lineItemConfig.lineStyle,
color: item.color color: item.color
} }
// areaStyle: {
// color: new LinearGradient(0, 0, 0, 1, [
// {
// offset: 0,
// color: colors[0]
// },
// {
// offset: 1,
// color: colors[1]
// }
// ])
// }
}; };
}); });
+24 -2
View File
@@ -1,18 +1,40 @@
import type { LegendComponentOption } from 'echarts/components'; import type {
LegendComponentOption,
TitleComponentOption
} from 'echarts/components';
export interface ChartProps { export interface ChartProps {
seriesData: any[]; seriesData: any[];
showEmpty?: boolean; showEmpty?: boolean;
showArea?: boolean;
xAxisData: string[]; xAxisData: string[];
legendData?: LegendComponentOption['data']; legendData?: LegendComponentOption['data'];
legendOptions?: {
[K in keyof LegendComponentOption]?: LegendComponentOption[K];
};
gridOptions?: {
left?: string | number;
right?: string | number;
top?: string | number;
bottom?: string | number;
};
labelFormatter?: (val?: any, index?: number) => string; labelFormatter?: (val?: any, index?: number) => string;
tooltipValueFormatter?: (val: any) => string; tooltipValueFormatter?: (val: any) => string;
height: string | number; height: string | number;
width?: string | number; width?: string | number;
title?: string; title?: string | TitleComponentOption;
titleOptions?: {
[K in keyof TitleComponentOption]?: TitleComponentOption[K];
};
value?: number; value?: number;
smooth?: boolean; smooth?: boolean;
color?: string; color?: string;
yAxisName?: string; yAxisName?: string;
gaugeConfig?: {
radius?: string;
center?: string[];
startAngle?: number;
endAngle?: number;
};
} }
export interface AreaChartItemProps { export interface AreaChartItemProps {
+40 -48
View File
@@ -1,74 +1,66 @@
import classNames from 'classnames';
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import CopyButton from '../copy-button';
import SegmentLine from '../segment-line';
import './index.less'; import './index.less';
const Wrapper = styled.div<{ $height: number }>` const HeaderWrapper = styled.div<{ $height?: number }>`
height: ${(props) => props.$height}px; height: ${(props) => (props.$height ? `${props.$height}px` : 'auto')};
display: flex;
padding-block: 0;
justify-content: space-between;
align-items: center;
`;
const Wrapper = styled.div`
border-radius: var(--border-radius-mini);
overflow: hidden;
&.bordered {
border: 1px solid var(--ant-color-border);
}
&.borderless {
border: none;
}
.code-pre {
margin-bottom: 0;
}
.scrollbar {
.slider {
border-radius: 6px;
}
}
`; `;
interface EditorwrapProps { interface EditorwrapProps {
headerHeight?: number; headerHeight?: number;
header?: React.ReactNode; header?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
showHeader?: boolean; variant?: 'bordered' | 'borderless';
copyText: string;
defaultValue?: string;
langOptions?: Global.BaseOption<string | number>[];
styles?: { styles?: {
wrapper?: React.CSSProperties; wrapper?: React.CSSProperties;
header?: React.CSSProperties; header?: React.CSSProperties;
content?: React.CSSProperties; content?: React.CSSProperties;
}; };
onChangeLang?: (value: string | number) => void;
} }
const EditorWrap: React.FC<EditorwrapProps> = ({ const EditorWrap: React.FC<EditorwrapProps> = ({
headerHeight = 40, headerHeight = 40,
header, header,
children, children,
copyText, variant = 'borderless',
langOptions = [], styles = {}
onChangeLang,
defaultValue,
styles = {},
showHeader = true
}) => { }) => {
const handleChangeLang = (value: string | number) => {
onChangeLang?.(value);
};
const renderHeader = () => {
if (header) {
return <div className="editor-header">{header}</div>;
}
if (showHeader) {
return ( return (
<Wrapper className="editor-header" $height={headerHeight}> <Wrapper
<SegmentLine style={{ ...styles.wrapper }}
height={headerHeight} className={classNames({
defaultValue={defaultValue} bordered: variant === 'bordered',
size="small" borderless: variant === 'borderless'
options={langOptions} })}
onChange={handleChangeLang} >
></SegmentLine> {header && <HeaderWrapper $height={headerHeight}>{header}</HeaderWrapper>}
<CopyButton <div>{children}</div>
text={copyText}
size="small"
style={{
color: 'rgba(255,255,255,.7)'
}}
/>
</Wrapper> </Wrapper>
); );
}
return null;
};
return (
<div className="editor-wrap" style={{ ...styles.wrapper }}>
{renderHeader()}
<div className="editor-content">{children}</div>
</div>
);
}; };
export default React.memo(EditorWrap); export default EditorWrap;
@@ -1,30 +1,39 @@
import { LoadingOutlined } from '@ant-design/icons';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import React, { useEffect, useRef, useState } from 'react'; import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef
} from 'react';
import EditorWrap from '../editor-wrap'; import EditorWrap from '../editor-wrap';
interface ViewerProps { interface ViewerProps {
ref?: any;
lang: string; lang: string;
defaultLang?: string; defaultLang?: string;
langOptions?: Global.BaseOption<string>[];
config?: any; config?: any;
value: string; value: string;
height?: string | number; height?: string | number;
theme?: string; theme?: string;
showHeader?: boolean; header?: React.ReactNode;
placeholder?: string;
variant?: 'bordered' | 'borderless';
} }
const ViewerEditor: React.FC<ViewerProps> = (props) => { const ViewerEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
const editorRef = useRef<any>(null);
const { const {
lang, lang,
value, value,
config, config,
langOptions,
defaultLang, defaultLang,
height = 380, height = 380,
showHeader theme = 'vs-dark',
header,
variant = 'borderless',
placeholder
} = props; } = props;
const [langType, setLangType] = useState(defaultLang);
const editorRef = useRef<any>(null);
const handleBeforeMount = (monaco: any) => { const handleBeforeMount = (monaco: any) => {
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
@@ -36,12 +45,8 @@ const ViewerEditor: React.FC<ViewerProps> = (props) => {
const handleEditorDidMount = (editor: any, monaco: any) => { const handleEditorDidMount = (editor: any, monaco: any) => {
editorRef.current = editor; editorRef.current = editor;
console.log('loaded====', editor, monaco);
}; };
const handleOnChangeLang = (value: string) => {
setLangType(value);
};
const formatCode = () => { const formatCode = () => {
if (editorRef.current) { if (editorRef.current) {
setTimeout(() => { setTimeout(() => {
@@ -55,35 +60,50 @@ const ViewerEditor: React.FC<ViewerProps> = (props) => {
} }
}; };
useImperativeHandle(ref, () => ({
format: () => {
formatCode();
},
getValue: () => {
return editorRef.current?.getValue?.();
},
setValue: (val: string) => {
editorRef.current?.setValue?.(val);
},
editor: editorRef.current
}));
useEffect(() => { useEffect(() => {
formatCode(); formatCode();
setTimeout(() => { setTimeout(() => {
const lineCount = editorRef.current?.getModel().getLineCount(); // 获取总行数 const lineCount = editorRef.current?.getModel().getLineCount();
editorRef.current?.revealLine(lineCount); // 滚动到最后一行 editorRef.current?.revealLine(lineCount);
}, 100); // 可以调整延时,确保编辑器完全加载 }, 100);
}, [value]); }, [value]);
return ( return (
<EditorWrap <EditorWrap header={header} variant={variant}>
copyText={value}
langOptions={langOptions}
defaultValue={lang}
showHeader={showHeader}
onChangeLang={handleOnChangeLang}
>
<Editor <Editor
height={height} height={height}
theme="vs-dark" theme={theme}
className="monaco-editor" className="monaco-editor"
defaultLanguage={defaultLang} defaultLanguage={defaultLang}
language={langType} language={lang}
value={value} value={value}
options={config} options={{
minimap: { enabled: false },
scrollbar: {
verticalScrollbarSize: 6,
horizontalScrollbarSize: 6
},
placeholder: placeholder
}}
loading={<LoadingOutlined style={{ fontSize: 24 }}></LoadingOutlined>}
beforeMount={handleBeforeMount} beforeMount={handleBeforeMount}
onMount={handleEditorDidMount} onMount={handleEditorDidMount}
/> />
</EditorWrap> </EditorWrap>
); );
}; });
export default React.memo(ViewerEditor); export default ViewerEditor;
+172 -4
View File
@@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: iconfont; /* Project id 4613488 */ font-family: iconfont; /* Project id 4613488 */
src: url('iconfont.woff2?t=1755160439217') format('woff2'), src: url('iconfont.woff2?t=1763436184844') format('woff2'),
url('iconfont.woff?t=1755160439217') format('woff'), url('iconfont.woff?t=1763436184844') format('woff'),
url('iconfont.ttf?t=1755160439217') format('truetype'); url('iconfont.ttf?t=1763436184844') format('truetype');
} }
.iconfont { .iconfont {
@@ -13,6 +13,174 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-basic::before {
content: "\e6bb";
}
.icon-settings::before {
content: "\e6b8";
}
.icon-speed::before {
content: "\e6ba";
}
.icon-permission::before {
content: "\e6b6";
}
.icon-captive_portal::before {
content: "\e6b7";
}
.icon-lock_open_right::before {
content: "\e6b3";
}
.icon-lock_person::before {
content: "\e6b4";
}
.icon-lock_open::before {
content: "\e6b5";
}
.icon-question::before {
content: "\e6b2";
}
.icon-aws::before {
content: "\e616";
}
.icon-aws1::before {
content: "\e62f";
}
.icon-manage_user::before {
content: "\e6b1";
}
.icon-private::before {
content: "\e6b0";
}
.icon-stop3::before {
content: "\e6af";
}
.icon-version::before {
content: "\e6ae";
}
.icon-edit-content::before {
content: "\e6ab";
}
.icon-code_block::before {
content: "\e6ac";
}
.icon-parameters::before {
content: "\e6ad";
}
.icon-backend-filled::before {
content: "\e6a9";
}
.icon-backend::before {
content: "\e6aa";
}
.icon-down2::before {
content: "\e6a7";
}
.icon-nvidia2::before {
content: "\e60d";
}
.icon-centos::before {
content: "\e6cd";
}
.icon-redhat::before {
content: "\ec7b";
}
.icon-ubuntu::before {
content: "\edd3";
}
.icon-debian::before {
content: "\eb74";
}
.icon-alma-linux::before {
content: "\e6a8";
}
.icon-fedora::before {
content: "\e61a";
}
.icon-rocky-linux::before {
content: "\e620";
}
.icon-nvidia1::before {
content: "\e980";
}
.icon-nvidia::before {
content: "\e60c";
}
.icon-amd::before {
content: "\e6a6";
}
.icon-huawei::before {
content: "\e615";
}
.icon-metax::before {
content: "\e6a4";
}
.icon-ascend::before {
content: "\e6a3";
}
.icon-huaweicloud::before {
content: "\e614";
}
.icon-alicloud::before {
content: "\e784";
}
.icon-tencentcloud::before {
content: "\e609";
}
.icon-cluster2-outline::before {
content: "\e6a1";
}
.icon-cluster2-filled::before {
content: "\e6a2";
}
.icon-cluster-filled::before {
content: "\e69f";
}
.icon-cluster-outline::before {
content: "\e6a0";
}
.icon-admin-user::before { .icon-admin-user::before {
content: "\e69d"; content: "\e69d";
} }
@@ -21,7 +189,7 @@
content: "\e69e"; content: "\e69e";
} }
.icon-quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op::before { .icon-detail-info::before {
content: "\e69b"; content: "\e69b";
} }
File diff suppressed because one or more lines are too long
+296 -2
View File
@@ -5,6 +5,300 @@
"css_prefix_text": "icon-", "css_prefix_text": "icon-",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "46055620",
"name": "basic",
"font_class": "basic",
"unicode": "e6bb",
"unicode_decimal": 59067
},
{
"icon_id": "46055265",
"name": "settings",
"font_class": "settings",
"unicode": "e6b8",
"unicode_decimal": 59064
},
{
"icon_id": "46055263",
"name": "speed",
"font_class": "speed",
"unicode": "e6ba",
"unicode_decimal": 59066
},
{
"icon_id": "46039838",
"name": "permission",
"font_class": "permission",
"unicode": "e6b6",
"unicode_decimal": 59062
},
{
"icon_id": "46039837",
"name": "captive_portal",
"font_class": "captive_portal",
"unicode": "e6b7",
"unicode_decimal": 59063
},
{
"icon_id": "46001737",
"name": "lock_open_right",
"font_class": "lock_open_right",
"unicode": "e6b3",
"unicode_decimal": 59059
},
{
"icon_id": "46001736",
"name": "lock_person",
"font_class": "lock_person",
"unicode": "e6b4",
"unicode_decimal": 59060
},
{
"icon_id": "46001735",
"name": "lock_open",
"font_class": "lock_open",
"unicode": "e6b5",
"unicode_decimal": 59061
},
{
"icon_id": "45937227",
"name": "question_mark_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz",
"font_class": "question",
"unicode": "e6b2",
"unicode_decimal": 59058
},
{
"icon_id": "10786885",
"name": "aws",
"font_class": "aws",
"unicode": "e616",
"unicode_decimal": 58902
},
{
"icon_id": "12014352",
"name": "aws",
"font_class": "aws1",
"unicode": "e62f",
"unicode_decimal": 58927
},
{
"icon_id": "45704693",
"name": "manage_accounts_24dp_1F1F1F_FILL1_wght300_GRAD0_op",
"font_class": "manage_user",
"unicode": "e6b1",
"unicode_decimal": 59057
},
{
"icon_id": "45702717",
"name": "private",
"font_class": "private",
"unicode": "e6b0",
"unicode_decimal": 59056
},
{
"icon_id": "45702697",
"name": "stop",
"font_class": "stop3",
"unicode": "e6af",
"unicode_decimal": 59055
},
{
"icon_id": "45688107",
"name": "version",
"font_class": "version",
"unicode": "e6ae",
"unicode_decimal": 59054
},
{
"icon_id": "45631011",
"name": "contract_edit_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz",
"font_class": "edit-content",
"unicode": "e6ab",
"unicode_decimal": 59051
},
{
"icon_id": "45631010",
"name": "code_blocks_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz24",
"font_class": "code_block",
"unicode": "e6ac",
"unicode_decimal": 59052
},
{
"icon_id": "45631009",
"name": "settings_applications_24dp_1F1F1F_FILL0_wght300_GR",
"font_class": "parameters",
"unicode": "e6ad",
"unicode_decimal": 59053
},
{
"icon_id": "45623375",
"name": "bolt_24dp_1F1F1F_FILL1_wght300_GRAD0_opsz24",
"font_class": "backend-filled",
"unicode": "e6a9",
"unicode_decimal": 59049
},
{
"icon_id": "45623374",
"name": "bolt_24dp_1F1F1F_FILL0_wght300_GRAD0_opsz24",
"font_class": "backend",
"unicode": "e6aa",
"unicode_decimal": 59050
},
{
"icon_id": "45554217",
"name": "arrow_down",
"font_class": "down2",
"unicode": "e6a7",
"unicode_decimal": 59047
},
{
"icon_id": "11155543",
"name": "nvidia",
"font_class": "nvidia2",
"unicode": "e60d",
"unicode_decimal": 58893
},
{
"icon_id": "11425971",
"name": "centos",
"font_class": "centos",
"unicode": "e6cd",
"unicode_decimal": 59085
},
{
"icon_id": "15378743",
"name": "redhat",
"font_class": "redhat",
"unicode": "ec7b",
"unicode_decimal": 60539
},
{
"icon_id": "6808499",
"name": "Ubuntu",
"font_class": "ubuntu",
"unicode": "edd3",
"unicode_decimal": 60883
},
{
"icon_id": "15378275",
"name": "debian",
"font_class": "debian",
"unicode": "eb74",
"unicode_decimal": 60276
},
{
"icon_id": "38395859",
"name": "almalinux",
"font_class": "alma-linux",
"unicode": "e6a8",
"unicode_decimal": 59048
},
{
"icon_id": "43280042",
"name": "linux-fedora",
"font_class": "fedora",
"unicode": "e61a",
"unicode_decimal": 58906
},
{
"icon_id": "43280143",
"name": "linux-rocky_linux",
"font_class": "rocky-linux",
"unicode": "e620",
"unicode_decimal": 58912
},
{
"icon_id": "36227448",
"name": "nvidia",
"font_class": "nvidia1",
"unicode": "e980",
"unicode_decimal": 59776
},
{
"icon_id": "40723469",
"name": "nvidia",
"font_class": "nvidia",
"unicode": "e60c",
"unicode_decimal": 58892
},
{
"icon_id": "45463937",
"name": "amd",
"font_class": "amd",
"unicode": "e6a6",
"unicode_decimal": 59046
},
{
"icon_id": "24164616",
"name": "华为",
"font_class": "huawei",
"unicode": "e615",
"unicode_decimal": 58901
},
{
"icon_id": "45461362",
"name": "metax",
"font_class": "metax",
"unicode": "e6a4",
"unicode_decimal": 59044
},
{
"icon_id": "45459906",
"name": "ascend",
"font_class": "ascend",
"unicode": "e6a3",
"unicode_decimal": 59043
},
{
"icon_id": "8152996",
"name": "HUAWEI",
"font_class": "huaweicloud",
"unicode": "e614",
"unicode_decimal": 58900
},
{
"icon_id": "34290284",
"name": "alicloud",
"font_class": "alicloud",
"unicode": "e784",
"unicode_decimal": 59268
},
{
"icon_id": "36654764",
"name": "tencent",
"font_class": "tencentcloud",
"unicode": "e609",
"unicode_decimal": 58889
},
{
"icon_id": "45310241",
"name": "cluster2-outline",
"font_class": "cluster2-outline",
"unicode": "e6a1",
"unicode_decimal": 59041
},
{
"icon_id": "45310242",
"name": "cluster2-filled",
"font_class": "cluster2-filled",
"unicode": "e6a2",
"unicode_decimal": 59042
},
{
"icon_id": "45310070",
"name": "cluster-filled",
"font_class": "cluster-filled",
"unicode": "e69f",
"unicode_decimal": 59039
},
{
"icon_id": "45310069",
"name": "cluster-outline",
"font_class": "cluster-outline",
"unicode": "e6a0",
"unicode_decimal": 59040
},
{ {
"icon_id": "45240470", "icon_id": "45240470",
"name": "admin-user", "name": "admin-user",
@@ -21,8 +315,8 @@
}, },
{ {
"icon_id": "45221182", "icon_id": "45221182",
"name": "quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op", "name": "detail-info",
"font_class": "quick_reference_24dp_1F1F1F_FILL0_wght300_GRAD0_op", "font_class": "detail-info",
"unicode": "e69b", "unicode": "e69b",
"unicode_decimal": 59035 "unicode_decimal": 59035
}, },
Binary file not shown.
Binary file not shown.
Binary file not shown.
+33 -1
View File
@@ -2,9 +2,13 @@ import IconFont from '@/components/icon-font';
import { import {
ApiOutlined, ApiOutlined,
DeleteOutlined, DeleteOutlined,
DockerOutlined,
DownloadOutlined, DownloadOutlined,
EditOutlined, EditOutlined,
ExperimentOutlined, ExperimentOutlined,
FileTextOutlined,
KubernetesOutlined,
ProfileOutlined,
RetweetOutlined, RetweetOutlined,
ThunderboltOutlined ThunderboltOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
@@ -17,7 +21,11 @@ const icons = {
ThunderboltOutlined: React.createElement(ThunderboltOutlined), ThunderboltOutlined: React.createElement(ThunderboltOutlined),
RetweetOutlined: React.createElement(RetweetOutlined), RetweetOutlined: React.createElement(RetweetOutlined),
DownloadOutlined: React.createElement(DownloadOutlined), DownloadOutlined: React.createElement(DownloadOutlined),
FileTextOutlined: React.createElement(FileTextOutlined),
ApiOutlined: React.createElement(ApiOutlined), ApiOutlined: React.createElement(ApiOutlined),
KubernetesOutlined: React.createElement(KubernetesOutlined),
ProfileOutlined: React.createElement(ProfileOutlined),
DockerOutlined: React.createElement(DockerOutlined),
Stop: React.createElement(IconFont, { type: 'icon-stop1' }), Stop: React.createElement(IconFont, { type: 'icon-stop1' }),
Play: React.createElement(IconFont, { type: 'icon-outline-play' }), Play: React.createElement(IconFont, { type: 'icon-outline-play' }),
Catalog: React.createElement(IconFont, { type: 'icon-catalog' }), Catalog: React.createElement(IconFont, { type: 'icon-catalog' }),
@@ -26,7 +34,31 @@ const icons = {
ModelScope: React.createElement(IconFont, { type: 'icon-tu2' }), ModelScope: React.createElement(IconFont, { type: 'icon-tu2' }),
LocalPath: React.createElement(IconFont, { type: 'icon-hard-disk' }), LocalPath: React.createElement(IconFont, { type: 'icon-hard-disk' }),
Launch: React.createElement(IconFont, { type: 'icon-rocket-launch' }), Launch: React.createElement(IconFont, { type: 'icon-rocket-launch' }),
Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' }) Deployment: React.createElement(IconFont, { type: 'icon-rocket-launch1' }),
Docker: React.createElement(IconFont, { type: 'icon-docker' }),
DigitalOcean: React.createElement(IconFont, { type: 'icon-digitalocean' }),
DetailInfo: React.createElement(IconFont, { type: 'icon-detail-info' }),
HuaweiCloud: React.createElement(IconFont, { type: 'icon-huaweicloud' }),
AliCloud: React.createElement(IconFont, { type: 'icon-alicloud' }),
TencentCloud: React.createElement(IconFont, { type: 'icon-tencentcloud' }),
Nvidia: React.createElement(IconFont, { type: 'icon-nvidia' }),
Ascend: React.createElement(IconFont, { type: 'icon-ascend' }),
Catalog1: React.createElement(IconFont, { type: 'icon-catalog1' }),
AMD: React.createElement(IconFont, { type: 'icon-amd' }),
KubernetesFilled: React.createElement(IconFont, { type: 'icon-k8s-filled' }),
EditContent: React.createElement(IconFont, { type: 'icon-edit-content' }),
Yaml: React.createElement(IconFont, { type: 'icon-code_block' }),
Version: React.createElement(IconFont, { type: 'icon-version' }),
Parameter: React.createElement(IconFont, { type: 'icon-parameters' }),
Private: React.createElement(IconFont, { type: 'icon-private' }),
AWS: React.createElement(IconFont, { type: 'icon-aws' }),
LockOpenRight: React.createElement(IconFont, {
type: 'icon-lock_open_right'
}),
LockPerson: React.createElement(IconFont, { type: 'icon-lock_person' }),
LockOpen: React.createElement(IconFont, { type: 'icon-lock_open' }),
Permission: React.createElement(IconFont, { type: 'icon-permission' }),
CaptivePortal: React.createElement(IconFont, { type: 'icon-captive_portal' })
}; };
export default icons; export default icons;
+3 -1
View File
@@ -48,12 +48,14 @@ const ToolsBar: React.FC<ToolsBarProps> = (props) => {
<Tooltip <Tooltip
placement="bottomLeft" placement="bottomLeft"
arrow={false} arrow={false}
overlayInnerStyle={{ styles={{
body: {
background: 'var(--color-white-1)', background: 'var(--color-white-1)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'flex-start', justifyContent: 'flex-start',
width: 160 width: 160
}
}} }}
title={ title={
<div className="flex-column" style={{ width: '100%' }}> <div className="flex-column" style={{ width: '100%' }}>
+32
View File
@@ -0,0 +1,32 @@
import AutoTooltip from '@/components/auto-tooltip';
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
const LabelsWrapper = styled.div`
display: flex;
flex-wrap: wrap;
gap: 6px;
`;
interface LabelCellProps {
labels: Record<string, any>;
}
const LabelsCell: React.FC<LabelCellProps> = ({ labels }) => (
<LabelsWrapper>
{_.map(labels, (value: string, key: string) => (
<AutoTooltip
key={key}
className="m-r-0"
maxWidth={155}
style={{ paddingInline: 8, borderRadius: 12 }}
>
<span>{key}</span>
<span>:{value}</span>
</AutoTooltip>
))}
</LabelsWrapper>
);
export default LabelsCell;
@@ -0,0 +1,138 @@
import AutoComplete from '@/components/seal-form/auto-complete';
import { MinusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tooltip } from 'antd';
import _ from 'lodash';
import React, { useMemo, useState } from 'react';
import { useLabelSelectorContext } from './context';
import './styles/label-item.less';
interface LabelItemProps {
label: {
key: string;
value: string;
};
labels?: Record<string, any>;
labelKey?: string;
labelValue?: string;
keyAddon?: React.ReactNode;
valueAddon?: React.ReactNode;
seperator?: string;
labelList: { key: string; value: string }[];
disabled?: boolean;
onDelete?: () => void;
onChange?: (params: { key: string; value: string }) => void;
onPaste?: (e: any) => void;
onBlur?: (e: any, type: string) => void;
}
const LabelItem: React.FC<LabelItemProps> = ({
labels,
label,
labelList,
seperator,
keyAddon,
valueAddon,
disabled,
onChange,
onDelete,
onBlur
}) => {
const intl = useIntl();
const [open, setOpen] = useState(false);
const { options } = useLabelSelectorContext();
const keyOptions = useMemo(() => {
return options?.filter(
(item) => !_.has(labels, item.value) || item.value === label.key
);
}, [labels, options]);
const valueOptions = useMemo(() => {
return options?.find((item) => item.value === label.key)?.children || [];
}, [label.key, options]);
const handleOnValueChange = (value: string) => {
onChange?.({
key: label.key,
value: value
});
};
const handleOnKeyChange = (key: any) => {
onChange?.({
key,
value: label.value
});
};
const handleKeyOnBlur = (e: any, type: string) => {
const val = e.target.value;
// has duplicate key
const duplicates = _.filter(
labelList,
(item: Global.BaseListItem<string>) => val && val === item.key
);
if (duplicates.length > 1) {
setOpen(true);
onChange?.({
key: '',
value: label.value
});
setTimeout(() => {
setOpen(false);
}, 1000);
} else {
setOpen(false);
}
onBlur?.(e, type);
};
return (
<div className="label-item">
<div className="label-key">
{keyAddon ?? (
<Tooltip
open={open}
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
>
<AutoComplete
options={keyOptions}
disabled={disabled}
checkStatus="success"
label={intl.formatMessage({ id: 'common.input.key' })}
value={label.key}
onChange={handleOnKeyChange}
onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
></AutoComplete>
</Tooltip>
)}
</div>
{seperator && <span className="seprator">{seperator}</span>}
<div className="label-value">
{valueAddon ?? (
<AutoComplete
options={valueOptions}
disabled={disabled}
checkStatus={label.value ? 'success' : ''}
label={intl.formatMessage({ id: 'common.input.value' })}
value={label.value}
onChange={handleOnValueChange}
onBlur={(e: any) => onBlur?.(e, 'value')}
></AutoComplete>
)}
</div>
{!disabled && (
<Button
size="small"
className="btn"
type="default"
shape="circle"
onClick={onDelete}
>
<MinusOutlined />
</Button>
)}
</div>
);
};
export default LabelItem;
+25
View File
@@ -0,0 +1,25 @@
import React from 'react';
interface LabelSelectorContextProps {
options?: Array<{
label: string;
value: string | number;
children?: { label: string; value: string | number }[];
}>;
currentData?: Record<string, any>;
}
export const LabelSelectorContext =
React.createContext<LabelSelectorContextProps>(
{} as LabelSelectorContextProps
);
export const useLabelSelectorContext = () => {
const context = React.useContext(LabelSelectorContext);
if (!context) {
throw new Error(
'useLabelSelectorContext must be used within a LabelSelectorProvider'
);
}
return context;
};
+30 -13
View File
@@ -1,6 +1,6 @@
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Inner from './inner'; import Inner from './inner';
interface LabelSelectorProps { interface LabelSelectorProps {
@@ -8,6 +8,8 @@ interface LabelSelectorProps {
label?: string; label?: string;
btnText?: string; btnText?: string;
description?: React.ReactNode; description?: React.ReactNode;
disabled?: boolean;
isAutoComplete?: boolean;
onChange?: (labels: Record<string, any>) => void; onChange?: (labels: Record<string, any>) => void;
onBlur?: (e: any, type: string, index: number) => void; onBlur?: (e: any, type: string, index: number) => void;
onDelete?: (index: number) => void; onDelete?: (index: number) => void;
@@ -18,9 +20,11 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
onChange, onChange,
onBlur, onBlur,
onDelete, onDelete,
disabled,
label, label,
btnText, btnText,
description description,
isAutoComplete
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const [labelsData, setLabelsData] = useState({}); const [labelsData, setLabelsData] = useState({});
@@ -41,18 +45,30 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
} }
}, [labels]); }, [labels]);
const handleLabelListChange = useCallback( const handleLabelListChange = (list: { key: string; value: string }[]) => {
(list: { key: string; value: string }[]) => {
setLabelList(list); setLabelList(list);
}, };
[setLabelList]
);
const handleLabelsChange = (data: Record<string, any>) => { const handleLabelsChange = (data: Record<string, any>) => {
console.log('handleLabelsChange', data); console.log('handleLabelsChange', data);
setLabelsData(data); setLabelsData(data);
onChange?.(data); onChange?.(data);
}; };
const updateLabels = (list: { key: string; value: string }[]) => {
const newLabels = _.reduce(
list,
(result: any, item: any) => {
if (item.key) {
result[item.key] = item.value;
}
return result;
},
{}
);
onChange?.(newLabels);
};
const handleOnPaste = ( const handleOnPaste = (
e: React.ClipboardEvent<HTMLTextAreaElement>, e: React.ClipboardEvent<HTMLTextAreaElement>,
index: number index: number
@@ -70,22 +86,23 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
const [key, value] = line.split('=').map((part) => part.trim()); const [key, value] = line.split('=').map((part) => part.trim());
return { key, value }; return { key, value };
}); });
console.log(lines, parsedData);
setLabelList((prevPairs) => { const newPairs = [...labelList];
const newPairs = [...prevPairs];
newPairs.splice(index, 1, ...parsedData); newPairs.splice(index, 1, ...parsedData);
return newPairs;
}); updateLabels(newPairs);
setLabelList(newPairs);
}; };
return ( return (
<Inner <Inner
disabled={disabled}
label={label} label={label}
btnText={btnText} btnText={btnText}
description={ description={
description ?? intl.formatMessage({ id: 'models.form.keyvalue.paste' }) description ?? intl.formatMessage({ id: 'models.form.keyvalue.paste' })
} }
isAutoComplete={isAutoComplete}
labels={labelsData} labels={labelsData}
labelList={labelList} labelList={labelList}
onChange={handleLabelsChange} onChange={handleLabelsChange}
@@ -97,4 +114,4 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
); );
}; };
export default React.memo(LabelSelector); export default LabelSelector;
+38 -26
View File
@@ -1,14 +1,14 @@
import { PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useRef } from 'react'; import React, { useEffect } from 'react';
import AutoCompleteItem from './autocomplete-item';
import LabelItem from './label-item'; import LabelItem from './label-item';
import Wrapper from './wrapper'; import Wrapper from './wrapper';
interface LabelSelectorProps { interface LabelSelectorProps {
labels: Record<string, any>; labels: Record<string, any>;
label?: string; label?: string;
btnText?: string; btnText?: string;
isAutoComplete?: boolean;
labelList: Array<{ key: string; value: string }>; labelList: Array<{ key: string; value: string }>;
onLabelListChange: (list: { key: string; value: string }[]) => void; onLabelListChange: (list: { key: string; value: string }[]) => void;
onChange?: (labels: Record<string, any>) => void; onChange?: (labels: Record<string, any>) => void;
@@ -16,6 +16,7 @@ interface LabelSelectorProps {
onBlur?: (e: any, type: string, index: number) => void; onBlur?: (e: any, type: string, index: number) => void;
onDelete?: (index: number) => void; onDelete?: (index: number) => void;
description?: React.ReactNode; description?: React.ReactNode;
disabled?: boolean;
} }
const Inner: React.FC<LabelSelectorProps> = ({ const Inner: React.FC<LabelSelectorProps> = ({
@@ -26,13 +27,17 @@ const Inner: React.FC<LabelSelectorProps> = ({
onPaste, onPaste,
onBlur, onBlur,
onDelete, onDelete,
disabled,
label, label,
btnText, btnText,
description description,
isAutoComplete
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const buttonRef = useRef<HTMLButtonElement>(null);
const boxRef = useRef<HTMLDivElement>(null); useEffect(() => {
console.log('labels changed in Inner', labels);
}, [labels]);
const updateLabels = (list: { key: string; value: string }[]) => { const updateLabels = (list: { key: string; value: string }[]) => {
const newLabels = _.reduce( const newLabels = _.reduce(
@@ -75,11 +80,35 @@ const Inner: React.FC<LabelSelectorProps> = ({
}; };
return ( return (
<Wrapper label={label} description={description}> <Wrapper
label={label}
description={description}
onAdd={handleAddLabel}
disabled={disabled}
btnText={btnText}
>
<> <>
{labelList?.map((item: any, index: number) => { {isAutoComplete
? labelList?.map((item: any, index: number) => {
return (
<AutoCompleteItem
disabled={disabled}
key={index}
label={item}
seperator=":"
labels={labels}
labelList={labelList}
onDelete={() => handleOnDelete(index)}
onChange={(obj) => handleOnChange(index, obj)}
onPaste={(e) => onPaste?.(e, index)}
onBlur={(e: any, type: string) => onBlur?.(e, type, index)}
/>
);
})
: labelList?.map((item: any, index: number) => {
return ( return (
<LabelItem <LabelItem
disabled={disabled}
key={index} key={index}
label={item} label={item}
seperator=":" seperator=":"
@@ -91,26 +120,9 @@ const Inner: React.FC<LabelSelectorProps> = ({
/> />
); );
})} })}
<div className="flex justify-center">
<Button
ref={buttonRef}
type="text"
block
style={{
marginTop: 16,
backgroundColor: 'var(--ant-color-fill-secondary)'
}}
onClick={handleAddLabel}
>
<PlusOutlined className="font-size-14" />{' '}
{intl.formatMessage({
id: btnText || 'common.button.addSelector'
})}
</Button>
</div>
</> </>
</Wrapper> </Wrapper>
); );
}; };
export default React.memo(Inner); export default Inner;
+10 -2
View File
@@ -1,4 +1,5 @@
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import { Global } from '@/config/global';
import { MinusOutlined } from '@ant-design/icons'; import { MinusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
@@ -15,8 +16,9 @@ interface LabelItemProps {
keyAddon?: React.ReactNode; keyAddon?: React.ReactNode;
valueAddon?: React.ReactNode; valueAddon?: React.ReactNode;
seperator?: string; seperator?: string;
onDelete?: () => void;
labelList: { key: string; value: string }[]; labelList: { key: string; value: string }[];
disabled?: boolean;
onDelete?: () => void;
onChange?: (params: { key: string; value: string }) => void; onChange?: (params: { key: string; value: string }) => void;
onPaste?: (e: any) => void; onPaste?: (e: any) => void;
onBlur?: (e: any, type: string) => void; onBlur?: (e: any, type: string) => void;
@@ -27,6 +29,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
seperator, seperator,
keyAddon, keyAddon,
valueAddon, valueAddon,
disabled,
onChange, onChange,
onDelete, onDelete,
onPaste, onPaste,
@@ -82,6 +85,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
title={intl.formatMessage({ id: 'resources.table.key.tips' })} title={intl.formatMessage({ id: 'resources.table.key.tips' })}
> >
<SealInput.Input <SealInput.Input
disabled={disabled}
checkStatus="success" checkStatus="success"
label={intl.formatMessage({ id: 'common.input.key' })} label={intl.formatMessage({ id: 'common.input.key' })}
value={label.key} value={label.key}
@@ -96,6 +100,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
<div className="label-value"> <div className="label-value">
{valueAddon ?? ( {valueAddon ?? (
<SealInput.Input <SealInput.Input
trim={false}
disabled={disabled}
checkStatus={label.value ? 'success' : ''} checkStatus={label.value ? 'success' : ''}
label={intl.formatMessage({ id: 'common.input.value' })} label={intl.formatMessage({ id: 'common.input.value' })}
value={label.value} value={label.value}
@@ -104,6 +110,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
></SealInput.Input> ></SealInput.Input>
)} )}
</div> </div>
{!disabled && (
<Button <Button
size="small" size="small"
className="btn" className="btn"
@@ -113,8 +120,9 @@ const LabelItem: React.FC<LabelItemProps> = ({
> >
<MinusOutlined /> <MinusOutlined />
</Button> </Button>
)}
</div> </div>
); );
}; };
export default React.memo(LabelItem); export default LabelItem;
@@ -1,20 +0,0 @@
.wrapper {
position: relative;
padding: 14px;
padding-top: 34px;
border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base);
display: flex;
width: 100%;
flex-direction: column;
:global {
.label {
position: absolute;
left: 16px;
line-height: 1;
top: 12px;
color: var(--ant-color-text-tertiary);
}
}
}
+60 -8
View File
@@ -1,15 +1,56 @@
import LabelInfo from '@/components/seal-form/components/label-info'; import LabelInfo from '@/components/seal-form/components/label-info';
import { PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import React from 'react'; import React from 'react';
import styles from './styles/wrapper.less'; import styled from 'styled-components';
const Wrapper: React.FC<{ interface WrapperProps {
label?: React.ReactNode; label?: React.ReactNode;
description?: React.ReactNode; description?: React.ReactNode;
labelExtra?: React.ReactNode; labelExtra?: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
}> = ({ children, label, description, labelExtra, ...rest }) => { btnText?: string;
disabled?: boolean;
onAdd?: () => void;
button?: React.ReactNode;
}
const Container = styled.div`
position: relative;
padding: 14px;
padding-top: 34px;
border: 1px solid var(--ant-color-border);
border-radius: var(--border-radius-base);
display: flex;
width: 100%;
flex-direction: column;
.label {
position: absolute;
left: 16px;
line-height: 1;
top: 12px;
color: var(--ant-color-text-tertiary);
}
`;
const ButtonWrapper = styled.div`
margin-top: 16px;
`;
const Wrapper: React.FC<WrapperProps> = ({
children,
label,
description,
labelExtra,
onAdd,
btnText,
disabled,
button
}) => {
const intl = useIntl();
return ( return (
<div className={styles['wrapper']}> <Container>
{label && ( {label && (
<span className="label"> <span className="label">
<LabelInfo <LabelInfo
@@ -19,10 +60,21 @@ const Wrapper: React.FC<{
></LabelInfo> ></LabelInfo>
</span> </span>
)} )}
{React.isValidElement(children) {children}
? React.cloneElement(children, { ...rest }) {!disabled && (
: children} <ButtonWrapper>
</div> {button || (
<Button variant="filled" color="default" block onClick={onAdd}>
<PlusOutlined className="font-size-14" />
{btnText ||
intl.formatMessage({
id: 'common.button.addSelector'
})}
</Button>
)}
</ButtonWrapper>
)}
</Container>
); );
}; };
+5 -7
View File
@@ -8,13 +8,14 @@ interface HintInputProps {
onChange: (value: string) => void; onChange: (value: string) => void;
onBlur?: (e: any) => void; onBlur?: (e: any) => void;
placeholder?: string; placeholder?: string;
trim?: boolean;
sourceOptions?: Global.HintOptions[]; sourceOptions?: Global.HintOptions[];
} }
const matchReg = /[^=]+=[^=]*$/; const matchReg = /[^=]+=[^=]*$/;
const HintInput: React.FC<HintInputProps> = (props) => { const HintInput: React.FC<HintInputProps> = (props) => {
const { value, label, onChange, onBlur, sourceOptions } = props; const { value, label, onChange, onBlur, sourceOptions, trim = true } = props;
const cursorPosRef = React.useRef(0); const cursorPosRef = React.useRef(0);
const contextBeforeCursorRef = React.useRef(''); const contextBeforeCursorRef = React.useRef('');
const [options, setOptions] = React.useState< const [options, setOptions] = React.useState<
@@ -70,11 +71,7 @@ const HintInput: React.FC<HintInputProps> = (props) => {
const handleInput = (e: any) => { const handleInput = (e: any) => {
getContextBeforeCursor(e); getContextBeforeCursor(e);
onChange(e.target.value?.trim()); onChange(e.target.value);
};
const handleOnChange = (value: string) => {
onChange(value?.trim());
}; };
const handleOnSelect = (value: string) => { const handleOnSelect = (value: string) => {
@@ -93,9 +90,10 @@ const HintInput: React.FC<HintInputProps> = (props) => {
onBlur={onBlur} onBlur={onBlur}
label={label} label={label}
options={options} options={options}
trim={trim}
style={{ width: '100%' }} style={{ width: '100%' }}
/> />
); );
}; };
export default React.memo(HintInput); export default HintInput;
+13 -29
View File
@@ -1,6 +1,3 @@
import { PlusOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import Wrapper from '../label-selector/wrapper'; import Wrapper from '../label-selector/wrapper';
@@ -8,19 +5,19 @@ import ListItem from './list-item';
interface ListInputProps { interface ListInputProps {
dataList: string[]; dataList: string[];
label: React.ReactNode; label?: React.ReactNode;
description?: React.ReactNode; description?: React.ReactNode;
btnText?: string; btnText?: string;
options?: Global.HintOptions[]; options?: Global.HintOptions[];
placeholder?: string; placeholder?: string;
labelExtra?: React.ReactNode; labelExtra?: React.ReactNode;
trim?: boolean;
onChange: (data: string[]) => void; onChange: (data: string[]) => void;
onBlur?: (e: any, index: number) => void; onBlur?: (e: any, index: number) => void;
onDelete?: (index: number) => void; onDelete?: (index: number) => void;
} }
const ListInput: React.FC<ListInputProps> = (props) => { const ListInput: React.FC<ListInputProps> = (props) => {
const intl = useIntl();
const { const {
dataList, dataList,
label, label,
@@ -30,11 +27,11 @@ const ListInput: React.FC<ListInputProps> = (props) => {
onDelete, onDelete,
btnText, btnText,
options, options,
labelExtra labelExtra,
trim = true
} = props; } = props;
const [list, setList] = React.useState<{ value: string; uid: number }[]>([]); const [list, setList] = React.useState<{ value: string; uid: number }[]>([]);
const countRef = React.useRef(0); const countRef = React.useRef(0);
const buttonRef = React.useRef<HTMLButtonElement>(null);
const updateCountRef = () => { const updateCountRef = () => {
countRef.current = countRef.current + 1; countRef.current = countRef.current + 1;
@@ -65,9 +62,6 @@ const ListInput: React.FC<ListInputProps> = (props) => {
uid: countRef.current uid: countRef.current
}); });
setList(values); setList(values);
// setTimeout(() => {
// buttonRef.current?.scrollIntoView?.({ behavior: 'smooth' });
// }, 100);
}; };
React.useEffect(() => { React.useEffect(() => {
@@ -85,7 +79,13 @@ const ListInput: React.FC<ListInputProps> = (props) => {
}, [dataList]); }, [dataList]);
return ( return (
<Wrapper label={label} description={description} labelExtra={labelExtra}> <Wrapper
label={label}
description={description}
labelExtra={labelExtra}
onAdd={handleOnAdd}
btnText={btnText}
>
<> <>
{_.map(list, (item: any, index: number) => { {_.map(list, (item: any, index: number) => {
return ( return (
@@ -97,29 +97,13 @@ const ListInput: React.FC<ListInputProps> = (props) => {
onBlur={(e) => onBlur?.(e, index)} onBlur={(e) => onBlur?.(e, index)}
onRemove={() => handleOnRemove(index)} onRemove={() => handleOnRemove(index)}
onChange={(val) => handleOnChange(val, index)} onChange={(val) => handleOnChange(val, index)}
trim={trim}
/> />
); );
})} })}
<div className="flex justify-center">
<Button
ref={buttonRef}
variant="filled"
color="default"
block
style={{
marginTop: 16
}}
onClick={handleOnAdd}
>
<PlusOutlined className="font-size-14" />{' '}
{intl.formatMessage({
id: btnText
})}
</Button>
</div>
</> </>
</Wrapper> </Wrapper>
); );
}; };
export default React.memo(ListInput); export default ListInput;
+12 -2
View File
@@ -13,10 +13,19 @@ interface LabelItemProps {
label?: string; label?: string;
placeholder?: string; placeholder?: string;
options?: Global.HintOptions[]; options?: Global.HintOptions[];
trim?: boolean;
} }
const ListItem: React.FC<LabelItemProps> = (props) => { const ListItem: React.FC<LabelItemProps> = (props) => {
const { onRemove, onChange, onBlur, label, value, options } = props; const {
onRemove,
onChange,
onBlur,
label,
value,
options,
trim = true
} = props;
const handleOnChange = (value: any) => { const handleOnChange = (value: any) => {
onChange(value); onChange(value);
@@ -30,6 +39,7 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
onBlur={onBlur} onBlur={onBlur}
label={label} label={label}
sourceOptions={options} sourceOptions={options}
trim={trim}
placeholder={props.placeholder} placeholder={props.placeholder}
/> />
<Button <Button
@@ -44,4 +54,4 @@ const ListItem: React.FC<LabelItemProps> = (props) => {
); );
}; };
export default React.memo(ListItem); export default ListItem;
+3 -3
View File
@@ -64,9 +64,9 @@ const LogsList: React.FC<LogsListProps> = forwardRef((props, ref) => {
const handleOnWheel = useCallback( const handleOnWheel = useCallback(
(e: any) => { (e: any) => {
const scrollTop = scrollEventElement?.scrollTop; const scrollTop = scrollEventElement?.current.scrollTop;
const scrollHeight = scrollEventElement?.scrollHeight; const scrollHeight = scrollEventElement?.current.scrollHeight;
const clientHeight = scrollEventElement?.clientHeight; const clientHeight = scrollEventElement?.current.clientHeight;
stopScroll.current = scrollTop + clientHeight <= scrollHeight; stopScroll.current = scrollTop + clientHeight <= scrollHeight;
+14 -7
View File
@@ -1,25 +1,32 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
const useResizeObserver = (ref: React.RefObject<HTMLElement>) => { export const useResizeObserver = (ref: React.RefObject<HTMLElement>) => {
const [size, setSize] = useState({ width: 0, height: 0 }); const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => { useEffect(() => {
const element = ref.current; const element = ref.current;
if (!element) return; if (!element) return;
const observer = new ResizeObserver((entries) => { const updateSize = () => {
if (entries[0]) { const rect = element.getBoundingClientRect();
const { width, height } = entries[0].contentRect; setSize((prev) => {
setSize({ width, height }); if (prev.width === rect.width && prev.height === rect.height)
} return prev;
return { width: rect.width, height: rect.height };
});
};
const observer = new ResizeObserver(() => {
updateSize();
}); });
observer.observe(element); observer.observe(element);
updateSize();
return () => { return () => {
observer.disconnect(); observer.disconnect();
}; };
}, [ref]); }, [ref.current]);
return size; return size;
}; };
@@ -1,4 +1,5 @@
import useSetChunkFetch from '@/hooks/use-chunk-fetch'; import useSetChunkFetch from '@/hooks/use-chunk-fetch';
import { useMemoizedFn } from 'ahooks';
import { Spin } from 'antd'; import { Spin } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
@@ -177,7 +178,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
}); });
}; };
const handleOnScroll = useCallback( const handleOnScroll = useMemoizedFn(
async (data: { isTop: boolean; isBottom: boolean }) => { async (data: { isTop: boolean; isBottom: boolean }) => {
const { isTop, isBottom } = data; const { isTop, isBottom } = data;
setIsAtTop(isTop); setIsAtTop(isTop);
@@ -225,17 +226,7 @@ const LogsViewer: React.FC<LogsViewerProps> = forwardRef((props, ref) => {
} else if (isBottom && page < totalPage) { } else if (isBottom && page < totalPage) {
// getNextPage(); // getNextPage();
} }
}, }
[
loading,
logs.length,
pageSize,
enableScorllLoad,
page,
totalPage,
setScrollPos,
createChunkConnection
]
); );
const debouncedScroll = useCallback( const debouncedScroll = useCallback(
+9 -4
View File
@@ -20,8 +20,9 @@ export const OverlayScroller: React.FC<
maxHeight?: number; maxHeight?: number;
style?: React.CSSProperties; style?: React.CSSProperties;
children: React.ReactNode; children: React.ReactNode;
onScroll?: (e: React.UIEvent<HTMLDivElement, UIEvent>) => void;
} }
> = ({ children, maxHeight, scrollbars, oppositeTheme, style }) => { > = ({ children, maxHeight, scrollbars, oppositeTheme, style, onScroll }) => {
const scroller = React.useRef<any>(null); const scroller = React.useRef<any>(null);
const { initialize } = useOverlayScroller({ const { initialize } = useOverlayScroller({
options: { options: {
@@ -43,6 +44,7 @@ export const OverlayScroller: React.FC<
className="overlay-scroller-wrapper" className="overlay-scroller-wrapper"
$maxHeight={maxHeight || 200} $maxHeight={maxHeight || 200}
hidden={false} hidden={false}
onWheel={onScroll}
style={{ style={{
paddingInlineStart: 8, paddingInlineStart: 8,
paddingInlineEnd: 8, paddingInlineEnd: 8,
@@ -75,12 +77,15 @@ export const TooltipOverlayScroller: React.FC<
scrollbars, scrollbars,
oppositeTheme oppositeTheme
}) => { }) => {
const { overlayInnerStyle, ...rest } = toolTipProps || {}; const { styles, ...rest } = toolTipProps || {};
return ( return (
<Tooltip <Tooltip
overlayInnerStyle={{ styles={{
...styles,
body: {
paddingInline: 0, paddingInline: 0,
...overlayInnerStyle ...styles?.body
}
}} }}
title={ title={
title && ( title && (
+38 -24
View File
@@ -3,11 +3,13 @@ import {
DeleteOutlined, DeleteOutlined,
DownOutlined, DownOutlined,
PlusOutlined, PlusOutlined,
SearchOutlined,
SyncOutlined SyncOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Input, Select, Space } from 'antd'; import { Button, Input, Space } from 'antd';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import BaseSelect from '../seal-form/base/select';
import './index.less'; import './index.less';
type PageToolsProps = { type PageToolsProps = {
@@ -62,7 +64,7 @@ interface FilterBarProps {
actionItems?: ActionItem[]; actionItems?: ActionItem[];
selectOptions?: Global.BaseOption<string | number>[]; selectOptions?: Global.BaseOption<string | number>[];
showSelect?: boolean; showSelect?: boolean;
buttonText: string; buttonText?: string;
buttonIcon?: React.ReactNode; buttonIcon?: React.ReactNode;
marginBottom?: number; marginBottom?: number;
marginTop?: number; marginTop?: number;
@@ -71,6 +73,8 @@ interface FilterBarProps {
actionType?: 'dropdown' | 'button'; actionType?: 'dropdown' | 'button';
showPrimaryButton?: boolean; showPrimaryButton?: boolean;
showDeleteButton?: boolean; showDeleteButton?: boolean;
right?: React.ReactNode;
left?: React.ReactNode;
width?: { width?: {
input?: number; input?: number;
select?: number; select?: number;
@@ -82,10 +86,10 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
handleInputChange, handleInputChange,
handleSelectChange, handleSelectChange,
handleSearch, handleSearch,
handleDeleteByBatch, handleDeleteByBatch = null,
handleClickPrimary, handleClickPrimary = null,
rowSelection, rowSelection,
actionItems, actionItems = [],
selectOptions, selectOptions,
showSelect, showSelect,
buttonText, buttonText,
@@ -93,21 +97,22 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
actionType = 'button', actionType = 'button',
marginBottom = 10, marginBottom = 10,
marginTop = 10, marginTop = 10,
inputHolder = 'common.filter.name', inputHolder,
selectHolder, selectHolder,
showPrimaryButton = true, right,
showDeleteButton = true, left,
width width
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
const renderRight = useMemo(() => { const renderRight = useMemo(() => {
if (!showPrimaryButton && !showDeleteButton) { if (!handleClickPrimary && !handleDeleteByBatch) {
return null; return null;
} }
return ( return (
<Space size={20}> <Space size={20}>
{actionType === 'dropdown' ? ( {handleClickPrimary ? (
actionType === 'dropdown' ? (
<DropDownActions <DropDownActions
menu={{ menu={{
items: actionItems, items: actionItems,
@@ -130,20 +135,23 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
> >
{buttonText} {buttonText}
</Button> </Button>
)} )
) : null}
{handleDeleteByBatch && (
<Button <Button
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
danger danger
onClick={handleDeleteByBatch} onClick={handleDeleteByBatch}
disabled={!rowSelection.selectedRowKeys.length} disabled={!rowSelection?.selectedRowKeys?.length}
> >
<span> <span>
{intl?.formatMessage?.({ id: 'common.button.delete' })} {intl?.formatMessage?.({ id: 'common.button.delete' })}
{rowSelection.selectedRowKeys.length > 0 && ( {rowSelection?.selectedRowKeys?.length > 0 && (
<span>({rowSelection.selectedRowKeys?.length})</span> <span>({rowSelection?.selectedRowKeys?.length})</span>
)} )}
</span> </span>
</Button> </Button>
)}
</Space> </Space>
); );
}, [ }, [
@@ -157,29 +165,35 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
return ( return (
<PageTools <PageTools
marginBottom={marginBottom} marginBottom={marginBottom}
marginTop={marginTop} marginTop={0}
left={ left={
<Space> <Space>
<Input <Input
placeholder={intl.formatMessage({ prefix={
id: inputHolder <SearchOutlined
})} style={{ color: 'var(--ant-color-text-placeholder)' }}
></SearchOutlined>
}
placeholder={
inputHolder ||
intl.formatMessage({
id: 'common.filter.name'
})
}
style={{ width: width?.input || 230 }} style={{ width: width?.input || 230 }}
allowClear allowClear
onChange={handleInputChange} onChange={handleInputChange}
></Input> ></Input>
{showSelect && ( {showSelect && (
<Select <BaseSelect
allowClear allowClear
showSearch={false} showSearch={false}
placeholder={intl.formatMessage({ placeholder={selectHolder}
id: selectHolder
})}
style={{ width: width?.select || 230 }} style={{ width: width?.select || 230 }}
size="large" size="large"
onChange={handleSelectChange} onChange={handleSelectChange}
options={selectOptions} options={selectOptions}
></Select> ></BaseSelect>
)} )}
<Button <Button
type="text" type="text"
@@ -189,7 +203,7 @@ export const FilterBar: React.FC<FilterBarProps> = (props) => {
></Button> ></Button>
</Space> </Space>
} }
right={renderRight} right={right || renderRight}
></PageTools> ></PageTools>
); );
}; };
+5 -1
View File
@@ -65,7 +65,11 @@ const RenderProgress = memo(
title={label} title={label}
open={open} open={open}
onOpenChange={setOpen} onOpenChange={setOpen}
overlayInnerStyle={{ paddingInline: 12 }} styles={{
body: {
paddingInline: 12
}
}}
> >
{renderProgress} {renderProgress}
</Tooltip> </Tooltip>
+30 -4
View File
@@ -1,15 +1,41 @@
import { useEscHint } from '@/hooks/use-esc-hint'; import { useEscHint } from '@/hooks/use-esc-hint';
import { Drawer, type DrawerProps } from 'antd'; import { CloseOutlined } from '@ant-design/icons';
import { Button, Drawer, type DrawerProps } from 'antd';
const ScrollerModal = (props: DrawerProps) => { const ScrollerModal = (props: DrawerProps) => {
const { title, closable = true, maskClosable = false, ...restProps } = props;
const { EscHint } = useEscHint({ const { EscHint } = useEscHint({
enabled: !props.keyboard && props.open enabled: !props.keyboard && props.open
}); });
const handleCancel = (e: React.MouseEvent<HTMLElement>) => {
props.onClose?.(e);
};
return ( return (
<> <>
<Drawer {...props}> <Drawer
{props.children} {...restProps}
closable={false}
maskClosable={maskClosable}
title={
<div className="flex-between flex-center">
<span
style={{
color: 'var(--ant-color-text)',
fontWeight: 'var(--font-weight-medium)',
fontSize: 'var(--font-size-middle)'
}}
>
{title}
</span>
{closable && (
<Button type="text" size="small" onClick={handleCancel}>
<CloseOutlined></CloseOutlined>
</Button>
)}
</div>
}
>
{restProps.children}
{EscHint} {EscHint}
</Drawer> </Drawer>
</> </>
+68 -2
View File
@@ -1,9 +1,22 @@
import useBodyScroll from '@/hooks/use-body-scroll'; import useBodyScroll from '@/hooks/use-body-scroll';
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import { Modal, type ModalProps } from 'antd'; import { Modal, type ModalProps } from 'antd';
import React from 'react'; import React from 'react';
import styled from 'styled-components';
const ScrollerModal = (props: ModalProps) => { const Wrapper = styled.div<{ $maxHeight?: number | string }>`
max-height: ${({ $maxHeight }) =>
typeof $maxHeight === 'number' ? `${$maxHeight}px` : $maxHeight};
overflow-y: auto;
width: 100%;
`;
const ScrollerModal = (
props: ModalProps & { maxContentHeight?: number | string }
) => {
const scroller = React.useRef<any>(null);
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll(); const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const { initialize, destroyInstance } = useOverlayScroller();
React.useEffect(() => { React.useEffect(() => {
if (props.open) { if (props.open) {
@@ -13,7 +26,60 @@ const ScrollerModal = (props: ModalProps) => {
} }
}, [props.open]); }, [props.open]);
return <Modal {...props} />; // init scroller, delay to ensure modal is fully open
React.useEffect(() => {
let timeout = null;
if (props.open) {
timeout = setTimeout(() => {
if (scroller.current) {
initialize(scroller.current);
}
}, 100);
}
return () => {
if (timeout) {
clearTimeout(timeout);
}
destroyInstance();
};
}, [props.open, initialize]);
return (
<Modal
{...props}
destroyOnHidden={true}
styles={{
content: {
padding: 0
},
header: {
padding: 'var(--ant-modal-content-padding)',
paddingBottom: '0'
},
body: {
padding: '0',
paddingBlockEnd: props.footer ? '0' : '24px'
},
footer: props.footer
? {
padding: '12px 24px 24px',
margin: '0'
}
: {}
}}
>
<Wrapper
ref={scroller}
data-overlayscrollbars-initialize
className="overlay-scroller-wrapper"
$maxHeight={props.maxContentHeight || 500}
hidden={false}
style={{ paddingInline: 24, paddingBlockEnd: 0 }}
>
{props.children}
</Wrapper>
</Modal>
);
}; };
export default ScrollerModal; export default ScrollerModal;
+50 -7
View File
@@ -1,12 +1,20 @@
import { AutoComplete, Form, Spin } from 'antd'; import { LoadingOutlined } from '@ant-design/icons';
import { AutoComplete, Form, Typography } from 'antd';
import type { AutoCompleteProps } from 'antd/lib'; import type { AutoCompleteProps } from 'antd/lib';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { LoadingContent } from './components/not-found-content';
import { SealFormItemProps } from './types'; import { SealFormItemProps } from './types';
import Wrapper from './wrapper'; import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select'; import SelectWrapper from './wrapper/select';
const Link = Typography.Link;
const SealAutoComplete: React.FC< const SealAutoComplete: React.FC<
AutoCompleteProps & SealFormItemProps & { onInput?: (e: Event) => void } AutoCompleteProps &
SealFormItemProps & {
onInput?: (e: Event) => void;
clearSpaceOnBlur?: boolean;
}
> = (props) => { > = (props) => {
const { const {
label, label,
@@ -17,10 +25,13 @@ const SealAutoComplete: React.FC<
trim = true, trim = true,
onSelect, onSelect,
onBlur, onBlur,
checkStatus,
extra, extra,
style, style,
addAfter, addAfter,
loading, loading,
allowClear,
clearSpaceOnBlur,
...rest ...rest
} = props; } = props;
const [isFocus, setIsFocus] = useState(false); const [isFocus, setIsFocus] = useState(false);
@@ -45,6 +56,7 @@ const SealAutoComplete: React.FC<
}; };
const handleChange = (val: string, option: any) => { const handleChange = (val: string, option: any) => {
console.log('handleChange val:', val);
let value = val; let value = val;
if (trim) { if (trim) {
value = value?.trim?.(); value = value?.trim?.();
@@ -61,7 +73,13 @@ const SealAutoComplete: React.FC<
if (!props.value) { if (!props.value) {
setIsFocus(false); setIsFocus(false);
} }
e.target.value = e.target.value?.trim?.();
if (clearSpaceOnBlur) {
e.target.value = e.target.value?.replace(/\s+/g, '');
props.onChange?.(e.target.value);
} else {
e.target.value = e.target.value?.trim();
}
props.onBlur?.(e); props.onBlur?.(e);
}; };
@@ -72,29 +90,48 @@ const SealAutoComplete: React.FC<
const handleOnSelect = (value: any, option: any) => { const handleOnSelect = (value: any, option: any) => {
onSelect?.(value, option); onSelect?.(value, option);
}; };
const handleOnInput = (e: any) => {
if (trim) {
e.target.value = e.target.value?.trim();
}
props.onInput?.(e);
};
const renderAfter = () => { const renderAfter = () => {
if (loading) { if (loading) {
return <Spin size="small"></Spin>; return (
<Link>
<LoadingOutlined />
</Link>
);
} }
return addAfter; return null;
};
const popupRender = (originNode: React.ReactElement): React.ReactElement => {
if (loading) {
return <LoadingContent />;
}
return originNode || null;
}; };
return ( return (
<SelectWrapper style={style}> <SelectWrapper style={style}>
<Wrapper <Wrapper
className="seal-select-wrapper" className="seal-select-wrapper"
status={status} status={checkStatus || status}
extra={extra} extra={extra}
label={label} label={label}
isFocus={isFocus} isFocus={isFocus}
required={required} required={required}
description={description} description={description}
disabled={props.disabled} disabled={props.disabled}
addAfter={renderAfter()}
onClick={handleClickWrapper} onClick={handleClickWrapper}
> >
<AutoComplete <AutoComplete
{...rest} {...rest}
trim={trim}
ref={inputRef} ref={inputRef}
placeholder={ placeholder={
isFocus || !label ? ( isFocus || !label ? (
@@ -103,11 +140,17 @@ const SealAutoComplete: React.FC<
'' ''
) )
} }
allowClear={!loading && allowClear}
suffixIcon={renderAfter()}
// @ts-ignore
status={checkStatus || status}
onSelect={handleOnSelect} onSelect={handleOnSelect}
onFocus={handleOnFocus} onFocus={handleOnFocus}
onBlur={handleOnBlur} onBlur={handleOnBlur}
onSearch={handleSearch} onSearch={handleSearch}
onChange={handleChange} onChange={handleChange}
popupRender={popupRender}
onInput={handleOnInput}
></AutoComplete> ></AutoComplete>
</Wrapper> </Wrapper>
</SelectWrapper> </SelectWrapper>
+69
View File
@@ -0,0 +1,69 @@
import IconFont from '@/components/icon-font';
import type { SelectProps } from 'antd';
import { Select } from 'antd';
import React, { forwardRef, useImperativeHandle } from 'react';
import styled from 'styled-components';
import NotFoundContent from '../components/not-found-content';
const Footer = styled.div`
color: var(--ant-color-text-tertiary);
margin-top: 8px;
margin-bottom: 0;
padding: 8px 12px;
border-top: 1px solid var(--ant-color-split);
`;
const BaseSelect: React.FC<
SelectProps & { ref?: any; footer?: React.ReactNode }
> = forwardRef((props, ref) => {
const { notFoundContent, loading, ...restProps } = props;
const [isFocus, setIsFocus] = React.useState(false);
const inputRef = React.useRef<any>(null);
useImperativeHandle(ref, () => ({
...(inputRef.current || ({} as any))
}));
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
setIsFocus(true);
props.onFocus?.(e);
};
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
setIsFocus(false);
props.onBlur?.(e);
};
const renderSuffixIcon = () => {
if (props.suffixIcon) {
return props.suffixIcon;
}
if (!props.showSearch) {
return <IconFont type="icon-down"></IconFont>;
}
return !isFocus ? <IconFont type="icon-down"></IconFont> : undefined;
};
return (
<Select
{...restProps}
notFoundContent={
<NotFoundContent loading={loading} notFoundContent={notFoundContent} />
}
ref={inputRef}
onFocus={handleFocus}
onBlur={handleBlur}
suffixIcon={renderSuffixIcon()}
popupRender={
props.footer
? (originNode) => (
<>
{originNode}
{props.footer && <Footer>{props.footer}</Footer>}
</>
)
: undefined
}
/>
);
});
export default BaseSelect;
@@ -0,0 +1,29 @@
import { QuestionCircleOutlined } from '@ant-design/icons';
import { Checkbox, CheckboxProps, Tooltip } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React from 'react';
const CheckboxField: React.FC<
{
description?: React.ReactNode;
label: React.ReactNode;
checked?: boolean;
onChange?: (e: CheckboxChangeEvent) => void;
} & CheckboxProps
> = ({ description, label, checked, onChange, ...rest }) => {
return (
<Checkbox className="p-l-6" checked={checked} onChange={onChange} {...rest}>
<Tooltip title={description || false}>
<span style={{ color: 'var(--ant-color-text-tertiary)' }}>{label}</span>
{!!description && (
<QuestionCircleOutlined
className="m-l-4"
style={{ color: 'var(--ant-color-text-tertiary)' }}
/>
)}
</Tooltip>
</Checkbox>
);
};
export default CheckboxField;
@@ -5,12 +5,18 @@
color: var(--ant-color-text-tertiary); color: var(--ant-color-text-tertiary);
font-size: var(--font-size-base); font-size: var(--font-size-base);
> span {
display: flex;
align-items: center;
}
.note-info { .note-info {
margin-left: 4px; margin-left: 4px;
display: flex;
align-items: center;
} }
.star { .star {
position: relative; font-size: 0.7rem;
top: 2px;
} }
} }
@@ -14,7 +14,7 @@ const NoteInfo: React.FC<NoteInfoProps> = (props) => {
if (!label) return null; if (!label) return null;
const renderRequiredStar = required ? ( const renderRequiredStar = required ? (
<span className="star" style={{ color: 'red' }}> <span className="star" style={{ color: 'red' }}>
*
</span> </span>
) : null; ) : null;
@@ -41,11 +41,7 @@ const NoteInfo: React.FC<NoteInfoProps> = (props) => {
return ( return (
<span className="label-text"> <span className="label-text">
{description ? ( <Tooltip title={description || false}>{labelContent}</Tooltip>
<Tooltip title={description}>{labelContent}</Tooltip>
) : (
labelContent
)}
{labelExtra} {labelExtra}
</span> </span>
); );
@@ -0,0 +1,47 @@
import { LoadingOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Empty, Typography } from 'antd';
import React from 'react';
import styled from 'styled-components';
const { Link } = Typography;
const NoContent = styled.div`
display: flex;
justify-content: center;
align-items: center;
padding-block: 12px;
`;
export const LoadingContent: React.FC = () => {
return (
<NoContent>
<Link>
<LoadingOutlined />
</Link>
</NoContent>
);
};
const NotFoundContent: React.FC<{
loading?: boolean;
notFoundContent: React.ReactNode;
}> = ({ loading, notFoundContent }) => {
const intl = useIntl();
if (loading) {
return <LoadingContent />;
}
return (
<NoContent>
{notFoundContent || (
<Empty
style={{ marginBlock: 0 }}
description={intl.formatMessage({ id: 'common.data.empty' })}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)}
</NoContent>
);
};
export default NotFoundContent;
@@ -1,4 +1,5 @@
import { Checkbox } from 'antd'; import { Checkbox } from 'antd';
import AutoComplete from '../auto-complete';
import SealInput from '../seal-input'; import SealInput from '../seal-input';
import SealSelect from '../seal-select'; import SealSelect from '../seal-select';
import Slider from '../seal-slider'; import Slider from '../seal-slider';
@@ -12,6 +13,7 @@ const components: {
Input: typeof SealInput.Input; Input: typeof SealInput.Input;
Checkbox: typeof Checkbox; Checkbox: typeof Checkbox;
Switch: typeof Switch; Switch: typeof Switch;
AutoComplete: typeof AutoComplete;
} = { } = {
InputNumber: SealInput.Number, InputNumber: SealInput.Number,
Select: SealSelect, Select: SealSelect,
@@ -19,7 +21,8 @@ const components: {
TextArea: SealInput.TextArea, TextArea: SealInput.TextArea,
Input: SealInput.Input, Input: SealInput.Input,
Checkbox: Checkbox, Checkbox: Checkbox,
Switch: Switch Switch: Switch,
AutoComplete: AutoComplete
}; };
export default components; export default components;
@@ -23,6 +23,8 @@ const SealInputNumber: React.FC<InputNumberProps & SealFormItemProps> = (
if (isInFormItems) { if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.(); const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || ''; status = statusData?.status || '';
} else {
status = props.status || '';
} }
useEffect(() => { useEffect(() => {
@@ -0,0 +1,121 @@
import { isNotEmptyValue } from '@/utils/index';
import { useIntl } from '@umijs/max';
import type { SelectProps } from 'antd';
import { Form } from 'antd';
import { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import SimpleSelect from './simple-select';
import { SealFormItemProps } from './types';
import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select';
const SealSelect: React.FC<
SelectProps & SealFormItemProps & { showTags?: boolean }
> = (props) => {
const {
label,
placeholder,
children,
required,
description,
options,
allowNull,
isInFormItems = true,
notFoundContent = null,
...rest
} = props;
const intl = useIntl();
const [isFocus, setIsFocus] = useState(false);
const inputRef = useRef<any>(null);
let status = '';
// the status can be controlled by Form.Item
if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || '';
} else {
status = props.status || '';
}
const _options = useMemo(() => {
if (!options?.length) {
return [];
}
const list = cloneDeep(options);
return list.map((item: any) => {
if (item.locale) {
item.label = intl.formatMessage({ id: item.label as string });
}
return item;
});
}, [options, intl]);
useEffect(() => {
if (
isNotEmptyValue(props.value) ||
(allowNull && (props.value === null || props.value === undefined))
) {
setIsFocus(true);
}
}, [props.value, allowNull]);
const handleClickWrapper = () => {
if (!props.disabled && !isFocus) {
inputRef.current?.focus?.();
setIsFocus(true);
}
};
const handleChange = (val: any, options: any) => {
if (isNotEmptyValue(val) || (allowNull && val === null)) {
setIsFocus(true);
} else {
setIsFocus(false);
}
props.onChange?.(val, options);
};
const handleOnFocus = (e: any) => {
setIsFocus(true);
props.onFocus?.(e);
};
const handleOnBlur = (e: any) => {
if (allowNull && props.value === null) {
setIsFocus(true);
} else if (!props.value) {
setIsFocus(false);
}
props.onBlur?.(e);
};
return (
<SelectWrapper>
<Wrapper
className="seal-select-wrapper"
status={status}
label={label}
isFocus={isFocus}
required={required}
description={description}
disabled={props.disabled}
onClick={handleClickWrapper}
>
<SimpleSelect
{...rest}
ref={inputRef}
options={children ? null : _options}
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onChange={handleChange}
notFoundContent={notFoundContent}
>
{children}
</SimpleSelect>
</Wrapper>
</SelectWrapper>
);
};
export default SealSelect;
+1
View File
@@ -64,6 +64,7 @@ const SealPassword: React.FC<InputProps & SealFormItemProps> = (props) => {
required={required} required={required}
description={description} description={description}
disabled={props.disabled} disabled={props.disabled}
labelExtra={props.labelExtra}
hasPrefix={!!props.prefix} hasPrefix={!!props.prefix}
onClick={handleClickWrapper} onClick={handleClickWrapper}
> >
+14 -7
View File
@@ -1,3 +1,4 @@
import IconFont from '@/components/icon-font';
import { isNotEmptyValue } from '@/utils/index'; import { isNotEmptyValue } from '@/utils/index';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import type { CascaderAutoProps } from 'antd'; import type { CascaderAutoProps } from 'antd';
@@ -20,10 +21,10 @@ const tag = (props: any) => {
const renderTag = (props: any) => { const renderTag = (props: any) => {
return ( return (
<AutoTooltip <AutoTooltip
className="m-r-4"
closable={props.closable} closable={props.closable}
onClose={props.onClose} onClose={props.onClose}
maxWidth={240} maxWidth={240}
style={{ marginRight: 4 }}
filled filled
> >
{tag(props)} {tag(props)}
@@ -33,10 +34,11 @@ const renderTag = (props: any) => {
const OptionNodes = (props: { const OptionNodes = (props: {
data: any; data: any;
notFoundContent?: React.ReactNode;
optionNode: React.FC<{ data: any }>; optionNode: React.FC<{ data: any }>;
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { data, optionNode: OptionNode } = props; const { data, optionNode: OptionNode, notFoundContent } = props;
if (data.value === '__EMPTY__') { if (data.value === '__EMPTY__') {
return ( return (
<Empty <Empty
@@ -48,15 +50,17 @@ const OptionNodes = (props: {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center' alignItems: 'center'
}} }}
description={intl.formatMessage({ description={
notFoundContent ||
intl.formatMessage({
id: 'common.search.empty' id: 'common.search.empty'
})} })
}
></Empty> ></Empty>
); );
} }
let width: any = { let width: any = {
maxWidth: 140, maxWidth: 140
minWidth: 140
}; };
if (!data.parent) { if (!data.parent) {
width = undefined; width = undefined;
@@ -85,6 +89,7 @@ const SealCascader: React.FC<
allowNull, allowNull,
isInFormItems = true, isInFormItems = true,
optionNode, optionNode,
notFoundContent,
tagRender, tagRender,
...rest ...rest
} = props; } = props;
@@ -168,11 +173,13 @@ const SealCascader: React.FC<
> >
<Cascader <Cascader
{...rest} {...rest}
suffixIcon={<IconFont type="icon-down"></IconFont>}
optionRender={ optionRender={
optionNode optionNode
? (data) => ( ? (data) => (
<OptionNodes <OptionNodes
data={data} data={data}
notFoundContent={notFoundContent}
optionNode={optionNode} optionNode={optionNode}
></OptionNodes> ></OptionNodes>
) )
@@ -185,7 +192,7 @@ const SealCascader: React.FC<
onBlur={handleOnBlur} onBlur={handleOnBlur}
onChange={handleChange} onChange={handleChange}
notFoundContent={null} notFoundContent={null}
onDropdownVisibleChange={handleDropdownVisibleChange} onOpenChange={handleDropdownVisibleChange}
></Cascader> ></Cascader>
</Wrapper> </Wrapper>
</SelectWrapper> </SelectWrapper>
@@ -0,0 +1,74 @@
import { Checkbox, Form, type CheckboxProps } from 'antd';
import React from 'react';
import styled from 'styled-components';
import LabelInfo from './components/label-info';
import { SealFormItemProps } from './types';
import Wrapper from './wrapper';
const Inner = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-inline: 14px;
cursor: pointer;
.ant-checkbox-wrapper {
margin-top: 0 !important;
}
`;
interface SealCheckboxProps extends CheckboxProps, SealFormItemProps {}
const SealCheckbox: React.FC<SealCheckboxProps> = (props) => {
const {
label,
checked,
required,
description,
isInFormItems = true,
defaultChecked,
checkStatus,
...rest
} = props;
let status = '';
if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || '';
}
const handleChange = (e: any) => {
props.onChange?.(e);
};
const handleContainerClick = () => {
// Create a synthetic event to toggle the checkbox
const syntheticEvent = {
target: {
checked: !checked
}
};
handleChange(syntheticEvent);
};
return (
<Wrapper
required={required}
status={checkStatus || status}
className="no-focus"
>
<Inner onClick={handleContainerClick}>
<LabelInfo label={label} description={description}></LabelInfo>
<Checkbox
{...rest}
defaultChecked={defaultChecked}
style={{ marginBottom: 0, marginTop: 16, marginInline: 0 }}
checked={checked}
onChange={handleChange}
/>
</Inner>
</Wrapper>
);
};
export default SealCheckbox;
+7 -2
View File
@@ -22,6 +22,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
trim = true, trim = true,
loading, loading,
labelExtra, labelExtra,
style,
...rest ...rest
} = props; } = props;
const [isFocus, setIsFocus] = useState(false); const [isFocus, setIsFocus] = useState(false);
@@ -30,6 +31,8 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
if (isInFormItems) { if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.(); const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || ''; status = statusData?.status || '';
} else {
status = props.status || '';
} }
useEffect(() => { useEffect(() => {
@@ -60,11 +63,13 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
}; };
const handleOnBlur = (e: any) => { const handleOnBlur = (e: any) => {
e.target.value = e.target.value?.trim?.();
if (!inputRef.current?.input?.value) { if (!inputRef.current?.input?.value) {
setIsFocus(false); setIsFocus(false);
} }
if (!trim) {
e.target.value = e.target.value?.trim?.();
props.onChange?.(e); props.onChange?.(e);
}
props.onBlur?.(e); props.onBlur?.(e);
}; };
@@ -73,7 +78,7 @@ const SealInput: React.FC<InputProps & SealFormItemProps> = (props) => {
}; };
return ( return (
<InputWrapper> <InputWrapper style={style}>
<Wrapper <Wrapper
status={checkStatus || status} status={checkStatus || status}
label={label} label={label}
+24 -8
View File
@@ -1,14 +1,18 @@
import { isNotEmptyValue } from '@/utils/index'; import { isNotEmptyValue } from '@/utils/index';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import type { SelectProps } from 'antd'; import type { SelectProps } from 'antd';
import { Form, Select } from 'antd'; import { Form } from 'antd';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import BaseSelect from './base/select';
import NotFoundContent from './components/not-found-content';
import { SealFormItemProps } from './types'; import { SealFormItemProps } from './types';
import Wrapper from './wrapper'; import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select'; import SelectWrapper from './wrapper/select';
const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => { const SealSelect: React.FC<
SelectProps & SealFormItemProps & { footer?: React.ReactNode }
> = (props) => {
const { const {
label, label,
placeholder, placeholder,
@@ -18,18 +22,22 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
options, options,
allowNull, allowNull,
isInFormItems = true, isInFormItems = true,
notFoundContent = null,
loading,
...rest ...rest
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
const [isFocus, setIsFocus] = useState(false); const [isFocus, setIsFocus] = useState(false);
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
const boxRef = useRef<any>(null);
let status = ''; let status = '';
// the status can be controlled by Form.Item // the status can be controlled by Form.Item
if (isInFormItems) { if (isInFormItems) {
const statusData = Form?.Item?.useStatus?.(); const statusData = Form?.Item?.useStatus?.();
status = statusData?.status || ''; status = statusData?.status || '';
} else {
status = props.status || '';
} }
const _options = useMemo(() => { const _options = useMemo(() => {
@@ -46,7 +54,10 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
}, [options, intl]); }, [options, intl]);
useEffect(() => { useEffect(() => {
if (isNotEmptyValue(props.value) || (allowNull && props.value === null)) { if (
isNotEmptyValue(props.value) ||
(allowNull && (props.value === null || props.value === undefined))
) {
setIsFocus(true); setIsFocus(true);
} }
}, [props.value, allowNull]); }, [props.value, allowNull]);
@@ -64,7 +75,7 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
} else { } else {
setIsFocus(false); setIsFocus(false);
} }
props.onChange?.(val, options); props.onChange?.(val || null, options);
}; };
const handleOnFocus = (e: any) => { const handleOnFocus = (e: any) => {
@@ -93,17 +104,22 @@ const SealSelect: React.FC<SelectProps & SealFormItemProps> = (props) => {
disabled={props.disabled} disabled={props.disabled}
onClick={handleClickWrapper} onClick={handleClickWrapper}
> >
<Select <BaseSelect
{...rest} {...rest}
ref={inputRef} ref={inputRef}
options={children ? null : _options} options={children ? null : _options}
onFocus={handleOnFocus} onFocus={handleOnFocus}
onBlur={handleOnBlur} onBlur={handleOnBlur}
onChange={handleChange} onChange={handleChange}
notFoundContent={null} notFoundContent={
<NotFoundContent
loading={loading}
notFoundContent={notFoundContent}
/>
}
> >
{children} {children}
</Select> </BaseSelect>
</Wrapper> </Wrapper>
</SelectWrapper> </SelectWrapper>
); );
+2 -1
View File
@@ -34,6 +34,7 @@ const SealSlider: React.FC<SealSwitchProps> = (props) => {
defaultValue, defaultValue,
checkStatus, checkStatus,
labelWidth, labelWidth,
size = 'small',
...rest ...rest
} = props; } = props;
@@ -57,7 +58,7 @@ const SealSlider: React.FC<SealSwitchProps> = (props) => {
<LabelInfo label={label} description={description}></LabelInfo> <LabelInfo label={label} description={description}></LabelInfo>
<Switch <Switch
{...rest} {...rest}
size="small" size={size}
defaultValue={defaultValue} defaultValue={defaultValue}
style={{ marginBottom: 0, marginTop: 16, marginInline: 0 }} style={{ marginBottom: 0, marginTop: 16, marginInline: 0 }}
value={value} value={value}
+27 -33
View File
@@ -1,23 +1,24 @@
import { Form, Input } from 'antd'; import { Form, Input } from 'antd';
import type { TextAreaProps } from 'antd/es/input/TextArea'; import type { TextAreaProps } from 'antd/es/input/TextArea';
import React, { import React, { useEffect, useMemo, useRef, useState } from 'react';
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { SealFormItemProps } from './types'; import { SealFormItemProps } from './types';
import Wrapper from './wrapper'; import Wrapper from './wrapper';
import InputWrapper from './wrapper/input'; import InputWrapper from './wrapper/input';
const LabelWrapper = styled.div` const LabelWrapper = styled.div.attrs({
className: 'seal-textarea-label'
})`
display: flex;
align-items: center;
justify-content: flex-start;
padding-bottom: 2px;
background-color: var(--ant-color-bg-container); background-color: var(--ant-color-bg-container);
`; `;
interface InputTextareaProps extends TextAreaProps { interface InputTextareaProps extends TextAreaProps {
scaleSize?: boolean; scaleSize?: boolean;
alwaysFocus?: boolean; // it's order to display the placeholder
} }
const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = ( const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
@@ -39,6 +40,7 @@ const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
addAfter, addAfter,
trim, trim,
scaleSize, scaleSize,
alwaysFocus,
...rest ...rest
} = props; } = props;
const [isFocus, setIsFocus] = useState(false); const [isFocus, setIsFocus] = useState(false);
@@ -65,53 +67,43 @@ const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
return focusRows; return focusRows;
}, [props.autoSize, isFocus, scaleSize]); }, [props.autoSize, isFocus, scaleSize]);
const handleClickWrapper = useCallback(() => { const handleClickWrapper = () => {
if (!props.disabled && !isFocus) { if (!props.disabled && !isFocus) {
inputRef.current?.focus?.({ inputRef.current?.focus?.({
cursor: 'all' cursor: 'all'
}); });
setIsFocus(true); setIsFocus(true);
} }
}, [props.disabled, isFocus]); };
const handleChange = useCallback( const handleChange = (e: any) => {
(e: any) => {
onChange?.(e); onChange?.(e);
}, };
[onChange]
);
const handleOnFocus = useCallback( const handleOnFocus = (e: any) => {
(e: any) => {
setIsFocus(true); setIsFocus(true);
onFocus?.(e); onFocus?.(e);
}, };
[onFocus]
);
const handleOnBlur = useCallback( const handleOnBlur = (e: any) => {
(e: any) => {
if (!inputRef.current?.resizableTextArea?.textArea?.value) { if (!inputRef.current?.resizableTextArea?.textArea?.value) {
setIsFocus(false); setIsFocus(false);
onBlur?.(e);
} }
}, onBlur?.(e);
[onBlur, scaleSize] };
);
const handleInput = useCallback( const handleInput = (e: any) => {
(e: any) => {
onInput?.(e); onInput?.(e);
}, };
[onInput]
); console.log('style=========', style, rest);
return ( return (
<InputWrapper> <InputWrapper>
<Wrapper <Wrapper
status={status} status={status}
label={<LabelWrapper>{label}</LabelWrapper>} label={<LabelWrapper>{label}</LabelWrapper>}
isFocus={isFocus} isFocus={alwaysFocus || isFocus}
required={required} required={required}
description={description} description={description}
className="seal-textarea-wrapper" className="seal-textarea-wrapper"
@@ -122,9 +114,11 @@ const SealTextArea: React.FC<InputTextareaProps & SealFormItemProps> = (
> >
<Input.TextArea <Input.TextArea
{...rest} {...rest}
placeholder={placeholder}
spellCheck={rest.spellCheck ?? false}
autoSize={autoSize} autoSize={autoSize}
ref={inputRef} ref={inputRef}
style={{ minHeight: '80px', ...style }} style={{ ...style }}
className="seal-textarea" className="seal-textarea"
onFocus={handleOnFocus} onFocus={handleOnFocus}
onBlur={handleOnBlur} onBlur={handleOnBlur}
+35 -13
View File
@@ -1,10 +1,11 @@
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import type { SelectProps } from 'antd'; import type { SelectProps } from 'antd';
import { Checkbox, Select, Tag } from 'antd'; import { Checkbox, Tag } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox'; import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React, { useEffect } from 'react'; import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import AutoTooltip from '../auto-tooltip'; import AutoTooltip from '../auto-tooltip';
import BaseSelect from './base/select';
const OptionWrapper = styled.div` const OptionWrapper = styled.div`
display: flex; display: flex;
@@ -42,9 +43,10 @@ const TagWrapper = styled(Tag)`
border-radius: 12px; border-radius: 12px;
`; `;
const SimpleSelect: React.FC<SelectProps> = (props) => { const SimpleSelect: React.FC<SelectProps & { ref?: any; showTags?: boolean }> =
forwardRef((props, ref) => {
const intl = useIntl(); const intl = useIntl();
const { options = [], ...restProps } = props; const { options = [], showTags, ...restProps } = props;
const [allSelection, setAllSelection] = React.useState<{ const [allSelection, setAllSelection] = React.useState<{
checked: boolean; checked: boolean;
@@ -55,6 +57,7 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
}); });
const [optionsList, setOptionsList] = React.useState<any[]>(options || []); const [optionsList, setOptionsList] = React.useState<any[]>(options || []);
const selectRef = React.useRef<any>(null); const selectRef = React.useRef<any>(null);
const selectorRef = React.useRef<any>(null);
useEffect(() => { useEffect(() => {
setOptionsList(options || []); setOptionsList(options || []);
@@ -64,7 +67,7 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
const { value, label } = option; const { value, label } = option;
return ( return (
<OptionWrapper> <OptionWrapper>
{restProps.value?.includes(value) ? ( {restProps.value?.includes?.(value) ? (
<Checkbox checked></Checkbox> <Checkbox checked></Checkbox>
) : ( ) : (
<Checkbox></Checkbox> <Checkbox></Checkbox>
@@ -173,6 +176,8 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
return ( return (
<TagWrapper <TagWrapper
bordered={false} bordered={false}
closable={props.closable}
onClose={props.onClose}
style={{ style={{
height: 24, height: 24,
backgroundColor: 'var(--ant-color-fill-tertiary)', backgroundColor: 'var(--ant-color-fill-tertiary)',
@@ -180,7 +185,12 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
}} }}
className="flex-center" className="flex-center"
> >
{intl.formatMessage({ id: 'common.select.count' }, { count: count })} {showTags
? label
: intl.formatMessage(
{ id: 'common.select.count' },
{ count: count }
)}
</TagWrapper> </TagWrapper>
); );
}; };
@@ -234,14 +244,24 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
}; };
}, [selectRef.current]); }, [selectRef.current]);
useImperativeHandle(ref, () => ({
focus: () => {
selectorRef.current?.focus();
},
blur: () => {
selectorRef.current?.blur();
}
}));
return ( return (
<div ref={selectRef}> <div ref={selectRef} style={{ width: 'inherit' }}>
<Select <BaseSelect
{...restProps} {...restProps}
ref={selectorRef}
options={optionsList} options={optionsList}
maxTagCount={0} maxTagCount={restProps.maxTagCount || 0}
defaultActiveFirstOption={false} defaultActiveFirstOption={false}
dropdownRender={dropdownRender} popupRender={dropdownRender}
optionRender={optionRender} optionRender={optionRender}
menuItemSelectedIcon={false} menuItemSelectedIcon={false}
onChange={handleOnChange} onChange={handleOnChange}
@@ -250,10 +270,12 @@ const SimpleSelect: React.FC<SelectProps> = (props) => {
onFocus={handleOnFocus} onFocus={handleOnFocus}
onSearch={handleOnSearch} onSearch={handleOnSearch}
filterOption={filterOption} filterOption={filterOption}
onDropdownVisibleChange={handleOnOpenChange} onOpenChange={handleOnOpenChange}
></Select> >
{props.children}
</BaseSelect>
</div> </div>
); );
}; });
export default SimpleSelect; export default SimpleSelect;
@@ -0,0 +1,28 @@
import styled from 'styled-components';
import { BGCOLOR, INPUT_INNER_PADDING } from '../config';
const AutoCompleteLabel = styled.span`
position: absolute;
left: ${INPUT_INNER_PADDING}px;
height: 20px;
line-height: 20px;
top: 26px;
pointer-events: none;
transition: all 0.2s;
background-color: var(--ant-color-bg-container);
z-index: 10;
&.disabled {
background-color: #f5f5f5;
}
&.isfoucs-has-value {
// display: none;
z-index: -1;
// top: 9px;
// font-size: 12px;
// color: var(--ant-color-text);
// background-color: ${BGCOLOR};
// padding: 0 4px;
}
`;
export default AutoCompleteLabel;
+10 -3
View File
@@ -154,8 +154,11 @@ export const Label = styled.div.attrs<{
`; `;
// inner // inner
const Inner = styled.div` const Inner = styled.div.attrs({
className: '__inner__'
})`
width: 100%; width: 100%;
display: flex;
`; `;
const Extra = styled.div` const Extra = styled.div`
@@ -167,9 +170,13 @@ const Extra = styled.div`
const AddAfter = styled.div` const AddAfter = styled.div`
position: relative; position: relative;
color: var(--ant-color-text-quaternary); color: var(--ant-color-text-tertiary);
font-size: var(--font-size-base); font-size: var(--font-size-base);
padding-right: calc(var(--ant-padding-sm) - 1px); padding-inline: calc(var(--ant-padding-sm) - 1px);
height: 100%;
display: flex;
align-items: center;
background-color: var(--ant-color-fill-secondary);
`; `;
// for wrapper // for wrapper

Some files were not shown because too many files have changed in this diff Show More