Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63fb8c69b6 | ||
|
|
aca6490da1 | ||
|
|
04511a63a3 | ||
|
|
28da5d52e8 | ||
|
|
730cbc9b64 | ||
|
|
2a69ba65cb | ||
|
|
aefb8eadce | ||
|
|
49c80b2ccb | ||
|
|
654eca7544 | ||
|
|
5ee62f267c | ||
|
|
ba0b2bb2c3 | ||
|
|
c3bdbc200e | ||
|
|
63b06e595e | ||
|
|
27210da13a | ||
|
|
831a760246 | ||
|
|
ae06ad7aae | ||
|
|
b3b819a080 | ||
|
|
aa47c626ba | ||
|
|
79e4a60d52 | ||
|
|
d9a467f9f9 | ||
|
|
c6b89f688e | ||
|
|
2164a8198b | ||
|
|
0f0e92379b | ||
|
|
d8cb5261b9 | ||
|
|
e30dd10fe4 | ||
|
|
67e2d914f4 | ||
|
|
938aef6c26 | ||
|
|
eb029a3e2a | ||
|
|
2dd287bbc1 | ||
|
|
3c00317aeb | ||
|
|
06a2777632 | ||
|
|
8f0691395b | ||
|
|
8eb871d0f5 | ||
|
|
6e321d2d99 | ||
|
|
97179365bb | ||
|
|
046d466e16 | ||
|
|
77a2abad32 | ||
|
|
efb63335e7 | ||
|
|
d0b6498bda | ||
|
|
35ed397c2a | ||
|
|
cb380d676e | ||
|
|
03c47530bd | ||
|
|
ab92641541 | ||
|
|
1f861c5844 | ||
|
|
f92499cbe5 | ||
|
|
6a9cba8020 | ||
|
|
4f4b527014 | ||
|
|
0d5812ba88 | ||
|
|
650091b57a | ||
|
|
5bdfb36721 | ||
|
|
0436872093 | ||
|
|
3d88b2825b | ||
|
|
dc54c21d70 | ||
|
|
794ac724ca | ||
|
|
d91fbec9d7 | ||
|
|
555aab714e | ||
|
|
eb037a3613 | ||
|
|
b1efa415e8 | ||
|
|
e79d49f2a8 | ||
|
|
7d9f68da2d | ||
|
|
c691df25c0 | ||
|
|
cd4b1b8629 | ||
|
|
70229a9757 | ||
|
|
644aba3fc2 | ||
|
|
c025257fea | ||
|
|
fdafdfc3d1 | ||
|
|
f18c12662b | ||
|
|
71840cfc8f | ||
|
|
e75beb7324 | ||
|
|
90cc6ec753 | ||
|
|
bd4954f161 | ||
|
|
32fab0fa62 | ||
|
|
d12e92a916 | ||
|
|
dc41b61d69 | ||
|
|
3a1565690d | ||
|
|
1bb9fd73db | ||
|
|
39024b1d46 | ||
|
|
4ecaa0a326 | ||
|
|
e1af27e11c | ||
|
|
f4ebd8d7d1 | ||
|
|
bd58a911d5 | ||
|
|
5321973ebe | ||
|
|
0a1f875590 | ||
|
|
ea2c9870a7 | ||
|
|
23f0e4398a | ||
|
|
b342174fc2 | ||
|
|
d07d3b2c99 | ||
|
|
9d0a5d01b0 | ||
|
|
1e97d29fca | ||
|
|
350f398cde | ||
|
|
8a15172316 | ||
|
|
185e3475e7 | ||
|
|
64720ccbfa | ||
|
|
db17192e55 | ||
|
|
a1dd1ec861 | ||
|
|
a516e6ce72 | ||
|
|
2e375c6603 | ||
|
|
7d94c77c15 | ||
|
|
457d2f2f72 | ||
|
|
5e7d83e5dd | ||
|
|
b1cffe047c | ||
|
|
6509f2a4ff | ||
|
|
f719c11606 | ||
|
|
b8d7873b77 | ||
|
|
a9248a98a5 | ||
|
|
8f85e9a082 | ||
|
|
1a6d1654b2 | ||
|
|
abe705c034 | ||
|
|
8b34f68824 | ||
|
|
e37828a2cd | ||
|
|
4709661f93 | ||
|
|
1d7543f19c | ||
|
|
1cf146ca32 | ||
|
|
b05c510776 | ||
|
|
e79d5f8962 | ||
|
|
5503fdc21e | ||
|
|
36a1038d12 | ||
|
|
ee42a9d0ed | ||
|
|
28976dea4b | ||
|
|
22e5cc3aa5 | ||
|
|
3052c0b23e | ||
|
|
1c9cabf7e7 | ||
|
|
03842d1741 | ||
|
|
84304eadb7 | ||
|
|
ab3739219e | ||
|
|
7cf4932f3a | ||
|
|
032f1eefef | ||
|
|
e35be94f7c | ||
|
|
e76e2ad85a | ||
|
|
cb19cd2476 | ||
|
|
c532732ce9 | ||
|
|
2bc10dbb2e | ||
|
|
b634a03ef9 | ||
|
|
ac64bb98ad | ||
|
|
aa49539210 | ||
|
|
1f5678f781 | ||
|
|
7adc089e94 | ||
|
|
6e1e9c07ee | ||
|
|
1d55382f3b | ||
|
|
e68bb0bc4e | ||
|
|
c8e14dade5 | ||
|
|
e4c1a3f5c2 | ||
|
|
cdd6ef9cb3 | ||
|
|
737d43fd16 | ||
|
|
9f5163531c | ||
|
|
df5cf0f4e5 | ||
|
|
e160b64697 | ||
|
|
7d36c6ac4a | ||
|
|
0820edf55f | ||
|
|
12e324893f | ||
|
|
4017c4dd33 | ||
|
|
17b65d80e1 | ||
|
|
47cb3321ab | ||
|
|
547b9f617b | ||
|
|
9e7bd78693 | ||
|
|
c53d60b628 | ||
|
|
9566e0cdb2 | ||
|
|
ed3e779d22 | ||
|
|
3e2133753d | ||
|
|
736a853192 | ||
|
|
08ef2ffa9a | ||
|
|
6cf512cccd | ||
|
|
6f56e614f3 | ||
|
|
8c06862705 | ||
|
|
48159171c2 | ||
|
|
d5d0749b72 | ||
|
|
dc4dd81e11 | ||
|
|
c736e2186d | ||
|
|
7a76bda484 | ||
|
|
778ea36a9d | ||
|
|
04e1e1e566 | ||
|
|
01f66085ab | ||
|
|
ddf723525a | ||
|
|
7b18a92388 | ||
|
|
b985dcee63 | ||
|
|
77e92432ed | ||
|
|
a7b33a928b | ||
|
|
1218c6d096 | ||
|
|
6337431ed4 | ||
|
|
2ba5118bc2 | ||
|
|
12649c7ccf | ||
|
|
5d96932870 | ||
|
|
e6cfa6cf77 | ||
|
|
55d16c5681 | ||
|
|
ab75685955 | ||
|
|
bd0bf0bf63 | ||
|
|
fd050b33d9 | ||
|
|
698090b17a | ||
|
|
6f96260993 | ||
|
|
98a6b19298 | ||
|
|
ded270ff25 | ||
|
|
c8860207e8 | ||
|
|
8d045ffd8b | ||
|
|
fd7437e74a | ||
|
|
807ba866d3 | ||
|
|
5e2808d7c4 | ||
|
|
a26a3ff8f9 | ||
|
|
a2e9f69b96 | ||
|
|
ac800a303a | ||
|
|
dd542a9f29 | ||
|
|
26ce456bde | ||
|
|
7ce1faaf6c | ||
|
|
878bdaa5a7 | ||
|
|
68a7e3f66a | ||
|
|
feccc81c5f | ||
|
|
620045d7a3 | ||
|
|
6dc28d61b0 | ||
|
|
754cc29793 | ||
|
|
ed525509ea | ||
|
|
2b6f26e6c8 | ||
|
|
09821aab85 | ||
|
|
636190b8e6 | ||
|
|
e420c22a45 | ||
|
|
993f6edcc4 | ||
|
|
bda8264788 | ||
|
|
ab8e792276 | ||
|
|
0e4be63b6b | ||
|
|
d3ebea9090 | ||
|
|
15a9e23243 | ||
|
|
eab7d9c77f | ||
|
|
4a86b71018 | ||
|
|
6350f7f11a | ||
|
|
ce6388253c | ||
|
|
d8f6064d5e | ||
|
|
9f5d95f565 | ||
|
|
0cabbeb81c | ||
|
|
76618aa3a1 | ||
|
|
b19db0f67d | ||
|
|
76f228bfe7 | ||
|
|
be40692773 | ||
|
|
42ac377c17 | ||
|
|
46f859cc3c | ||
|
|
f2bff0d1fb | ||
|
|
0bd98b9662 | ||
|
|
1ac2c02197 | ||
|
|
907187d53a | ||
|
|
faf6246fe6 | ||
|
|
0c16e2be7a | ||
|
|
3212882895 | ||
|
|
62f34ffcaf | ||
|
|
50601b0aff | ||
|
|
4c5d42cb13 | ||
|
|
85268b8917 | ||
|
|
973d2c4529 | ||
|
|
ab766b8c54 | ||
|
|
4945242c87 | ||
|
|
c4a72eb375 | ||
|
|
b12d11728d | ||
|
|
8f0e3a7576 | ||
|
|
4810ff2dbe | ||
|
|
a221b22be9 | ||
|
|
24b458530d | ||
|
|
caea43590a | ||
|
|
c3d7a0b85e | ||
|
|
5c6f28027d | ||
|
|
dc04529f5b | ||
|
|
2bb04a419d | ||
|
|
8e31b71668 | ||
|
|
f055453889 | ||
|
|
820a6cecdd | ||
|
|
c3a779e4c8 | ||
|
|
00008abf03 | ||
|
|
d9c6f2f780 | ||
|
|
9a4d77e869 | ||
|
|
2407416e33 | ||
|
|
234e42ccfa | ||
|
|
4426b8dc30 | ||
|
|
9541f49f39 | ||
|
|
9f7a99f370 | ||
|
|
12f0bb2f98 | ||
|
|
ac45a72e0e | ||
|
|
9705818b15 | ||
|
|
39374eafda | ||
|
|
110bc3205e | ||
|
|
57fec89f2d | ||
|
|
72e794c281 | ||
|
|
fadbcc3e44 | ||
|
|
9e30e964e6 | ||
|
|
fbb707d014 | ||
|
|
d2d82ee5ee | ||
|
|
9061fa08ba | ||
|
|
f29236518e | ||
|
|
6537de5224 | ||
|
|
0413d470a8 | ||
|
|
c01f4d3ce8 | ||
|
|
dd9d359bb0 | ||
|
|
b444c35643 | ||
|
|
abd0d52fad | ||
|
|
c8771dd3b9 | ||
|
|
6d16f56183 | ||
|
|
95ffe0350e | ||
|
|
e314f25a77 | ||
|
|
65ea65e9ae | ||
|
|
4ce975562e | ||
|
|
07e625f2bc | ||
|
|
d95fd8dc90 | ||
|
|
55028cf3a3 | ||
|
|
16a97772d3 | ||
|
|
d33167d070 | ||
|
|
f62e0ba372 | ||
|
|
1b6466ae78 | ||
|
|
29c5987bf1 | ||
|
|
fcd1d01e83 | ||
|
|
a80b889760 | ||
|
|
f71bf1b074 | ||
|
|
7e048ee537 | ||
|
|
34098e8950 | ||
|
|
faf0d4d605 | ||
|
|
85ade88d55 | ||
|
|
7d0b94330d | ||
|
|
5cff1298fc | ||
|
|
d3a3d4e96e | ||
|
|
f99621ec92 | ||
|
|
e47a9304f4 | ||
|
|
9488fc1251 | ||
|
|
b590aa746b | ||
|
|
ef61134b8b | ||
|
|
a9daa0b586 | ||
|
|
ae0ad25ed6 | ||
|
|
a4c48c1981 | ||
|
|
3497590d2e | ||
|
|
11cf1329c8 | ||
|
|
ba5b41cf3a | ||
|
|
1d70446cc4 | ||
|
|
f15d0ab243 | ||
|
|
34be4800d7 | ||
|
|
e40b0c2e07 | ||
|
|
45e0ad9e93 | ||
|
|
96e6ddfe8a | ||
|
|
6b47bacfac | ||
|
|
099b419e34 | ||
|
|
e343ec0a1e | ||
|
|
e3d6d08916 | ||
|
|
d0bc206c83 | ||
|
|
7287f8c81b | ||
|
|
1878753cde | ||
|
|
2f04e01f4f | ||
|
|
a51f44c135 | ||
|
|
7f14eb544c | ||
|
|
c6e34db329 | ||
|
|
acb90531b2 | ||
|
|
91c3d0b814 | ||
|
|
95e03d11c1 | ||
|
|
88ab927755 | ||
|
|
81c7e2ee61 | ||
|
|
b5ed64de51 | ||
|
|
921a2a0d81 | ||
|
|
5195b1e5fc | ||
|
|
f886422f91 | ||
|
|
27d57d7563 | ||
|
|
2ebb8f44b7 | ||
|
|
a0b408e8cb | ||
|
|
858e0dae09 | ||
|
|
7bee337554 | ||
|
|
7944b3f579 | ||
|
|
5e889563ca | ||
|
|
aad3b458c8 | ||
|
|
acef2ef89a | ||
|
|
d99c25dd03 | ||
|
|
f0a1dad48e | ||
|
|
1e334903dd | ||
|
|
342583f8d1 | ||
|
|
6af18275c4 | ||
|
|
9c519d937d | ||
|
|
4b14fa6ca8 | ||
|
|
dc69e51042 | ||
|
|
052d5aa474 | ||
|
|
0c55c1c1b4 | ||
|
|
6f896b4f94 | ||
|
|
e7a376db70 | ||
|
|
15cf4d89b7 | ||
|
|
c38fcc494a | ||
|
|
44e5b78d29 | ||
|
|
38107a67fe | ||
|
|
0274bcb8cd | ||
|
|
a91c1545f8 | ||
|
|
0b3884b1a5 | ||
|
|
bcea46fa8f | ||
|
|
abc3c2c25a | ||
|
|
db7a8a702d | ||
|
|
288bfebc13 | ||
|
|
c7996d3e14 | ||
|
|
0d9b6325f9 | ||
|
|
baf1508644 | ||
|
|
d04604abe5 | ||
|
|
bf09784fad | ||
|
|
67f5c794e1 | ||
|
|
55fbe118d5 | ||
|
|
ae8ec8572c | ||
|
|
38b91a558b | ||
|
|
a001f83abb | ||
|
|
87c8a3a64d | ||
|
|
c6e7075bf5 | ||
|
|
5d75cfdfc5 | ||
|
|
3f9852c932 | ||
|
|
9dfaf990fd | ||
|
|
1bef1637e0 | ||
|
|
451d5bfe47 | ||
|
|
faa1f935b7 | ||
|
|
c428fe175a | ||
|
|
9767c8bf39 | ||
|
|
96b50538ed | ||
|
|
2dae886fd1 | ||
|
|
beb74b3c47 | ||
|
|
d93ee98473 | ||
|
|
f081f0c1cf | ||
|
|
4e49c399d0 | ||
|
|
2527555945 | ||
|
|
5ef671f047 | ||
|
|
10781da086 | ||
|
|
c279a7f186 | ||
|
|
7dd1c9ac5e | ||
|
|
125a5be91f | ||
|
|
e25d48cc89 | ||
|
|
5f96f367a6 | ||
|
|
01fe2e66fb | ||
|
|
1b74285de9 | ||
|
|
b13ba0a229 | ||
|
|
9e0dd5b003 | ||
|
|
4128de61f9 | ||
|
|
fc900c8ce6 | ||
|
|
63ac4e0e75 | ||
|
|
7ee1635833 | ||
|
|
7d0cfeff77 | ||
|
|
7fa44644fd | ||
|
|
e9152e8725 | ||
|
|
7ea7f6666d | ||
|
|
3ae49dc826 | ||
|
|
75d7eff63b | ||
|
|
8618718b71 | ||
|
|
8d17000272 | ||
|
|
df53774628 | ||
|
|
68235e188a | ||
|
|
f37fad11b1 | ||
|
|
93fa277bdd | ||
|
|
ea7fa63079 | ||
|
|
d15db4b4dd | ||
|
|
ed06d57a31 | ||
|
|
168c55990f | ||
|
|
2889ba7c78 | ||
|
|
896b8ef326 | ||
|
|
f08d5c904c | ||
|
|
4be5fb4d3d | ||
|
|
a2d5d55518 | ||
|
|
e1b28b01df | ||
|
|
b6bbe880a0 | ||
|
|
65ad57fc4c | ||
|
|
469e63b898 | ||
|
|
d471233f83 | ||
|
|
01576212a3 | ||
|
|
898c7f1be2 | ||
|
|
705b33e307 | ||
|
|
2592f44bc2 | ||
|
|
9914db6b96 | ||
|
|
bb93ad37a1 | ||
|
|
93bc632088 | ||
|
|
ab89ddffc8 | ||
|
|
7794e57bb1 | ||
|
|
7c15bb6ef6 | ||
|
|
ac99156c90 | ||
|
|
c68f546be9 | ||
|
|
217285223c | ||
|
|
3144328874 | ||
|
|
26880b4612 | ||
|
|
e7aac14ef1 | ||
|
|
52211cd4ea | ||
|
|
331c59b7c1 | ||
|
|
b605b17561 | ||
|
|
33163ac96c | ||
|
|
757af11ab3 | ||
|
|
81a87ef362 | ||
|
|
0cb260f854 | ||
|
|
0ffc2ee924 | ||
|
|
e4992e8313 | ||
|
|
519020077a | ||
|
|
6fccee3711 | ||
|
|
597a86562a | ||
|
|
16449613e0 | ||
|
|
6c161c6ae7 | ||
|
|
48b357b1c6 | ||
|
|
a795544074 | ||
|
|
8711c27b78 | ||
|
|
d48aa99dcc | ||
|
|
5f3b9b88ff | ||
|
|
f566ea3b63 | ||
|
|
8ba77beeed | ||
|
|
128f59ed6f | ||
|
|
c92f56ec8f | ||
|
|
daa3d74500 | ||
|
|
ad7fc3a993 | ||
|
|
084822814d | ||
|
|
3a58bd1eef | ||
|
|
94d3fa356c | ||
|
|
7d75714662 | ||
|
|
b057d52499 | ||
|
|
2d726ded30 | ||
|
|
f45d9bda61 | ||
|
|
684c1ab600 | ||
|
|
3ac2720ba8 | ||
|
|
7b743f35b3 | ||
|
|
e1b2a4aee2 | ||
|
|
308c350582 | ||
|
|
41c9b86fd9 | ||
|
|
46b9855c87 | ||
|
|
c667de90ed | ||
|
|
cf13e3bed0 | ||
|
|
0956538b79 | ||
|
|
f2a4ab2654 | ||
|
|
aeabdab781 | ||
|
|
f04b5cdc27 | ||
|
|
b947e3a8b9 | ||
|
|
ae3148a86f | ||
|
|
78bf76e965 | ||
|
|
7beb5756f7 | ||
|
|
a2b0236ea3 | ||
|
|
144914110a | ||
|
|
c73609d155 | ||
|
|
53cd8ba00c | ||
|
|
1c09cc6c8c | ||
|
|
06160546a6 | ||
|
|
0b292829fa | ||
|
|
392c51fced | ||
|
|
144d5a6d5f | ||
|
|
43dfc7e1f0 | ||
|
|
adde7fba37 | ||
|
|
33fe180997 | ||
|
|
a95b7e9561 | ||
|
|
08330e0b82 | ||
|
|
c74118e913 | ||
|
|
bb3cef89c1 | ||
|
|
589936199c | ||
|
|
710e2e1996 | ||
|
|
aa794c7646 | ||
|
|
48779aed29 | ||
|
|
3907e1ac76 | ||
|
|
4d6af4fbf8 | ||
|
|
9c3b0efb30 | ||
|
|
8bcb7ffe4d | ||
|
|
77f0740ec9 | ||
|
|
8b5e407758 | ||
|
|
6c88a5554f | ||
|
|
d779ee06b1 | ||
|
|
f873d4e245 | ||
|
|
0a0e03dc68 | ||
|
|
f0e19c8934 | ||
|
|
a6af51dc32 | ||
|
|
fd1a079271 | ||
|
|
9840a48301 | ||
|
|
2dc7c9adf1 | ||
|
|
a86bfe7fd3 | ||
|
|
d99475bb80 | ||
|
|
194b7aff2e | ||
|
|
f6d5733183 | ||
|
|
0ca055b8e8 | ||
|
|
9aae2c7f50 | ||
|
|
c29ad1270b | ||
|
|
49a60c25af | ||
|
|
39bcc158bb | ||
|
|
4f02bcd40c | ||
|
|
556c4a434d | ||
|
|
d389b035b7 | ||
|
|
9447088f89 | ||
|
|
64955c26a7 | ||
|
|
68519223fa | ||
|
|
e72cacc4b2 | ||
|
|
ed4be2328f | ||
|
|
e241c6cb6f | ||
|
|
6e98fb4274 | ||
|
|
83f6f41787 | ||
|
|
18c10e5839 | ||
|
|
11de47c813 | ||
|
|
85ea619b18 | ||
|
|
3f57cf21f7 | ||
|
|
b75c773879 | ||
|
|
95070ae382 | ||
|
|
353ee3a9ae | ||
|
|
972e147fdf | ||
|
|
73e52fe334 | ||
|
|
cf521ad79e | ||
|
|
76a3e5cfcf | ||
|
|
200d63e5fc | ||
|
|
0aeefb1217 | ||
|
|
7dfeb859e2 | ||
|
|
5151f26b9e | ||
|
|
053e5cca86 | ||
|
|
89765c19c4 | ||
|
|
0824f0c13b | ||
|
|
04092c3fa5 | ||
|
|
d8258cf676 | ||
|
|
97d6eee5ba | ||
|
|
511297ed11 | ||
|
|
88aafbaf7b | ||
|
|
af9890b16c | ||
|
|
c67a8c4120 | ||
|
|
16c0e00b4f | ||
|
|
d1c1c5acf9 | ||
|
|
9cb7145b4e | ||
|
|
978d327bcd | ||
|
|
07664b5f22 | ||
|
|
b21fe0ac4c | ||
|
|
932c30cd7f | ||
|
|
59c19f3fae | ||
|
|
c9245f957e | ||
|
|
228cd4e664 | ||
|
|
289a651d52 | ||
|
|
1b8c17ce92 | ||
|
|
e66de732ac | ||
|
|
2ba94f7524 | ||
|
|
3f90642726 | ||
|
|
35ae7b9643 | ||
|
|
15d635322b | ||
|
|
c3d5234710 | ||
|
|
9b498d10d5 | ||
|
|
9418e5b635 | ||
|
|
8779b79bbf | ||
|
|
ba11689029 | ||
|
|
90e212a1b2 | ||
|
|
198a52e230 | ||
|
|
e7890dda3d | ||
|
|
83086c46e6 | ||
|
|
1314123ad7 | ||
|
|
cb13873098 | ||
|
|
fba430999d | ||
|
|
293c53d8dd | ||
|
|
33ce334bd7 | ||
|
|
379eeedde2 | ||
|
|
ec6f5d1ad3 | ||
|
|
f35ba8f8e5 | ||
|
|
21ffd2a564 | ||
|
|
3c5b095510 | ||
|
|
096227e3da | ||
|
|
4a582ec8b1 | ||
|
|
4132494a49 | ||
|
|
1bd31d2936 | ||
|
|
18ffeb82a2 | ||
|
|
da985871cc | ||
|
|
4ff5b3e05a | ||
|
|
7f3d6bb55f | ||
|
|
d54722609c | ||
|
|
2765d5edeb | ||
|
|
a1a7979fb3 | ||
|
|
4e8b5e26ae | ||
|
|
47596fdb1a | ||
|
|
332521a7f3 | ||
|
|
8690de00a9 | ||
|
|
7fd2b43704 | ||
|
|
05c05419fc | ||
|
|
5ad9ac277c | ||
|
|
42f24245b5 | ||
|
|
7548836b67 | ||
|
|
a2c33affcd | ||
|
|
11039a5c82 | ||
|
|
46969702a8 | ||
|
|
134827a0f7 | ||
|
|
f0390682ce | ||
|
|
2a6846d2c0 | ||
|
|
dc8b3b9210 | ||
|
|
dcda9be50b | ||
|
|
04c25ab0f7 | ||
|
|
051d9421b0 | ||
|
|
6d65797b4f | ||
|
|
497813eced | ||
|
|
26e3a1944b | ||
|
|
e188d788b9 | ||
|
|
d86d061f97 | ||
|
|
35918d2a00 | ||
|
|
51bab71291 | ||
|
|
6fd886b27a | ||
|
|
2211009cce | ||
|
|
eb65cee280 | ||
|
|
a3dd126931 | ||
|
|
0b2be1e54b | ||
|
|
337d93ff51 | ||
|
|
df5c2be7fa | ||
|
|
15cd9795d5 | ||
|
|
5fcdf4ea00 | ||
|
|
e8e982559d | ||
|
|
b2a8d27178 | ||
|
|
679e10abd6 | ||
|
|
34331063eb | ||
|
|
2dfdd4a0d9 | ||
|
|
cb5413c2ce | ||
|
|
9e7f2261f3 | ||
|
|
5efed8f193 | ||
|
|
0af7e95af2 | ||
|
|
cf3b225d3e | ||
|
|
32f95f5d7d | ||
|
|
9a5a1bf555 | ||
|
|
b7dfb0af0b | ||
|
|
4a71387098 | ||
|
|
f651ab7fd9 | ||
|
|
fc599a0637 | ||
|
|
9796233897 | ||
|
|
0849a2f114 | ||
|
|
043218937f | ||
|
|
7b4d60b576 | ||
|
|
f803b910b5 | ||
|
|
8d660b20aa | ||
|
|
1e298bbbc0 | ||
|
|
14eb54481d | ||
|
|
7a5c28228b | ||
|
|
f278716b7e | ||
|
|
b3e186fea4 | ||
|
|
f0cdefad90 | ||
|
|
507b0e44d5 | ||
|
|
56985be26b | ||
|
|
d3a0c6a330 | ||
|
|
0d5cd28d38 | ||
|
|
b6bd9cd7dd | ||
|
|
2dab5093dc | ||
|
|
4a4a02eb0d | ||
|
|
4000aacb2c | ||
|
|
a2be7f6b36 | ||
|
|
af6f90c228 | ||
|
|
3df2d23bf5 | ||
|
|
d173e2d49b | ||
|
|
5747cae87a | ||
|
|
667751dc5f | ||
|
|
577996f507 | ||
|
|
22e42e279c | ||
|
|
6c42b87c57 | ||
|
|
3fe63f836f | ||
|
|
1a2e82f079 | ||
|
|
8f2eea70f6 | ||
|
|
358df925e7 | ||
|
|
9ab8f233a2 | ||
|
|
fb0e45ebed | ||
|
|
37b814b70a | ||
|
|
8b282afb03 | ||
|
|
ec9a4b2607 | ||
|
|
d5f3158638 | ||
|
|
eb5e344b41 | ||
|
|
9d13b27c9e | ||
|
|
b4f69a4e2e | ||
|
|
3e359a69c7 | ||
|
|
9de4054851 | ||
|
|
9676e63b5e | ||
|
|
337cb67cd9 | ||
|
|
fe79258766 | ||
|
|
0a9cc1e17a | ||
|
|
6445061836 | ||
|
|
19df710a0d | ||
|
|
0d8333eac6 | ||
|
|
b1cb4bbe32 | ||
|
|
11b9430b15 | ||
|
|
1230c3bd28 | ||
|
|
2ee645b1ee | ||
|
|
84ce308d9d | ||
|
|
be631d021e | ||
|
|
a37458fd9e | ||
|
|
095b9478cc | ||
|
|
94fec52329 | ||
|
|
fc4d71139a | ||
|
|
c813344c7c | ||
|
|
f0bfbfe693 | ||
|
|
741cf07f2e | ||
|
|
5ff26e1ce8 | ||
|
|
342d977fe4 | ||
|
|
9c1011fbd8 | ||
|
|
daaf77a89f | ||
|
|
420600510d | ||
|
|
0ae9a7f42f | ||
|
|
a1cddfa18b | ||
|
|
89fe8e68f4 | ||
|
|
cd3b0607a5 | ||
|
|
0cde6f7dbc | ||
|
|
42943a33fe | ||
|
|
fc76822bdf | ||
|
|
05ad296b43 | ||
|
|
2e5b075d1b | ||
|
|
f47348106e | ||
|
|
f351447f09 | ||
|
|
10b2a8b4f9 | ||
|
|
afcd3e5693 | ||
|
|
7b5ce83cbd | ||
|
|
0cff804cd7 | ||
|
|
f4e6bf7fd5 | ||
|
|
ba4f277863 | ||
|
|
d35bd16543 | ||
|
|
065841530d | ||
|
|
28a73d2a6e | ||
|
|
bbf09c2ea9 | ||
|
|
8f9fae9210 | ||
|
|
0eba1da6c4 | ||
|
|
0ba8300959 | ||
|
|
f8ae7680ad | ||
|
|
d51f00d35d | ||
|
|
0f7a475ec9 | ||
|
|
d8b3331648 | ||
|
|
2816fedc36 | ||
|
|
17de9d98f4 | ||
|
|
b2f5eb8850 | ||
|
|
c821a2e463 | ||
|
|
66e8f83395 | ||
|
|
41ebceca8f | ||
|
|
632685672c | ||
|
|
bd2e4bb633 | ||
|
|
835633d44b | ||
|
|
75773e6b00 | ||
|
|
a21b9b5244 | ||
|
|
38bf76dc14 | ||
|
|
9461341104 | ||
|
|
5b5b2c6364 | ||
|
|
a006263e51 | ||
|
|
febdd38f7c | ||
|
|
1b4d335b09 | ||
|
|
725409a576 | ||
|
|
9b25cc51e4 | ||
|
|
9dcdb6b6e7 | ||
|
|
73b987358f | ||
|
|
84c2bf130c | ||
|
|
b25189feea | ||
|
|
a9b58a5564 | ||
|
|
530b20c633 | ||
|
|
fa0aeac10b | ||
|
|
1b661bb8cd | ||
|
|
530e8f7537 | ||
|
|
9e019a98f6 | ||
|
|
10c6cf9f7e | ||
|
|
480b238e44 | ||
|
|
32bdbc693a | ||
|
|
ab146b6b6b | ||
|
|
763929eafc | ||
|
|
711b3fee98 | ||
|
|
0d0592eee9 | ||
|
|
cc7948ba45 | ||
|
|
7e25b64689 | ||
|
|
d844cdadb5 | ||
|
|
e3b3f51e87 | ||
|
|
45efda7f1d | ||
|
|
db45c861e4 | ||
|
|
42f8e6a6d9 | ||
|
|
23a761226a | ||
|
|
6738dfe0d3 | ||
|
|
6e16b2eb0c | ||
|
|
707eb15465 | ||
|
|
8008099e71 | ||
|
|
79ccb96c95 | ||
|
|
bd76934e40 | ||
|
|
bb793a41fc | ||
|
|
ad048a92ed | ||
|
|
14d944ad2f | ||
|
|
e1b8b5696c | ||
|
|
78368e9979 | ||
|
|
e83bff6923 | ||
|
|
e6c12d81ea | ||
|
|
3a73d0c1b2 | ||
|
|
b524587a9a | ||
|
|
e4bd753355 | ||
|
|
30536cb04a | ||
|
|
da1efa9270 | ||
|
|
bd6f4b3f76 | ||
|
|
5e48d75ead | ||
|
|
bd46fcdc75 | ||
|
|
6968f9fa6b | ||
|
|
5225fd328d | ||
|
|
fa42474612 | ||
|
|
d2eda78af0 | ||
|
|
7a0ee6aac4 | ||
|
|
b327eedb45 | ||
|
|
a68e3ff9da | ||
|
|
0500dd2e35 | ||
|
|
cb43131ccb | ||
|
|
883e0ab115 | ||
|
|
b189160888 | ||
|
|
3dbb0de683 | ||
|
|
b38bc8a207 | ||
|
|
3328828da6 | ||
|
|
7fdabd90a5 | ||
|
|
d8452e98f5 | ||
|
|
dda2213593 | ||
|
|
04f995b80c | ||
|
|
a08f1e2512 | ||
|
|
af4849d6cc | ||
|
|
087675889f | ||
|
|
daa10617c0 | ||
|
|
c3b5c13cc5 | ||
|
|
1dd83645bd | ||
|
|
2df7c17bc1 | ||
|
|
a274349ace | ||
|
|
1c699fe307 | ||
|
|
f0a3e84e4f | ||
|
|
f87989d32e | ||
|
|
7355be9e3a | ||
|
|
d8f1e95e1a | ||
|
|
59eace702b | ||
|
|
8a24ba0252 | ||
|
|
03999a9d2c | ||
|
|
f335cb1775 | ||
|
|
b9c034469b | ||
|
|
ce3a82ddda | ||
|
|
5cbb38922c | ||
|
|
14f0afec08 | ||
|
|
769cc09906 | ||
|
|
306daf7591 | ||
|
|
d1c1132e6e | ||
|
|
962df859f1 | ||
|
|
97601280d1 | ||
|
|
ede42e99de | ||
|
|
47cae0134c | ||
|
|
33defab7df | ||
|
|
6b402e944b | ||
|
|
f8a6f4c5e5 | ||
|
|
adb999d3a9 | ||
|
|
f4ea23dd14 | ||
|
|
5bd0020e49 | ||
|
|
782e81568d | ||
|
|
2ecf83ddae | ||
|
|
de0982d590 | ||
|
|
e5755a2eec | ||
|
|
e755c34ab5 | ||
|
|
85e4f1a5ed | ||
|
|
3eb403a2c4 | ||
|
|
79442a6a6c | ||
|
|
f49001b742 | ||
|
|
e80ecfac8d | ||
|
|
ff67fba2c9 | ||
|
|
3f63831488 | ||
|
|
19babbc73d | ||
|
|
26a4d5474d | ||
|
|
af3e03230a | ||
|
|
551f04011a | ||
|
|
ea39a45f31 | ||
|
|
ebff5695ff | ||
|
|
9b63c1a515 | ||
|
|
de882dc62e | ||
|
|
8f9562d6a4 | ||
|
|
5d383ec39b | ||
|
|
2687fb187d | ||
|
|
ca43c0b4b2 | ||
|
|
71bbf59467 | ||
|
|
18f861f771 | ||
|
|
e85d59dfb0 | ||
|
|
78c8b60121 | ||
|
|
e22b5ed516 | ||
|
|
906f59a566 | ||
|
|
d1c3edfd9b | ||
|
|
e8a0f33bde | ||
|
|
825687f2a8 | ||
|
|
2852648a65 | ||
|
|
0f3f31ef45 | ||
|
|
864cf76c07 | ||
|
|
98e6c71bf8 | ||
|
|
aee9566bbb | ||
|
|
b61c0d6dee | ||
|
|
f963b3a000 | ||
|
|
03963b736a | ||
|
|
a34c375e2d | ||
|
|
e8eea9573e | ||
|
|
b7656d65e7 | ||
|
|
d1fa6dd03c | ||
|
|
2045bfaf78 | ||
|
|
653ada5f0f | ||
|
|
cbc000d35a | ||
|
|
11a676be83 | ||
|
|
7eedea8035 | ||
|
|
2d4ddea915 | ||
|
|
76d7a50fac | ||
|
|
84187eedc4 | ||
|
|
ee5c30934b | ||
|
|
b4b25d4300 | ||
|
|
9eb1dba4a1 | ||
|
|
285e274818 | ||
|
|
0077d109e7 | ||
|
|
9cd508a92a | ||
|
|
9ffd4def64 | ||
|
|
fdb6ac7575 | ||
|
|
64e2cfa35a | ||
|
|
39efca0d4c | ||
|
|
cd2942405a | ||
|
|
fadff34f5f | ||
|
|
7334bcdf83 | ||
|
|
e1e38f0404 | ||
|
|
38cc179068 | ||
|
|
5147d0d004 | ||
|
|
c237c9dbde | ||
|
|
34bd2c3031 | ||
|
|
6173f3b459 | ||
|
|
333b430b97 | ||
|
|
4c22d08da5 | ||
|
|
219f9871a7 | ||
|
|
80527d5985 | ||
|
|
7e5edd6387 | ||
|
|
4ca6a221e4 | ||
|
|
e5733a7f6e | ||
|
|
23b7936924 | ||
|
|
9be465a8cf | ||
|
|
4bbae673dc | ||
|
|
e66ee5d3f1 | ||
|
|
20872100d1 | ||
|
|
06f33ded10 | ||
|
|
adb5cd72b9 | ||
|
|
303429c52c | ||
|
|
1a92cc8543 | ||
|
|
d5cc617848 | ||
|
|
4af71b5b52 | ||
|
|
2dab89363d | ||
|
|
ab9f9a9679 | ||
|
|
c5166f07f1 | ||
|
|
86c2f8eda1 | ||
|
|
57d41e79c7 | ||
|
|
5cd9b8c22f | ||
|
|
56eba0d4fc | ||
|
|
7936d0063d | ||
|
|
3613fb3f69 | ||
|
|
608b2e31c7 | ||
|
|
2006827968 | ||
|
|
f6a3497b0c | ||
|
|
d7ef64746c | ||
|
|
b733b3615d | ||
|
|
e9fa3e3aa3 | ||
|
|
b77f796661 | ||
|
|
c077db1129 | ||
|
|
801c88a3e2 | ||
|
|
646d235c65 | ||
|
|
a86bed294e | ||
|
|
697f1f9c25 | ||
|
|
61065ee0b7 | ||
|
|
df65d3e2f6 | ||
|
|
6969cb0b9c | ||
|
|
7e81871118 | ||
|
|
ba80ade643 | ||
|
|
0401cf5fb9 | ||
|
|
53b431f100 | ||
|
|
1a082529c6 | ||
|
|
bf57290197 | ||
|
|
a815e71bfe | ||
|
|
cc0580301e | ||
|
|
5853bd1eeb | ||
|
|
ab93c53092 | ||
|
|
818c4fc9a2 | ||
|
|
fc7350b7e2 | ||
|
|
1e4018350f | ||
|
|
f017d609fb | ||
|
|
ccb6e72057 | ||
|
|
32ad9525a6 | ||
|
|
afedd4337b | ||
|
|
1f7acc7cf6 | ||
|
|
e6a5c19f0b | ||
|
|
8e07302ac3 | ||
|
|
0238431b96 | ||
|
|
3a7a4ccf30 | ||
|
|
9b843151fe | ||
|
|
19e275a17a | ||
|
|
49c6bb71a1 | ||
|
|
005295ac97 | ||
|
|
37a3aa75cc | ||
|
|
07243b206e | ||
|
|
f732f5c390 | ||
|
|
1da16a04fe | ||
|
|
e2da7c5f96 | ||
|
|
ca9ceda2f7 | ||
|
|
059416bb47 | ||
|
|
4196351e48 | ||
|
|
f02b972dc0 | ||
|
|
8398ac59a1 | ||
|
|
83403a1a1a | ||
|
|
620c113fae | ||
|
|
903500dcfa | ||
|
|
68d6993acd | ||
|
|
2c9f5062a9 | ||
|
|
d3f39ae06b | ||
|
|
f42f159785 | ||
|
|
bb7c8e9c85 | ||
|
|
663b6c7b4e | ||
|
|
1fa2124efd | ||
|
|
bcd1a339f9 | ||
|
|
c41e5d9c55 | ||
|
|
b5fa40047a | ||
|
|
caabeb065a | ||
|
|
d393f9ee9e | ||
|
|
eb509166ee | ||
|
|
0dfe77df37 | ||
|
|
46ecc2467b | ||
|
|
a5c1b8e15f | ||
|
|
54703c67c1 | ||
|
|
e863b84016 | ||
|
|
1a693f70ac | ||
|
|
a9de153c00 | ||
|
|
42217cf079 | ||
|
|
bca1e8140e | ||
|
|
ee95fabc47 | ||
|
|
67552c14af | ||
|
|
521bf211a2 | ||
|
|
8e77e19631 | ||
|
|
8b91a0546c | ||
|
|
715c71f9a5 | ||
|
|
1ec4a949b8 | ||
|
|
fc2388093d | ||
|
|
3a9bf17d7e | ||
|
|
a3002ce450 | ||
|
|
2515e8f598 | ||
|
|
dcc0db35b5 | ||
|
|
749adb56f8 | ||
|
|
f542ebecb4 | ||
|
|
b640963f65 | ||
|
|
8f5990c537 | ||
|
|
bf400547e1 | ||
|
|
4d9dec8511 | ||
|
|
1ac63e36df | ||
|
|
f36613227e | ||
|
|
d063dfd652 | ||
|
|
c9c9401b0b | ||
|
|
913f2f5077 | ||
|
|
336bafd6ce | ||
|
|
5e1a26b743 | ||
|
|
d9d5ca95df | ||
|
|
f1bb305cbc | ||
|
|
c467280170 | ||
|
|
deeee70807 | ||
|
|
07ade24ea1 | ||
|
|
8de272239e | ||
|
|
6e5a7035c8 | ||
|
|
632f206228 | ||
|
|
f04f1f828f | ||
|
|
4f91443957 | ||
|
|
0c1057d554 | ||
|
|
43936e21d9 | ||
|
|
949eded4ee | ||
|
|
84859d1b7c | ||
|
|
553ec724c2 | ||
|
|
207c90e26c | ||
|
|
64bbba7576 | ||
|
|
62dbd18ef8 | ||
|
|
55b0a20a4a | ||
|
|
29b74f85c5 | ||
|
|
ed49e03802 | ||
|
|
4711c848f8 | ||
|
|
33df825b73 | ||
|
|
89001dbb92 | ||
|
|
43e1dd0b0e | ||
|
|
f9cf111a86 | ||
|
|
99f8914830 | ||
|
|
6da877709d | ||
|
|
c4948dc11f | ||
|
|
3f243129a4 | ||
|
|
eb2cae7a77 | ||
|
|
a2bbcd3939 | ||
|
|
0650c4bc26 | ||
|
|
7c32bf97ae | ||
|
|
70aecc1f0c | ||
|
|
5bd5c7d2c5 | ||
|
|
d6dce4021d | ||
|
|
ccfdb99348 | ||
|
|
2ca712435a | ||
|
|
0af8e34a8f | ||
|
|
f2583e152a | ||
|
|
cf70bd2dc1 | ||
|
|
8f50af917f | ||
|
|
4bdde365cf | ||
|
|
7f1bc33eed | ||
|
|
d0d24aba4e | ||
|
|
0d68c0d683 | ||
|
|
e1ab735823 | ||
|
|
bb67a8cbbb | ||
|
|
c8a7ac7dc6 | ||
|
|
788a294200 | ||
|
|
f34935a634 | ||
|
|
a7331256fc | ||
|
|
8fa3941db9 | ||
|
|
ada120c1b9 | ||
|
|
72f7168893 | ||
|
|
75b70a26bd | ||
|
|
4433e5d8e5 | ||
|
|
351b2e1417 | ||
|
|
3c7dc5a49e | ||
|
|
9895231696 | ||
|
|
3fa3281c51 | ||
|
|
bbf11d0374 | ||
|
|
04ee696f6a | ||
|
|
ddbbe92a85 | ||
|
|
32ba5e1802 | ||
|
|
a1c6eb177a | ||
|
|
14d60264c7 | ||
|
|
f4d6abace7 | ||
|
|
a5e1717138 | ||
|
|
472a4d400b | ||
|
|
fda4786737 | ||
|
|
c3686579b2 | ||
|
|
f2dfb2f32d | ||
|
|
60ee9f1a15 | ||
|
|
f43d1af6a7 | ||
|
|
b631b4c7b7 | ||
|
|
e3aba0529f | ||
|
|
cf682feae9 | ||
|
|
03d798760c | ||
|
|
51450c2b02 | ||
|
|
751ddbb3f3 | ||
|
|
36b3620d0c | ||
|
|
fd3eb513ff | ||
|
|
af0aaddbfd | ||
|
|
b167170aa9 | ||
|
|
642ac3ebc4 | ||
|
|
69cceb4fc7 | ||
|
|
ac32e23e87 | ||
|
|
04de4ff606 | ||
|
|
27761566c9 | ||
|
|
3d93eda92d | ||
|
|
6943571a6d | ||
|
|
28994cb9da | ||
|
|
5866634b3f | ||
|
|
e3d91f1cc7 | ||
|
|
8ac06259f5 | ||
|
|
c8933a6669 | ||
|
|
6c2da264db | ||
|
|
993f01a301 | ||
|
|
9e29e7283c | ||
|
|
7da106798d | ||
|
|
b2e9cbb44a | ||
|
|
143f6df2c1 | ||
|
|
a64505fcc1 | ||
|
|
0aaf919827 | ||
|
|
bf8e988488 | ||
|
|
d26ea28028 | ||
|
|
abdd5e08a6 | ||
|
|
47a5ef915b | ||
|
|
5861508153 | ||
|
|
85754ccfb5 | ||
|
|
cbeea23295 | ||
|
|
e2d79a6802 | ||
|
|
c68d85096b | ||
|
|
fcafa83111 | ||
|
|
47d7418eb1 | ||
|
|
8751400ae3 | ||
|
|
3889c1fa71 | ||
|
|
a4d1d3b16c | ||
|
|
adfdf41e85 | ||
|
|
a6825f7ee3 | ||
|
|
830ee45161 | ||
|
|
5f7df3f871 | ||
|
|
34af730934 | ||
|
|
645801fe86 | ||
|
|
547f49a978 | ||
|
|
fd3d7a6f29 | ||
|
|
6be7140a47 | ||
|
|
ca7dc02d88 | ||
|
|
1924c45358 | ||
|
|
be92f92991 | ||
|
|
a9b909cca1 | ||
|
|
175b83dd68 | ||
|
|
df85284a60 | ||
|
|
17ffa93393 | ||
|
|
73c525127f | ||
|
|
3e76d7c321 | ||
|
|
9295f9cef3 | ||
|
|
9d1e6df8f0 | ||
|
|
0805d70c07 | ||
|
|
0133524f09 | ||
|
|
2c5100b2ed | ||
|
|
51296cdc15 | ||
|
|
f261bec248 | ||
|
|
97c7087f6c | ||
|
|
109906d083 | ||
|
|
81833c4938 | ||
|
|
1b6cfdbdc4 | ||
|
|
281d8bddc1 | ||
|
|
0eaa3aeeb7 | ||
|
|
4dcb0bcfed | ||
|
|
e69b7dfd6a | ||
|
|
38d63c71ee | ||
|
|
6880fd3f5f | ||
|
|
1771ca363a | ||
|
|
c896c88c23 | ||
|
|
5ef733729b | ||
|
|
f73e29937c | ||
|
|
8383547943 | ||
|
|
8070d3f1e6 | ||
|
|
a161fa953e | ||
|
|
471e1ae96f | ||
|
|
ed28bcdfb0 | ||
|
|
31ea81c211 | ||
|
|
283189a17c | ||
|
|
40020df865 | ||
|
|
aa6b5ec24f | ||
|
|
f53fe042a9 | ||
|
|
034a54bf4b | ||
|
|
894dd1e353 | ||
|
|
ca99db5a97 | ||
|
|
61e506010c | ||
|
|
4b4343a9fd | ||
|
|
9efacefba3 | ||
|
|
f288c9a9ad | ||
|
|
ec45f23485 | ||
|
|
1c11e73ff8 |
@@ -0,0 +1,143 @@
|
||||
---
|
||||
name: create-crud-page
|
||||
description: Scaffold a CRUD list/table page module in the gpustack-ui monorepo. Use when creating a new page module, building a list/table page, adding a create/edit drawer, or setting up the components/config/forms/hooks/services structure for a feature.
|
||||
---
|
||||
|
||||
# Create a CRUD Table Page
|
||||
|
||||
## Inputs (do this first)
|
||||
|
||||
- The argument passed to this skill is the **module name** (e.g. `/create-crud-page api-keys` → module `api-keys`). If no name was given, ask for it.
|
||||
- **Always ask the user for the API documentation before generating any code**, even if a module name was provided:
|
||||
|
||||
> Where is the API documentation for this module? (OpenAPI/Swagger URL, schema file path, or an interface description)
|
||||
|
||||
- Wait for the answer, then read/fetch it. Derive `config/types.ts` (`FormData`, `ListItem`), the `services` request hooks, and form fields from that schema. Do not guess field names or endpoints — if the doc is missing details, ask.
|
||||
|
||||
- **Also ask which form layout to scaffold:**
|
||||
|
||||
> Should the form use tabs? (1) a plain form without tabs, or (2) a tabbed form
|
||||
|
||||
Choose the form structure in section 3 accordingly. Default to **no tabs** unless the user picks tabs or the schema clearly has many grouped sections.
|
||||
|
||||
---
|
||||
|
||||
Before anything: **reuse common `components`, `hooks`, and `utils` from `@gpustack/core-ui` whenever possible.**
|
||||
|
||||
Reference implementation for sections below: `src/pages/model-routes`.
|
||||
|
||||
## Module structure
|
||||
|
||||
Create the module under `src/pages/{module}`:
|
||||
|
||||
```text
|
||||
{module}
|
||||
├── components
|
||||
├── config
|
||||
├── forms
|
||||
├── hooks
|
||||
├── index.tsx
|
||||
└── services
|
||||
```
|
||||
|
||||
## 1. components
|
||||
|
||||
Module-specific components.
|
||||
|
||||
- The create/edit form component is named `add-xxx-modal.tsx` (repo convention — keep the `-modal` suffix even though it is built with `FormDrawer`).
|
||||
- Use `FormDrawer` from `@gpustack/core-ui`.
|
||||
- If a table cell's render logic/structure is complex, extract it into `xxx-cell.tsx`.
|
||||
|
||||
## 2. config
|
||||
|
||||
```text
|
||||
config
|
||||
├── index.ts # static configs & constants
|
||||
└── types.ts # TypeScript types
|
||||
```
|
||||
|
||||
Naming: form types → `FormData`; table list item types → `ListItem`.
|
||||
|
||||
## 3. forms
|
||||
|
||||
Main form component goes in `forms/index.tsx`.
|
||||
|
||||
- **Complex interactions** (Form.Item split across components): create a dedicated Form Context and wrap with `FormContext.Provider`.
|
||||
- **Tab-based forms**: use `ScrollSpyTabs` from `@gpustack/core-ui`, wrapping the `Form` or `FormContext.Provider`. Do not use tabs unless necessary.
|
||||
- **Required-field validation**: use `getRuleMessage` for standard `input`/`select`.
|
||||
- For cascading selectors and async race protection, follow the **form-patterns** skill.
|
||||
|
||||
## 4. hooks
|
||||
|
||||
- Table columns → `use-xxx-columns.tsx`.
|
||||
- Open/close hooks for `add-xxx-modal.tsx` → `use-create-xxx.ts`.
|
||||
|
||||
## 5. index.tsx (list page entry)
|
||||
|
||||
- **Data fetching**: `useTableFetch` from `@gpustack/core-ui`.
|
||||
- **Data display**:
|
||||
- Standard table → Ant Design `Table`. Ref: `src/pages/users/index.tsx`.
|
||||
- Expandable/collapsible rows → `Table` from `@gpustack/core-ui`. Ref: `src/pages/model-routes/index.tsx`.
|
||||
- Card-style lists → use `InfiniteScrollerProvider`. Ref: `src/pages/backends/index.tsx`.
|
||||
|
||||
## 6. services
|
||||
|
||||
`request` is injected via a provider — do **not** create a centralized `apis` directory like in `gpustack-ui`. Define request hooks directly in `services`.
|
||||
|
||||
- Use `useRequest` from `@gpustack/core-ui`, or `useQueryData` (same underlying method).
|
||||
- Ref: `src/pages/gpu-service/storage-types/services/use-create-storage-type.ts`.
|
||||
|
||||
## 7. Empty data
|
||||
|
||||
- Page table lists → `NoResult`.
|
||||
- Simple (non-page) tables → `Empty` with `image={Empty.PRESENTED_IMAGE_SIMPLE}`.
|
||||
|
||||
## Common UI conventions
|
||||
|
||||
- **Drawer/Modal open/close**: use `useBodyScroll` from `@gpustack/core-ui`. Ref: `src/pages/model-routes/hooks/use-create-route.ts`.
|
||||
- **Status display** (success/failed/processing/warning): use `StatusTag`, never `Tag` from `antd` directly. See **Status display** below. Ref: `src/pages/llmodels/components/table-list.tsx`.
|
||||
- **Permission-gated visibility**: use `Access` / `useAccess`. Ref: `src/pages/access/index.tsx`.
|
||||
- **Styles**: avoid `styled-components` for complex/large styling. Prefer `createStyles` for component-scoped dynamic styles, CSS Modules (`xxx.module.less`) for static structured styles.
|
||||
|
||||
## Status display
|
||||
|
||||
Use `StatusTag` from `@gpustack/core-ui` for status display. Do not use `Tag` from `antd` directly.
|
||||
|
||||
1. Define the mapping from business status values to UI status values in the module's `config/index.ts`:
|
||||
|
||||
```ts
|
||||
import { StatusMaps } from '@/config';
|
||||
import { StatusType } from '@/config/types';
|
||||
|
||||
export const XxxStatusValueMap = {
|
||||
Running: 'running',
|
||||
Pending: 'pending',
|
||||
Failed: 'failed'
|
||||
};
|
||||
|
||||
export const XxxStatusLabelMap: Record<string, string> = {
|
||||
[XxxStatusValueMap.Running]: 'Running',
|
||||
[XxxStatusValueMap.Pending]: 'Pending',
|
||||
[XxxStatusValueMap.Failed]: 'Failed'
|
||||
};
|
||||
|
||||
export const status: Record<string, StatusType> = {
|
||||
[XxxStatusValueMap.Running]: StatusMaps.success,
|
||||
[XxxStatusValueMap.Pending]: StatusMaps.transitioning,
|
||||
[XxxStatusValueMap.Failed]: StatusMaps.error
|
||||
};
|
||||
```
|
||||
|
||||
2. In table columns, pass only the UI status value and display text required by `StatusTag`:
|
||||
|
||||
```tsx
|
||||
<StatusTag
|
||||
statusValue={{
|
||||
status: status[value],
|
||||
text: XxxStatusLabelMap[value] || value,
|
||||
message: record.state_message
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
`statusValue.status` must be a value mapped from `StatusMaps` (`success`, `transitioning`, `warning`, `error`, `inactive`). Do not pass business status values such as `running` or `pending` directly.
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: form-patterns
|
||||
description: Patterns for forms with cascading/dependent selections in the gpustack-ui monorepo. Use when building a form where picking one field derives another (pick A → auto-pick B → write form), handling async option loading on modal open, or protecting against stale async results.
|
||||
---
|
||||
|
||||
# Form Patterns
|
||||
|
||||
Theme: keep cascading selections (pick A → derive B → write form) on a single, predictable path.
|
||||
|
||||
## Accessing the form: `form` vs `form.current`
|
||||
|
||||
This is **not** absolute — it depends on the call site:
|
||||
|
||||
- **Inside the form component** (`forms/index.tsx`), or anywhere holding a `Form.useForm()` instance → call it directly: `form.setFieldsValue(...)`.
|
||||
- **In the outer Drawer/Modal wrapper** that opens the form and holds it via `ref={form}` (`const form = useRef(null)`), driven by an `open` prop → go through the ref: `form.current?.setFieldsValue(...)`.
|
||||
|
||||
The reference template below is written for the **Drawer-wrapper scenario** (it reacts to `open` and owns the shared `selection` state), so it uses `form.current?` throughout. If you lift this logic into the form body with a `useForm()` instance, drop the `.current`.
|
||||
|
||||
## 1. No fallback for derived selection
|
||||
|
||||
When "pick A then auto-pick B", match by rule and return `undefined` if no match — let the form field stay empty. Do **not** silently fall back to `list[0]`; a fallback hides data issues and fakes a valid selection.
|
||||
|
||||
```ts
|
||||
const findB = (key, list) =>
|
||||
key ? list.find((x) => x.key === key) : undefined;
|
||||
```
|
||||
|
||||
For form fields, clear with `undefined`, not `''`. In Ant Design `undefined` restores the placeholder; `''` is treated as a real value.
|
||||
|
||||
## 2. Async race protection
|
||||
|
||||
For fetches triggered by a lifecycle entry (e.g. modal open), tag each invocation with a session ref. Discard stale results if the session rotated (modal closed and re-opened) before the response arrives.
|
||||
|
||||
```ts
|
||||
const sessionRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
sessionRef.current += 1;
|
||||
return;
|
||||
}
|
||||
const session = ++sessionRef.current;
|
||||
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
|
||||
if (sessionRef.current !== session) return;
|
||||
applySelection(as.items[0], findB(as.items[0].key, bs.items));
|
||||
});
|
||||
}, [open]);
|
||||
```
|
||||
|
||||
## 3. Reference template
|
||||
|
||||
Two cascading selectors backed by a single shared state, with a single atomic write (state + form together):
|
||||
|
||||
```ts
|
||||
type Selection = { a?: string; b?: number };
|
||||
|
||||
const [selection, setSelection] = useState<Selection>({});
|
||||
const sessionRef = useRef(0);
|
||||
const form = useRef<any>(null); // wrapper holds the form via <Form ref={form} /> — see "Accessing the form" above
|
||||
|
||||
const findB = (key, list) =>
|
||||
key ? list.find((x) => x.key === key) : undefined;
|
||||
|
||||
// Single atomic write: state + form together.
|
||||
const applySelection = (a, b) => {
|
||||
setSelection({ a: a.name, b: b?.id });
|
||||
form.current?.setFieldsValue({
|
||||
field: b?.field,
|
||||
spec: { ...currentSpec, ...b?.spec }
|
||||
});
|
||||
};
|
||||
|
||||
// Trigger 1: modal opened
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
sessionRef.current++;
|
||||
setSelection({});
|
||||
return;
|
||||
}
|
||||
const session = ++sessionRef.current;
|
||||
Promise.all([fetchA(), fetchB()]).then(([as, bs]) => {
|
||||
if (sessionRef.current !== session) return;
|
||||
const first = as.items[0];
|
||||
applySelection(first, findB(first.key, bs.items));
|
||||
});
|
||||
}, [open]);
|
||||
|
||||
// Trigger 2: user picks A
|
||||
const handleAChange = (a) => {
|
||||
applySelection(a, findB(a.key, listB));
|
||||
};
|
||||
|
||||
// Trigger 3: user picks B
|
||||
const handleBChange = (b) => {
|
||||
setSelection((prev) => ({ ...prev, b: b.id }));
|
||||
form.current?.setFieldsValue({ ...b.fields });
|
||||
};
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- Module/file structure for forms lives in the **create-crud-page** skill (section 3).
|
||||
- Required-field validation: use `getRuleMessage`.
|
||||
@@ -1 +0,0 @@
|
||||
src/components/icon-font/iconfont/iconfont.js
|
||||
@@ -1,15 +0,0 @@
|
||||
module.exports = {
|
||||
extends: require.resolve('@umijs/max/eslint'),
|
||||
rules: {
|
||||
'react/no-unstable-nested-components': 1,
|
||||
'no-unused-vars': 'off',
|
||||
'no-undef': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/class-name-casing': 'off'
|
||||
},
|
||||
globals: {
|
||||
Global: 'readonly',
|
||||
React: 'readonly'
|
||||
},
|
||||
ignorePatterns: ['public/static/']
|
||||
};
|
||||
@@ -1,17 +1,20 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'v*-dev'
|
||||
tags:
|
||||
- '*.*.*'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'v*-dev'
|
||||
|
||||
env:
|
||||
NODE_VERSION: '21'
|
||||
NODE_VERSION: '22'
|
||||
|
||||
jobs:
|
||||
deps:
|
||||
@@ -30,7 +33,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '${{ env.NODE_VERSION }}'
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
- name: Deps
|
||||
run: scripts/deps
|
||||
@@ -72,11 +75,6 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
version: ['latest', '${{ github.ref_name }}']
|
||||
exclude:
|
||||
- version: 'main'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -92,10 +90,23 @@ jobs:
|
||||
${{ github.workspace }}/node_modules
|
||||
- name: Build
|
||||
run: scripts/build
|
||||
- name: Determine Version
|
||||
id: version
|
||||
run: |
|
||||
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
|
||||
echo "version=latest" >> $GITHUB_OUTPUT
|
||||
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
||||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
elif [[ "${GITHUB_REF}" == refs/heads/v*-dev ]]; then
|
||||
echo "version=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Not a publish target"
|
||||
exit 1
|
||||
fi
|
||||
- name: Package
|
||||
run: scripts/package
|
||||
env:
|
||||
VERSION: '${{ matrix.version }}'
|
||||
VERSION: '${{ steps.version.outputs.version }}'
|
||||
- name: Release
|
||||
uses: TencentCloud/cos-action@b0aa648235fb35a1bdd6a77f529eb0ac4c2f1c25
|
||||
with:
|
||||
@@ -103,7 +114,24 @@ jobs:
|
||||
secret_key: ${{ secrets.CI_TECENTCOS_SECRET_KEY }}
|
||||
cos_bucket: ${{ secrets.COS_BUCKET }}
|
||||
cos_region: ${{ secrets.COS_REGION }}
|
||||
local_path: dist/${{ matrix.version }}.tar.gz
|
||||
remote_path: releases/${{ matrix.version }}.tar.gz
|
||||
local_path: dist/${{ steps.version.outputs.version }}.tar.gz
|
||||
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
|
||||
accelerate: true
|
||||
clean: false
|
||||
|
||||
trigger-backend:
|
||||
needs: build-publish
|
||||
if: (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Dispatch backend build
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.DISPATCH_PAT }}
|
||||
repository: gpustack/gpustack
|
||||
event-type: ui-built
|
||||
client-payload: |
|
||||
{
|
||||
"ref": "${{ github.ref }}",
|
||||
"sha": "${{ github.sha }}"
|
||||
}
|
||||
|
||||
@@ -12,4 +12,7 @@
|
||||
/.mfsu
|
||||
.swc
|
||||
.DS_Store
|
||||
.idea
|
||||
.idea
|
||||
.claude/settings.local.json
|
||||
/dist.zip
|
||||
.cache
|
||||
@@ -1,4 +1,2 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
npx lint-staged
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
{
|
||||
"*.{md,json}": ["prettier --cache --write"],
|
||||
"*.{js,jsx}": ["max lint --fix --eslint-only", "prettier --cache --write"],
|
||||
"*.{css,less}": [
|
||||
"max lint --fix --stylelint-only",
|
||||
"prettier --cache --write"
|
||||
],
|
||||
"*.{css,less}": ["prettier --cache --write"],
|
||||
"!public/vs/**": [],
|
||||
"*.ts?(x)": [
|
||||
"max lint --fix --eslint-only",
|
||||
"prettier --cache --parser=typescript --write"
|
||||
],
|
||||
"src/locales/**/*.ts": ["npx tsx src/locales/check.ts"]
|
||||
"src/locales/**/*.ts": ["node --import tsx src/locales/check.ts"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ node_modules
|
||||
.umi-production
|
||||
public/static/*.js
|
||||
public/static/*.css
|
||||
src/components/icon-font/iconfont/iconfont.js
|
||||
src/components/icon-font/iconfont/*.css
|
||||
src/components/iconfont/
|
||||
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@ module.exports = {
|
||||
rules: {
|
||||
'selector-class-pattern': null
|
||||
},
|
||||
ignoreFiles: ['public/static/*.css']
|
||||
ignoreFiles: ['public/static/*.css', 'src/components/iconfont/iconfont.css']
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Agent Instructions
|
||||
|
||||
This project keeps a single source of truth for agent/contributor conventions in [`CLAUDE.md`](./CLAUDE.md). **Read [`CLAUDE.md`](./CLAUDE.md) and follow it.**
|
||||
|
||||
@CLAUDE.md
|
||||
@@ -0,0 +1,151 @@
|
||||
# Repo
|
||||
|
||||
This is the **open source UI** (`gpustack-ui`). Common `components`, `hooks`, and `utils` are published as `@gpustack/core-ui` and consumed throughout `src`.
|
||||
|
||||
**Always prioritize reusing common `components`, `hooks`, and `utils` from `@gpustack/core-ui`.**
|
||||
|
||||
Task-specific conventions live in skills: use **create-crud-page** when building a page module, **form-patterns** when building cascading/dependent forms.
|
||||
|
||||
# React State and Request Patterns
|
||||
|
||||
Keep data flow explicit, predictable, and performant. The triggering **action** is the source of truth for UI updates — not effect-driven synchronization.
|
||||
|
||||
## 1. Avoid effect-driven requests
|
||||
|
||||
Do not use request functions as `useEffect` dependencies. Trigger requests explicitly from user actions or lifecycle entry points.
|
||||
|
||||
```ts
|
||||
// Avoid
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
```
|
||||
|
||||
## 2. Form requests should be action-driven
|
||||
|
||||
- Fetch form data (e.g. `Select` options) when the form first opens.
|
||||
- If later requests depend on interactions, trigger them inside the interaction handler.
|
||||
- Do not rely on `useEffect` dependency changes.
|
||||
|
||||
```ts
|
||||
// Recommended
|
||||
const handleOnChange = (value) => {
|
||||
fetchData(value);
|
||||
};
|
||||
```
|
||||
|
||||
## 3. Update related states together
|
||||
|
||||
When one action updates multiple related states, update them all directly in the handler. Do not sync via `useEffect` or derive indirectly via `useMemo`.
|
||||
|
||||
```ts
|
||||
const handleOnChange = (value) => {
|
||||
setState1(...);
|
||||
setState2(...);
|
||||
buildState(...);
|
||||
};
|
||||
```
|
||||
|
||||
## 4. Group strongly related state
|
||||
|
||||
If multiple states always update together, use a single state object instead of multiple `useState` calls — fewer rerenders, more predictable transitions.
|
||||
|
||||
```ts
|
||||
const [state, setState] = useState({ state1: ..., state2: ..., state3: ... });
|
||||
```
|
||||
|
||||
## 5. Prefer explicit state flow
|
||||
|
||||
Keep request execution, state updates, and derived calculations close to the triggering action. Avoid chaining business logic through multiple `useEffect` hooks.
|
||||
|
||||
```ts
|
||||
// Prefer
|
||||
const handleAction = () => {
|
||||
fetchData();
|
||||
setTableData(...);
|
||||
setSelectedRow(...);
|
||||
};
|
||||
```
|
||||
|
||||
## 6. Avoid premature memoization
|
||||
|
||||
Do not use `useMemo` / `useCallback` unless there is a confirmed bottleneck. Overuse adds complexity, obscures state flow, and risks stale dependencies. Optimize only when necessary.
|
||||
|
||||
## 7. Keep request logic predictable
|
||||
|
||||
A user interaction should clearly show: what request fires, which states update, how the UI changes. Avoid indirect update chains from dependency-driven effects.
|
||||
|
||||
## 8. Prefer action-driven architecture
|
||||
|
||||
Prefer action-driven updates, explicit handlers, and localized state transitions over effect-driven synchronization, cross-hook implicit updates, and reactive chains between states.
|
||||
|
||||
# Styles
|
||||
|
||||
**Future direction (apply to all new code):** avoid `styled-components`. Prefer:
|
||||
|
||||
1. `createStyles` for component-scoped dynamic styles
|
||||
2. CSS Modules (`xxx.module.less`) for structured static styles
|
||||
|
||||
Existing `styled-components` usage is legacy tech debt — do not migrate it wholesale, but do not add new `styled-components` either. Theme tokens (`var(--ant-color-*)`) work in all three approaches.
|
||||
|
||||
## Layout
|
||||
|
||||
Compose layout with Ant components, not hand-written `display: flex`.
|
||||
|
||||
- **1D flex** (row/column with `gap`, `align`, `justify`) → `Flex`. Do not write raw `display: flex` in new code.
|
||||
- **Inline sequence** of a few elements with uniform spacing → `Space`.
|
||||
- **Page/grid columns** → `Row` / `Col`.
|
||||
|
||||
Drive spacing with the theme scale (`Flex`/`Space` `gap`, or `var(--ant-*)` spacing tokens), not scattered `px` literals.
|
||||
|
||||
# Naming conventions
|
||||
|
||||
A page module lives under `src/pages/{module}` with this sub-structure: `components/`, `config/`, `forms/`, `hooks/`, `services/`, `index.tsx`. File naming:
|
||||
|
||||
- **Create/edit modal**: `add-{feature}-modal.tsx` (keep the `-modal` suffix even when built with `FormDrawer`).
|
||||
- **Table columns hook**: `use-{feature}-columns.tsx`.
|
||||
- **Open/close & request hooks**: `use-{verb}-{noun}.ts` (e.g. `use-create-user.ts`, `use-query-user-list.ts`).
|
||||
- **Complex table cell**: extract into `{feature}-cell.tsx`.
|
||||
|
||||
# Config & types
|
||||
|
||||
- `config/types.ts` — TypeScript types. Form shape → `FormData`; table/list row → `ListItem`.
|
||||
- `config/index.ts` — static constants, enums, and value/label maps (e.g. `XxxStatusValueMap`, `XxxStatusLabelMap`). Keep constants out of `types.ts`.
|
||||
- **`Select` options that need i18n**: set `label` to the message key and add `locale: true` on the option — the field translates it at render. Omit `locale` for options whose label is already final text. Ref `src/pages/benchmark/config/index.ts`.
|
||||
|
||||
# Common components
|
||||
|
||||
Always check `@gpustack/core-ui` first. Frequently reused:
|
||||
|
||||
- **Drawer/Modal open/close**: `useBodyScroll`.
|
||||
- **Form drawer / footer**: `FormDrawer`, `ModalFooter`.
|
||||
- **Delete confirmation**: `DeleteModal`.
|
||||
- **Search + bulk actions bar**: `FilterBar`.
|
||||
- **Form fields**: `BaseSelect`, `Input` (labeled).
|
||||
- **Text overflow**: `AutoTooltip`.
|
||||
- **Icons**: `IconFont`.
|
||||
- **Tags & status** (4 variants): see the section below.
|
||||
- **Permission-gated visibility**: `Access` / `useAccess`.
|
||||
- **Request hooks**: `useRequest` / `useQueryData` / `useQueryDataList`.
|
||||
- **Table data fetching**: `useTableFetch`.
|
||||
- **Submit guard** (prevent double-submit): `useSubmitLock`.
|
||||
- **Tabbed forms**: `ScrollSpyTabs`.
|
||||
|
||||
# Tags & status indicators
|
||||
|
||||
Four core-ui components cover tag/status display in tables and lists. Pick by **what the value means**, not by how it looks — don't reach for a generic antd `Tag`:
|
||||
|
||||
- **`StatusTag`** — semantic status with a **dynamic message/detail** (tooltip, download, extra content). Use when a row's status carries variable text, e.g. a failed job with an error message. Colors come from `StatusColorMap` (error/warning/transitioning/success/inactive).
|
||||
- **`StatusDot`** — colored dot + short label, **no message**. Use for a plain status/type cell where the value is a fixed enum (e.g. an event-type or log column). Same `StatusColorMap` palette; `inactive` dot is quaternary. If the status needs dynamic text, use `StatusTag` instead.
|
||||
- **`ThemeTag`** — a **standalone category label** (independent content, e.g. a permission scope or a model name). Default neutral; wraps antd `Tag`.
|
||||
- **`TextAttribute`** — a small neutral pill that is a **subordinate annotation following a primary text** (e.g. `key-name [custom]`), not a standalone tag. Manages its own leading margin. Two variants: `filled` (default) and `outlined`. Ref the name column in `src/pages/api-keys/hooks/use-keys-columns.tsx`.
|
||||
|
||||
Rule of thumb: semantic + dynamic text → `StatusTag`; semantic + fixed enum → `StatusDot`; independent category → `ThemeTag`; annotation of nearby text → `TextAttribute`.
|
||||
|
||||
# Dynamic add-item form fields
|
||||
|
||||
When building a form, select the add-item component from the **shape of the field's data** (its schema). Match the schema, don't hand-roll a list UI:
|
||||
|
||||
- **Plain object** (key→value map) → `LabelSelector`.
|
||||
- **String array** → `ListInput`. Ref `src/pages/llmodels/forms/backend-parameters-list.tsx`.
|
||||
- **Object array** → `MetadataList` with a custom item renderer per entry. Ref `src/pages/llmodels/forms/model-lora-list.tsx`.
|
||||
@@ -1,9 +1,10 @@
|
||||
import { defineConfig } from '@umijs/max';
|
||||
import keepAlive from './keep-alive';
|
||||
import { extraMfsuExclude } from './mfsu.extensions';
|
||||
import { compressionPluginConfig, monacoPluginConfig } from './plugins';
|
||||
import proxy from './proxy';
|
||||
import routes from './routes';
|
||||
import { getBranchInfo } from './utils';
|
||||
const CompressionWebpackPlugin = require('compression-webpack-plugin');
|
||||
|
||||
const versionInfo = getBranchInfo();
|
||||
process.env.VERSION = JSON.stringify(versionInfo);
|
||||
@@ -19,6 +20,9 @@ export default defineConfig({
|
||||
history: {
|
||||
type: 'hash'
|
||||
},
|
||||
define: {
|
||||
'process.env.ENABLE_ENTERPRISE': process.env.ENABLE_ENTERPRISE
|
||||
},
|
||||
analyze: {
|
||||
analyzerMode: 'server',
|
||||
analyzerPort: 8888,
|
||||
@@ -28,6 +32,9 @@ export default defineConfig({
|
||||
logLevel: 'info',
|
||||
defaultSizes: 'parsed' // stat // gzip
|
||||
},
|
||||
mfsu: {
|
||||
exclude: ['lodash', 'ml-pca', ...extraMfsuExclude]
|
||||
},
|
||||
base: process.env.npm_config_base || '/',
|
||||
...(isProduction
|
||||
? {
|
||||
@@ -50,29 +57,18 @@ export default defineConfig({
|
||||
chunkFilename: `css/[name].${t}.chunk.css`
|
||||
}
|
||||
]);
|
||||
config.module
|
||||
.rule('worker')
|
||||
.test(/\.worker\.js$/)
|
||||
.use('worker-loader')
|
||||
.loader('worker-loader');
|
||||
config.output
|
||||
.filename(`js/[name].${t}.js`)
|
||||
.chunkFilename(`js/[name].${t}.chunk.js`);
|
||||
config
|
||||
.plugin('compression-webpack-plugin')
|
||||
.use(CompressionWebpackPlugin, [
|
||||
{
|
||||
filename: '[path][base].gz',
|
||||
algorithm: 'gzip',
|
||||
test: /\.(js|css|html|svg)$/,
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
}
|
||||
]);
|
||||
compressionPluginConfig(config);
|
||||
monacoPluginConfig(config);
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
|
||||
: {
|
||||
chainWebpack(config) {
|
||||
monacoPluginConfig(config);
|
||||
}
|
||||
}),
|
||||
favicons: ['/static/favicon.png'],
|
||||
jsMinifier: 'terser',
|
||||
cssMinifier: 'cssnano',
|
||||
@@ -81,11 +77,15 @@ export default defineConfig({
|
||||
antd: {
|
||||
style: 'less'
|
||||
},
|
||||
title: 'GPUStack',
|
||||
hash: true,
|
||||
access: {},
|
||||
model: {},
|
||||
initialState: {},
|
||||
request: {},
|
||||
routePrefetch: {
|
||||
defaultPrefetch: 'intent'
|
||||
},
|
||||
keepalive: keepAlive,
|
||||
locale: {
|
||||
antd: true,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Identity hook for build-time mfsu.exclude extensions. Tooling may
|
||||
// overwrite this file to add package names that must skip MFSU's
|
||||
// pre-bundling; the original is restored on cleanup. Mirrors
|
||||
// `src/request.extensions.ts` / `src/access.extensions.ts`.
|
||||
//
|
||||
// MFSU bundles node_modules into immutable chunks at dev startup, so
|
||||
// workspace-linked packages whose source you edit during dev must be
|
||||
// excluded here or HMR won't pick up changes.
|
||||
export const extraMfsuExclude: string[] = [];
|
||||
@@ -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.js'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
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,11 +1,13 @@
|
||||
const proxyTableList = [
|
||||
'cli',
|
||||
'v1',
|
||||
'v2',
|
||||
'auth',
|
||||
'v1-openai',
|
||||
'version',
|
||||
'proxy',
|
||||
'update'
|
||||
'update',
|
||||
'grafana'
|
||||
];
|
||||
|
||||
// @ts-ingore
|
||||
@@ -20,12 +22,6 @@ export default function createProxyTable(target?: string) {
|
||||
ws: true,
|
||||
log: 'debug',
|
||||
pathRewrite: (pth: string) => pth.replace(`/^/${api}`, `/${api}`),
|
||||
// onProxyRes: (proxyRes: any, req: any, res: any) => {
|
||||
// console.log('headers=========', {
|
||||
// res: proxyRes.headers,
|
||||
// req: req.headers
|
||||
// });
|
||||
// },
|
||||
headers: {
|
||||
origin: newTarget,
|
||||
Connection: 'keep-alive'
|
||||
@@ -35,5 +31,6 @@ export default function createProxyTable(target?: string) {
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return proxyTable;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Identity hook for build-time route extensions. Tooling may overwrite
|
||||
// this file to inject additional routes; the original is restored on cleanup.
|
||||
export const applyRouteExtensions = <T>(base: T): T => base;
|
||||
@@ -1,17 +1,26 @@
|
||||
import { keepAliveRoutes } from './keep-alive';
|
||||
import { applyRouteExtensions } from './routes.extensions';
|
||||
|
||||
export default [
|
||||
const baseRoutes = [
|
||||
{
|
||||
name: 'dashboard',
|
||||
path: '/dashboard',
|
||||
key: 'dashboard',
|
||||
icon: 'AppstoreOutlined',
|
||||
access: 'canSeeAdmin',
|
||||
component: './dashboard'
|
||||
icon: 'icon-dashboard',
|
||||
selectedIcon: 'icon-dashboard-filled',
|
||||
defaultIcon: 'icon-dashboard',
|
||||
// `canSeeOrgAdmin` widens to anyone the access seam grants
|
||||
// admin-ish visibility — by default platform admin, plus
|
||||
// whatever the routes extension chooses to allow.
|
||||
access: 'canSeeOrgAdmin',
|
||||
component: './dashboard',
|
||||
routes: []
|
||||
},
|
||||
{
|
||||
name: 'playground',
|
||||
icon: 'ExperimentOutlined',
|
||||
icon: 'icon-experiment',
|
||||
selectedIcon: 'icon-experiment-filled',
|
||||
defaultIcon: 'icon-experiment',
|
||||
path: '/playground',
|
||||
key: 'playground',
|
||||
routes: [
|
||||
@@ -24,86 +33,375 @@ export default [
|
||||
title: 'Chat',
|
||||
path: '/playground/chat',
|
||||
key: 'chat',
|
||||
icon: 'Comment',
|
||||
component: './playground/index'
|
||||
},
|
||||
{
|
||||
name: 'text2images',
|
||||
title: 'Text2Images',
|
||||
path: keepAliveRoutes.text2images,
|
||||
key: 'text2images',
|
||||
icon: 'Comment',
|
||||
component: './playground/images'
|
||||
},
|
||||
{
|
||||
name: 'speech',
|
||||
title: 'Speech',
|
||||
path: keepAliveRoutes.speech,
|
||||
key: 'speech',
|
||||
icon: 'Comment',
|
||||
component: './playground/speech'
|
||||
icon: 'icon-chat',
|
||||
selectedIcon: 'icon-chat-filled',
|
||||
defaultIcon: 'icon-chat',
|
||||
component: './playground/chat/index'
|
||||
},
|
||||
{
|
||||
name: 'embedding',
|
||||
title: 'embedding',
|
||||
path: '/playground/embedding',
|
||||
key: 'embedding',
|
||||
icon: 'Comment',
|
||||
component: './playground/embedding'
|
||||
icon: 'icon-embedding',
|
||||
selectedIcon: 'icon-embedding-filled',
|
||||
defaultIcon: 'icon-embedding',
|
||||
component: './playground/embedding/index'
|
||||
},
|
||||
{
|
||||
name: 'rerank',
|
||||
title: 'Rerank',
|
||||
path: '/playground/rerank',
|
||||
key: 'rerank',
|
||||
icon: 'Comment',
|
||||
component: './playground/rerank'
|
||||
icon: 'icon-reranker',
|
||||
selectedIcon: 'icon-reranker-filled',
|
||||
defaultIcon: 'icon-reranker',
|
||||
component: './playground/rerank/index'
|
||||
},
|
||||
{
|
||||
name: 'text2images',
|
||||
title: 'Text2Images',
|
||||
path: keepAliveRoutes.text2images,
|
||||
key: 'text2images',
|
||||
icon: 'icon-image1',
|
||||
selectedIcon: 'icon-image-filled',
|
||||
defaultIcon: 'icon-image1',
|
||||
component: './playground/images/index'
|
||||
},
|
||||
{
|
||||
name: 'speech',
|
||||
title: 'Speech',
|
||||
path: keepAliveRoutes.speech,
|
||||
key: 'speech',
|
||||
icon: 'icon-audio1',
|
||||
selectedIcon: 'icon-audio-filled',
|
||||
defaultIcon: 'icon-audio1',
|
||||
component: './playground/speech/index'
|
||||
}
|
||||
// {
|
||||
// name: 'video',
|
||||
// title: 'Video',
|
||||
// path: '/playground/video',
|
||||
// key: 'video',
|
||||
// icon: 'icon-video-outline',
|
||||
// hideInMenu: false,
|
||||
// selectedIcon: 'icon-video-filled02',
|
||||
// defaultIcon: 'icon-video-outline',
|
||||
// component: './playground/video'
|
||||
// }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'models',
|
||||
path: '/models',
|
||||
key: 'models',
|
||||
routes: [
|
||||
{
|
||||
path: '/models',
|
||||
redirect: '/models/deployments'
|
||||
},
|
||||
{
|
||||
name: 'userModels',
|
||||
path: '/models/user-models',
|
||||
key: 'userModels',
|
||||
icon: 'icon-models',
|
||||
selectedIcon: 'icon-models-filled',
|
||||
defaultIcon: 'icon-models',
|
||||
component: './llmodels/user-models'
|
||||
},
|
||||
{
|
||||
name: 'modelCatalog',
|
||||
path: '/models/catalog',
|
||||
key: 'modelsCatalog',
|
||||
icon: 'icon-layers',
|
||||
selectedIcon: 'icon-layers-filled',
|
||||
defaultIcon: 'icon-layers',
|
||||
access: 'canSeeOrgAdmin',
|
||||
component: './llmodels/catalog'
|
||||
},
|
||||
{
|
||||
name: 'deployment',
|
||||
path: '/models/deployments',
|
||||
key: 'modelDeployments',
|
||||
icon: 'icon-rocket-launch1',
|
||||
selectedIcon: 'icon-rocket-launch-fill',
|
||||
defaultIcon: 'icon-rocket-launch1',
|
||||
access: 'canSeeOrgAdmin',
|
||||
component: './llmodels/index'
|
||||
},
|
||||
{
|
||||
name: 'routes',
|
||||
path: '/models/routes',
|
||||
key: 'routes',
|
||||
icon: 'icon-captive_portal',
|
||||
selectedIcon: 'icon-captive_portal',
|
||||
defaultIcon: 'icon-captive_portal',
|
||||
access: 'canSeeOrgAdmin',
|
||||
component: './model-routes/index'
|
||||
},
|
||||
{
|
||||
name: 'providers',
|
||||
path: '/models/providers',
|
||||
key: 'modelProviders',
|
||||
icon: 'icon-extension-outline',
|
||||
selectedIcon: 'icon-extension-filled',
|
||||
defaultIcon: 'icon-extension-outline',
|
||||
access: 'canSeeOrgAdmin',
|
||||
component: './maas-provider/index'
|
||||
},
|
||||
{
|
||||
name: 'benchmark',
|
||||
path: '/models/benchmark',
|
||||
key: 'benchmark',
|
||||
icon: 'icon-speed',
|
||||
selectedIcon: 'icon-speed-filled',
|
||||
defaultIcon: 'icon-speed',
|
||||
access: 'canSeeOrgAdmin',
|
||||
component: './benchmark/index'
|
||||
},
|
||||
{
|
||||
name: 'benchmarkDetail',
|
||||
path: '/models/benchmark/detail',
|
||||
key: 'benchmarkDetail',
|
||||
icon: 'icon-speed',
|
||||
selectedIcon: 'icon-speed-filled',
|
||||
defaultIcon: 'icon-speed',
|
||||
access: 'canSeeOrgAdmin',
|
||||
hideInMenu: true,
|
||||
component: './benchmark/details'
|
||||
},
|
||||
{
|
||||
name: 'backendsList',
|
||||
path: '/models/backends',
|
||||
key: 'backendsList',
|
||||
icon: 'icon-backend',
|
||||
selectedIcon: 'icon-backend-filled',
|
||||
defaultIcon: 'icon-backend',
|
||||
access: 'canSeeOrgAdmin',
|
||||
component: './backends/index'
|
||||
},
|
||||
{
|
||||
name: 'modelfiles',
|
||||
path: '/models/modelfiles',
|
||||
key: 'modelfiles',
|
||||
icon: 'icon-files',
|
||||
selectedIcon: 'icon-files-filled',
|
||||
defaultIcon: 'icon-files',
|
||||
access: 'canSeeOrgAdmin',
|
||||
component: './resources/components/model-files'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'modelCatalog',
|
||||
path: '/models/catalog',
|
||||
key: 'modelsCatalog',
|
||||
icon: 'icon-catalog',
|
||||
access: 'canSeeAdmin',
|
||||
component: './llmodels/catalog'
|
||||
},
|
||||
{
|
||||
name: 'models',
|
||||
path: '/models/list',
|
||||
key: 'models',
|
||||
icon: 'Block',
|
||||
access: 'canSeeAdmin',
|
||||
component: './llmodels/index'
|
||||
name: 'gpuService',
|
||||
path: '/gpu-service',
|
||||
key: 'gpuService',
|
||||
access: 'canSeeGpuService',
|
||||
routes: [
|
||||
{
|
||||
path: '/gpu-service',
|
||||
redirect: '/gpu-service/instances'
|
||||
},
|
||||
{
|
||||
name: 'instances',
|
||||
path: '/gpu-service/instances',
|
||||
key: 'gpuServiceList',
|
||||
icon: 'icon-cloud-outlined',
|
||||
selectedIcon: 'icon-cloud-filled',
|
||||
defaultIcon: 'icon-cloud-outlined',
|
||||
component: './gpu-service/instances'
|
||||
},
|
||||
{
|
||||
name: 'templates',
|
||||
path: '/gpu-service/templates',
|
||||
key: 'gpuServiceTemplates',
|
||||
icon: 'icon-instance-template-outlined',
|
||||
selectedIcon: 'icon-instance-template-filled',
|
||||
defaultIcon: 'icon-instance-template-outlined',
|
||||
component: './gpu-service/templates'
|
||||
},
|
||||
{
|
||||
name: 'storage',
|
||||
path: '/gpu-service/storage',
|
||||
key: 'gpuServiceStorage',
|
||||
icon: 'icon-database-outlined',
|
||||
selectedIcon: 'icon-database-filled',
|
||||
defaultIcon: 'icon-database-outlined',
|
||||
component: './gpu-service/storage'
|
||||
},
|
||||
{
|
||||
name: 'storageTypes',
|
||||
path: '/gpu-service/storage-types',
|
||||
key: 'gpuServiceStorageTypes',
|
||||
icon: 'icon-storage-outlined',
|
||||
// Storage types are tenant-scoped on the backend (Org owners
|
||||
// can create/list their own), so the menu shouldn't be
|
||||
// platform-admin-only. ``canSeeOrgAdmin`` keeps the gate at
|
||||
// "admin or current-org owner" — Org members still don't see
|
||||
// it, which matches the read/write model in the route.
|
||||
access: 'canSeeOrgAdmin',
|
||||
selectedIcon: 'icon-storage-filled',
|
||||
defaultIcon: 'icon-storage-outlined',
|
||||
component: './gpu-service/storage-types'
|
||||
},
|
||||
{
|
||||
name: 'publicKeys',
|
||||
path: '/gpu-service/public-keys',
|
||||
key: 'gpuServicePublicKeys',
|
||||
icon: 'icon-ssh-outlined',
|
||||
selectedIcon: 'icon-ssh-filled',
|
||||
defaultIcon: 'icon-ssh-outlined',
|
||||
component: './gpu-service/public-keys'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'resources',
|
||||
path: '/resources',
|
||||
key: 'resources',
|
||||
icon: 'CloudServer',
|
||||
access: 'canSeeAdmin',
|
||||
component: './resources'
|
||||
access: 'canSeeOrgAdmin',
|
||||
routes: [
|
||||
{
|
||||
path: '/resources',
|
||||
redirect: '/resources/workers'
|
||||
},
|
||||
{
|
||||
name: 'clusters',
|
||||
path: '/resources/clusters/list',
|
||||
key: 'clusters',
|
||||
icon: 'icon-cluster2-outline',
|
||||
selectedIcon: 'icon-cluster2-filled',
|
||||
defaultIcon: 'icon-cluster2-outline',
|
||||
component: './cluster-management/clusters',
|
||||
subMenu: ['/resources/clusters/create']
|
||||
},
|
||||
{
|
||||
name: 'workers',
|
||||
path: '/resources/workers',
|
||||
key: 'workers',
|
||||
icon: 'icon-resources',
|
||||
selectedIcon: 'icon-resources-filled',
|
||||
defaultIcon: 'icon-resources',
|
||||
component: './resources/components/workers'
|
||||
},
|
||||
{
|
||||
name: 'gpus',
|
||||
path: '/resources/gpus',
|
||||
key: 'gpus',
|
||||
icon: 'icon-gpu1',
|
||||
selectedIcon: 'icon-gpu-filled',
|
||||
defaultIcon: 'icon-gpu1',
|
||||
component: './resources/components/gpus'
|
||||
},
|
||||
{
|
||||
name: 'credentials',
|
||||
path: '/resources/credentials',
|
||||
key: 'credentials',
|
||||
icon: 'icon-credential-outline',
|
||||
selectedIcon: 'icon-credential-filled',
|
||||
defaultIcon: 'icon-credential-outline',
|
||||
component: './cluster-management/credentials'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// Cross-resource consumption (tokens + GPU/CPU instances + storage).
|
||||
// A folder so it matches the other top-level groups; more usage views can
|
||||
// graduate in here later.
|
||||
name: 'billingAndUsage',
|
||||
path: '/usage',
|
||||
key: 'usageGroup',
|
||||
icon: 'icon-usage-outlined',
|
||||
selectedIcon: 'icon-usage-filled',
|
||||
defaultIcon: 'icon-usage-outlined',
|
||||
routes: [
|
||||
{
|
||||
path: '/usage',
|
||||
redirect: '/usage/overview'
|
||||
},
|
||||
{
|
||||
name: 'usage',
|
||||
path: '/usage/overview',
|
||||
key: 'usage',
|
||||
icon: 'icon-usage-outlined',
|
||||
selectedIcon: 'icon-usage-filled',
|
||||
defaultIcon: 'icon-usage-outlined',
|
||||
component: './usage/index'
|
||||
},
|
||||
{
|
||||
name: 'billing',
|
||||
path: '/usage/billing',
|
||||
key: 'billing',
|
||||
icon: 'icon-billing-outlined',
|
||||
selectedIcon: 'icon-billing-filled',
|
||||
defaultIcon: 'icon-billing-outlined',
|
||||
hideInMenu: process.env.ENABLE_ENTERPRISE === 'true',
|
||||
// OSS exposes the menu as a teaser for the enterprise billing
|
||||
// module. The page itself just renders an upsell notice — the real
|
||||
// billing UI lives in the enterprise plugin and shadows this route
|
||||
// via `routes.extensions.ts`.
|
||||
component: './billing'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'accessControl',
|
||||
path: '/access-control',
|
||||
key: 'accessControl',
|
||||
routes: [
|
||||
{
|
||||
path: '/access-control',
|
||||
redirect: '/access-control/users'
|
||||
},
|
||||
{
|
||||
name: 'organizations',
|
||||
path: '/access-control/organizations',
|
||||
key: 'organizations',
|
||||
icon: 'icon-org-outlined',
|
||||
selectedIcon: 'icon-org-filled',
|
||||
defaultIcon: 'icon-org-outlined',
|
||||
// OSS exposes the menu to platform admins as a teaser for the
|
||||
// enterprise multi-tenancy module. The page itself just renders
|
||||
// an upsell notice — the real CRUD UI lives in the enterprise
|
||||
// plugin and shadows this route via `routes.extensions.ts`.
|
||||
access: 'canSeeAdmin',
|
||||
component: './organizations'
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
path: '/access-control/users',
|
||||
key: 'users',
|
||||
icon: 'icon-users',
|
||||
selectedIcon: 'icon-users-filled',
|
||||
defaultIcon: 'icon-users',
|
||||
access: 'canSeeAdmin',
|
||||
component: './users'
|
||||
},
|
||||
{
|
||||
name: 'apikeys',
|
||||
path: '/access-control/api-keys',
|
||||
key: 'apikeys',
|
||||
selectedIcon: 'icon-key-filled',
|
||||
icon: 'icon-key',
|
||||
defaultIcon: 'icon-key',
|
||||
component: './api-keys'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'apikeys',
|
||||
path: '/api-keys',
|
||||
key: 'apikeys',
|
||||
icon: 'KeyOutlined',
|
||||
hideInMenu: true,
|
||||
selectedIcon: 'icon-key-filled',
|
||||
icon: 'icon-key',
|
||||
defaultIcon: 'icon-key',
|
||||
component: './api-keys'
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
path: '/users',
|
||||
key: 'users',
|
||||
icon: 'Team',
|
||||
access: 'canSeeAdmin',
|
||||
component: './users'
|
||||
},
|
||||
{
|
||||
name: 'profile',
|
||||
path: '/profile',
|
||||
key: 'profile',
|
||||
path: '/preferences',
|
||||
key: 'preferences',
|
||||
hideInMenu: true,
|
||||
component: './profile',
|
||||
icon: 'User'
|
||||
@@ -124,3 +422,5 @@ export default [
|
||||
component: './404'
|
||||
}
|
||||
];
|
||||
|
||||
export default applyRouteExtensions(baseRoutes);
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
const child_process = require('child_process');
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export const getBranchInfo = () => {
|
||||
const latestCommit = child_process
|
||||
.execSync('git rev-parse HEAD')
|
||||
.toString()
|
||||
.trim();
|
||||
const versionTag = child_process
|
||||
.execSync(`git tag --contains ${latestCommit}`)
|
||||
.toString()
|
||||
.trim();
|
||||
return { version: versionTag || '', commitId: latestCommit.slice(0, 7) };
|
||||
// git may be absent (source archive, bare container) or this tree may
|
||||
// not be a git checkout. Swallow the failure and fall back to the env
|
||||
// overrides below — losing build info shouldn't fail the build.
|
||||
let latestCommit = '';
|
||||
let versionTag = '';
|
||||
try {
|
||||
latestCommit = execSync('git rev-parse HEAD').toString().trim();
|
||||
versionTag = execSync(`git tag --contains ${latestCommit}`)
|
||||
.toString()
|
||||
.trim();
|
||||
} catch {
|
||||
// Not a git checkout / git unavailable; rely on env overrides.
|
||||
}
|
||||
// Respect explicit GPUSTACK_UI_* overrides so a wrapping build that
|
||||
// checks this source tree out as a sub-package can stamp its own
|
||||
// release tag and commit id onto the UI (otherwise the panel reports
|
||||
// the host tree's git HEAD, which the wrapper doesn't control).
|
||||
const overrideVersion = process.env.GPUSTACK_UI_VERSION?.trim();
|
||||
const overrideCommitId = process.env.GPUSTACK_UI_COMMIT_ID?.trim();
|
||||
return {
|
||||
version: overrideVersion || versionTag || '',
|
||||
commitId: overrideCommitId || latestCommit.slice(0, 7)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import js from '@eslint/js';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import unusedImports from 'eslint-plugin-unused-imports';
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores([
|
||||
'public/static/',
|
||||
'dist',
|
||||
'src/.umi/',
|
||||
'src/.umi-production/',
|
||||
'src/.umi-test/',
|
||||
'src/components/iconfont/'
|
||||
]),
|
||||
{
|
||||
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
prettier
|
||||
],
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
import: importPlugin,
|
||||
'unused-imports': unusedImports
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
},
|
||||
'import/resolver': {
|
||||
node: true,
|
||||
typescript: true
|
||||
}
|
||||
},
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
Global: 'readonly',
|
||||
React: 'readonly',
|
||||
JSX: 'readonly'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'react/no-unstable-nested-components': 'warn',
|
||||
'no-unused-vars': 'off',
|
||||
'no-undef': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/no-unnecessary-type-constraint': 'off',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'unused-imports/no-unused-vars': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-duplicates': 'error',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react-hooks/preserve-manual-memoization': 'off',
|
||||
'react-hooks/set-state-in-effect': 'off',
|
||||
'react-hooks/refs': 'off',
|
||||
'react-hooks/use-memo': 'off',
|
||||
'react-hooks/immutability': 'off',
|
||||
'no-unsafe-optional-chaining': 'off',
|
||||
'no-empty': 'off',
|
||||
'no-constant-condition': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0, maxBOF: 0 }]
|
||||
}
|
||||
}
|
||||
]);
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"private": true,
|
||||
"author": "jialin",
|
||||
"author": "gpustack",
|
||||
"scripts": {
|
||||
"build": "max build",
|
||||
"check:locales": "npx tsx ./src/locales/check.ts",
|
||||
"check:locales": "node --import tsx ./src/locales/check.ts",
|
||||
"dev": "max dev",
|
||||
"format": "prettier --cache --write .",
|
||||
"postinstall": "max setup",
|
||||
@@ -12,13 +12,12 @@
|
||||
"setup": "max setup",
|
||||
"start": "npm run dev"
|
||||
},
|
||||
"resolutions": {
|
||||
"immer": "^9.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.5.1",
|
||||
"@ant-design/pro-components": "^2.7.19",
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@ant-design/pro-components": "3.1.0-0",
|
||||
"@antv/g6": "^5.0.51",
|
||||
"@braintree/sanitize-url": "^7.1.1",
|
||||
"@gpustack/core-ui": "^1.0.42",
|
||||
"@huggingface/gguf": "^0.1.7",
|
||||
"@huggingface/hub": "^0.15.1",
|
||||
"@huggingface/tasks": "^0.11.6",
|
||||
@@ -26,78 +25,104 @@
|
||||
"@orcid/bibtex-parse-js": "^0.0.25",
|
||||
"@react-hook/resize-observer": "^2.0.2",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@umijs/max": "^4.2.11",
|
||||
"@umijs/max": "^4.6.15",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"ahooks": "^3.8.5",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"antd": "^5.21.6",
|
||||
"antd": "^6.3.3",
|
||||
"antd-style": "^3.6.2",
|
||||
"axios": "^1.7.2",
|
||||
"axios": "^1.8.2",
|
||||
"classnames": "^2.5.1",
|
||||
"clipboard": "^2.0.11",
|
||||
"crypto-js": "^4.2.0",
|
||||
"culori": "^4.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"driver.js": "^1.3.1",
|
||||
"dompurify": "^3.2.6",
|
||||
"echarts": "^5.5.1",
|
||||
"epubjs": "^0.3.93",
|
||||
"file-saver": "^2.0.5",
|
||||
"has-ansi": "^5.0.1",
|
||||
"highlight.js": "^11.10.0",
|
||||
"jdenticon": "^3.3.0",
|
||||
"jotai": "^2.8.4",
|
||||
"jszip": "^3.10.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"katex": "^0.16.21",
|
||||
"lamejs": "github:zhuker/lamejs",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mammoth": "^1.8.0",
|
||||
"marked": "^14.1.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"ml-dataset-iris": "^1.2.1",
|
||||
"ml-pca": "^4.1.1",
|
||||
"monaco-editor": "^0.30.1",
|
||||
"monaco-yaml": "^4.0.0",
|
||||
"numeral": "^2.0.6",
|
||||
"overlayscrollbars": "^2.10.0",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"pdfjs-dist": "^4.7.76",
|
||||
"query-string": "^9.0.0",
|
||||
"rc-resize-observer": "^1.4.0",
|
||||
"rc-resize-observer": "^1.4.3",
|
||||
"rc-virtual-list": "^3.14.8",
|
||||
"re-resizable": "^6.10.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"semver": "^7.7.3",
|
||||
"simplebar-react": "^3.2.6",
|
||||
"styled-components": "^6.1.15",
|
||||
"umi-presets-pro": "^2.0.3",
|
||||
"wavesurfer.js": "^7.8.8",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
"wavesurfer.js": "^7.8.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/marked": "^6.0.0",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.1",
|
||||
"@umijs/case-sensitive-paths-webpack-plugin": "^1.0.1",
|
||||
"@umijs/plugins": "^4.4.11",
|
||||
"babel-plugin-named-asset-import": "^0.3.8",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||
"compression-webpack-plugin": "^11.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-unused-imports": "^4.4.1",
|
||||
"extract-css-loader": "^0.0.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"globals": "^17.4.0",
|
||||
"husky": "^9.0.11",
|
||||
"less-loader": "^12.2.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"monaco-editor-webpack-plugin": "^4.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"prettier-plugin-packagejson": "^2.5.0",
|
||||
"prettier-plugin-two-style-order": "^1.0.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"worker-loader": "^3.0.8"
|
||||
},
|
||||
"packageManager": "pnpm@9.3.0",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"elliptic": "^6.6.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ export default (api: IApi) => {
|
||||
const info = JSON.parse(process.env.VERSION || '{}');
|
||||
const env = process.env.NODE_ENV;
|
||||
|
||||
$('html').attr('lang', 'en');
|
||||
|
||||
$('html').attr('data-env', env);
|
||||
|
||||
$('html').attr(
|
||||
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,32 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const pnpmDir = path.resolve(__dirname, '../../../node_modules/.pnpm');
|
||||
|
||||
function findDir(prefix) {
|
||||
return fs.readdirSync(pnpmDir).find((name) => name.startsWith(prefix));
|
||||
}
|
||||
|
||||
const stylelintDir = findDir('stylelint@14.8.2');
|
||||
if (!stylelintDir) {
|
||||
console.error('Compatible stylelint@14.8.2 not found in workspace node_modules/.pnpm');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const stylelintBin = path.join(
|
||||
pnpmDir,
|
||||
stylelintDir,
|
||||
'node_modules/stylelint/bin/stylelint.js'
|
||||
);
|
||||
|
||||
const result = spawnSync(process.execPath, [stylelintBin, ...process.argv.slice(2)], {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(result.error);
|
||||
}
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
@@ -0,0 +1,14 @@
|
||||
// Identity hook for build-time access-predicate extensions. Tooling may
|
||||
// overwrite this file to widen predicates; the original is restored on
|
||||
// cleanup. Mirrors `config/routes.extensions.ts`.
|
||||
export type AccessPredicates = {
|
||||
canSeeAdmin: boolean;
|
||||
canSeeOrgAdmin: boolean;
|
||||
canManageCurrentOrg: boolean;
|
||||
canSeeUser: boolean;
|
||||
canDelete: boolean;
|
||||
canLogin: boolean;
|
||||
};
|
||||
|
||||
export const applyAccessExtensions = <T extends AccessPredicates>(base: T): T =>
|
||||
base;
|
||||
@@ -1,12 +1,56 @@
|
||||
export default (initialState: { currentUser?: Global.UserInfo }) => {
|
||||
const canSeeAdmin = !!(
|
||||
import { applyAccessExtensions } from './access.extensions';
|
||||
|
||||
export default (initialState: {
|
||||
currentUser?: Global.UserInfo;
|
||||
hasKubernetesCluster?: boolean;
|
||||
hasResourceEvents?: boolean;
|
||||
}) => {
|
||||
const isPlatformAdmin = !!(
|
||||
initialState &&
|
||||
initialState.currentUser &&
|
||||
initialState.currentUser.is_admin
|
||||
);
|
||||
return {
|
||||
canSeeAdmin,
|
||||
const canSeeUser = !!(
|
||||
initialState &&
|
||||
initialState.currentUser &&
|
||||
!initialState.currentUser.is_admin
|
||||
);
|
||||
// GPU Service is Kubernetes-only. We only gate visibility down when
|
||||
// the probe in `getInitialState` came back with a definitive answer;
|
||||
// `undefined` (probe failed / not yet ready) collapses to the
|
||||
// role-based default so a transient network blip can't lock anyone
|
||||
// out of the menu.
|
||||
const hasKubernetesCluster = initialState?.hasKubernetesCluster;
|
||||
// Having run GPU/CPU instances or storage (any resource_events) also unlocks
|
||||
// GPU Service / the full Usage page — a user who used it keeps seeing it even
|
||||
// without a current cluster. MaaS-only users (no cluster, no events) don't.
|
||||
const hasResourceEvents = !!initialState?.hasResourceEvents;
|
||||
|
||||
// Predicate roles, top-down by strictness:
|
||||
// * `canSeeAdmin` — strictly platform admin (`users.is_admin`).
|
||||
// Gates Users.
|
||||
// * `canSeeOrgAdmin` — admin-style menus that work cross-org
|
||||
// (Dashboard, Resources, Models, Cluster Management). Defaults
|
||||
// to platform admin; extensions widen to include org admins.
|
||||
// * `canSeeGpuService` — GPU Service menu. Anyone allowed to
|
||||
// manage clusters (admins, Org owners) sees it; non-admins fall
|
||||
// through to "show only if a Kubernetes cluster is actually
|
||||
// reachable" so Org members without scheduling access don't see
|
||||
// a dead-end menu item.
|
||||
// * `canManageCurrentOrg` — pages that only make sense inside a
|
||||
// specific org context (member / group management). Defaults to
|
||||
// `false`; extensions widen when both an org is selected AND
|
||||
// the caller is admin of it.
|
||||
// Pass through `applyAccessExtensions` so build-time tooling can
|
||||
// widen these without editing this file. Default is a no-op.
|
||||
return applyAccessExtensions({
|
||||
canSeeAdmin: isPlatformAdmin,
|
||||
canSeeOrgAdmin: isPlatformAdmin,
|
||||
canSeeGpuService:
|
||||
isPlatformAdmin || hasKubernetesCluster !== false || hasResourceEvents,
|
||||
canManageCurrentOrg: false,
|
||||
canSeeUser,
|
||||
canDelete: true,
|
||||
canLogin: true
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
||||
import { userSettingsHelperAtom } from '@/atoms/settings';
|
||||
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||
import { setAtomStorage } from '@/atoms/utils';
|
||||
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
||||
import { getGPUStackPlugin } from '@/plugins';
|
||||
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
||||
import { GPUStackPluginManager } from '@/plugins/manager';
|
||||
import { requestConfig } from '@/request-config';
|
||||
import {
|
||||
queryCurrentUserState,
|
||||
queryVersionInfo,
|
||||
updateCheck
|
||||
} from '@/services/profile/apis';
|
||||
import { fetchSystemConfig } from '@/services/system/query-system-config';
|
||||
import { isOnline } from '@/utils';
|
||||
import {
|
||||
markInitialStateProbed,
|
||||
probeAccessFlags
|
||||
} from '@/utils/access-probes';
|
||||
import { installTenantFetch } from '@/utils/install-fetch';
|
||||
import {
|
||||
IS_FIRST_LOGIN,
|
||||
readState,
|
||||
writeState
|
||||
} from '@/utils/localstore/index';
|
||||
import { RequestConfig, history } from '@umijs/max';
|
||||
import '@gpustack/core-ui/style.css';
|
||||
import { RequestConfig, history, request as umiRequest } from '@umijs/max';
|
||||
import { message } from 'antd';
|
||||
|
||||
const loginPath = '/login';
|
||||
installTenantFetch();
|
||||
|
||||
// only for the first login and access from http://localhost
|
||||
|
||||
@@ -23,7 +37,7 @@ const checkDefaultPage = async (userInfo: any) => {
|
||||
if (isFirstLogin === null && isOnline()) {
|
||||
writeState(IS_FIRST_LOGIN, true);
|
||||
if (userInfo && userInfo?.is_admin) {
|
||||
history.push('/models/list');
|
||||
history.push(DEFAULT_ENTER_PAGE.adminForFirst);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -32,9 +46,29 @@ const checkDefaultPage = async (userInfo: any) => {
|
||||
export async function getInitialState(): Promise<{
|
||||
fetchUserInfo: () => Promise<Global.UserInfo>;
|
||||
currentUser?: Global.UserInfo;
|
||||
pluginData?: Record<string, any>;
|
||||
hasKubernetesCluster?: boolean;
|
||||
hasResourceEvents?: boolean;
|
||||
}> {
|
||||
const { location } = history;
|
||||
|
||||
// In open-source builds the promise resolves immediately.
|
||||
await enterprisePluginReady;
|
||||
|
||||
// initialize plugins and merge enterprise locales
|
||||
let pluginData = {};
|
||||
try {
|
||||
pluginData = await GPUStackPluginManager.initialize({
|
||||
request: umiRequest,
|
||||
setUserSettings: (value) => setAtomStorage(userSettingsHelperAtom, value),
|
||||
setStorageUserSettings: (value) =>
|
||||
setAtomStorage(userSettingsHelperAtom, value),
|
||||
defaultColorPrimary: COLOR_PRIMARY
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize plugins:', error);
|
||||
}
|
||||
|
||||
const getUpdateCheck = async () => {
|
||||
try {
|
||||
const data = await updateCheck();
|
||||
@@ -48,17 +82,61 @@ export async function getInitialState(): Promise<{
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserInfo = async (): Promise<Global.UserInfo> => {
|
||||
const fetchUserInfo = async (config?: {
|
||||
skipErrorHandler?: boolean;
|
||||
}): Promise<Global.UserInfo> => {
|
||||
try {
|
||||
const data = await queryCurrentUserState({
|
||||
skipErrorHandler: true
|
||||
});
|
||||
if (data.is_admin) {
|
||||
getUpdateCheck();
|
||||
fetchSystemConfig();
|
||||
}
|
||||
// Only commit a substantive user object. A truthy-but-empty
|
||||
// `data` (e.g. server responded 200 with an empty body) would
|
||||
// otherwise look like "logged in" to every `currentUser`
|
||||
// reader and the access seam — break out instead and let the
|
||||
// caller treat the request as failed.
|
||||
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
|
||||
// Commit the identity to atom storage (and so to localStorage)
|
||||
// before returning. The access function — memoized on
|
||||
// `initialState` and run once per commit — reads identity from
|
||||
// localStorage; without this preemptive write the predicate
|
||||
// sees the prior session's identity on its first evaluation
|
||||
// after login, and stays stale until the next identity change
|
||||
// (which usually doesn't come without a manual refresh).
|
||||
try {
|
||||
setAtomStorage(userAtom, data);
|
||||
} catch (err) {
|
||||
console.error('userAtom commit error:', err);
|
||||
}
|
||||
// Fire `onUserFetched` so plugins maintaining identity-scoped
|
||||
// caches can seed them under the new identity before any
|
||||
// caller commits this user to `initialState`. Errors here are
|
||||
// swallowed and logged — fetchUserInfo must still return.
|
||||
try {
|
||||
await getGPUStackPlugin()?.login?.onUserFetched?.(data, {
|
||||
request: umiRequest
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('onUserFetched plugin hook error:', err);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
history.push(loginPath);
|
||||
} catch (error: any) {
|
||||
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;
|
||||
};
|
||||
@@ -66,10 +144,15 @@ export async function getInitialState(): Promise<{
|
||||
const getAppVersionInfo = async () => {
|
||||
try {
|
||||
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, {
|
||||
...data,
|
||||
isProduction
|
||||
isProd: !isDev && !isRc,
|
||||
isDev,
|
||||
isRc
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('queryVersionInfo error', error);
|
||||
@@ -78,20 +161,34 @@ export async function getInitialState(): Promise<{
|
||||
|
||||
getAppVersionInfo();
|
||||
|
||||
if (![loginPath].includes(location.pathname)) {
|
||||
const userInfo = await fetchUserInfo();
|
||||
if (![DEFAULT_ENTER_PAGE.login].includes(location.pathname)) {
|
||||
const [userInfo, accessFlags] = await Promise.all([
|
||||
fetchUserInfo(),
|
||||
probeAccessFlags()
|
||||
]);
|
||||
// Record that the probes ran for an authenticated user this page load
|
||||
// (the refresh path) so the layout doesn't re-probe. A failed
|
||||
// fetch (empty user — e.g. unauthenticated deep link that bounces to
|
||||
// login) is NOT marked: the user will log in via SPA afterwards and
|
||||
// the layout becomes responsible for probing.
|
||||
if (userInfo?.username) {
|
||||
markInitialStateProbed();
|
||||
}
|
||||
checkDefaultPage(userInfo);
|
||||
return {
|
||||
fetchUserInfo,
|
||||
currentUser: userInfo
|
||||
currentUser: userInfo,
|
||||
pluginData,
|
||||
...accessFlags
|
||||
};
|
||||
}
|
||||
return {
|
||||
fetchUserInfo
|
||||
fetchUserInfo,
|
||||
pluginData
|
||||
};
|
||||
}
|
||||
|
||||
export const request: RequestConfig = {
|
||||
baseURL: ' /v1',
|
||||
baseURL: `/${GPUSTACK_API_BASE_URL}`,
|
||||
...requestConfig
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1755020187776" class="icon" viewBox="0 0 1105 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7392" xmlns:xlink="http://www.w3.org/1999/xlink" width="138.125" height="128"><path d="M507.2896 133.44768l130.98496-71.4496v820.9408l-130.97984 69.71392V133.44768z" fill="#F38019" p-id="7393"></path><path d="M479.39072 323.968a724.30592 724.30592 0 0 0-77.056 16.6144C118.35904 417.01376 37.77536 603.8016 67.95776 701.696c30.11584 97.8944 161.03424 263.2192 439.2704 250.19904 0.13824-27.41248 0.13824-57.73824 0-90.89536-200.49408-26.37824-302.67904-88.82688-306.55488-187.40736-5.74464-147.74272 134.1696-216.90368 217.87136-229.0176 16.82432-2.49344 37.66272-5.67808 61.19936-8.6528l-0.27648-111.9488h-0.07168z m185.12384 107.65312c52.1984 5.6832 103.57248 18.69312 147.0464 44.44672-10.65984 5.19168-34.26816 19.73248-70.82496 43.47904l290.49344 66.39104-16.54272-206.58176-78.37184 44.30336c-116.10112-57.8048-205.19936-91.0336-267.29984-99.6864l-4.50048 107.648z" fill="#ADAFB3" p-id="7394"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M140-180v-88.92q0-29.39 15.96-54.43 15.96-25.03 42.66-38.49 59.3-29.08 119.65-43.62Q378.62-420 440-420q21.06 0 42.12 1.89 21.05 1.88 42.11 5.65-1.69 51.46 20.81 96.81Q567.54-270.31 609-240v60H140ZM753.85-72.31l-53.35-53.14v-164.73q-39.11-11.51-63.85-43.97-24.73-32.46-24.73-74.7 0-51.46 36.38-87.84t87.85-36.38q51.46 0 87.66 36.39Q860-460.28 860-408.79q0 39.94-22.42 70.71-22.43 30.77-57.2 44.23l44.23 44.23-53.07 53.2 53.07 53.19-70.76 70.92ZM440-484.62q-57.75 0-98.87-41.12Q300-566.86 300-624.61q0-57.75 41.13-98.88 41.12-41.12 98.87-41.12 57.75 0 98.87 41.12Q580-682.36 580-624.61q0 57.75-41.13 98.87-41.12 41.12-98.87 41.12Zm296.15 93.47q14.7 0 25.04-10.54 10.35-10.54 10.35-25.23 0-14.69-10.35-25.04-10.34-10.35-25.04-10.35-14.69 0-25.23 10.35-10.54 10.35-10.54 25.04t10.54 25.23q10.54 10.54 25.23 10.54Z"/></svg>
|
||||
|
After Width: | Height: | Size: 934 B |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
␍<g fill="#C22E33">
|
||||
␍<path d="M7.754 2l.463.41c.343.304.687.607 1.026.915C11.44 5.32 13.3 7.565 14.7 10.149c.072.132.137.268.202.403l.098.203-.108.057-.081-.115-.21-.299-.147-.214c-1.019-1.479-2.04-2.96-3.442-4.145a6.563 6.563 0 00-1.393-.904c-1.014-.485-1.916-.291-2.69.505-.736.757-1.118 1.697-1.463 2.653-.045.123-.092.245-.139.367l-.082.215-.172-.055c.1-.348.192-.698.284-1.049.21-.795.42-1.59.712-2.356.31-.816.702-1.603 1.093-2.39.169-.341.338-.682.5-1.025h.092z"/>
|
||||
␍<path d="M8.448 11.822c-1.626.77-5.56 1.564-7.426 1.36C.717 11.576 3.71 4.05 5.18 2.91l-.095.218a4.638 4.638 0 01-.138.303l-.066.129c-.76 1.462-1.519 2.926-1.908 4.53a7.482 7.482 0 00-.228 1.689c-.01 1.34.824 2.252 2.217 2.309.67.027 1.347-.043 2.023-.114.294-.03.587-.061.88-.084.108-.008.214-.021.352-.039l.231-.028z"/>
|
||||
␍<path d="M3.825 14.781c-.445.034-.89.068-1.333.108 4.097.39 8.03-.277 11.91-1.644-1.265-2.23-2.97-3.991-4.952-5.522.026.098.084.169.141.239l.048.06c.17.226.348.448.527.67.409.509.818 1.018 1.126 1.578.778 1.42.356 2.648-1.168 3.296-1.002.427-2.097.718-3.18.892-1.03.164-2.075.243-3.119.323z"/>
|
||||
␍</g>
|
||||
␍</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 393 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 286 64"><g><g><defs><path id="SVGID_1_" d="M47.5 17.6L25 4.8v52.6l9-5.2V37.4l6.8 3.9-.1-10.1-6.7-3.9v-5.9l13.5 7.9z"/></defs><clipPath id="SVGID_2_"><use xlink:href="#SVGID_1_" overflow="visible"/></clipPath><g clip-path="url(#SVGID_2_)"><linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-1.6" y1="335.05" x2="53.6" y2="335.05" gradientTransform="translate(0 -304)"><stop offset="0" stop-color="#ff6f00"/><stop offset="1" stop-color="#ffa800"/></linearGradient><path d="M-1.6 4.6h55.2v52.9H-1.6V4.6z" fill="url(#SVGID_3_)"/></g></g></g><g><g><defs><path id="SVGID_4_" d="M.5 17.6L23 4.8v52.6l-9-5.2V21.4L.5 29.3z"/></defs><clipPath id="SVGID_5_"><use xlink:href="#SVGID_4_" overflow="visible"/></clipPath><g clip-path="url(#SVGID_5_)"><linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-1.9" y1="335.05" x2="53.3" y2="335.05" gradientTransform="translate(0 -304)"><stop offset="0" stop-color="#ff6f00"/><stop offset="1" stop-color="#ffa800"/></linearGradient><path d="M-1.9 4.6h55.2v52.9H-1.9V4.6z" fill="url(#SVGID_6_)"/></g></g></g><path style="fill:#425066" d="M88.2 21.1h-10v27.7h-5.6V21.1h-10v-4.5h25.6v4.5z"/><path style="fill:#425066" d="M94.9 49.2c-3.4 0-6.2-1.1-8.3-3.2-2.1-2.1-3.2-5-3.2-8.6v-.7c0-2.2.4-4.4 1.4-6.4.9-1.8 2.2-3.3 3.9-4.4 1.7-1.1 3.6-1.6 5.6-1.6 3.3 0 5.8 1 7.6 3.1s2.7 5 2.7 8.8v2.2H88.9c.1 1.8.8 3.4 2 4.7 1.2 1.2 2.7 1.8 4.4 1.7 2.4.1 4.6-1.1 6-3l2.9 2.8c-1 1.4-2.3 2.6-3.8 3.3-1.8 1-3.6 1.4-5.5 1.3zm-.6-20.5c-1.4-.1-2.7.5-3.6 1.5-1 1.2-1.6 2.7-1.7 4.3h10.3v-.4c-.1-1.8-.6-3.2-1.4-4.1-1-.8-2.3-1.4-3.6-1.3zm19.4-3.9l.2 2.8c1.7-2.1 4.3-3.3 7-3.2 5 0 7.5 2.9 7.6 8.6v15.8h-5.4V33.3c0-1.5-.3-2.6-1-3.4-.7-.7-1.7-1.1-3.2-1.1-2.1-.1-4 1.1-4.9 2.9v17h-5.4v-24l5.1.1zm32.2 17.5c0-.9-.4-1.7-1.2-2.2-1.2-.7-2.6-1.1-3.9-1.3-1.6-.3-3.1-.8-4.6-1.5-2.7-1.3-4-3.2-4-5.6 0-2 1-4 2.6-5.2 1.7-1.4 4-2.1 6.6-2.1 2.9 0 5.2.7 6.9 2.1 1.7 1.3 2.7 3.4 2.6 5.5h-5.4c0-1-.4-1.9-1.2-2.6-.9-.7-1.9-1.1-3.1-1-1 0-2 .2-2.9.8-.7.5-1.1 1.3-1.1 2.2 0 .8.4 1.5 1 1.9.7.5 2.1.9 4.2 1.4 1.7.3 3.4.9 5 1.7 1.1.5 2 1.3 2.7 2.3.6 1 .9 2.1.9 3.3 0 2.1-1 4-2.7 5.2-1.8 1.3-4.1 2-7 2-1.8 0-3.6-.3-5.2-1.1-1.4-.6-2.7-1.6-3.6-2.9-.8-1.2-1.3-2.6-1.3-4h5.2c0 1.1.5 2.2 1.4 2.9 1 .7 2.3 1.1 3.5 1 1.4 0 2.5-.3 3.2-.8 1-.4 1.4-1.2 1.4-2zm8.1-5.7c0-2.2.4-4.4 1.4-6.3.9-1.8 2.2-3.3 3.9-4.3 1.8-1 3.8-1.6 5.8-1.5 3.2 0 5.9 1 7.9 3.1s3.1 4.8 3.3 8.3v1.3c0 2.2-.4 4.3-1.4 6.3-.8 1.8-2.2 3.3-3.9 4.3-1.8 1-3.8 1.6-5.9 1.5-3.4 0-6.1-1.1-8.1-3.4-2-2.2-3-5.2-3.1-9l.1-.3zm5.3.5c0 2.5.5 4.4 1.5 5.8 1.8 2.3 5.1 2.8 7.5 1 .4-.3.7-.6 1-1 1-1.4 1.5-3.5 1.5-6.2 0-2.4-.5-4.3-1.6-5.8-1.7-2.3-5-2.8-7.4-1.1-.4.3-.8.7-1.1 1-.8 1.4-1.4 3.5-1.4 6.3zm33.1-7.3c-.7-.1-1.5-.2-2.2-.2-2.5 0-4.1.9-5 2.8v16.4h-5.4v-24h5.1l.1 2.7c1.3-2.1 3.1-3.1 5.4-3.1.6 0 1.3.1 1.9.3l.1 5.1zm22.5 5.3h-13v13.7h-5.6V16.6h20.5v4.5h-14.9v9.6h13v4.4zm10.7 13.7h-5.4V16.5h5.4v32.3zm3.9-12.2c0-2.2.4-4.4 1.4-6.3.9-1.8 2.2-3.3 3.9-4.3 1.8-1 3.8-1.6 5.8-1.5 3.2 0 5.9 1 7.9 3.1s3.1 4.8 3.3 8.3v1.3c0 2.2-.4 4.3-1.3 6.3-.8 1.8-2.2 3.3-3.9 4.3-1.8 1-3.8 1.6-5.9 1.5-3.4 0-6.1-1.1-8.1-3.4-2-2.2-3.1-5.2-3.1-9v-.3zm5.4.5c0 2.5.5 4.4 1.5 5.8 1 1.4 2.6 2.2 4.3 2.1 1.7.1 3.3-.7 4.2-2.1 1-1.4 1.5-3.5 1.5-6.2 0-2.4-.5-4.3-1.6-5.8-1.7-2.3-5-2.8-7.4-1.1-.4.3-.8.7-1.1 1-.9 1.4-1.4 3.5-1.4 6.3zm41.2 4.3l3.8-16.5h5.2l-6.5 24h-4.4l-5.1-16.5-5.1 16.5h-4.4l-6.6-24h5.3l3.9 16.4 4.9-16.4h4.1l4.9 16.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<defs>
|
||||
<!-- 顶面填充(亮面,高透明度) -->
|
||||
<linearGradient id="cube-top-grad" x1="12" y1="2" x2="12" y2="12" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#f759ab" stop-opacity="0.18"/>
|
||||
<stop offset="100%" stop-color="#c41d7f" stop-opacity="0.1"/>
|
||||
</linearGradient>
|
||||
<!-- 左侧面填充(暗面,低透明度) -->
|
||||
<linearGradient id="cube-left-grad" x1="2" y1="7" x2="12" y2="17" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#c41d7f" stop-opacity="0.12"/>
|
||||
<stop offset="100%" stop-color="#c41d7f" stop-opacity="0.04"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cube-stroke-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#f759ab"/>
|
||||
<stop offset="100%" stop-color="#c41d7f"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 顶面填充 -->
|
||||
<polygon points="12,2 21.5,6.7 12,11.5 2.5,6.7" fill="url(#cube-top-grad)" />
|
||||
<!-- 左侧面填充 -->
|
||||
<polygon points="2.5,6.7 12,11.5 12,21.3 2.5,16.5" fill="url(#cube-left-grad)" />
|
||||
<!-- 立方体全纯线外骨架(细化为圆角衔接) -->
|
||||
<path d="M12 2L2.5 6.7M12 2l9.5 4.7M21.5 6.7L12 11.5M2.5 6.7L12 11.5M2.5 6.7v9.8l9.5 4.8M21.5 6.7v9.8l-9.5 4.8M12 11.5v9.8" stroke="url(#cube-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<defs>
|
||||
<!-- 1. 定义专属微通透渐变填充 -->
|
||||
<linearGradient id="img-fill-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#d46b08" stop-opacity="0.12"/>
|
||||
<stop offset="100%" stop-color="#d46b08" stop-opacity="0.04"/>
|
||||
</linearGradient>
|
||||
<!-- 2. 定义边框高精度渐变(亮橙到深橙,拉开层次) -->
|
||||
<linearGradient id="img-stroke-grad" x1="4" y1="4" x2="20" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#fa8c16"/>
|
||||
<stop offset="100%" stop-color="#d46b08"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 3. 精装底色充填层 -->
|
||||
<rect x="3" y="3" width="18" height="18" rx="4" fill="url(#img-fill-grad)" />
|
||||
<!-- 4. 高级柔和微圆角边框层 -->
|
||||
<rect x="3" y="3" width="18" height="18" rx="4" stroke="url(#img-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- 内部几何现代山脉线条 -->
|
||||
<path d="M3 16l4-4a2 2 0 0 1 2.8 0l5.2 5.2M13 15l2.5-2.5a2 2 0 0 1 2.8 0l2.7 2.7" stroke="url(#img-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- 标志性通透小钻石 -->
|
||||
<rect x="14" y="6" width="4" height="4" rx="1.5" transform="rotate(45 16 8)" fill="#fa8c16" fill-opacity="0.3" stroke="url(#img-stroke-grad)" stroke-width="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="chat-fill-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#389e0d" stop-opacity="0.1"/>
|
||||
<stop offset="100%" stop-color="#389e0d" stop-opacity="0.02"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="chat-stroke-grad" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#73d13d"/>
|
||||
<stop offset="100%" stop-color="#389e0d"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- 主现代对话框体(全部改为圆润R角,精装填充) -->
|
||||
<path d="M18 4H6a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h7.5l3.5 3.5a1 1 0 0 0 1.5-.5V17a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3z" fill="url(#chat-fill-grad)" stroke="url(#chat-stroke-grad)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- 内部通透对话线条(细化为圆角代码采样块感) -->
|
||||
<rect x="7" y="8" width="8" height="1.5" rx="0.75" fill="#73d13d" fill-opacity="0.3"/>
|
||||
<rect x="7" y="11.5" width="10" height="1.5" rx="0.75" fill="#73d13d" fill-opacity="0.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 640 B |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.8 KiB |