Compare commits
371 Commits
feat/serve
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
162954606a | ||
|
3bf86b3e6e | ||
|
843a581d42 | ||
|
fffc989336 | ||
|
ea75ed7f88 | ||
|
34f9fe6957 | ||
|
71f75c27dd | ||
|
a12fa3e6fe | ||
|
ae5f5a97d9 | ||
|
31ad64cd95 | ||
|
1096e9ca9a | ||
|
b71bf6542e | ||
|
e4b98b1c36 | ||
|
fa1ff3b5f6 | ||
|
f0ddf6c5dd | ||
|
74d391afc1 | ||
|
c70e69879f | ||
|
6a4bdd324c | ||
|
f7b1d33c5d | ||
|
7aec9e7237 | ||
|
f637ade70f | ||
|
5207338ac1 | ||
|
cb476f7361 | ||
|
f77acf9eac | ||
|
325ab38fbb | ||
|
9949b973bd | ||
|
6312ec6eed | ||
|
59b874644f | ||
|
266b08f2da | ||
|
a8a47ed94d | ||
|
272505669e | ||
|
6b3631eae1 | ||
|
f592466d62 | ||
|
98298c4367 | ||
|
09d0f02d84 | ||
|
59d32e0119 | ||
|
1c5737e588 | ||
|
ba580dd70b | ||
|
e402ee1688 | ||
|
1df32dc257 | ||
|
79667a9644 | ||
|
554f902584 | ||
|
fcb8f22116 | ||
|
f080830407 | ||
|
c7e20df516 | ||
|
83850f2981 | ||
|
3dca8fc27c | ||
|
4e3fd9db64 | ||
|
b4ab20ad32 | ||
|
7f70557c77 | ||
|
2a503ca250 | ||
|
527f734bc4 | ||
|
c9f2458775 | ||
|
f553f157dd | ||
|
61980b37d3 | ||
|
820b25baed | ||
|
279e616bee | ||
|
dcff57fe69 | ||
|
316b95467d | ||
|
e5e77dbdee | ||
|
bbb8d88116 | ||
|
f1513fe3f7 | ||
|
8c5c417a19 | ||
|
763810e8b7 | ||
|
c0e2ef0fe8 | ||
|
a218c22397 | ||
|
f00163b2f1 | ||
|
de572426eb | ||
|
5d54ca1cbc | ||
|
29f184c15d | ||
|
6474cefd89 | ||
|
9b9799ec6f | ||
|
e51a88044f | ||
|
1e57905f32 | ||
|
de38363315 | ||
|
3d9d03296e | ||
|
384224cb62 | ||
|
a32f3d9824 | ||
|
eaffe3ab21 | ||
|
43b4c9fe37 | ||
|
9e6e03117c | ||
|
064dbe9985 | ||
|
7bda5420c5 | ||
|
f5933ec054 | ||
|
7322ad741d | ||
|
e46f97097a | ||
|
4b7877155f | ||
|
f3d8f5543d | ||
|
6da0e6f415 | ||
|
9bc8c63fe2 | ||
|
9d559b93d1 | ||
|
572d96babb | ||
|
88fa90c2ca | ||
|
7301eeb82a | ||
|
e09d7eef87 | ||
|
2d5a09c79c | ||
|
1fe50092ba | ||
|
2cb80584cf | ||
|
1be03ccf53 | ||
|
5eb7696ead | ||
|
79b75f55e3 | ||
|
9f7fbafcf0 | ||
|
92196e4e5b | ||
|
33a0a60eee | ||
|
04dc1e98dd | ||
|
b778f8c982 | ||
|
1337eaa2c0 | ||
|
055f57e087 | ||
|
50a35732ff | ||
|
bb0c574893 | ||
|
6c2a093842 | ||
|
9cf46e679c | ||
|
b16a7c3c2c | ||
|
fe432f1332 | ||
|
de09059e65 | ||
|
e942769af2 | ||
|
d73fa10897 | ||
|
13227416c0 | ||
|
f59793d6f1 | ||
|
e6df595af8 | ||
|
57ebaf6ad3 | ||
|
8b6a74033c | ||
|
23c691541d | ||
|
bcc215ca5d | ||
|
b44e57dde8 | ||
|
ee72f74e2c | ||
|
9d3e9d89db | ||
|
63e6bfe0d1 | ||
|
7c271dc3c1 | ||
|
9c35bca685 | ||
|
d48af2b8fe | ||
|
3011304b9e | ||
|
4f2c1129a0 | ||
|
cdc3ce1223 | ||
|
31e8ce4ab9 | ||
|
f2ce1fb10c | ||
|
22d76a0f87 | ||
|
205864720c | ||
|
f16ccb5689 | ||
|
6bf65cb529 | ||
|
79ed059d99 | ||
|
4f4f9b5d3f | ||
|
d29785a311 | ||
|
fb75a8b654 | ||
|
d1820416f4 | ||
|
6a1f413a38 | ||
|
4a1d704fbb | ||
|
5cd383895f | ||
|
84e4722f2f | ||
|
63484d0db5 | ||
|
d0afdf5c91 | ||
|
90953e490c | ||
|
32adb75e9a | ||
|
12b8ba95b7 | ||
|
ef3d34423b | ||
|
8b86dcdcea | ||
|
4d39cb5ef4 | ||
|
e323e104e0 | ||
|
946ecaf9f9 | ||
|
72a1e7b024 | ||
|
ed2141af22 | ||
|
42f41cdbcb | ||
|
f5151aa2a4 | ||
|
427e9e3eb7 | ||
|
6160d7bcb9 | ||
|
ef30750802 | ||
|
fc1e67e005 | ||
|
95b51ca2e1 | ||
|
6ce2f7fd4d | ||
|
2b9a14c969 | ||
|
943f7f594b | ||
|
28d982e497 | ||
|
91ade2ab55 | ||
|
f309000a0c | ||
|
1a39f00aef | ||
|
f74289ff05 | ||
|
1895ac772c | ||
|
e770e42893 | ||
|
2e609452b5 | ||
|
583ff227fd | ||
|
d2afa54301 | ||
|
ae3f5fce2d | ||
|
cf4531c5dd | ||
|
983bcd37cf | ||
|
c4211c270f | ||
|
8ccace127b | ||
|
2cc098a5f1 | ||
|
791141ee5a | ||
|
546055e555 | ||
|
8534ab7ba0 | ||
|
01d774d395 | ||
|
921dd53a50 | ||
|
88f47db118 | ||
|
9966c1277c | ||
|
e095a081b9 | ||
|
5588aca522 | ||
|
87b4000c47 | ||
|
3270164710 | ||
|
33de808f3e | ||
|
9fcc6dda60 | ||
|
be444d76c9 | ||
|
b9f5582a02 | ||
|
17c6a7fa22 | ||
|
5f47831f8e | ||
|
b64ca8b300 | ||
|
3cc678f09e | ||
|
d136460e39 | ||
|
79a7a923d2 | ||
|
40df49e1db | ||
|
fa328fb0bf | ||
|
446ddafa0a | ||
|
cbdb1c4a07 | ||
|
e0e044945f | ||
|
7f33e2de0d | ||
|
8c8b960f61 | ||
|
bb84661612 | ||
|
491807165c | ||
|
b862dd7427 | ||
|
f7e1c8114b | ||
|
e983092037 | ||
|
3990b0a872 | ||
|
6fecde0caa | ||
|
fac0838d8c | ||
|
ebd1e5eb66 | ||
|
552835800e | ||
|
3e3dc4c22d | ||
|
49d0da3a6d | ||
|
22fc5f98f8 | ||
|
d9862105ed | ||
|
5f6147e3b6 | ||
|
5447f53b30 | ||
|
59840b5f7b | ||
|
9d2ba6cc55 | ||
|
914046aefa | ||
|
f1aaa7040e | ||
|
ad18666851 | ||
|
bb76c8e895 | ||
|
b04ddd40ad | ||
|
7e38e327bf | ||
|
f1496429d3 | ||
|
0a0a27549a | ||
|
7f7c95b11c | ||
|
03bc9b5125 | ||
|
20e95ef973 | ||
|
c7ff3666a7 | ||
|
e5c2b9484f | ||
|
af4792024f | ||
|
e9c64c57e7 | ||
|
3afac062c4 | ||
|
06d6ecd2a3 | ||
|
d5d04468cb | ||
|
5dca262482 | ||
|
37757f6563 | ||
|
3cf3cfa427 | ||
|
275f30f048 | ||
|
b2dccec283 | ||
|
827cf07c2a | ||
|
05c358b2e5 | ||
|
73dd8c25b7 | ||
|
e4eee420ea | ||
|
7542d88b5a | ||
|
0835fc588b | ||
|
f112adc696 | ||
|
35a6e20717 | ||
|
8585ea4196 | ||
|
9a7afed08c | ||
|
e7189c6395 | ||
|
f2b20c5ef9 | ||
|
f814691538 | ||
|
1b89c3b5a8 | ||
|
9796d42846 | ||
|
6e68a8044d | ||
|
7736bf89dc | ||
|
03904d26e0 | ||
|
82bb2ad267 | ||
|
a2cb8b0538 | ||
|
7bfd92be0b | ||
|
ab179e9af6 | ||
|
b6bca6c250 | ||
|
63de6d7aa5 | ||
|
1d4aecff95 | ||
|
01d81f3929 | ||
|
15c6290587 | ||
|
2ce5597dfe | ||
|
b355a677d3 | ||
|
b2480b0ed5 | ||
|
616a623e40 | ||
|
67bfda30bc | ||
|
2f6e92d166 | ||
|
e10cdfdf26 | ||
|
537503f288 | ||
|
503df4546d | ||
|
685d05074b | ||
|
c34b0124fa | ||
|
865e56f40e | ||
|
6ccd0ede7b | ||
|
caf7e9ca72 | ||
|
1b859e3176 | ||
|
b7670da7db | ||
|
66ec94fd08 | ||
|
17f87c191a | ||
|
56bbe09005 | ||
|
4d15cccd1b | ||
|
85a2a598d7 | ||
|
a4c31fe2da | ||
|
765cc41c06 | ||
|
478d0c2af3 | ||
|
adb1cc3919 | ||
|
fc6ee73366 | ||
|
a7688f02af | ||
|
926ea980ff | ||
|
af5f6ad9f5 | ||
|
3d9b67a430 | ||
|
29939b6709 | ||
|
12fe9f0384 | ||
|
ac930cd05e | ||
|
96a5a33ad6 | ||
|
f459c6beea | ||
|
c6747073b1 | ||
|
3bfd11a7b6 | ||
|
0d2c4f97f9 | ||
|
a91d1ffffe | ||
|
61c1b0e065 | ||
|
90df8e8e36 | ||
|
ee16e6cd76 | ||
|
4943d2dd8e | ||
|
f06e788f45 | ||
|
3d11d84545 | ||
|
fdce6b42f1 | ||
|
e9a1b61a7f | ||
|
cae0c1d6c0 | ||
|
0deec1fc55 | ||
|
95a8e9968b | ||
|
caab72dac5 | ||
|
f91110b313 | ||
|
14d1923908 | ||
|
4485e68b1e | ||
|
f6f104773b | ||
|
b90ef89c8a | ||
|
d250178cc6 | ||
|
94343b0392 | ||
|
bc376ed4af | ||
|
27aa22077a | ||
|
1c98cf5c7d | ||
|
ef49d9ebd2 | ||
|
8ac5b11d49 | ||
|
4e8d7613a4 | ||
|
80713e0fce | ||
|
4564347698 | ||
|
1dafea61c7 | ||
|
618aedf196 | ||
|
98a887825f | ||
|
0c5c993236 | ||
|
a0ab1da6b6 | ||
|
d74ba8d283 | ||
|
26da461368 | ||
|
ad4b67ca45 | ||
|
328a4e856c | ||
|
52a89276c8 | ||
|
bffb9d6729 | ||
|
ed0c2e9d1d | ||
|
154b8b4b64 | ||
|
58445f9249 | ||
|
a05db11518 | ||
|
e3d0555c45 | ||
|
3ecd7aa171 | ||
|
8f37c47a57 | ||
|
dec6a8b7c5 | ||
|
8e96c06d94 | ||
|
7c94caf0ed | ||
|
a307b6f2f9 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
@ -2,8 +2,8 @@
|
||||
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
|
||||
|
||||
# Whether allow feature
|
||||
ALLOW_REGISTER=
|
||||
ALLOW_OPENAPI=
|
||||
ALLOW_REGISTER=false
|
||||
ALLOW_OPENAPI=true
|
||||
|
||||
# For analyze tianji self
|
||||
WEBSITE_ID=
|
||||
|
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@ -6,6 +6,8 @@ on:
|
||||
- master
|
||||
paths:
|
||||
- "src/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@ -17,11 +19,11 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
version: 9.7.1
|
||||
run_install: false
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
|
2
.npmrc
Normal file
2
.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
package-manager-strict=true
|
||||
package-manager-strict-version=true
|
@ -3,7 +3,9 @@
|
||||
"release": true
|
||||
},
|
||||
"git": {
|
||||
"commitMessage": "chore: release v${version}"
|
||||
"commitMessage": "chore: release v${version}",
|
||||
"tag": true,
|
||||
"tagName": "v${version}"
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
|
699
CHANGELOG.md
699
CHANGELOG.md
@ -1,5 +1,704 @@
|
||||
|
||||
|
||||
## [1.16.5](https://github.com/msgbyte/tianji/compare/v1.16.4...v1.16.5) (2024-11-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add webhookSignature in feed channel ([6b3631e](https://github.com/msgbyte/tianji/commit/6b3631eae186b9cacf64d0ddcfbb66378e041281))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add key to Fragment in map for monitor items ([9949b97](https://github.com/msgbyte/tianji/commit/9949b973bd63b4ad6b5e71f7b819442f505c09a6))
|
||||
* retrieve date as string ([a8a47ed](https://github.com/msgbyte/tianji/commit/a8a47ed94dda87c3fe4cdecc0acb9a31f53f00a5))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* fix ci problem ([59b8746](https://github.com/msgbyte/tianji/commit/59b874644fd3427bc86cd2a7e948054e827de080))
|
||||
* refactor status header and add typescript and translation support ([f637ade](https://github.com/msgbyte/tianji/commit/f637ade70f230fbf472bdee84105c9b284d6b8d4))
|
||||
* update amount in stripe ([2725056](https://github.com/msgbyte/tianji/commit/272505669e450d882930cbf594dac39a879b2072))
|
||||
* update webhooks signature api guide ([266b08f](https://github.com/msgbyte/tianji/commit/266b08f2da16d0457a5a44b4a7a251d28502abc9))
|
||||
|
||||
## [1.16.4](https://github.com/msgbyte/tianji/compare/v1.16.3...v1.16.4) (2024-10-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add stripe feed integration ([09d0f02](https://github.com/msgbyte/tianji/commit/09d0f02d844159565e97bb64f076e0bbe218ce98))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* update currency symbols in feed ([98298c4](https://github.com/msgbyte/tianji/commit/98298c43670326b4e2300a6bbdeee3daa53f0eb3))
|
||||
|
||||
## [1.16.3](https://github.com/msgbyte/tianji/compare/v1.16.2...v1.16.3) (2024-10-24)
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* fix ci problem and upgrade version ([1c5737e](https://github.com/msgbyte/tianji/commit/1c5737e588d19e0657be6437792cf4484b6fdddb))
|
||||
|
||||
## [1.16.2](https://github.com/msgbyte/tianji/compare/v1.16.1...v1.16.2) (2024-10-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add prometheus report support ([fcb8f22](https://github.com/msgbyte/tianji/commit/fcb8f221168281ab710d3d3f12064a99d17b39e7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix a bug which will match incorrect path [#115](https://github.com/msgbyte/tianji/issues/115) ([79667a9](https://github.com/msgbyte/tianji/commit/79667a9644b78451400acb6a6bbf07b6ca61e6e0))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* update README ([1df32dc](https://github.com/msgbyte/tianji/commit/1df32dc2579f32649afd6c008512c1190a45fd9e))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* fix ci problem ([554f902](https://github.com/msgbyte/tianji/commit/554f9025847defe0b05492cf07a5dc8acc6c3685))
|
||||
* update openapi document ([e402ee1](https://github.com/msgbyte/tianji/commit/e402ee1688bb77d83463ce70c5e730c97f68a695))
|
||||
|
||||
## [1.16.1](https://github.com/msgbyte/tianji/compare/v1.16.0...v1.16.1) (2024-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add test notify ([4e3fd9d](https://github.com/msgbyte/tianji/commit/4e3fd9db64629f7721e6092b86b06144c47f521d))
|
||||
* add timezone support [#114](https://github.com/msgbyte/tianji/issues/114) ([c7e20df](https://github.com/msgbyte/tianji/commit/c7e20df516bf3a991ce46c937223948bcdb6b8f0))
|
||||
* add workspace settings manage ([3dca8fc](https://github.com/msgbyte/tianji/commit/3dca8fc27c82bd96dbab423b111e4de57f3b4bd8))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* update cronjob clear time ([83850f2](https://github.com/msgbyte/tianji/commit/83850f2981ded0b6624556ee3430f684752b8ea3))
|
||||
|
||||
## [1.16.0](https://github.com/msgbyte/tianji/compare/v1.15.8...v1.16.0) (2024-10-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add click event for status page item which allow hide/show chart ([279e616](https://github.com/msgbyte/tianji/commit/279e616bee510ee5b0c5a3c9a3705a79efd5d3cb))
|
||||
* add daily monitor data display for public ([dcff57f](https://github.com/msgbyte/tianji/commit/dcff57fe69273c7f9b3dd9c28e8acc9cb6e430a9))
|
||||
* add monitor summary function ([bbb8d88](https://github.com/msgbyte/tianji/commit/bbb8d881168df695ccc70743f46320b39c1d7718))
|
||||
* add MonitorLatestResponse and up status summary ([316b954](https://github.com/msgbyte/tianji/commit/316b95467d49b3ebe93d03006d4b90f9ca482262))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix reporter memory leak problem [#103](https://github.com/msgbyte/tianji/issues/103) ([7f70557](https://github.com/msgbyte/tianji/commit/7f70557c776c35e4e01a5533d2c05cecc711e113))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add border radius in smtp template ([f553f15](https://github.com/msgbyte/tianji/commit/f553f157dd9708d553c9d6cfca4d119a62d849c3))
|
||||
* change public summary display logic ([e5e77db](https://github.com/msgbyte/tianji/commit/e5e77dbdeeeecb773237b84e3c671dd16e61d458))
|
||||
* fix ci problem ([820b25b](https://github.com/msgbyte/tianji/commit/820b25baedc6fec02010ca19b43e4da99bf4b820))
|
||||
* ignore unknown sentry log ([527f734](https://github.com/msgbyte/tianji/commit/527f734bc442458018d86df9a7e750a8e8de4495))
|
||||
* let version text more prominent ([61980b3](https://github.com/msgbyte/tianji/commit/61980b37d3cecce32fa87b2b9810f4c715990a71))
|
||||
* rename old tsconfig paths ([2a503ca](https://github.com/msgbyte/tianji/commit/2a503ca2501e705430c0c35cb7c8279927c1d4d5))
|
||||
|
||||
## [1.15.8](https://github.com/msgbyte/tianji/compare/v1.15.7...v1.15.8) (2024-10-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add payload for feed event integration and send function ([572d96b](https://github.com/msgbyte/tianji/commit/572d96babb348858911105659bfe304e869915e4))
|
||||
* add ping animation in website realtime visitor ([6da0e6f](https://github.com/msgbyte/tianji/commit/6da0e6f415e863448cd36246eb16e1f09dcd8a79))
|
||||
* add plausible tracking(for testing) ([6474cef](https://github.com/msgbyte/tianji/commit/6474cefd896b36b872460786b11005b8deaf4436))
|
||||
* add realtime datarange which can visit data more easy ([f3d8f55](https://github.com/msgbyte/tianji/commit/f3d8f5543d4e277fe34940ce98cd552dee45f2a8))
|
||||
* add survey curl example code ([5d54ca1](https://github.com/msgbyte/tianji/commit/5d54ca1cbc01b69b9bd03d167edd0db242df350e))
|
||||
* add survey webhook ([de57242](https://github.com/msgbyte/tianji/commit/de572426ebf99e99ff97ce49caf0b8ac13b68154))
|
||||
* sdk add send feed function export ([f5933ec](https://github.com/msgbyte/tianji/commit/f5933ec0548fb4ac327152b6d7afe7ca2978bade))
|
||||
* survey add webhook url field which can send webhook when receive any survey ([f00163b](https://github.com/msgbyte/tianji/commit/f00163b2f107bcf08d4ff398fa5dbd92ac36fda8))
|
||||
* time event chart legend add some interaction ([4b78771](https://github.com/msgbyte/tianji/commit/4b7877155fd54416fb9231a02aca0e868aec97d2))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* add shacdn to website ([763810e](https://github.com/msgbyte/tianji/commit/763810e8b7e6cd41fdc0d83d28262dc7d747e4bf))
|
||||
* add sitemap to improve SEO ([384224c](https://github.com/msgbyte/tianji/commit/384224cb624030522057df22527312957665d8e9))
|
||||
* add website more language: de, fr, ja, zh-Hans ([7bda542](https://github.com/msgbyte/tianji/commit/7bda5420c5f22f6205d2dbd5086dd0bdbbc7f558))
|
||||
* change code command line style ([8c5c417](https://github.com/msgbyte/tianji/commit/8c5c417a197531c187452fc4937d034d3bdc05a7))
|
||||
* remove used blog directory ([3d9d032](https://github.com/msgbyte/tianji/commit/3d9d03296e9fb26eb363b2dba2f830f2eb9d58f3))
|
||||
* resolve build problem with update source document content ([9e6e031](https://github.com/msgbyte/tianji/commit/9e6e03117cc3bd37058563e31817034876d35450))
|
||||
* update depenpendency to resolve issue of docusaurus build ([de38363](https://github.com/msgbyte/tianji/commit/de38363315275ecbc7384c935c313940fca1d4fc))
|
||||
* upgrade openapi ([1e57905](https://github.com/msgbyte/tianji/commit/1e57905f3239cc4fb3a949b469161dc9c8d2b40c))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add CodeExample component ([29f184c](https://github.com/msgbyte/tianji/commit/29f184c15d36e42af750261f81ad67b49fe58c0b))
|
||||
* comment sitemap to make sure its can build safe ([9b9799e](https://github.com/msgbyte/tianji/commit/9b9799ec6f846eb40762f5ca8cc0aa6c27fb7e02))
|
||||
* fix ci problem ([a32f3d9](https://github.com/msgbyte/tianji/commit/a32f3d9824a09a2d8c9e97ba211ee5d44c8e2763))
|
||||
* fix isolated-vm version ([43b4c9f](https://github.com/msgbyte/tianji/commit/43b4c9fe3763673dc54b61b01e50bc5b3d24a371))
|
||||
* fix version of postman-code-generators ([eaffe3a](https://github.com/msgbyte/tianji/commit/eaffe3ab21022215a76c3441ef0b9b2c37386227))
|
||||
* improve display of visitor map if data is too much ([9bc8c63](https://github.com/msgbyte/tianji/commit/9bc8c63fe2ea2ab080e7db3cf7d0c7636fabf8d1))
|
||||
* migrate monitor data chart to recharts and remove @ant-design/charts ([c0e2ef0](https://github.com/msgbyte/tianji/commit/c0e2ef0fe8f5520a7b935eeeb44f6be9224e56a4))
|
||||
* update ci run trigger path ([7322ad7](https://github.com/msgbyte/tianji/commit/7322ad741dcfc5c033b5057e1862e91d27244f7f))
|
||||
* update pnpm lock file to resolve some magic problem ([064dbe9](https://github.com/msgbyte/tianji/commit/064dbe9985767b32492de4264d08c26206318cd4))
|
||||
* update survey edit form ([a218c22](https://github.com/msgbyte/tianji/commit/a218c2239725deb5bcdee2e8d2de377e04dec941))
|
||||
* upgrade @radix-ui/react-scroll-area version ([9d559b9](https://github.com/msgbyte/tianji/commit/9d559b93d16c130cf58649e0f12edf9e795ba8a5))
|
||||
* upgrade @tianji/website docusaurus version ([e46f970](https://github.com/msgbyte/tianji/commit/e46f97097a593fe4bd5a8946237fd2f46fea69f6))
|
||||
* use prebuilt rather than deploy build ([e51a880](https://github.com/msgbyte/tianji/commit/e51a88044fcef7727b2e7f17c5dd9eff08329cdc))
|
||||
|
||||
## [1.15.7](https://github.com/msgbyte/tianji/compare/v1.15.6...v1.15.7) (2024-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix a problem which will make request list incorrect ([2d5a09c](https://github.com/msgbyte/tianji/commit/2d5a09c79cae48f62029b8767bae552376e68639))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* fix update code to new version ([1fe5009](https://github.com/msgbyte/tianji/commit/1fe50092bab5e0824c1b96b70d40d33f751f4135))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* split website from monorepo ([e09d7ee](https://github.com/msgbyte/tianji/commit/e09d7eef8788e27ac651f18dcf4d04de895a35ee))
|
||||
* update workspace config and remove unused lock file ([7301eeb](https://github.com/msgbyte/tianji/commit/7301eeb82a4fdc4ae6853dbe98c1f1d7b9b79bca))
|
||||
|
||||
## [1.15.6](https://github.com/msgbyte/tianji/compare/v1.15.5...v1.15.6) (2024-10-02)
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add build dependency for build zeromq ([79b75f5](https://github.com/msgbyte/tianji/commit/79b75f55e39c057c330bc1fb0b3b15dc10e28a78))
|
||||
* improve install package time in docker build static stage ([1be03cc](https://github.com/msgbyte/tianji/commit/1be03ccf532a7dd6d23334a5666dec34e2e68d77))
|
||||
* update NODE_OPTIONS in static layer to make sure build can pass ([5eb7696](https://github.com/msgbyte/tianji/commit/5eb7696ead2da961d6ac5223a2badb48502142ba))
|
||||
|
||||
## [1.15.5](https://github.com/msgbyte/tianji/compare/v1.15.4...v1.15.5) (2024-10-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add error message for lighthouse ([bb0c574](https://github.com/msgbyte/tianji/commit/bb0c57489347242300c6153ed3908d1822bb692c))
|
||||
* add lighthouse score in database fields ([6c2a093](https://github.com/msgbyte/tianji/commit/6c2a0938423385d67309deefa67a3d971bf8d7c8))
|
||||
* add webhook playground ([33a0a60](https://github.com/msgbyte/tianji/commit/33a0a60eee53d1ac08cc9accc2e96f06e56ebb52))
|
||||
* add webhook playground entry ([92196e4](https://github.com/msgbyte/tianji/commit/92196e4e5bb9b183cfe85aad876115c0e17f824e))
|
||||
* add zeromq to make sure lighthouse can only run one at same time ([50a3573](https://github.com/msgbyte/tianji/commit/50a35732ff202f2452b344c2df17aba677426ec3))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* improve avatar display timing for non-avatar user ([04dc1e9](https://github.com/msgbyte/tianji/commit/04dc1e98dd448c0fd6661559722cf594ab2e751e))
|
||||
* refactor time event chart to recharts ([1337eaa](https://github.com/msgbyte/tianji/commit/1337eaa2c0ff55651a05878edd722ac1b46a5067))
|
||||
* update style of website page card ([b778f8c](https://github.com/msgbyte/tianji/commit/b778f8c982f7df8328c2fffe67783b15cde51c15))
|
||||
* upgrade shadcn cli and add recharts ([055f57e](https://github.com/msgbyte/tianji/commit/055f57e087f002b8f891053509e3cad865f1d52b))
|
||||
|
||||
## [1.15.4](https://github.com/msgbyte/tianji/compare/v1.15.3...v1.15.4) (2024-09-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow rename workspace ([63e6bfe](https://github.com/msgbyte/tianji/commit/63e6bfe0d1a989479a6c4658d01ea9d84fc84b45))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix login view split incorrect if not any extra login way ([b16a7c3](https://github.com/msgbyte/tianji/commit/b16a7c3c2c203394c94ccfee8e829bc7685a2457))
|
||||
* remove workspace name validation ([7c271dc](https://github.com/msgbyte/tianji/commit/7c271dc3c14fc6c751fb69b16adf6f08bfd5ac7b))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add ignore in docker build ([ee72f74](https://github.com/msgbyte/tianji/commit/ee72f74e2c68c9baec500b78a3b994c8083abeed))
|
||||
* add logger for lighthouse ([9d3e9d8](https://github.com/msgbyte/tianji/commit/9d3e9d89db40aad4a78df8b64ad3d7bfccb94d2e))
|
||||
* add no sandbox args in puppeteer ([8b6a740](https://github.com/msgbyte/tianji/commit/8b6a74033c2838a6921c56990e13eedfbc8a559a))
|
||||
* docker add puppeteer support ([23c6915](https://github.com/msgbyte/tianji/commit/23c691541db0a51b4765dd0943488def7741c0f4))
|
||||
* downgrade alpine version to 3.19 to avoid issue ([e6df595](https://github.com/msgbyte/tianji/commit/e6df595af8ecddf734fe7e72a797921d84dad2c3))
|
||||
* improve docker build and lighthouse config ([57ebaf6](https://github.com/msgbyte/tianji/commit/57ebaf6ad361cae3403263009b94566cc7de2293))
|
||||
* improve websocket log ([b44e57d](https://github.com/msgbyte/tianji/commit/b44e57dde8d027eb05b7e8db20d490d2b62607cc))
|
||||
* try to resolve no screenshot problem by remove single process. ([fe432f1](https://github.com/msgbyte/tianji/commit/fe432f13325adf5fb4cde3dbb2f4f1218cb789e7)), closes [/github.com/GoogleChrome/lighthouse/issues/11537#issuecomment-799895027](https://github.com/msgbyte//github.com/GoogleChrome/lighthouse/issues/11537/issues/issuecomment-799895027)
|
||||
* unity esbuild version to resolve vulnerabilities which cause by esbuild ([bcc215c](https://github.com/msgbyte/tianji/commit/bcc215ca5d33126b368b58a9056d02fd93d5a99a))
|
||||
* update dockerfile, carry back auto install dependency ([de09059](https://github.com/msgbyte/tianji/commit/de09059e6561a27e160f0e39e7987da8ad05edaa))
|
||||
* update translation ([9c35bca](https://github.com/msgbyte/tianji/commit/9c35bca68508f2009434ebf578f0488a948e6b75))
|
||||
* upgrade axios version to latest to resolve vulnerabilities ([d73fa10](https://github.com/msgbyte/tianji/commit/d73fa108978b3c965b07dda725fbe6ae20bc4140))
|
||||
* upgrade puppeteer to make sure can fit with alpine image chromium version ([f59793d](https://github.com/msgbyte/tianji/commit/f59793d6f18625ad66b26d0a74aaa14c604ea812))
|
||||
* upgrade puppeteer usage to fit new version ([1322741](https://github.com/msgbyte/tianji/commit/13227416c05e2eae5a1a99e5d5f3396679e83d89))
|
||||
* upgrade puppeteer version to 23.4.1 ([e942769](https://github.com/msgbyte/tianji/commit/e942769af2e2570da71eb23f3ad489e8dcb72e95))
|
||||
|
||||
## [1.15.3](https://github.com/msgbyte/tianji/compare/v1.15.2...v1.15.3) (2024-09-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add fixed server list ([4f2c112](https://github.com/msgbyte/tianji/commit/4f2c1129a0421934b43d3b6e02b17d629d275614))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add language fallback to make sure its can be display correct ([31e8ce4](https://github.com/msgbyte/tianji/commit/31e8ce4ab9beec4af730b9302c0c3b861234123e))
|
||||
* clear unused code ([cdc3ce1](https://github.com/msgbyte/tianji/commit/cdc3ce122386e632f6b4350bc3dd4bdf4c17e0ed))
|
||||
* improve monitor detail style, enhance style difference ([f2ce1fb](https://github.com/msgbyte/tianji/commit/f2ce1fb10c92a2e2583ec3b36866308954958e1e))
|
||||
|
||||
## [1.15.2](https://github.com/msgbyte/tianji/compare/v1.15.1...v1.15.2) (2024-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add admin role and change most owner permission to admin ([79ed059](https://github.com/msgbyte/tianji/commit/79ed059d995da6eaabc452a0844b9acb69dc981c))
|
||||
* add label map for device type in website ([f16ccb5](https://github.com/msgbyte/tianji/commit/f16ccb56895f65dea530be295b19f04a03c8ed99))
|
||||
* add lighthouse reporter generate in website ([d29785a](https://github.com/msgbyte/tianji/commit/d29785a31184fe48913f7c49833c2d35a92c244a))
|
||||
* add status page incident model ([d182041](https://github.com/msgbyte/tianji/commit/d1820416f4924b2fc1920383b2d22b042f6e0381))
|
||||
* add workspace role permission check, hide non permission action ([4f4f9b5](https://github.com/msgbyte/tianji/commit/4f4f9b5d3f36192ea0f416997a43691674aa79fd))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* change default workspace name ([2058647](https://github.com/msgbyte/tianji/commit/205864720cdcbb5f46bee92fa6c577769a05f167))
|
||||
* fix light mode color issues ([fb75a8b](https://github.com/msgbyte/tianji/commit/fb75a8b6545a507c81acafa9cb526afccd39cd35))
|
||||
* invite add id support ([6a1f413](https://github.com/msgbyte/tianji/commit/6a1f413a384021d3c91e136be36a8c6375c74f99))
|
||||
* update README roadmap ([4a1d704](https://github.com/msgbyte/tianji/commit/4a1d704fbb88bb87f7e9db61f3da1364fb7543c0))
|
||||
* update translation ([6bf65cb](https://github.com/msgbyte/tianji/commit/6bf65cb529a2b0c52204063f2a10ad33e7b39aa5))
|
||||
|
||||
## [1.15.1](https://github.com/msgbyte/tianji/compare/v1.15.0...v1.15.1) (2024-09-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add custom oidc/oauth provider support ([d0afdf5](https://github.com/msgbyte/tianji/commit/d0afdf5c91d2112d177ab7bb0315586cb64ad8d7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix website cannot delete problem [#91](https://github.com/msgbyte/tianji/issues/91) ([90953e4](https://github.com/msgbyte/tianji/commit/90953e490ceea8e5256fd564e4d220b2e7da50b3))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add account provider ([84e4722](https://github.com/msgbyte/tianji/commit/84e4722f2fc8026de25fa33d23e17db61a6d4437))
|
||||
* fix ci problem ([63484d0](https://github.com/msgbyte/tianji/commit/63484d0db59e1ccc178c7427ec7d60fd7f1484b0))
|
||||
|
||||
## [1.15.0](https://github.com/msgbyte/tianji/compare/v1.14.7...v1.15.0) (2024-09-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add delete workspace feature [#96](https://github.com/msgbyte/tianji/issues/96) ([2b9a14c](https://github.com/msgbyte/tianji/commit/2b9a14c969c824d630452e0e4e30834f2a9a1b47))
|
||||
* add group feature in backend ([4d39cb5](https://github.com/msgbyte/tianji/commit/4d39cb5ef4d95626e600289a1e949e78ccd7906f))
|
||||
* add lighthouse endpoint ([28d982e](https://github.com/msgbyte/tianji/commit/28d982e497bfd04351c8495ec9ddd58fc205e771))
|
||||
* add lighthouse html report endpoint ([943f7f5](https://github.com/msgbyte/tianji/commit/943f7f594ba90aa037ab5cb1b057f496e8a5fcb2))
|
||||
* add logout button in switch workspace page ([6ce2f7f](https://github.com/msgbyte/tianji/commit/6ce2f7fd4dbcd56b5a5f493108138e8c8447619b))
|
||||
* add sortable group component ([ef30750](https://github.com/msgbyte/tianji/commit/ef307508026bf516d321da933c298d87efd0b902))
|
||||
* refactor sortable group component and add edit body component ([946ecaf](https://github.com/msgbyte/tianji/commit/946ecaf9f946dfb2d85541170a7441ba6e782e5a))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add body spaces ([12b8ba9](https://github.com/msgbyte/tianji/commit/12b8ba95b7720d384f8ae607cea0f4133d5f4fc4))
|
||||
* add new editable text component which allow to change group title ([e323e10](https://github.com/msgbyte/tianji/commit/e323e104e03569b7131dc1aac1e15c548ebe8485))
|
||||
* add sortable group component which using react-beautiful-dnd ([91ade2a](https://github.com/msgbyte/tianji/commit/91ade2ab555e43dc36ec32c7e9cb2856ffccd5ae))
|
||||
* change edit style and logic, create new MonitorPicker component ([72a1e7b](https://github.com/msgbyte/tianji/commit/72a1e7b0249c69510af7650e19bf939ed88cf550))
|
||||
* fix ci problem and remove unused code ([95b51ca](https://github.com/msgbyte/tianji/commit/95b51ca2e160bf835aafb770e0a11b8ad0fc5858))
|
||||
* improve admin style in status page ([ed2141a](https://github.com/msgbyte/tianji/commit/ed2141af22a6103ea5be850eff57be7f24d9011b))
|
||||
* improve some style in server status page ([427e9e3](https://github.com/msgbyte/tianji/commit/427e9e3eb7684a58a4c2cb597283c5a5319d9dd3))
|
||||
* refactor server status edit form with react-hook-form ([6160d7b](https://github.com/msgbyte/tianji/commit/6160d7bcb9d3bf2a8b617b422e80fe7df8839967))
|
||||
* remove sender name in notification ([f309000](https://github.com/msgbyte/tianji/commit/f309000a0c3a58a4e1df9c44803ea6a0299fe9ac))
|
||||
* remove unused code and improve display view in status page ([f5151aa](https://github.com/msgbyte/tianji/commit/f5151aa2a4185714f1f988a2bad336b90f410b69))
|
||||
* update translation ([ef3d344](https://github.com/msgbyte/tianji/commit/ef3d34423b71969f9a9afee0f64394e17a66143f))
|
||||
* update translation ([8b86dcd](https://github.com/msgbyte/tianji/commit/8b86dcdceaf738cd27bbe6253825c9ef967c675f))
|
||||
* update translation ([42f41cd](https://github.com/msgbyte/tianji/commit/42f41cdbcb2a4452fbdcf6013451d6008d6d97f1))
|
||||
* upgrade @radix-ui/react-scroll-area version ([fc1e67e](https://github.com/msgbyte/tianji/commit/fc1e67e005fa7f4502807df1ab02c49b863bc4e4))
|
||||
|
||||
## [1.14.7](https://github.com/msgbyte/tianji/compare/v1.14.6...v1.14.7) (2024-09-10)
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* add document and website entry in app ([f74289f](https://github.com/msgbyte/tianji/commit/f74289ff0539b4cf4141eafbc6fc6cec12529357))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* improve data table resizer width to make it more easy to use ([2e60945](https://github.com/msgbyte/tianji/commit/2e609452b55c6aa957fdff3dd96b6b617bda13ed))
|
||||
* improve notification and feed channel filter logic ([e770e42](https://github.com/msgbyte/tianji/commit/e770e428936aa84c4ce0811c820ef4e43c2cdea3))
|
||||
* update sentry feed content ([1895ac7](https://github.com/msgbyte/tianji/commit/1895ac772cec64c92c490a1a06d78df2eae05191))
|
||||
|
||||
## [1.14.6](https://github.com/msgbyte/tianji/compare/v1.14.5...v1.14.6) (2024-09-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add unknown integration log ([d2afa54](https://github.com/msgbyte/tianji/commit/d2afa54301bcdd6a40fe5116b8191196c9b7bb33))
|
||||
|
||||
## [1.14.5](https://github.com/msgbyte/tianji/compare/v1.14.4...v1.14.5) (2024-09-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix row header style issue ([cf4531c](https://github.com/msgbyte/tianji/commit/cf4531c5ddc1758a2d11776f058559622771b311))
|
||||
|
||||
## [1.14.4](https://github.com/msgbyte/tianji/compare/v1.14.3...v1.14.4) (2024-09-03)
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* fix edit page url ([8ccace1](https://github.com/msgbyte/tianji/commit/8ccace127ba6aae012e5c164dbcaa42ee299196c))
|
||||
* update manual install to include code update ([2cc098a](https://github.com/msgbyte/tianji/commit/2cc098a5f1f184fa8e627e3d8d65a7910d9967c6))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* fix ci problem which cause build failed ([c4211c2](https://github.com/msgbyte/tianji/commit/c4211c270ffd6b1a6355a871676f0abed0f1e24f))
|
||||
|
||||
## [1.14.3](https://github.com/msgbyte/tianji/compare/v1.14.2...v1.14.3) (2024-09-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add feed event url support ([8534ab7](https://github.com/msgbyte/tianji/commit/8534ab7ba029e4c98a642f4c927902455e97d4a9))
|
||||
* add sentry webhook integration ([546055e](https://github.com/msgbyte/tianji/commit/546055e5559cf460e7d0f1dcce835e905baafc1e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix health bar style problem in page ([01d774d](https://github.com/msgbyte/tianji/commit/01d774d3958abd5ee15631eb771e22d5771f405a))
|
||||
|
||||
## [1.14.2](https://github.com/msgbyte/tianji/compare/v1.14.1...v1.14.2) (2024-09-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add archive feature ([3270164](https://github.com/msgbyte/tianji/commit/3270164710179a534692eabca77285dd28d887a7))
|
||||
* add curl feed api guide ([5588aca](https://github.com/msgbyte/tianji/commit/5588aca522646d88b7c9ddb5bff98e23c1d3bc15))
|
||||
* add feed archive page ([87b4000](https://github.com/msgbyte/tianji/commit/87b4000c4791a935a0f9247d1c219529105d0801))
|
||||
* add socket state ([e095a08](https://github.com/msgbyte/tianji/commit/e095a081b949880a088fd3e2512005d71d024769))
|
||||
* feishu add markdown syntax support ([33de808](https://github.com/msgbyte/tianji/commit/33de808f3e4e6a763fc36de96f24e01055907aba))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* update Chinese translation ([9fcc6dd](https://github.com/msgbyte/tianji/commit/9fcc6dda60a6496fb1909cdc5facd0ebb60e9448))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* improve feed event report style ([88f47db](https://github.com/msgbyte/tianji/commit/88f47db118968aa323b6ee0eac6b14e4fe9aa608))
|
||||
* update translations ([9966c12](https://github.com/msgbyte/tianji/commit/9966c1277c6bc8df74d9c3ca742b9e98c7577087))
|
||||
|
||||
## [1.14.1](https://github.com/msgbyte/tianji/compare/v1.14.0...v1.14.1) (2024-08-29)
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* update pnpm version ([b9f5582](https://github.com/msgbyte/tianji/commit/b9f5582a02afffaff5777c85126e91e243ef82aa))
|
||||
|
||||
## [1.14.0](https://github.com/msgbyte/tianji/compare/v1.13.1...v1.14.0) (2024-08-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add create workspace and switch workspace ([fac0838](https://github.com/msgbyte/tianji/commit/fac0838d8c7b14c7940170b733db0a33ca297b73))
|
||||
* add delete workspace endpoint ([6fecde0](https://github.com/msgbyte/tianji/commit/6fecde0caa422c25ddbb9ec564afb44031b761da))
|
||||
* add invite endpoint ([8c8b960](https://github.com/msgbyte/tianji/commit/8c8b960f61926aae415954dbef772e263d738ec5))
|
||||
* add invite user form ([e0e0449](https://github.com/msgbyte/tianji/commit/e0e044945f02451ad2b1db8041e91a738589cd5d))
|
||||
* add tick trpc endpoint ([7f33e2d](https://github.com/msgbyte/tianji/commit/7f33e2de0d0e0e1b4c2ff5d172f5d6c89e8dcd15))
|
||||
* add unstar feed ([446ddaf](https://github.com/msgbyte/tianji/commit/446ddafa0afb534e02f767457973a1594a190f8f))
|
||||
* add workspace page ([4918071](https://github.com/msgbyte/tianji/commit/491807165c7a4bd27b30961fdb3d525187d05ca3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix a style issue which workspace switch style broken with long name ([cbdb1c4](https://github.com/msgbyte/tianji/commit/cbdb1c4a079fcd19f03750dc3379f3e1aaaeb772))
|
||||
* fix some case(maybe) can not key problem ([b64ca8b](https://github.com/msgbyte/tianji/commit/b64ca8b300f2bcbbbcbdf95b4e0d9780c1f64b1b))
|
||||
* fix virtualize table loading and column style problem ([bb84661](https://github.com/msgbyte/tianji/commit/bb846616127e8ef823441b945390327a87b2f689))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* add private-policy page ([d136460](https://github.com/msgbyte/tianji/commit/d136460e39a69510e66952e76d42bca4016337a4))
|
||||
* update openapi files ([79a7a92](https://github.com/msgbyte/tianji/commit/79a7a923d247a2581cca5b0e912bcbe35a892851))
|
||||
* update website feed feature list ([ebd1e5e](https://github.com/msgbyte/tianji/commit/ebd1e5eb6648a424f078047125f31ce3a93bff03))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add default error style problem ([3cc678f](https://github.com/msgbyte/tianji/commit/3cc678f09ec8d743f17b7b2f296475b2d37c8841))
|
||||
* fix ci problem ([f7e1c81](https://github.com/msgbyte/tianji/commit/f7e1c8114b38c740f37e69125bd31fb26e1c2a1f))
|
||||
* fix tsconfig problem in tsx ([40df49e](https://github.com/msgbyte/tianji/commit/40df49e1dbb5afda4a2d01b94e298e4f2dfaa2d5))
|
||||
* improve healthbar display, will responsive with container size ([3990b0a](https://github.com/msgbyte/tianji/commit/3990b0a872d963900f52a97e869e7f26227c8107))
|
||||
* update translation ([e983092](https://github.com/msgbyte/tianji/commit/e9830920378c71371946d526ef20335176cc8f18))
|
||||
* upgrade @radix-ui/react-scroll-area to resolve scroll problem ([b862dd7](https://github.com/msgbyte/tianji/commit/b862dd74273faa78c65de916dce0a8fdafe9e834))
|
||||
* upgrade package manager ([fa328fb](https://github.com/msgbyte/tianji/commit/fa328fb0bfe9ef47cce1d44ae16aee2628921e30))
|
||||
* workspace switcher style and submit form reset ([5f47831](https://github.com/msgbyte/tianji/commit/5f47831f8e3f8df48fd287f6c4a23cb65270c21b))
|
||||
|
||||
## [1.13.1](https://github.com/msgbyte/tianji/compare/v1.13.0...v1.13.1) (2024-08-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add feed template string of survey ([22fc5f9](https://github.com/msgbyte/tianji/commit/22fc5f98f8b646d6505a7a518074f5ce3f40215f))
|
||||
* add FeedChannelPicker component ([5f6147e](https://github.com/msgbyte/tianji/commit/5f6147e3b6329c6fbeb7f8b1ac981f38fbe3e97a))
|
||||
* add survey result send to feed channel feature ([d986210](https://github.com/msgbyte/tianji/commit/d9862105edd0f528dcf91d29142eaca9d78a8001))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* remove unmaintained readme ([5447f53](https://github.com/msgbyte/tianji/commit/5447f53b303fd43f8121fe9cccf5efc0326b7ace))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* fix ci problem ([3e3dc4c](https://github.com/msgbyte/tianji/commit/3e3dc4c22d765d7eea1d38e9f85c913982c656b6))
|
||||
* remove lodash ([49d0da3](https://github.com/msgbyte/tianji/commit/49d0da3a6d54a65db7e11b1e9bb2e45fee228bdc))
|
||||
* upgrade i18next-toolkit version ([59840b5](https://github.com/msgbyte/tianji/commit/59840b5f7b1cde4e6173dc1fa3b1bf39d3f701a7))
|
||||
|
||||
## [1.13.0](https://github.com/msgbyte/tianji/compare/v1.12.1...v1.13.0) (2024-08-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add authjs backend support ([06d6ecd](https://github.com/msgbyte/tianji/commit/06d6ecd2a3384056be017c5608df282968802196))
|
||||
* add avatar and nickname display in user info scope ([03bc9b5](https://github.com/msgbyte/tianji/commit/03bc9b5125070d2675b422723404c96fd2ac95ad))
|
||||
* add duplicate feature for monitor ([827cf07](https://github.com/msgbyte/tianji/commit/827cf07c2a70b3437a8381ab3ee16838a348fd91))
|
||||
* add email restrict ([0a0a275](https://github.com/msgbyte/tianji/commit/0a0a27549ace51bf0b8c9ef135c50fd859980525))
|
||||
* add feed channel into search command panel ([275f30f](https://github.com/msgbyte/tianji/commit/275f30f0487a02e96289d8df5ea107bdd591212e))
|
||||
* add github auth integrate ([7f7c95b](https://github.com/msgbyte/tianji/commit/7f7c95b11c664a15732f18b28ea1d154f289fca9))
|
||||
* add logout and socketio auth ([e9c64c5](https://github.com/msgbyte/tianji/commit/e9c64c57e7b8bd8912669aef93d3958bb754a057))
|
||||
* add none in feed channel ([73dd8c2](https://github.com/msgbyte/tianji/commit/73dd8c25b7f782a882682039a3cba94526af9906))
|
||||
* add prisma migrate ([37757f6](https://github.com/msgbyte/tianji/commit/37757f6563d6de71a59aa1b021e7e29e9235eb3b))
|
||||
* add support for legacy traditional login methods ([3afac06](https://github.com/msgbyte/tianji/commit/3afac062c417bfeef0536ee49a459de96ac7ae72))
|
||||
* add survey count and feed event count ([f149642](https://github.com/msgbyte/tianji/commit/f1496429d30e13af8e810ae1dbc7ba74707d621d))
|
||||
* add virtualized data table resizer ([f1aaa70](https://github.com/msgbyte/tianji/commit/f1aaa7040e7953d85b956f398df65873d9104205))
|
||||
* add VirtualizedInfiniteDataTable and refactor survey result list ([b2dccec](https://github.com/msgbyte/tianji/commit/b2dccec2834a486dc018ac7b1d267bb327d48422))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix tencentCloudAlarmMetricSchema incorrect problem ([914046a](https://github.com/msgbyte/tianji/commit/914046aefacafc0500585d55f8a2119685777ac3))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* add custom example for match text ([e4eee42](https://github.com/msgbyte/tianji/commit/e4eee420ea013068bc3d5fc0d9de436a6f69d65f))
|
||||
* update README preview images ([bb76c8e](https://github.com/msgbyte/tianji/commit/bb76c8e895d5cb2ba5c8e5889d1a8ae15a605b48))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add error log for tencent alarm ([af47920](https://github.com/msgbyte/tianji/commit/af4792024f3adc0b152f101e066f84077c98ecf7))
|
||||
* add more log for tencent cloud alarm ([ad18666](https://github.com/msgbyte/tianji/commit/ad186668515c40e4127108a689accacc5b782760))
|
||||
* add more translations ([05c358b](https://github.com/msgbyte/tianji/commit/05c358b2e5b18d74596fac8bb7ef2a0caf06b527))
|
||||
* change all import with .js suffix, which will help nodejs(esm) to import code clear. ([d5d0446](https://github.com/msgbyte/tianji/commit/d5d04468cb210d0e2313ab66d494e09a8337a9d0))
|
||||
* fix ci issue of typescript type check ([e5c2b94](https://github.com/msgbyte/tianji/commit/e5c2b9484fb761a2dff2f1e9f3dff3368f371712))
|
||||
* fix ci problem ([c7ff366](https://github.com/msgbyte/tianji/commit/c7ff3666a7814936d8e5266df8e183fafbec6f96))
|
||||
* fix react-router version ([20e95ef](https://github.com/msgbyte/tianji/commit/20e95ef97328fa1c0a3caab6a0ae20d54480eea0))
|
||||
* remove ts-node and change to tsx ([b04ddd4](https://github.com/msgbyte/tianji/commit/b04ddd40ad483b773eeba0141b21ce80bfb4edbe))
|
||||
* translate server side code into esm ([5dca262](https://github.com/msgbyte/tianji/commit/5dca262482adaadf6e25385bba0b1061ed9d33a4))
|
||||
* update translation file ([7e38e32](https://github.com/msgbyte/tianji/commit/7e38e327bf3d8662e86a9c4320589f15d122ecd1))
|
||||
* wip: add auth.js ([3cf3cfa](https://github.com/msgbyte/tianji/commit/3cf3cfa427b1d3ff6704e8839b60781eb7c15b32))
|
||||
|
||||
## [1.12.1](https://github.com/msgbyte/tianji/compare/v1.12.0...v1.12.1) (2024-07-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add tencent cloud integration ([8585ea4](https://github.com/msgbyte/tianji/commit/8585ea4196e61934508f33e5120df8d854d6b18f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix code block not display well in light mode [#80](https://github.com/msgbyte/tianji/issues/80) ([0835fc5](https://github.com/msgbyte/tianji/commit/0835fc588bd0967d578abe34af9596fea2f29390))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* update pnpm version in manual install document ([9a7afed](https://github.com/msgbyte/tianji/commit/9a7afed08cb4aa80839fb83328a8ff300ac2141e))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add fade in animation ([35a6e20](https://github.com/msgbyte/tianji/commit/35a6e20717d42ed4719b7dd441a89b079973d30e))
|
||||
* change website config tabs to shadcn ui and improve ui ([f112adc](https://github.com/msgbyte/tianji/commit/f112adc696f7d2ded5d7619cf900e8156ea92d37))
|
||||
|
||||
## [1.12.0](https://github.com/msgbyte/tianji/compare/v1.11.4...v1.12.0) (2024-07-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add channel feed notification ([67bfda3](https://github.com/msgbyte/tianji/commit/67bfda30bc95b5b8d11ff3994c6d097106e2c248))
|
||||
* add colorized text for server status which help user find problem ([f2b20c5](https://github.com/msgbyte/tianji/commit/f2b20c5ef9a8d46aecb51b7c33720831c4e1ddf6))
|
||||
* add custom feed integration ([765cc41](https://github.com/msgbyte/tianji/commit/765cc41c0637879c08f0cbb882d0decd03fc6e66))
|
||||
* add date range and improve report display ([6e68a80](https://github.com/msgbyte/tianji/commit/6e68a8044dbda7a391800589ebb4cc2c828a8dbc))
|
||||
* add dialog wrapper and improve display of webhook modal ([adb1cc3](https://github.com/msgbyte/tianji/commit/adb1cc391926b9fcea71f2db780e387b980b0b1d))
|
||||
* add feed channel count ([a7688f0](https://github.com/msgbyte/tianji/commit/a7688f02af6a51d3786d4c91114a0e398c880fab))
|
||||
* add feed endpoint ([f459c6b](https://github.com/msgbyte/tianji/commit/f459c6beeadaea6368e07375efa4845fe994305b))
|
||||
* add feed event item created time ([926ea98](https://github.com/msgbyte/tianji/commit/926ea980ff130ba93c491fce9445b668f88175aa))
|
||||
* add feed event notification with event and daily ([7bfd92b](https://github.com/msgbyte/tianji/commit/7bfd92be0b90cc5b9556a91f38627bdf015492d9))
|
||||
* add feed page ([96a5a33](https://github.com/msgbyte/tianji/commit/96a5a33ad61c8b7bb4e4eae535acc659632aa914))
|
||||
* add github integration support ([12fe9f0](https://github.com/msgbyte/tianji/commit/12fe9f0384ab08bc9013b22eb29140b14dae559f))
|
||||
* add integration modal ([af5f6ad](https://github.com/msgbyte/tianji/commit/af5f6ad9f5853c344ceefe7a5e34730f364bfeae))
|
||||
* add list content token ([7736bf8](https://github.com/msgbyte/tianji/commit/7736bf89dc94524d0cce6dc0b42a6ef430ecab2e))
|
||||
* add more clear job ([b6bca6c](https://github.com/msgbyte/tianji/commit/b6bca6c250ded389b863e6e11adb77e5aa1b2911))
|
||||
* add realtime feed event and desc feed list ([478d0c2](https://github.com/msgbyte/tianji/commit/478d0c2af3dd65d743a1ab36c0099a8449dc5224))
|
||||
* add VirtualList support for feed events ([caf7e9c](https://github.com/msgbyte/tianji/commit/caf7e9ca72358772f3e47b593e7553b78578b226))
|
||||
* add weekly and monthly cron job ([03904d2](https://github.com/msgbyte/tianji/commit/03904d26e08fbb755983568ed4dec7667f52982a))
|
||||
* feed add markdown support ([56bbe09](https://github.com/msgbyte/tianji/commit/56bbe09005013276c0ac7324354cb63533ecd18e))
|
||||
* github feed add star and issue support ([29939b6](https://github.com/msgbyte/tianji/commit/29939b6709e143ab9f68d008b79be38b3f13a6e7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix auditlog cannot fetch more data problem ([1b859e3](https://github.com/msgbyte/tianji/commit/1b859e31768b0f5b1a844989745e37e30d6ed478))
|
||||
* fix problem of send notification ([82bb2ad](https://github.com/msgbyte/tianji/commit/82bb2ad267ae1f2922924fddb986b9f4e619eb1e))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* update category order ([b2480b0](https://github.com/msgbyte/tianji/commit/b2480b0ed57eccbc741de0652edbda8548b68db9))
|
||||
* update roadmap ([e10cdfd](https://github.com/msgbyte/tianji/commit/e10cdfdf2612448938711b42fab2b8f160c3cab2))
|
||||
* update webhooks document ([b355a67](https://github.com/msgbyte/tianji/commit/b355a677d3e36de4f53a73c59e23ab1aa0cb690e))
|
||||
* update wechat qrcode ([503df45](https://github.com/msgbyte/tianji/commit/503df4546da7d11035f301db18650b4552d484f0))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add document for endpoint ([537503f](https://github.com/msgbyte/tianji/commit/537503f288735de3c73f759291338ca74ce8d5d1))
|
||||
* add dynamic virtual list ([01d81f3](https://github.com/msgbyte/tianji/commit/01d81f39296b80899c4fcaadda8dffa3f3f28803))
|
||||
* add empty description message ([66ec94f](https://github.com/msgbyte/tianji/commit/66ec94fd08c24b901a0b8814cec5e246b6f6454f))
|
||||
* add env openapi default value ([685d050](https://github.com/msgbyte/tianji/commit/685d05074b9a994081eade85cceb5f3b001c74df))
|
||||
* add feed event url ([ac930cd](https://github.com/msgbyte/tianji/commit/ac930cd05e19e69e5ec9a7b059e56043685426ee))
|
||||
* add preview text ([3d9b67a](https://github.com/msgbyte/tianji/commit/3d9b67a430536d04adaab035e29bb21d4f2b0051))
|
||||
* add simple virtual list ([b7670da](https://github.com/msgbyte/tianji/commit/b7670da7db231d7f46f0cafc4ed30c2d46981c0a))
|
||||
* change create feed event to local ([ab179e9](https://github.com/msgbyte/tianji/commit/ab179e9af6f2f17f2ac93be8e046a71b67f90eb1))
|
||||
* change feed channel notifyFrequency type to enum ([2ce5597](https://github.com/msgbyte/tianji/commit/2ce5597dfe1c51cbec86c13b1b6a3fe5eecf2e53))
|
||||
* change push message in github event ([1d4aecf](https://github.com/msgbyte/tianji/commit/1d4aecff9559e30d5274073f8c64002cce54aaef))
|
||||
* fix ci problem ([865e56f](https://github.com/msgbyte/tianji/commit/865e56f40e7351d21c995f7a9c0fafbcc7b75993))
|
||||
* fix ci problem ([4d15ccc](https://github.com/msgbyte/tianji/commit/4d15cccd1b8718f862a482564703eb905f1839c2))
|
||||
* improve display in feed channel list ([15c6290](https://github.com/msgbyte/tianji/commit/15c6290587521abd6ce4a6325fc13456d1b10f42))
|
||||
* improve feed event item display ([17f87c1](https://github.com/msgbyte/tianji/commit/17f87c191a9b1fcbcb39e73e1830ccb29b6e634c))
|
||||
* improve logger and test case ([9796d42](https://github.com/msgbyte/tianji/commit/9796d428466f21adcfa42e04f04cbfe2d15aff3c))
|
||||
* remove unused code ([2f6e92d](https://github.com/msgbyte/tianji/commit/2f6e92d166ac1635b87151147c12f13146136d38))
|
||||
* remove unused size changer ([6ccd0ed](https://github.com/msgbyte/tianji/commit/6ccd0ede7b4c1bd8caad0697f66e61db8ea1feb9))
|
||||
* skip event report if not have any events ([f814691](https://github.com/msgbyte/tianji/commit/f814691538578972bdeefe574e74a8dacb261a59))
|
||||
* split integration route from feed route ([a4c31fe](https://github.com/msgbyte/tianji/commit/a4c31fe2da2b4213726579042ac93aa0547f0cc7))
|
||||
* update feed guide ([c34b012](https://github.com/msgbyte/tianji/commit/c34b0124fac27fa2e48d2705cab8f07935d7fa02))
|
||||
* update openapi base url and regenerate openapi document ([616a623](https://github.com/msgbyte/tianji/commit/616a623e40ea86e68595449f7a9d290630a5b116))
|
||||
* update tag and content ([85a2a59](https://github.com/msgbyte/tianji/commit/85a2a598d76a5835e32772f04b6b2cf0e0d6dccf))
|
||||
* update translation ([fc6ee73](https://github.com/msgbyte/tianji/commit/fc6ee733663231347a22aa7df83e4f4a454f4bd6))
|
||||
* upgrade pnpm version in ci ([a2cb8b0](https://github.com/msgbyte/tianji/commit/a2cb8b0538d69d2209faa9574c509e49ec7d55ee))
|
||||
* upgrade pnpm version in dockerfile ([1b89c3b](https://github.com/msgbyte/tianji/commit/1b89c3b5a808b57f1a695b86a8f35e4199e0ed7c))
|
||||
* upgrade pnpm version to v9.5.0 ([63de6d7](https://github.com/msgbyte/tianji/commit/63de6d7aa514a5e1a2e206785a8426336cb323fa))
|
||||
|
||||
## [1.11.4](https://github.com/msgbyte/tianji/compare/v1.11.3...v1.11.4) (2024-06-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add server install script usage guide ([4943d2d](https://github.com/msgbyte/tianji/commit/4943d2dd8e4495f3e03631d7f332b9a630e79b49))
|
||||
* add webhook notification ([90df8e8](https://github.com/msgbyte/tianji/commit/90df8e8e36c618868110c3b9a0119b1b69184546))
|
||||
* webhook add title and time ([a91d1ff](https://github.com/msgbyte/tianji/commit/a91d1ffffe41b61b9792d92865e8d8f65db27b0f))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* add document about server status page custom domain ([f06e788](https://github.com/msgbyte/tianji/commit/f06e788f454df877d7030e603aff5b882fcf7a82))
|
||||
* add webhook document ([61c1b0e](https://github.com/msgbyte/tianji/commit/61c1b0e06504fca8fc35c41a7bb15130e7c08f24))
|
||||
* update changelog ([ee16e6c](https://github.com/msgbyte/tianji/commit/ee16e6cd76c9138ce343b208f6c8d0026f0ee6c9))
|
||||
* update wechat qrcode ([0d2c4f9](https://github.com/msgbyte/tianji/commit/0d2c4f97f96494a4874c4b3a30bd633202f35b2f))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* update release it ([3bfd11a](https://github.com/msgbyte/tianji/commit/3bfd11a7b6b2395cbe24653fea52c415c14bc1ca))
|
||||
|
||||
## [1.11.3](https://github.com/msgbyte/tianji/compare/tianji-0.1.17...1.11.3) (2024-06-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix setting page not display correct problem ([fdce6b4](https://github.com/msgbyte/tianji/commit/fdce6b42f1e9817dba76072adaf732040bf3f8d3))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* [#68](https://github.com/msgbyte/tianji/issues/68) add document to how to install with helm ([95a8e99](https://github.com/msgbyte/tianji/commit/95a8e9968ba72f6e13db227c0b5695f6d12e388a))
|
||||
* add improve monitor reporter usage roadmap [#75](https://github.com/msgbyte/tianji/issues/75) ([caab72d](https://github.com/msgbyte/tianji/commit/caab72dac58f2f6131d195f3bdbb29e41fa8bb0f))
|
||||
* update changelog ([0deec1f](https://github.com/msgbyte/tianji/commit/0deec1fc55e30dcb1a71f835ba51b48b46310e3d))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* improve mobile display for tianji ([e9a1b61](https://github.com/msgbyte/tianji/commit/e9a1b61a7f3eec1050df9cf7e4ad3644f787091b))
|
||||
* improve sidebar hide logic ([cae0c1d](https://github.com/msgbyte/tianji/commit/cae0c1d6c094a1662e1e390962ed10b8eabe73ea))
|
||||
* update cr config ([f91110b](https://github.com/msgbyte/tianji/commit/f91110b313fb7f874813d2f76919476a4cf24631))
|
||||
|
||||
## [1.11.2](https://github.com/msgbyte/tianji/compare/v1.11.1...v1.11.2) (2024-06-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add createdAt field in survey download csv ([618aedf](https://github.com/msgbyte/tianji/commit/618aedf1963559c07af696fb3483d4c073ba7c29))
|
||||
* add document entry ([ad4b67c](https://github.com/msgbyte/tianji/commit/ad4b67ca459837cabcb1f274c7e10ec03bf128f5))
|
||||
* add website view count in website list ([8ac5b11](https://github.com/msgbyte/tianji/commit/8ac5b11d4962de05cefe3d5be7c014f4f8bb7c9a))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* add install script uninstall document ([bffb9d6](https://github.com/msgbyte/tianji/commit/bffb9d6729adba7fd66468e9a618b68c68d09366))
|
||||
* add prepare markdown ([98a8878](https://github.com/msgbyte/tianji/commit/98a887825f5df8385dc14c45e7e8ce2bc49c4b87))
|
||||
* update changelog ([0c5c993](https://github.com/msgbyte/tianji/commit/0c5c993236ba51dfb0242af01459f4024e2038a6))
|
||||
* update environment document ([52a8927](https://github.com/msgbyte/tianji/commit/52a89276c8ef707e5283161336296c3f040354d2))
|
||||
* update manual document ([1dafea6](https://github.com/msgbyte/tianji/commit/1dafea61c78e01ed28e665ee9048afde433415a9))
|
||||
* update manual install document [#56](https://github.com/msgbyte/tianji/issues/56) ([154b8b4](https://github.com/msgbyte/tianji/commit/154b8b4b6405c721342a681f366d3914536dc62a))
|
||||
* update manual install faq ([4564347](https://github.com/msgbyte/tianji/commit/45643476985f65e730c4906a719a3931849cd9bb))
|
||||
* update readme roadmap ([58445f9](https://github.com/msgbyte/tianji/commit/58445f9249eb8785003d381cc4527835edba485c))
|
||||
* update wechat qrcode ([26da461](https://github.com/msgbyte/tianji/commit/26da4613683394bb628f9af444bf0f620d4b563a))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* increase timeout factor of interval ([4e8d761](https://github.com/msgbyte/tianji/commit/4e8d7613a40ba20e425caa2308846f663029ffe2))
|
||||
* remove unused code ([328a4e8](https://github.com/msgbyte/tianji/commit/328a4e856cee0cf38c0beb1611ac2bc643bbd981))
|
||||
* update env example ([80713e0](https://github.com/msgbyte/tianji/commit/80713e0fceac5ab07045532cd5759ff3a54522db))
|
||||
* update sdk publish file module type ([ed0c2e9](https://github.com/msgbyte/tianji/commit/ed0c2e9d1da882acaa55854229336aa05476fd06))
|
||||
* update translation ([d74ba8d](https://github.com/msgbyte/tianji/commit/d74ba8d283cb804cd7947ffc3804608ef24c41f7))
|
||||
* upgrade prisma version to 5.14.0 ([a0ab1da](https://github.com/msgbyte/tianji/commit/a0ab1da6b60b3c608fb72b24f36b80e6ef954fe9))
|
||||
|
||||
## [1.11.1](https://github.com/msgbyte/tianji/compare/v1.11.0...v1.11.1) (2024-05-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix display problem in docker panel ([e3d0555](https://github.com/msgbyte/tianji/commit/e3d0555c454cf7e49a9301a28f65cf863fc50573))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add survey add state ([3ecd7aa](https://github.com/msgbyte/tianji/commit/3ecd7aa171f7be0b8c7dfdeff7d140294d8819bc))
|
||||
|
||||
## [1.11.0](https://github.com/msgbyte/tianji/compare/v1.10.0...v1.11.0) (2024-05-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add reporter send docker info ([1dfa24d](https://github.com/msgbyte/tianji/commit/1dfa24df1b52544bee134cc6dcc94f744026bc03))
|
||||
* add server docker expend view ([c6433f3](https://github.com/msgbyte/tianji/commit/c6433f310b821b4e8b3cb55df1e9ccadda7d97f4))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* new homepage ([a20396a](https://github.com/msgbyte/tianji/commit/a20396ad97cec9a54461411e8e79af9fc6571c6b))
|
||||
* update website style ([8e96c06](https://github.com/msgbyte/tianji/commit/8e96c06d94b71c74235769eb5ff691c951cc2064))
|
||||
* uprade docs website to v3.3.2 ([eacf7fc](https://github.com/msgbyte/tianji/commit/eacf7fc56f2f23b301470eca51747d52fe1d78e4))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add loading state for common list ([00d40c8](https://github.com/msgbyte/tianji/commit/00d40c8410c0c7c94438518344cd7c946cc64879))
|
||||
* change datatable expend icon and add transition ([74bd9ef](https://github.com/msgbyte/tianji/commit/74bd9ef3d96c1f2940c0717b61466edc7d0b44ca))
|
||||
* move dependency place ([dec6a8b](https://github.com/msgbyte/tianji/commit/dec6a8b7c59deac561e68abed46334e7a072f8c5))
|
||||
* update survey icon ([0ea7515](https://github.com/msgbyte/tianji/commit/0ea7515ad21e8d4e3fd798bf3e1341f0caf56821))
|
||||
* upgrade tianji-client-sdk version ([9a0a1ea](https://github.com/msgbyte/tianji/commit/9a0a1eacb693dd816ee68db2e217f8f6e48528c6))
|
||||
* upgrade trpc version to 10.45.2 ([7c94caf](https://github.com/msgbyte/tianji/commit/7c94caf0ed777f9558bbdc84c26eba32d60105a1))
|
||||
|
||||
## [1.10.0](https://github.com/msgbyte/tianji/compare/v1.9.4...v1.10.0) (2024-05-15)
|
||||
|
||||
|
||||
|
30
Dockerfile
30
Dockerfile
@ -7,12 +7,28 @@ COPY ./reporter/ ./reporter/
|
||||
RUN apt update
|
||||
RUN cd reporter && go build .
|
||||
|
||||
# # Base ------------------------------
|
||||
FROM node:20-alpine AS base
|
||||
# Base ------------------------------
|
||||
# The current Chromium version in Alpine 3.20 is causing timeout issues with Puppeteer. Downgrading to Alpine 3.19 fixes the issue. See #11640, #12637, #12189
|
||||
FROM node:20-alpine3.19 AS base
|
||||
|
||||
RUN npm install -g pnpm@8.3.1
|
||||
RUN npm install -g pnpm@9.7.1
|
||||
|
||||
# For apprise
|
||||
RUN apk add --update --no-cache python3 py3-pip g++ make
|
||||
|
||||
# For puppeteer
|
||||
RUN apk upgrade --no-cache --available \
|
||||
&& apk add --no-cache \
|
||||
chromium-swiftshader \
|
||||
ttf-freefont \
|
||||
font-noto-emoji \
|
||||
&& apk add --no-cache \
|
||||
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
|
||||
font-wqy-zenhei
|
||||
|
||||
# For zeromq
|
||||
RUN apk add --update --no-cache curl cmake
|
||||
|
||||
# Tianji frontend ------------------------------
|
||||
FROM base AS static
|
||||
WORKDIR /app/tianji
|
||||
@ -22,9 +38,10 @@ ARG VERSION
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm install --filter @tianji/client... --config.dedupe-peer-dependents=false --frozen-lockfile
|
||||
|
||||
ENV VITE_VERSION=$VERSION
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
RUN pnpm build:static
|
||||
|
||||
@ -32,6 +49,11 @@ RUN pnpm build:static
|
||||
FROM base AS app
|
||||
WORKDIR /app/tianji
|
||||
|
||||
# We don't need the standalone Chromium in alpine.
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=true
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pnpm install --filter @tianji/server... --config.dedupe-peer-dependents=false
|
||||
|
30
README.md
30
README.md
@ -32,21 +32,33 @@ It's good to specialize in one thing, if we are experts in related abilities we
|
||||
- [x] telemetry
|
||||
- [x] openapi
|
||||
- [x] website
|
||||
- [ ] team collaboration
|
||||
- [x] team collaboration
|
||||
- [ ] utm track
|
||||
- [ ] waitlist
|
||||
- [ ] survey
|
||||
- [ ] lighthouse report
|
||||
- [ ] hooks
|
||||
- [ ] links
|
||||
- [x] waitlist
|
||||
- [x] survey
|
||||
- [ ] survey page
|
||||
- [x] lighthouse report
|
||||
- [x] hooks
|
||||
- [x] helm install support
|
||||
- [x] allow install from public
|
||||
- [ ] improve monitor reporter usage
|
||||
- [x] uninstall guide
|
||||
- [ ] download from server
|
||||
- [ ] custom params guide
|
||||
|
||||
## Preview
|
||||
|
||||
![](./website/static/img/preview1.png)
|
||||
![](./website/static/img/preview/1.png)
|
||||
|
||||
![](./website/static/img/preview2.png)
|
||||
![](./website/static/img/preview/2.png)
|
||||
|
||||
![](./website/static/img/preview3.png)
|
||||
![](./website/static/img/preview/3.png)
|
||||
|
||||
![](./website/static/img/preview/4.png)
|
||||
|
||||
![](./website/static/img/preview/5.png)
|
||||
|
||||
![](./website/static/img/preview/6.png)
|
||||
|
||||
## Translation
|
||||
|
||||
|
36
README.zh.md
36
README.zh.md
@ -1,36 +0,0 @@
|
||||
# 天机 Tianji
|
||||
|
||||
<img src="./website/static/img/logo.svg" width="128" />
|
||||
|
||||
**All-in-One 的数据洞察中心**
|
||||
|
||||
`网站分析器` + `状态监控器` + `服务状态上报` = `Tianji`
|
||||
|
||||
所有一切都在一起!
|
||||
|
||||
## Motivation
|
||||
|
||||
在我们对网站进行观察时。我们往往需要多个应用一起来组合使用。比如我们需要ga/umami等分析工具来查看pvuv以及各个页面的访问量,我们需要uptime监控器来检查服务器的网络质量与连通性,我们需要通关prometheus获取服务端上报的状态来检查服务器的质量。另外如果开发的是一个允许被开源部署的应用,我们往往还需要一个遥测系统来帮助我们对其他人的部署情况做一个最简单的信息收集。
|
||||
|
||||
我认为这些工具应当是为同一个目的而服务的,那么有没有一款应用能够轻量级的将这些常见的需求整合为一体呢?毕竟在大部分时候我们并不需要非常专业与深入的功能。但是我为了实现全面的监控却需要安装如此多的服务。
|
||||
|
||||
专精于一项这很好,如果我们是相关能力的专家我们需要这样的专业工具。但是对于大部分只有轻量级需求的用户而言,一个all in one的应用会更加方便与易于使用
|
||||
|
||||
|
||||
## Preview
|
||||
|
||||
![](./website/static/img/preview1.png)
|
||||
|
||||
![](./website/static/img/preview2.png)
|
||||
|
||||
![](./website/static/img/preview3.png)
|
||||
|
||||
## Open Source
|
||||
|
||||
`Tianji` is open source with `Apache 2.0` license.
|
||||
|
||||
And its inspired by `umami` license which under `MIT` and `uptime-kuma` which under `MIT` license too
|
||||
|
||||
### One-Click Deployment
|
||||
|
||||
[![Deploy on Sealos](https://cdn.jsdelivr.net/gh/labring-actions/templates@main/Deploy-on-Sealos.svg)](https://bja.sealos.run/?openapp=system-template%3FtemplateName%3Dtianji)
|
@ -2,6 +2,9 @@ version: '3'
|
||||
services:
|
||||
tianji:
|
||||
image: moonrailgun/tianji
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile
|
||||
ports:
|
||||
- "12345:12345"
|
||||
environment:
|
||||
|
1
k8s/helm/.gitignore
vendored
Normal file
1
k8s/helm/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
charts
|
23
k8s/helm/.helmignore
Normal file
23
k8s/helm/.helmignore
Normal file
@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
6
k8s/helm/Chart.lock
Normal file
6
k8s/helm/Chart.lock
Normal file
@ -0,0 +1,6 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 15.4.2
|
||||
digest: sha256:57308af2821726255fe0c52374260c2ea98f9091c03d82cf82b9c6d499e6f214
|
||||
generated: "2024-06-09T14:04:36.881437+08:00"
|
42
k8s/helm/Chart.yaml
Normal file
42
k8s/helm/Chart.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
apiVersion: v2
|
||||
name: tianji
|
||||
description: All-in-One Insight Hub
|
||||
icon: https://tianji.msgbyte.com/img/logo.svg
|
||||
maintainers:
|
||||
- name: moonrailgun
|
||||
email: moonrailgun@gmail.com
|
||||
keywords:
|
||||
- tianji
|
||||
- uptime
|
||||
- umami
|
||||
- server status
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.17
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.11.2"
|
||||
|
||||
sources:
|
||||
- https://github.com/msgbyte/tianji
|
||||
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 15.4.2
|
||||
repository: "https://charts.bitnami.com/bitnami"
|
||||
condition: postgresql.enabled
|
26
k8s/helm/templates/NOTES.txt
Normal file
26
k8s/helm/templates/NOTES.txt
Normal file
@ -0,0 +1,26 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "tianji.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "tianji.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "tianji.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "tianji.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
|
||||
2. Tianji's default account:
|
||||
Username: admin
|
||||
Password: admin
|
62
k8s/helm/templates/_helpers.tpl
Normal file
62
k8s/helm/templates/_helpers.tpl
Normal file
@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "tianji.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "tianji.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "tianji.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "tianji.labels" -}}
|
||||
helm.sh/chart: {{ include "tianji.chart" . }}
|
||||
{{ include "tianji.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "tianji.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "tianji.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "tianji.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "tianji.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
15
k8s/helm/templates/configmap.yaml
Normal file
15
k8s/helm/templates/configmap.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "tianji.fullname" . }}
|
||||
labels:
|
||||
{{- include "tianji.labels" . | nindent 4 }}
|
||||
data:
|
||||
DATABASE_URL: postgresql://{{ .Values.postgresql.auth.username }}:{{ .Values.postgresql.auth.password }}@{{ .Release.Name }}-postgresql:5432/{{ .Values.postgresql.auth.database }}
|
||||
{{/*
|
||||
Rather than maintain a comprehensive ConfigMap, we map all sub-keys of the "env" value here.
|
||||
This allows for more flexibility and less Chart churn as Drone evolves.
|
||||
*/}}
|
||||
{{- range $envKey, $envVal := .Values.env }}
|
||||
{{ $envKey | upper }}: {{ $envVal | quote }}
|
||||
{{- end }}
|
71
k8s/helm/templates/deployment.yaml
Normal file
71
k8s/helm/templates/deployment.yaml
Normal file
@ -0,0 +1,71 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "tianji.fullname" . }}
|
||||
labels:
|
||||
{{- include "tianji.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "tianji.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "tianji.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "tianji.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "tianji.fullname" . }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.volumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.volumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
32
k8s/helm/templates/hpa.yaml
Normal file
32
k8s/helm/templates/hpa.yaml
Normal file
@ -0,0 +1,32 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "tianji.fullname" . }}
|
||||
labels:
|
||||
{{- include "tianji.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "tianji.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
61
k8s/helm/templates/ingress.yaml
Normal file
61
k8s/helm/templates/ingress.yaml
Normal file
@ -0,0 +1,61 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "tianji.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "tianji.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
15
k8s/helm/templates/service.yaml
Normal file
15
k8s/helm/templates/service.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "tianji.fullname" . }}
|
||||
labels:
|
||||
{{- include "tianji.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "tianji.selectorLabels" . | nindent 4 }}
|
13
k8s/helm/templates/serviceaccount.yaml
Normal file
13
k8s/helm/templates/serviceaccount.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "tianji.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "tianji.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
15
k8s/helm/templates/tests/test-connection.yaml
Normal file
15
k8s/helm/templates/tests/test-connection.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "tianji.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "tianji.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "tianji.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
121
k8s/helm/values.yaml
Normal file
121
k8s/helm/values.yaml
Normal file
@ -0,0 +1,121 @@
|
||||
# Default values for tianji.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: moonrailgun/tianji
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: true
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
postgresql:
|
||||
enabled: true
|
||||
auth:
|
||||
username: "tianji"
|
||||
password: "tianji"
|
||||
database: "tianji"
|
||||
postgresPassword: "tianji"
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 12345
|
||||
|
||||
env:
|
||||
# DATABASE_URL: postgresql://tianji:tianji@postgresql:5432/tianji
|
||||
JWT_SECRET: replace-me-with-a-random-string
|
||||
ALLOW_REGISTER: "false"
|
||||
ALLOW_OPENAPI: "true"
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
23
package.json
23
package.json
@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "tianji",
|
||||
"private": true,
|
||||
"version": "1.10.0",
|
||||
"version": "1.16.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently --kill-others npm:dev:server npm:dev:web",
|
||||
"dev:web": "cd src/client && pnpm dev",
|
||||
@ -15,9 +16,9 @@
|
||||
"build:app": "pnpm build:server && pnpm build:client",
|
||||
"build:client": "cd src/client && pnpm build",
|
||||
"build:server": "cd src/server && pnpm build",
|
||||
"build:tracker": "ts-node scripts/build-tracker.ts",
|
||||
"build:geo": "ts-node scripts/build-geo.ts",
|
||||
"build:openapi": "ts-node --project ./tsconfig.base.json ./scripts/build-openapi-schema.ts && cd packages/client-sdk && pnpm generate:client",
|
||||
"build:tracker": "tsx scripts/build-tracker.ts",
|
||||
"build:geo": "tsx scripts/build-geo.ts",
|
||||
"build:openapi": "tsx --tsconfig ./tsconfig.base.json ./scripts/build-openapi-schema.ts && cd packages/client-sdk && pnpm generate:client",
|
||||
"check:type": "pnpm -r check:type",
|
||||
"release": "release-it",
|
||||
"release:patch": "release-it -i patch",
|
||||
@ -30,12 +31,14 @@
|
||||
"@types/tar": "^6.1.10",
|
||||
"concurrently": "^8.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.12",
|
||||
"release-it": "^17.0.1",
|
||||
"tar": "^6.1.15",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -43,9 +46,17 @@
|
||||
"eventemitter-strict": "^1.0.1",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"dayjs": "1.11.10"
|
||||
"@auth/core": "0.34.1",
|
||||
"dayjs": "1.11.10",
|
||||
"esbuild": "0.24.0",
|
||||
"postman-code-generators": "1.8.0",
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"zod-prisma@0.5.4": "patches/zod-prisma@0.5.4.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tianji-client-sdk",
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.1",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
@ -9,6 +9,10 @@
|
||||
"generate:client": "openapi-ts -i ../../website/openapi.json -o src/open/client",
|
||||
"test": "vitest"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"keywords": [
|
||||
"tianji"
|
||||
],
|
||||
|
13
packages/client-sdk/src/feed.ts
Normal file
13
packages/client-sdk/src/feed.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { $OpenApiTs, FeedService } from './open/client';
|
||||
|
||||
export async function sendFeed(
|
||||
channelId: string,
|
||||
payload: $OpenApiTs['/feed/{channelId}/send']['post']['req']['requestBody']
|
||||
) {
|
||||
const res = await FeedService.feedSendEvent({
|
||||
channelId,
|
||||
requestBody: payload,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
@ -2,3 +2,4 @@ export { initOpenapiSDK } from './config';
|
||||
export { openApiClient } from './open';
|
||||
export * from './tracker';
|
||||
export * from './survey';
|
||||
export * from './feed';
|
||||
|
@ -40,14 +40,14 @@ export type OpenAPIConfig = {
|
||||
};
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: '/open',
|
||||
BASE: 'http://localhost:12345/open',
|
||||
CREDENTIALS: 'include',
|
||||
ENCODE_PATH: undefined,
|
||||
HEADERS: undefined,
|
||||
PASSWORD: undefined,
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
VERSION: '1.9.3',
|
||||
VERSION: '1.16.1',
|
||||
WITH_CREDENTIALS: false,
|
||||
interceptors: {
|
||||
request: new Interceptors(),
|
||||
|
@ -69,16 +69,152 @@ export class UserService {
|
||||
}
|
||||
|
||||
export class WorkspaceService {
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static workspaceCreate(data: $OpenApiTs['/workspace//create']['post']['req']): CancelablePromise<$OpenApiTs['/workspace//create']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/workspace//create',
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static workspaceSwitch(data: $OpenApiTs['/workspace//switch']['post']['req']): CancelablePromise<$OpenApiTs['/workspace//switch']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/workspace//switch',
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static workspaceRename(data: $OpenApiTs['/workspace//rename']['patch']['req']): CancelablePromise<$OpenApiTs['/workspace//rename']['patch']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PATCH',
|
||||
url: '/workspace//rename',
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static workspaceGetServiceCount(data: $OpenApiTs['/workspace/{workspaceId}/getServiceCount']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/getServiceCount']['get']['res'][200]> {
|
||||
public static workspaceDelete(data: $OpenApiTs['/workspace//{workspaceId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/del']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace//{workspaceId}/del',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static workspaceMembers(data: $OpenApiTs['/workspace//{workspaceId}/members']['get']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/members']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/getServiceCount',
|
||||
url: '/workspace//{workspaceId}/members',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static workspaceUpdateSettings(data: $OpenApiTs['/workspace//{workspaceId}/updateSettings']['post']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/updateSettings']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/workspace//{workspaceId}/updateSettings',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static workspaceInvite(data: $OpenApiTs['/workspace//{workspaceId}/invite']['post']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/invite']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/workspace//{workspaceId}/invite',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Administrator kicks a user out of a workspace.
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.targetUserId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static workspaceTick(data: $OpenApiTs['/workspace//{workspaceId}/tick']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/tick']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace//{workspaceId}/tick',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
},
|
||||
query: {
|
||||
targetUserId: data.targetUserId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static workspaceGetServiceCount(data: $OpenApiTs['/workspace//{workspaceId}/getServiceCount']['get']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/getServiceCount']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace//{workspaceId}/getServiceCount',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
}
|
||||
@ -123,6 +259,23 @@ export class WebsiteService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @returns number Successful response
|
||||
* @returns unknown Error response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static websiteAllOverview(data: $OpenApiTs['/workspace/{workspaceId}/website/allOverview']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/allOverview']['get']['res'][200] | $OpenApiTs['/workspace/{workspaceId}/website/allOverview']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/website/allOverview',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
@ -324,6 +477,24 @@ export class WebsiteService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.websiteId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static websiteDelete(data: $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/{websiteId}']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace/{workspaceId}/website/{websiteId}',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
websiteId: data.websiteId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
@ -345,6 +516,68 @@ export class WebsiteService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.websiteId
|
||||
* @param data.requestBody
|
||||
* @returns string Successful response
|
||||
* @returns unknown Error response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static websiteGenerateLighthouseReport(data: $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport']['post']['res'][200] | $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
websiteId: data.websiteId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.websiteId
|
||||
* @param data.limit
|
||||
* @param data.cursor
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static websiteGetLighthouseReport(data: $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/getLighthouseReport']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/getLighthouseReport']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/website/{websiteId}/getLighthouseReport',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
websiteId: data.websiteId
|
||||
},
|
||||
query: {
|
||||
limit: data.limit,
|
||||
cursor: data.cursor
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.lighthouseId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static websiteGetLighthouseJson(data: $OpenApiTs['/lighthouse/{lighthouseId}']['get']['req']): CancelablePromise<$OpenApiTs['/lighthouse/{lighthouseId}']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/lighthouse/{lighthouseId}',
|
||||
path: {
|
||||
lighthouseId: data.lighthouseId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class MonitorService {
|
||||
@ -371,28 +604,10 @@ export class MonitorService {
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static monitorGet(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['get']['res'][200]> {
|
||||
public static monitorGet(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/get']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/get']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
monitorId: data.monitorId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.monitorId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static monitorDelete(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}/get',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
monitorId: data.monitorId
|
||||
@ -434,6 +649,24 @@ export class MonitorService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.monitorId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static monitorDelete(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/del']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}/del',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
monitorId: data.monitorId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
@ -501,6 +734,42 @@ export class MonitorService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.monitorId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static monitorPublicSummary(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicSummary']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicSummary']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}/publicSummary',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
monitorId: data.monitorId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.monitorId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static monitorPublicData(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicData']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicData']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}/publicData',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
monitorId: data.monitorId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
@ -944,10 +1213,10 @@ export class SurveyService {
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static surveyGet(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['res'][200]> {
|
||||
public static surveyGet(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/get']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/get']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/survey/{surveyId}',
|
||||
url: '/workspace/{workspaceId}/survey/{surveyId}/get',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
surveyId: data.surveyId
|
||||
@ -996,10 +1265,11 @@ export class SurveyService {
|
||||
* @param data.workspaceId
|
||||
* @param data.surveyId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @returns string Successful response
|
||||
* @returns unknown Error response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static surveySubmit(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/submit']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/submit']['post']['res'][200]> {
|
||||
public static surveySubmit(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/submit']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/submit']['post']['res'][200] | $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/submit']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/workspace/{workspaceId}/survey/{surveyId}/submit',
|
||||
@ -1076,6 +1346,8 @@ export class SurveyService {
|
||||
* @param data.surveyId
|
||||
* @param data.limit
|
||||
* @param data.cursor
|
||||
* @param data.startAt
|
||||
* @param data.endAt
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
@ -1089,7 +1361,9 @@ export class SurveyService {
|
||||
},
|
||||
query: {
|
||||
limit: data.limit,
|
||||
cursor: data.cursor
|
||||
cursor: data.cursor,
|
||||
startAt: data.startAt,
|
||||
endAt: data.endAt
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1143,3 +1417,278 @@ export class BillingService {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class FeedService {
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedChannels(data: $OpenApiTs['/workspace/{workspaceId}/feed/channels']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/channels']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/feed/channels',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.channelId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedChannelInfo(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/info']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/info']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/feed/{channelId}/info',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
channelId: data.channelId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.channelId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedUpdateChannelInfo(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/update']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/update']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/workspace/{workspaceId}/feed/{channelId}/update',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
channelId: data.channelId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch workspace feed channel events
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.channelId
|
||||
* @param data.limit
|
||||
* @param data.cursor
|
||||
* @param data.archived
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedFetchEventsByCursor(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/fetchEventsByCursor']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/fetchEventsByCursor']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/feed/{channelId}/fetchEventsByCursor',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
channelId: data.channelId
|
||||
},
|
||||
query: {
|
||||
limit: data.limit,
|
||||
cursor: data.cursor,
|
||||
archived: data.archived
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedCreateChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/createChannel']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/createChannel']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/workspace/{workspaceId}/feed/createChannel',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.channelId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedDeleteChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/del']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace/{workspaceId}/feed/{channelId}/del',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
channelId: data.channelId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.channelId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedSendEvent(data: $OpenApiTs['/feed/{channelId}/send']['post']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/send']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/feed/{channelId}/send',
|
||||
path: {
|
||||
channelId: data.channelId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.channelId
|
||||
* @param data.eventId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedArchiveEvent(data: $OpenApiTs['/feed/{channelId}/{eventId}/archive']['patch']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/{eventId}/archive']['patch']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PATCH',
|
||||
url: '/feed/{channelId}/{eventId}/archive',
|
||||
path: {
|
||||
channelId: data.channelId,
|
||||
eventId: data.eventId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.channelId
|
||||
* @param data.eventId
|
||||
* @param data.requestBody
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedUnarchiveEvent(data: $OpenApiTs['/feed/{channelId}/{eventId}/unarchive']['patch']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/{eventId}/unarchive']['patch']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PATCH',
|
||||
url: '/feed/{channelId}/{eventId}/unarchive',
|
||||
path: {
|
||||
channelId: data.channelId,
|
||||
eventId: data.eventId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.channelId
|
||||
* @param data.requestBody
|
||||
* @returns number Successful response
|
||||
* @returns unknown Error response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedClearAllArchivedEvents(data: $OpenApiTs['/feed/{channelId}/clearAllArchivedEvents']['patch']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/clearAllArchivedEvents']['patch']['res'][200] | $OpenApiTs['/feed/{channelId}/clearAllArchivedEvents']['patch']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PATCH',
|
||||
url: '/feed/{channelId}/clearAllArchivedEvents',
|
||||
path: {
|
||||
channelId: data.channelId
|
||||
},
|
||||
body: data.requestBody,
|
||||
mediaType: 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* webhook playground
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @returns string Successful response
|
||||
* @returns unknown Error response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedIntegrationPlayground(data: $OpenApiTs['/feed/playground/{workspaceId}']['post']['req']): CancelablePromise<$OpenApiTs['/feed/playground/{workspaceId}']['post']['res'][200] | $OpenApiTs['/feed/playground/{workspaceId}']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/feed/playground/{workspaceId}',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* integrate with github webhook
|
||||
* @param data The data for the request.
|
||||
* @param data.channelId
|
||||
* @returns string Successful response
|
||||
* @returns unknown Error response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedIntegrationGithub(data: $OpenApiTs['/feed/{channelId}/github']['post']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/github']['post']['res'][200] | $OpenApiTs['/feed/{channelId}/github']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/feed/{channelId}/github',
|
||||
path: {
|
||||
channelId: data.channelId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* integrate with tencent-cloud webhook
|
||||
* @param data The data for the request.
|
||||
* @param data.channelId
|
||||
* @returns string Successful response
|
||||
* @returns unknown Error response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedIntegrationTencentCloudAlarm(data: $OpenApiTs['/feed/{channelId}/tencent-cloud/alarm']['post']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/tencent-cloud/alarm']['post']['res'][200] | $OpenApiTs['/feed/{channelId}/tencent-cloud/alarm']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/feed/{channelId}/tencent-cloud/alarm',
|
||||
path: {
|
||||
channelId: data.channelId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* integrate with sentry webhook
|
||||
* @param data The data for the request.
|
||||
* @param data.channelId
|
||||
* @returns string Successful response
|
||||
* @returns unknown Error response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedIntegrationSentry(data: $OpenApiTs['/feed/{channelId}/sentry']['post']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/sentry']['post']['res'][200] | $OpenApiTs['/feed/{channelId}/sentry']['post']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/feed/{channelId}/sentry',
|
||||
path: {
|
||||
channelId: data.channelId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"module": "CommonJS",
|
||||
"outDir": "./lib",
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tianji-client-react",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
} from 'tianji-client-sdk';
|
||||
|
||||
type SurveyInfo =
|
||||
openApiClient.$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['res']['200'];
|
||||
openApiClient.$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/get']['get']['res']['200'];
|
||||
|
||||
interface UseTianjiSurveyOptions {
|
||||
baseUrl?: string;
|
||||
|
30
patches/zod-prisma@0.5.4.patch
Normal file
30
patches/zod-prisma@0.5.4.patch
Normal file
File diff suppressed because one or more lines are too long
31791
pnpm-lock.yaml
31791
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -59,7 +59,10 @@ func main() {
|
||||
|
||||
interval := *Interval
|
||||
|
||||
ticker := time.Tick(time.Duration(interval) * time.Second)
|
||||
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
httpClient := &http.Client{}
|
||||
|
||||
log.Println("Start reporting...")
|
||||
log.Println("Mode:", *Mode)
|
||||
@ -71,17 +74,17 @@ func main() {
|
||||
WorkspaceId: *WorkspaceId,
|
||||
Name: name,
|
||||
Hostname: hostname,
|
||||
Timeout: interval * 2,
|
||||
Timeout: interval * 5,
|
||||
Payload: utils.GetReportDataPaylod(interval, *IsVnstat),
|
||||
}
|
||||
|
||||
if *Mode == "udp" {
|
||||
sendUDPPack(*parsedURL, payload)
|
||||
} else {
|
||||
sendHTTPRequest(*parsedURL, payload)
|
||||
sendHTTPRequest(*parsedURL, payload, httpClient)
|
||||
}
|
||||
|
||||
<-ticker
|
||||
<-ticker.C
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,7 +128,7 @@ func sendUDPPack(url url.URL, payload ReportData) {
|
||||
/**
|
||||
* Send HTTP Request to report server data
|
||||
*/
|
||||
func sendHTTPRequest(_url url.URL, payload ReportData) {
|
||||
func sendHTTPRequest(_url url.URL, payload ReportData, client *http.Client) {
|
||||
jsonData, err := jsoniter.Marshal(payload)
|
||||
if err != nil {
|
||||
log.Println("Error encoding JSON:", err)
|
||||
@ -148,7 +151,6 @@ func sendHTTPRequest(_url url.URL, payload ReportData) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-tianji-report-version", version)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Println("Send request error:", err)
|
||||
|
@ -1,4 +1,4 @@
|
||||
require('dotenv').config();
|
||||
import 'dotenv/config';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import https from 'https';
|
||||
@ -20,7 +20,7 @@ if (process.env.MAXMIND_LICENSE_KEY) {
|
||||
`?edition_id=${db}&license_key=${process.env.MAXMIND_LICENSE_KEY}&suffix=tar.gz`;
|
||||
}
|
||||
|
||||
const dest = path.resolve(__dirname, '../geo');
|
||||
const dest = path.resolve(process.cwd(), './geo');
|
||||
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.mkdirSync(dest);
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { trpcOpenapiDocument } from '../src/server/trpc';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const target = path.resolve(__dirname, '../website/openapi.json');
|
||||
fs.writeJSON(target, trpcOpenapiDocument)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { resolve } from 'path';
|
||||
import vite from 'vite';
|
||||
import * as vite from 'vite';
|
||||
|
||||
console.log('Start Build Tracker');
|
||||
|
||||
@ -7,13 +7,13 @@ vite
|
||||
.build({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, '../src/tracker/index.js'),
|
||||
entry: resolve(process.cwd(), './src/tracker/index.js'),
|
||||
name: 'tianji',
|
||||
fileName: () => 'tracker.js',
|
||||
formats: ['iife'],
|
||||
},
|
||||
emptyOutDir: false,
|
||||
outDir: resolve(__dirname, '../src/client/public'),
|
||||
outDir: resolve(process.cwd(), './src/client/public'),
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
|
@ -43,7 +43,7 @@ const AppRouter: React.FC = React.memo(() => {
|
||||
<RouterProvider router={router} context={{ userInfo }} />
|
||||
</TooltipProvider>
|
||||
|
||||
<Toaster />
|
||||
<Toaster position="top-center" />
|
||||
</BrowserRouter>
|
||||
);
|
||||
});
|
||||
|
@ -1,15 +1,44 @@
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
const TOKEN_STORAGE_KEY = 'jsonwebtoken';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function getJWT(): string | null {
|
||||
const token = window.localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
|
||||
return token ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function setJWT(jwt: string) {
|
||||
window.localStorage.setItem(TOKEN_STORAGE_KEY, jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function clearJWT() {
|
||||
window.localStorage.removeItem(TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
export async function getSession(): Promise<{
|
||||
user: {
|
||||
email?: string;
|
||||
};
|
||||
expires: string;
|
||||
} | null> {
|
||||
const { data } = await axios.get('/api/auth/session');
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
186
src/client/api/authjs/lib.tsx
Normal file
186
src/client/api/authjs/lib.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
/**
|
||||
* This file is fork from next-auth/react
|
||||
*/
|
||||
|
||||
import type {
|
||||
ClientSafeProvider,
|
||||
LiteralUnion,
|
||||
SignInAuthorizationParams,
|
||||
SignInOptions,
|
||||
SignInResponse,
|
||||
SignOutParams,
|
||||
SignOutResponse,
|
||||
} from './types';
|
||||
import type {
|
||||
BuiltInProviderType,
|
||||
RedirectableProviderType,
|
||||
} from '@auth/core/providers';
|
||||
import { Session } from '@auth/core/types';
|
||||
import axios from 'axios';
|
||||
|
||||
export * from './types';
|
||||
|
||||
type UpdateSession = (data?: any) => Promise<Session | null>;
|
||||
|
||||
export type SessionContextValue<R extends boolean = false> = R extends true
|
||||
?
|
||||
| { update: UpdateSession; data: Session; status: 'authenticated' }
|
||||
| { update: UpdateSession; data: null; status: 'loading' }
|
||||
:
|
||||
| { update: UpdateSession; data: Session; status: 'authenticated' }
|
||||
| {
|
||||
update: UpdateSession;
|
||||
data: null;
|
||||
status: 'unauthenticated' | 'loading';
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current Cross Site Request Forgery Token (CSRF Token)
|
||||
* required to make POST requests (e.g. for signing in and signing out).
|
||||
* You likely only need to use this if you are not using the built-in
|
||||
* `signIn()` and `signOut()` methods.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#getcsrftoken)
|
||||
*/
|
||||
export async function getCsrfToken() {
|
||||
const { data } = await axios.get<{ csrfToken: string }>('/api/auth/csrf');
|
||||
|
||||
return data.csrfToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* It calls `/api/auth/providers` and returns
|
||||
* a list of the currently configured authentication providers.
|
||||
* It can be useful if you are creating a dynamic custom sign in page.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#getproviders)
|
||||
*/
|
||||
export async function getProviders() {
|
||||
const { data } = await axios.get<
|
||||
Record<LiteralUnion<BuiltInProviderType>, ClientSafeProvider>
|
||||
>('/api/auth/providers');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side method to initiate a signin flow
|
||||
* or send the user to the signin page listing all possible providers.
|
||||
* Automatically adds the CSRF token to the request.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#signin)
|
||||
*/
|
||||
export async function signIn<
|
||||
P extends RedirectableProviderType | undefined = undefined,
|
||||
>(
|
||||
provider?: LiteralUnion<
|
||||
P extends RedirectableProviderType
|
||||
? P | BuiltInProviderType
|
||||
: BuiltInProviderType
|
||||
>,
|
||||
options?: SignInOptions,
|
||||
authorizationParams?: SignInAuthorizationParams
|
||||
): Promise<
|
||||
P extends RedirectableProviderType ? SignInResponse | undefined : undefined
|
||||
> {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options ?? {};
|
||||
|
||||
const baseUrl = '/api/auth';
|
||||
const providers = await getProviders();
|
||||
|
||||
if (!providers) {
|
||||
window.location.href = `${baseUrl}/error`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!provider || !(provider in providers)) {
|
||||
window.location.href = `${baseUrl}/signin?${new URLSearchParams({
|
||||
callbackUrl,
|
||||
})}`;
|
||||
return;
|
||||
}
|
||||
|
||||
const isCredentials = providers[provider].type === 'credentials';
|
||||
const isEmail = providers[provider].type === 'email';
|
||||
const isSupportingReturn = isCredentials || isEmail;
|
||||
|
||||
const signInUrl = `${baseUrl}/${
|
||||
isCredentials ? 'callback' : 'signin'
|
||||
}/${provider}`;
|
||||
|
||||
const _signInUrl = `${signInUrl}${authorizationParams ? `?${new URLSearchParams(authorizationParams)}` : ''}`;
|
||||
|
||||
const res = await fetch(_signInUrl, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Auth-Return-Redirect': '1',
|
||||
},
|
||||
// @ts-expect-error
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// TODO: Do not redirect for Credentials and Email providers by default in next major
|
||||
if (redirect || !isSupportingReturn) {
|
||||
const url = data.url ?? callbackUrl;
|
||||
window.location.href = url;
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const error = new URL(data.url).searchParams.get('error');
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url,
|
||||
} as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the user out, by removing the session cookie.
|
||||
* Automatically adds the CSRF token to the request.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#signout)
|
||||
*/
|
||||
export async function signOut<R extends boolean = true>(
|
||||
options?: SignOutParams<R>
|
||||
): Promise<R extends true ? undefined : SignOutResponse> {
|
||||
const { callbackUrl = window.location.href } = options ?? {};
|
||||
const baseUrl = '/api/auth';
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Auth-Return-Redirect': '1',
|
||||
},
|
||||
// @ts-expect-error
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true,
|
||||
}),
|
||||
};
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions);
|
||||
const data = await res.json();
|
||||
|
||||
if (options?.redirect ?? true) {
|
||||
const url = data.url ?? callbackUrl;
|
||||
window.location.href = url;
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload();
|
||||
// @ts-expect-error
|
||||
return;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
55
src/client/api/authjs/types.ts
Normal file
55
src/client/api/authjs/types.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { Session } from '@auth/core/types';
|
||||
import type { BuiltInProviderType, ProviderType } from '@auth/core/providers';
|
||||
|
||||
/**
|
||||
* Util type that matches some strings literally, but allows any other string as well.
|
||||
* @source https://github.com/microsoft/TypeScript/issues/29729#issuecomment-832522611
|
||||
*/
|
||||
export type LiteralUnion<T extends U, U = string> =
|
||||
| T
|
||||
| (U & Record<never, never>);
|
||||
|
||||
export interface ClientSafeProvider {
|
||||
id: LiteralUnion<BuiltInProviderType>;
|
||||
name: string;
|
||||
type: ProviderType;
|
||||
signinUrl: string;
|
||||
callbackUrl: string;
|
||||
}
|
||||
|
||||
export interface SignInOptions extends Record<string, unknown> {
|
||||
/**
|
||||
* Specify to which URL the user will be redirected after signing in. Defaults to the page URL the sign-in is initiated from.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl)
|
||||
*/
|
||||
callbackUrl?: string;
|
||||
/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option) */
|
||||
redirect?: boolean;
|
||||
}
|
||||
|
||||
export interface SignInResponse {
|
||||
error: string | null;
|
||||
status: number;
|
||||
ok: boolean;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
/** Match `inputType` of `new URLSearchParams(inputType)` */
|
||||
export type SignInAuthorizationParams =
|
||||
| string
|
||||
| string[][]
|
||||
| Record<string, string>
|
||||
| URLSearchParams;
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1) */
|
||||
export interface SignOutResponse {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface SignOutParams<R extends boolean = true> {
|
||||
/** [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl-1) */
|
||||
callbackUrl?: string;
|
||||
/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1 */
|
||||
redirect?: R;
|
||||
}
|
84
src/client/api/authjs/useAuth.ts
Normal file
84
src/client/api/authjs/useAuth.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { signIn, SignInResponse, signOut } from './lib';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { toast } from 'sonner';
|
||||
import { trpc } from '../trpc';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { BuiltInProviderType } from '@auth/core/providers';
|
||||
|
||||
export function useAuth() {
|
||||
const trpcUtils = trpc.useUtils();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const loginWithPassword = useEvent(
|
||||
async (username: string, password: string) => {
|
||||
let res: SignInResponse | undefined;
|
||||
try {
|
||||
res = await signIn('account', {
|
||||
username,
|
||||
password,
|
||||
redirect: false,
|
||||
});
|
||||
} catch (err) {
|
||||
toast.error(t('Login failed'));
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res?.error) {
|
||||
toast.error(t('Login failed, please check your username and password'));
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
|
||||
const userInfo = await trpcUtils.user.info.fetch();
|
||||
if (!userInfo) {
|
||||
toast.error(t('Can not get current user info'));
|
||||
throw new Error('Login failed, ');
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
);
|
||||
|
||||
const loginWithOAuth = useEvent(
|
||||
async (provider: BuiltInProviderType | 'custom') => {
|
||||
let res: SignInResponse | undefined;
|
||||
try {
|
||||
res = await signIn(provider, {
|
||||
redirect: false,
|
||||
});
|
||||
console.log('res', res);
|
||||
} catch (err) {
|
||||
toast.error(t('Login failed'));
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res?.error) {
|
||||
toast.error(t('Login failed'));
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
|
||||
const userInfo = await trpcUtils.user.info.fetch();
|
||||
if (!userInfo) {
|
||||
toast.error(t('Can not get current user info'));
|
||||
throw new Error('Login failed, ');
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
);
|
||||
|
||||
const logout = useEvent(async () => {
|
||||
await signOut({
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
useUserStore.setState({ info: null });
|
||||
window.location.href = '/login'; // not good, need to invest to find better way.
|
||||
});
|
||||
|
||||
return {
|
||||
loginWithPassword,
|
||||
loginWithOAuth,
|
||||
logout,
|
||||
};
|
||||
}
|
@ -1,7 +1,4 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { useUserStore } from '../../store/user';
|
||||
import { useEvent } from '../../hooks/useEvent';
|
||||
import { clearJWT } from '../auth';
|
||||
|
||||
/**
|
||||
* Mock
|
||||
@ -10,14 +7,3 @@ import { clearJWT } from '../auth';
|
||||
export function getUserTimezone(): string {
|
||||
return dayjs.tz.guess() ?? 'utc';
|
||||
}
|
||||
|
||||
export function useLogout() {
|
||||
const logout = useEvent(() => {
|
||||
window.location.href = '/login'; // not good, need to invest to find better way.
|
||||
|
||||
useUserStore.setState({ info: null });
|
||||
clearJWT();
|
||||
});
|
||||
|
||||
return logout;
|
||||
}
|
||||
|
@ -4,15 +4,6 @@ import { AppRouterOutput } from '../trpc';
|
||||
|
||||
export type WebsiteInfo = NonNullable<AppRouterOutput['website']['info']>;
|
||||
|
||||
export async function deleteWorkspaceWebsite(
|
||||
workspaceId: string,
|
||||
websiteId: string
|
||||
) {
|
||||
await request.delete(`/api/workspace/${workspaceId}/website/${websiteId}`);
|
||||
|
||||
queryClient.resetQueries(['websites', workspaceId]);
|
||||
}
|
||||
|
||||
export function refreshWorkspaceWebsites(workspaceId: string) {
|
||||
queryClient.refetchQueries(['websites', workspaceId]);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { message } from 'antd';
|
||||
import axios from 'axios';
|
||||
import { get } from 'lodash-es';
|
||||
import { getJWT } from './auth';
|
||||
|
||||
class RequestError extends Error {}
|
||||
|
||||
@ -9,10 +8,6 @@ function createRequest() {
|
||||
const ins = axios.create();
|
||||
|
||||
ins.interceptors.request.use(async (val) => {
|
||||
if (!val.headers.Authorization) {
|
||||
val.headers.Authorization = `Bearer ${getJWT()}`;
|
||||
}
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { getJWT } from './auth';
|
||||
import type { SubscribeEventMap, SocketEventMap } from '../../server/ws/shared';
|
||||
import { create } from 'zustand';
|
||||
import { useEvent } from '../hooks/useEvent';
|
||||
@ -8,21 +7,42 @@ import { useIsLogined } from '../store/user';
|
||||
|
||||
const useSocketStore = create<{
|
||||
socket: Socket | null;
|
||||
connected: boolean;
|
||||
}>(() => ({
|
||||
socket: null,
|
||||
connected: false,
|
||||
}));
|
||||
|
||||
export function createSocketIOClient(workspaceId: string) {
|
||||
const token = getJWT();
|
||||
const prev = useSocketStore.getState().socket;
|
||||
if (prev) {
|
||||
prev.disconnect();
|
||||
}
|
||||
|
||||
const socket = io(`/${workspaceId}`, {
|
||||
transports: ['websocket'],
|
||||
reconnectionDelayMax: 10000,
|
||||
auth: {
|
||||
token,
|
||||
},
|
||||
forceNew: true,
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
useSocketStore.setState({
|
||||
connected: true,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
useSocketStore.setState({
|
||||
connected: false,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('connect_error', () => {
|
||||
useSocketStore.setState({
|
||||
connected: false,
|
||||
});
|
||||
});
|
||||
|
||||
useSocketStore.setState({
|
||||
socket,
|
||||
});
|
||||
@ -89,7 +109,24 @@ export function useSocket() {
|
||||
return { socket, emit, subscribe };
|
||||
}
|
||||
|
||||
export function useSocketSubscribe<T>(
|
||||
export function useSocketSubscribe<K extends keyof SubscribeEventMap>(
|
||||
name: K,
|
||||
cb: (data: SubscribeEventData<K>) => void
|
||||
) {
|
||||
const { subscribe } = useSocket();
|
||||
|
||||
const fn = useEvent(cb);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribe(name, fn);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [name]);
|
||||
}
|
||||
|
||||
export function useSocketSubscribeData<T>(
|
||||
name: keyof SubscribeEventMap,
|
||||
defaultData: T
|
||||
): T {
|
||||
@ -111,6 +148,10 @@ export function useSocketSubscribe<T>(
|
||||
return data;
|
||||
}
|
||||
|
||||
export function useSocketConnected() {
|
||||
return useSocketStore((state) => state.connected);
|
||||
}
|
||||
|
||||
interface UseSocketSubscribeListOptions<K, T> {
|
||||
filter?: (data: T) => boolean;
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
splitLink,
|
||||
TRPCClientErrorLike,
|
||||
} from '@trpc/client';
|
||||
import { getJWT } from './auth';
|
||||
import { message } from 'antd';
|
||||
import { isDev } from '../utils/env';
|
||||
|
||||
@ -22,9 +21,7 @@ export type AppRouterOutput = inferRouterOutputs<AppRouter>;
|
||||
const url = '/trpc';
|
||||
|
||||
function headers() {
|
||||
return {
|
||||
Authorization: `Bearer ${getJWT()}`,
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
export const trpcClient = trpc.createClient({
|
||||
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
Before Width: | Height: | Size: 4.0 KiB |
@ -40,11 +40,13 @@ export const AlertConfirm: React.FC<AlertConfirmProps> = React.memo((props) => {
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={props.onCancel}>
|
||||
{t('Cancel')}
|
||||
{props.onConfirm ? t('Cancel') : t('Continue')}
|
||||
</AlertDialogCancel>
|
||||
{props.onConfirm && (
|
||||
<AlertDialogAction onClick={props.onConfirm}>
|
||||
{t('Confirm')}
|
||||
</AlertDialogAction>
|
||||
)}
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
export const Code: React.FC<React.PropsWithChildren> = React.memo((props) => {
|
||||
return (
|
||||
<span className="rounded-sm border border-zinc-800 bg-zinc-900 px-1 py-0.5">
|
||||
<span className="rounded-sm border border-zinc-200 bg-zinc-100 px-1 py-0.5 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
|
@ -7,6 +7,7 @@ import { useTranslation } from '@i18next-toolkit/react';
|
||||
|
||||
export const CodeBlock: React.FC<{
|
||||
code: string;
|
||||
height?: number;
|
||||
}> = React.memo((props) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
@ -20,8 +21,13 @@ export const CodeBlock: React.FC<{
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="group relative overflow-auto">
|
||||
<pre className="rounded-sm border border-zinc-800 bg-zinc-900 p-3 pr-12 text-sm">
|
||||
<div className="group relative w-full overflow-auto">
|
||||
<pre
|
||||
className="rounded-sm border border-zinc-200 bg-zinc-100 p-3 pr-12 text-sm dark:border-zinc-800 dark:bg-zinc-900"
|
||||
style={{
|
||||
maxHeight: props.height || 384,
|
||||
}}
|
||||
>
|
||||
<code>{props.code}</code>
|
||||
</pre>
|
||||
<Button
|
||||
|
37
src/client/components/CodeExample.tsx
Normal file
37
src/client/components/CodeExample.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
|
||||
interface CodeExampleItem {
|
||||
label: string;
|
||||
code?: string;
|
||||
element?: React.ReactNode;
|
||||
}
|
||||
interface CodeExampleProps {
|
||||
className?: string;
|
||||
example: Record<string, CodeExampleItem>;
|
||||
}
|
||||
export const CodeExample: React.FC<CodeExampleProps> = React.memo((props) => {
|
||||
const keys = Object.keys(props.example);
|
||||
|
||||
return (
|
||||
<Tabs className={props.className} defaultValue={keys[0]}>
|
||||
<TabsList>
|
||||
{keys.map((key) => (
|
||||
<TabsTrigger key={key} value={key}>
|
||||
{props.example[key].label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{keys.map((key) => (
|
||||
<TabsContent key={key} value={key}>
|
||||
{props.example[key].element ?? (
|
||||
<CodeBlock code={props.example[key].code ?? ''} />
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
});
|
||||
CodeExample.displayName = 'CodeExample';
|
@ -10,9 +10,12 @@ import {
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command';
|
||||
import {
|
||||
LuActivitySquare,
|
||||
LuAreaChart,
|
||||
LuBellDot,
|
||||
LuFilePieChart,
|
||||
LuKanbanSquare,
|
||||
LuKeyRound,
|
||||
LuMonitorDot,
|
||||
LuSearch,
|
||||
LuServer,
|
||||
@ -143,6 +146,14 @@ export const CommandPanel: React.FC<CommandPanelProps> = React.memo((props) => {
|
||||
<RiSurveyLine className="mr-2 h-4 w-4" />
|
||||
{t('Survey')}
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={handleJump({
|
||||
to: '/feed',
|
||||
})}
|
||||
>
|
||||
<LuActivitySquare className="mr-2 h-4 w-4" />
|
||||
{t('Feed')}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading={t('Settings')}>
|
||||
@ -162,6 +173,22 @@ export const CommandPanel: React.FC<CommandPanelProps> = React.memo((props) => {
|
||||
<LuBellDot className="mr-2 h-4 w-4" />
|
||||
<span>{t('Notifications')}</span>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={handleJump({
|
||||
to: '/settings/apiKey',
|
||||
})}
|
||||
>
|
||||
<LuKeyRound className="mr-2 h-4 w-4" />
|
||||
<span>{t('Api Key')}</span>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={handleJump({
|
||||
to: '/settings/usage',
|
||||
})}
|
||||
>
|
||||
<LuKanbanSquare className="mr-2 h-4 w-4" />
|
||||
<span>{t('Usage')}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
@ -195,6 +222,9 @@ export const CommandPanelSearchGroup: React.FC<CommandPanelSearchGroupProps> =
|
||||
const { data: surveys = [] } = trpc.survey.all.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
const { data: feedChannels = [] } = trpc.feed.channels.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!search) {
|
||||
return null;
|
||||
@ -282,6 +312,22 @@ export const CommandPanelSearchGroup: React.FC<CommandPanelSearchGroupProps> =
|
||||
{s.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
{feedChannels.map((channel) => (
|
||||
<CommandItem
|
||||
key={channel.id}
|
||||
value={channel.id}
|
||||
keywords={[channel.name, channel.id]}
|
||||
onSelect={handleJump({
|
||||
to: '/feed/$channelId',
|
||||
params: {
|
||||
channelId: channel.id,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<LuActivitySquare className="mr-2 h-4 w-4" />
|
||||
{channel.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
);
|
||||
});
|
||||
|
@ -10,8 +10,10 @@ interface CommonHeaderProps {
|
||||
export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => {
|
||||
return (
|
||||
<div className="flex w-full items-center">
|
||||
<div className="flex flex-1 items-center">
|
||||
<h1 className="text-xl font-bold">{props.title}</h1>
|
||||
<div className="flex flex-1 flex-shrink items-center overflow-hidden">
|
||||
<h1 className="overflow-hidden text-ellipsis whitespace-nowrap text-xl font-bold">
|
||||
{props.title}
|
||||
</h1>
|
||||
|
||||
{props.desc && (
|
||||
<span className="text-muted-foreground ml-2 self-end text-sm">
|
||||
|
@ -9,6 +9,7 @@ import { useFuseSearch } from '@/hooks/useFuseSearch';
|
||||
import { Empty } from 'antd';
|
||||
import { globalEventBus } from '@/utils/event';
|
||||
import { Spinner } from './ui/spinner';
|
||||
import { formatNumber } from '@/utils/common';
|
||||
|
||||
export interface CommonListItem {
|
||||
id: string;
|
||||
@ -23,6 +24,7 @@ interface CommonListProps {
|
||||
isLoading?: boolean;
|
||||
hasSearch?: boolean;
|
||||
items: CommonListItem[];
|
||||
emptyDescription?: React.ReactNode;
|
||||
}
|
||||
export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
||||
const { location } = useRouterState();
|
||||
@ -76,7 +78,9 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{finalList.length === 0 && !props.isLoading && <Empty />}
|
||||
{finalList.length === 0 && !props.isLoading && (
|
||||
<Empty description={props.emptyDescription} />
|
||||
)}
|
||||
|
||||
{finalList.map((item) => {
|
||||
const isSelected = item.href === location.pathname;
|
||||
@ -85,8 +89,9 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
||||
<button
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'hover:bg-accent flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all',
|
||||
isSelected && 'bg-muted'
|
||||
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all',
|
||||
'hover:bg-gray-50 dark:hover:bg-gray-900',
|
||||
isSelected && 'bg-gray-50 dark:bg-gray-900'
|
||||
)}
|
||||
onClick={() => {
|
||||
globalEventBus.emit('commonListSelected');
|
||||
@ -96,10 +101,14 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-1">
|
||||
<div className="font-semibold">{item.title}</div>
|
||||
<div className="overflow-hidden text-ellipsis font-semibold">
|
||||
{item.title}
|
||||
</div>
|
||||
|
||||
{item.number && item.number > 0 && (
|
||||
<span className="opacity-60">{item.number}</span>
|
||||
<span className="opacity-60" title={String(item.number)}>
|
||||
{formatNumber(item.number)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
32
src/client/components/CopyableText.tsx
Normal file
32
src/client/components/CopyableText.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { cn } from '@/utils/style';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
|
||||
interface CopyableTextProps extends PropsWithChildren {
|
||||
className?: string;
|
||||
text: string;
|
||||
}
|
||||
export const CopyableText: React.FC<CopyableTextProps> = React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
const handleClick = useEvent(() => {
|
||||
copy(props.text);
|
||||
toast.success(t('Copied'));
|
||||
});
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'cursor-pointer select-none rounded bg-white bg-opacity-10 px-2',
|
||||
'hover:bg-white hover:bg-opacity-20',
|
||||
props.className
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{props.children ?? props.text}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
CopyableText.displayName = 'CopyableText';
|
@ -75,7 +75,7 @@ export function DataTable<TData>({
|
||||
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
<TableHead key={header.id} className="text-nowrap">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
@ -112,7 +112,7 @@ export function DataTable<TData>({
|
||||
)}
|
||||
|
||||
{row.getVisibleCells().map((cell, i) => (
|
||||
<TableCell key={cell.id}>
|
||||
<TableCell key={cell.id} className="text-nowrap">
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
@ -148,3 +148,4 @@ export function DataTable<TData>({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
DataTable.displayName = 'DataTable';
|
||||
|
@ -27,6 +27,15 @@ export const DateFilter: React.FC<DateFilterProps> = React.memo((props) => {
|
||||
setShowDropdown(false);
|
||||
},
|
||||
items: compact([
|
||||
{
|
||||
label: t('Realtime'),
|
||||
onClick: () => {
|
||||
useGlobalStateStore.setState({ dateRange: DateRange.Realtime });
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
label: t('Today'),
|
||||
onClick: () => {
|
||||
|
@ -9,7 +9,7 @@ export const DefaultError: React.FC = React.memo(() => {
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="min-w-[320px] bg-zinc-900">
|
||||
<Card className="min-w-[320px] bg-zinc-50 dark:bg-zinc-900">
|
||||
<CardHeader>
|
||||
<div className="text-center">
|
||||
<img className="m-auto h-24 w-24" src="/icon.svg" />
|
||||
|
@ -13,7 +13,7 @@ export const DefaultNotFound: React.FC = React.memo(() => {
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="min-w-[320px] bg-zinc-900">
|
||||
<Card className="min-w-[320px] bg-zinc-50 dark:bg-zinc-900">
|
||||
<CardHeader>
|
||||
<div className="text-center">
|
||||
<img className="m-auto h-24 w-24" src="/icon.svg" />
|
||||
|
34
src/client/components/DeprecatedBadge.tsx
Normal file
34
src/client/components/DeprecatedBadge.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
|
||||
interface DeprecatedBadgeProps {
|
||||
tip?: string;
|
||||
}
|
||||
|
||||
export const DeprecatedBadge: React.FC<DeprecatedBadgeProps> = React.memo(
|
||||
(props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const el = (
|
||||
<Badge className="mx-1 px-1 py-0.5 text-xs" variant="secondary">
|
||||
{t('Deprecated')}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
if (!props.tip) {
|
||||
return el;
|
||||
} else {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{el}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{props.tip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
DeprecatedBadge.displayName = 'DeprecatedBadge';
|
11
src/client/components/DevContainer.tsx
Normal file
11
src/client/components/DevContainer.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { isDev } from '@/utils/env';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
export const DevContainer: React.FC<PropsWithChildren> = React.memo((props) => {
|
||||
if (isDev) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
DevContainer.displayName = 'DevContainer';
|
31
src/client/components/DialogWrapper.tsx
Normal file
31
src/client/components/DialogWrapper.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './ui/dialog';
|
||||
|
||||
interface DialogProps extends React.PropsWithChildren {
|
||||
title?: string;
|
||||
description?: string;
|
||||
content?: React.ReactNode;
|
||||
}
|
||||
export const DialogWrapper: React.FC<DialogProps> = React.memo((props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{props.children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogDescription>{props.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{props.content}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
DialogWrapper.displayName = 'DialogWrapper';
|
106
src/client/components/DynamicVirtualList.tsx
Normal file
106
src/client/components/DynamicVirtualList.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import { useWatch } from '@/hooks/useWatch';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { Empty } from 'antd';
|
||||
import { last } from 'lodash-es';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
interface VirtualListProps<T = any> {
|
||||
allData: T[];
|
||||
hasNextPage: boolean | undefined;
|
||||
isFetchingNextPage: boolean;
|
||||
onFetchNextPage: () => void;
|
||||
estimateSize: number;
|
||||
renderItem: (item: T) => React.ReactElement;
|
||||
getItemKey?: (index: number) => string | number;
|
||||
renderEmpty?: () => React.ReactElement;
|
||||
}
|
||||
export const DynamicVirtualList: React.FC<VirtualListProps> = React.memo(
|
||||
(props) => {
|
||||
const {
|
||||
allData,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
onFetchNextPage,
|
||||
estimateSize,
|
||||
getItemKey,
|
||||
renderItem,
|
||||
renderEmpty,
|
||||
} = props;
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: hasNextPage ? allData.length + 1 : allData.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => estimateSize,
|
||||
overscan: 5,
|
||||
getItemKey,
|
||||
});
|
||||
|
||||
const virtualItems = rowVirtualizer.getVirtualItems();
|
||||
|
||||
useWatch([virtualItems], () => {
|
||||
const lastItem = last(virtualItems);
|
||||
|
||||
if (!lastItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
lastItem.index >= allData.length - 1 &&
|
||||
hasNextPage &&
|
||||
!isFetchingNextPage
|
||||
) {
|
||||
onFetchNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="h-full w-full overflow-y-auto"
|
||||
style={{
|
||||
contain: 'strict',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: rowVirtualizer.getTotalSize(),
|
||||
}}
|
||||
>
|
||||
{virtualItems.length === 0 &&
|
||||
(renderEmpty ? renderEmpty() : <Empty />)}
|
||||
|
||||
<div
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,
|
||||
}}
|
||||
>
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const isLoaderRow = virtualRow.index > allData.length - 1;
|
||||
const data = allData[virtualRow.index];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
data-index={virtualRow.index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
>
|
||||
{isLoaderRow
|
||||
? hasNextPage
|
||||
? t('Loading more...')
|
||||
: t('Nothing more to load')
|
||||
: renderItem(data)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
DynamicVirtualList.displayName = 'DynamicVirtualList';
|
@ -1,38 +1,46 @@
|
||||
import { Input } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { useWatch } from '../hooks/useWatch';
|
||||
import { Input } from './ui/input';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { cn } from '@/utils/style';
|
||||
|
||||
interface EditableTextProps {
|
||||
className?: string;
|
||||
enable?: boolean;
|
||||
defaultValue: string;
|
||||
onSave: (text: string) => void;
|
||||
}
|
||||
export const EditableText: React.FC<EditableTextProps> = React.memo((props) => {
|
||||
const [text, setText] = useState(props.defaultValue);
|
||||
const enable = props.enable ?? true;
|
||||
const [editing, setEditing] = useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
useWatch([props.defaultValue], () => {
|
||||
setText(props.defaultValue);
|
||||
});
|
||||
|
||||
const handleClick = useEvent(() => {
|
||||
setEditing(true);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{enable ? (
|
||||
<div className={cn('cursor-text', props.className)}>
|
||||
{editing ? (
|
||||
<Input
|
||||
className={clsx(
|
||||
props.className,
|
||||
'rounded-none border-0 p-0 !shadow-none outline-0'
|
||||
)}
|
||||
ref={inputRef}
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
className="h-[1.5em] border-none p-0 text-base shadow-none focus-visible:ring-0"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onBlur={(e) => props.onSave(e.target.value)}
|
||||
onBlur={() => {
|
||||
setEditing(false);
|
||||
props.onSave(text);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className={props.className}>{text}</span>
|
||||
<span onClick={handleClick}>{text}</span>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
EditableText.displayName = 'EditableText';
|
||||
|
@ -1,28 +1,47 @@
|
||||
import { useResizeObserver } from '@/hooks/useResizeObserver';
|
||||
import { getStatusBgColorClassName, HealthStatus } from '@/utils/health';
|
||||
import { cn } from '@/utils/style';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
type HealthStatus = 'health' | 'error' | 'warning' | 'none';
|
||||
|
||||
export interface HealthBarBeat {
|
||||
title?: string;
|
||||
status: HealthStatus;
|
||||
}
|
||||
|
||||
export interface HealthBarProps {
|
||||
className?: string;
|
||||
size?: 'small' | 'large';
|
||||
beats: HealthBarBeat[];
|
||||
}
|
||||
export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
|
||||
const size = props.size ?? 'small';
|
||||
const [containerRef, containerRect] = useResizeObserver();
|
||||
|
||||
const cellCount = props.beats.length;
|
||||
const cellNeedWidth = size === 'small' ? 8 : 12; // include gap
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('flex', {
|
||||
'gap-[3px]': size === 'small',
|
||||
'gap-1': size === 'large',
|
||||
})}
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
'flex',
|
||||
{
|
||||
'gap-[3px] px-0.5 py-1.5': size === 'small',
|
||||
'gap-1 px-0.5 py-2': size === 'large',
|
||||
},
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{props.beats.map((beat, i) => (
|
||||
{props.beats
|
||||
.slice(
|
||||
Math.floor(
|
||||
Math.max(cellNeedWidth * cellCount - containerRect.width, 0) /
|
||||
cellNeedWidth
|
||||
),
|
||||
cellCount
|
||||
)
|
||||
.map((beat, i) => (
|
||||
<div
|
||||
key={i}
|
||||
title={beat.title}
|
||||
@ -32,12 +51,7 @@ export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
|
||||
'h-4 w-[5px]': size === 'small',
|
||||
'h-8 w-2': size === 'large',
|
||||
},
|
||||
{
|
||||
'bg-green-500': beat.status === 'health',
|
||||
'bg-red-600': beat.status === 'error',
|
||||
'bg-yellow-400': beat.status === 'warning',
|
||||
'bg-gray-400': beat.status === 'none',
|
||||
}
|
||||
getStatusBgColorClassName(beat.status)
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Editor, EditorProps } from '@bytemd/react';
|
||||
import { plugins } from './plugins';
|
||||
import 'bytemd/dist/index.css';
|
||||
import './style.less';
|
||||
import { useLocale } from './useLocale';
|
||||
|
||||
interface MarkdownEditorProps extends EditorProps {}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import loadable from '@loadable/component';
|
||||
import 'bytemd/dist/index.css';
|
||||
import './style.less';
|
||||
|
||||
export const MarkdownEditor = loadable(() =>
|
||||
import('./editor').then((module) => module.MarkdownEditor)
|
||||
|
@ -1,6 +1,13 @@
|
||||
import gfm from '@bytemd/plugin-gfm';
|
||||
import { BytemdPlugin } from 'bytemd';
|
||||
import externalLinks from 'rehype-external-links';
|
||||
|
||||
export const plugins = [
|
||||
export const plugins: BytemdPlugin[] = [
|
||||
gfm(),
|
||||
// Add more plugins here
|
||||
{
|
||||
rehype: (p) =>
|
||||
p.use(externalLinks, {
|
||||
target: '_blank',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
@ -7,12 +7,20 @@
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
.markdown-body {
|
||||
a {
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.bytemd {
|
||||
@apply bg-zinc-900 border-zinc-800 text-foreground;
|
||||
@apply text-foreground border-zinc-800 bg-zinc-900;
|
||||
|
||||
.bytemd-toolbar {
|
||||
@apply bg-zinc-900 border-zinc-800;
|
||||
@apply border-zinc-800 bg-zinc-900;
|
||||
|
||||
.bytemd-toolbar-icon:hover {
|
||||
@apply bg-muted;
|
||||
@ -24,7 +32,7 @@
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
@apply bg-zinc-900 text-foreground;
|
||||
@apply text-foreground bg-zinc-900;
|
||||
|
||||
.CodeMirror-cursor {
|
||||
@apply border-foreground;
|
||||
|
@ -1,15 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Viewer } from '@bytemd/react';
|
||||
import { plugins } from './plugins';
|
||||
import 'bytemd/dist/index.css';
|
||||
import './style.less';
|
||||
|
||||
interface MarkdownViewerProps {
|
||||
value: string;
|
||||
}
|
||||
export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(
|
||||
(props) => {
|
||||
return <Viewer plugins={plugins} value={props.value ?? ''} />;
|
||||
return (
|
||||
<div className="markdown">
|
||||
<Viewer plugins={plugins} value={props.value ?? ''} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
MarkdownViewer.displayName = 'MarkdownViewer';
|
||||
|
92
src/client/components/SimpleVirtualList.tsx
Normal file
92
src/client/components/SimpleVirtualList.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { useWatch } from '@/hooks/useWatch';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { Empty } from 'antd';
|
||||
import { last } from 'lodash-es';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
interface VirtualListProps<T = any> {
|
||||
allData: T[];
|
||||
hasNextPage: boolean | undefined;
|
||||
isFetchingNextPage: boolean;
|
||||
onFetchNextPage: () => void;
|
||||
estimateSize: number;
|
||||
renderItem: (item: T) => React.ReactElement;
|
||||
renderEmpty?: () => React.ReactElement;
|
||||
}
|
||||
export const SimpleVirtualList: React.FC<VirtualListProps> = React.memo(
|
||||
(props) => {
|
||||
const {
|
||||
allData,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
onFetchNextPage,
|
||||
estimateSize,
|
||||
renderItem,
|
||||
renderEmpty,
|
||||
} = props;
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: hasNextPage ? allData.length + 1 : allData.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => estimateSize,
|
||||
overscan: 5,
|
||||
});
|
||||
|
||||
const virtualItems = rowVirtualizer.getVirtualItems();
|
||||
|
||||
useWatch([virtualItems], () => {
|
||||
const lastItem = last(virtualItems);
|
||||
|
||||
if (!lastItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
lastItem.index >= allData.length - 1 &&
|
||||
hasNextPage &&
|
||||
!isFetchingNextPage
|
||||
) {
|
||||
onFetchNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className="h-full w-full overflow-auto">
|
||||
{virtualItems.length === 0 && (renderEmpty ? renderEmpty() : <Empty />)}
|
||||
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{
|
||||
height: rowVirtualizer.getTotalSize(),
|
||||
}}
|
||||
>
|
||||
{virtualItems.map((virtualItem) => {
|
||||
const isLoaderRow = virtualItem.index > allData.length - 1;
|
||||
const data = allData[virtualItem.index];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualItem.index}
|
||||
className="absolute left-0 top-0 w-full"
|
||||
style={{
|
||||
height: `${virtualItem.size}px`,
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
>
|
||||
{isLoaderRow
|
||||
? hasNextPage
|
||||
? t('Loading more...')
|
||||
: t('Nothing more to load')
|
||||
: renderItem(data)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
SimpleVirtualList.displayName = 'SimpleVirtualList';
|
94
src/client/components/Sortable/SortableContext.tsx
Normal file
94
src/client/components/Sortable/SortableContext.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { DragDropContext, DropResult } from 'react-beautiful-dnd';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { reorder } from '@/utils/reorder';
|
||||
import { SortableItem } from './types';
|
||||
|
||||
interface SortableContextProps<T extends SortableItem> {
|
||||
list: T[];
|
||||
onChange: (list: T[]) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SortableContext = <T extends SortableItem>(
|
||||
props: SortableContextProps<T>
|
||||
) => {
|
||||
const { list, onChange, children } = props;
|
||||
|
||||
const handleDragEnd = useEvent((result: DropResult) => {
|
||||
// dropped outside the list
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.type === 'root') {
|
||||
const final = reorder(
|
||||
list,
|
||||
result.source.index,
|
||||
result.destination.index
|
||||
);
|
||||
onChange(final);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.type === 'group') {
|
||||
// move data from source to destination
|
||||
// NOTICE: now only support 1 level
|
||||
|
||||
const final = [...list];
|
||||
const sourceGroupIndex = final.findIndex(
|
||||
(group) => group.key === result.source.droppableId
|
||||
);
|
||||
if (sourceGroupIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const destinationGroupIndex = final.findIndex(
|
||||
(group) => group.key === result.destination?.droppableId
|
||||
);
|
||||
if (destinationGroupIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceGroupIndex === destinationGroupIndex) {
|
||||
if (!('children' in final[sourceGroupIndex])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// same group
|
||||
final[sourceGroupIndex].children = reorder(
|
||||
final[sourceGroupIndex].children!,
|
||||
result.source.index,
|
||||
result.destination.index
|
||||
);
|
||||
} else {
|
||||
// cross group
|
||||
if (
|
||||
!('children' in final[sourceGroupIndex]) ||
|
||||
!('children' in final[destinationGroupIndex])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceGroupItems = Array.from(
|
||||
final[sourceGroupIndex].children ?? []
|
||||
);
|
||||
const [removed] = sourceGroupItems.splice(result.source.index, 1);
|
||||
|
||||
const destinationGroupItems = Array.from(
|
||||
final[destinationGroupIndex].children ?? []
|
||||
);
|
||||
destinationGroupItems.splice(result.destination.index, 0, removed);
|
||||
|
||||
final[sourceGroupIndex].children = sourceGroupItems;
|
||||
final[destinationGroupIndex].children = destinationGroupItems;
|
||||
}
|
||||
|
||||
onChange(final);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleDragEnd}>{children}</DragDropContext>
|
||||
);
|
||||
};
|
||||
SortableContext.displayName = 'SortableGroup';
|
111
src/client/components/Sortable/SortableGroup.tsx
Normal file
111
src/client/components/Sortable/SortableGroup.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { SortableContext } from './SortableContext';
|
||||
import { StrictModeDroppable } from './StrictModeDroppable';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { SortableGroupItem, SortableItem, SortableLeafItem } from './types';
|
||||
|
||||
interface SortableGroupProps<GroupProps, ItemProps> {
|
||||
list: SortableItem<GroupProps, ItemProps>[];
|
||||
onChange: (list: SortableItem<GroupProps, ItemProps>[]) => void;
|
||||
renderGroup: (
|
||||
group: SortableGroupItem<GroupProps, ItemProps>,
|
||||
children: React.ReactNode,
|
||||
level: number
|
||||
) => React.ReactNode;
|
||||
renderItem: (
|
||||
item: SortableLeafItem<ItemProps>,
|
||||
index: number,
|
||||
group: SortableGroupItem<GroupProps, ItemProps>
|
||||
) => React.ReactNode;
|
||||
}
|
||||
|
||||
export const SortableGroup = <GroupProps, ItemProps>(
|
||||
props: SortableGroupProps<GroupProps, ItemProps>
|
||||
) => {
|
||||
const { list, onChange, renderGroup, renderItem } = props;
|
||||
|
||||
const renderItemEl = useEvent(
|
||||
(
|
||||
item: SortableLeafItem<ItemProps>,
|
||||
index: number,
|
||||
group: SortableGroupItem<GroupProps, ItemProps>
|
||||
) => {
|
||||
return (
|
||||
<Draggable key={item.key} draggableId={item.key} index={index}>
|
||||
{(dragProvided) => (
|
||||
<div
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
>
|
||||
{renderItem(item, index, group)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const renderGroupEl = useEvent(
|
||||
(group: SortableGroupItem<GroupProps, ItemProps>, level = 0) => {
|
||||
return (
|
||||
<StrictModeDroppable
|
||||
droppableId={group.key}
|
||||
type={level === 0 ? 'root' : 'group'}
|
||||
key={group.key}
|
||||
>
|
||||
{(dropProvided) => (
|
||||
<div ref={dropProvided.innerRef} {...dropProvided.droppableProps}>
|
||||
{renderGroup(
|
||||
group,
|
||||
<>
|
||||
{group.children.map((item, index) =>
|
||||
!('children' in item) ? (
|
||||
renderItemEl(item, index, group)
|
||||
) : (
|
||||
<Draggable
|
||||
draggableId={item.key}
|
||||
key={item.key}
|
||||
index={index}
|
||||
>
|
||||
{(dragProvided) => (
|
||||
<div
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
>
|
||||
{renderGroupEl(item, level + 1)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
)}
|
||||
</>,
|
||||
level
|
||||
)}
|
||||
|
||||
{dropProvided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<SortableContext<SortableItem<GroupProps, ItemProps>>
|
||||
list={list}
|
||||
onChange={onChange}
|
||||
>
|
||||
{renderGroupEl(
|
||||
{
|
||||
key: 'root',
|
||||
children: list,
|
||||
} as SortableGroupItem<GroupProps, ItemProps>,
|
||||
0
|
||||
)}
|
||||
</SortableContext>
|
||||
);
|
||||
};
|
||||
SortableGroup.displayName = 'SortableGroup';
|
27
src/client/components/Sortable/StrictModeDroppable.tsx
Normal file
27
src/client/components/Sortable/StrictModeDroppable.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Droppable, DroppableProps } from 'react-beautiful-dnd';
|
||||
|
||||
/**
|
||||
* https://github.com/atlassian/react-beautiful-dnd/issues/2350
|
||||
*/
|
||||
export const StrictModeDroppable: React.FC<DroppableProps> = React.memo(
|
||||
(props) => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const animation = requestAnimationFrame(() => setEnabled(true));
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animation);
|
||||
setEnabled(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Droppable {...props}>{props.children}</Droppable>;
|
||||
}
|
||||
);
|
||||
StrictModeDroppable.displayName = 'StrictModeDroppable';
|
14
src/client/components/Sortable/types.ts
Normal file
14
src/client/components/Sortable/types.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Id } from 'react-beautiful-dnd';
|
||||
|
||||
export type SortableItem<GroupProps = unknown, ItemProps = unknown> =
|
||||
| SortableLeafItem<ItemProps>
|
||||
| SortableGroupItem<GroupProps, ItemProps>;
|
||||
|
||||
export type SortableGroupItem<GroupProps = unknown, ItemProps = unknown> = {
|
||||
key: Id;
|
||||
children: SortableItem<GroupProps, ItemProps>[];
|
||||
} & GroupProps;
|
||||
|
||||
export type SortableLeafItem<ItemProps = unknown> = {
|
||||
key: Id;
|
||||
} & ItemProps;
|
@ -1,63 +1,115 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { DateUnit } from '@tianji/shared';
|
||||
import React from 'react';
|
||||
import { formatDate, formatDateWithUnit } from '../utils/date';
|
||||
import { Column, ColumnConfig } from '@ant-design/charts';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import React, { useState } from 'react';
|
||||
import { formatDateWithUnit } from '../utils/date';
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
Customized,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from './ui/chart';
|
||||
import { useStrokeDasharray } from '@/hooks/useStrokeDasharray';
|
||||
|
||||
const chartConfig = {
|
||||
pv: {
|
||||
label: 'PV',
|
||||
},
|
||||
uv: {
|
||||
label: 'UV',
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const TimeEventChart: React.FC<{
|
||||
labelMapping?: Record<string, string>;
|
||||
data: { x: string; y: number; type: string }[];
|
||||
data: { date: string; [key: string]: number | string }[];
|
||||
unit: DateUnit;
|
||||
}> = React.memo((props) => {
|
||||
const { colors } = useTheme();
|
||||
const isMobile = useIsMobile();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const labelMapping = props.labelMapping ?? {
|
||||
pageview: t('pageview'),
|
||||
session: t('session'),
|
||||
const [calcStrokeDasharray, strokes] = useStrokeDasharray({});
|
||||
const [strokeDasharray, setStrokeDasharray] = React.useState([...strokes]);
|
||||
const handleAnimationEnd = () => setStrokeDasharray([...strokes]);
|
||||
const getStrokeDasharray = (name: string) => {
|
||||
const lineDasharray = strokeDasharray.find((s) => s.name === name);
|
||||
return lineDasharray ? lineDasharray.strokeDasharray : undefined;
|
||||
};
|
||||
const [selectedItem, setSelectedItem] = useState<string[]>(['pv', 'uv']);
|
||||
|
||||
const config = useMemo(
|
||||
() =>
|
||||
({
|
||||
data: props.data,
|
||||
isStack: true,
|
||||
xField: 'x',
|
||||
yField: 'y',
|
||||
seriesField: 'type',
|
||||
label: {
|
||||
position: 'middle' as const,
|
||||
style: {
|
||||
fill: '#FFFFFF',
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
title: (t) => formatDate(t),
|
||||
},
|
||||
color: [colors.chart.pv, colors.chart.uv],
|
||||
legend: isMobile
|
||||
? false
|
||||
: {
|
||||
itemName: {
|
||||
formatter: (text) => labelMapping[text] ?? text,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
label: {
|
||||
autoHide: true,
|
||||
autoRotate: false,
|
||||
formatter: (text) => formatDateWithUnit(text, props.unit),
|
||||
},
|
||||
},
|
||||
}) satisfies ColumnConfig,
|
||||
[props.data, props.unit, props.labelMapping]
|
||||
return (
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
data={props.data}
|
||||
margin={{ top: 10, right: 0, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={colors.chart.pv} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={colors.chart.pv} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={colors.chart.uv} stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor={colors.chart.uv} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Customized component={calcStrokeDasharray} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={(text) => formatDateWithUnit(text, props.unit)}
|
||||
/>
|
||||
<YAxis mirror />
|
||||
<ChartLegend
|
||||
content={
|
||||
<ChartLegendContent
|
||||
selectedItem={selectedItem}
|
||||
onItemClick={(item) => {
|
||||
setSelectedItem((selected) => {
|
||||
if (selected.includes(item.value)) {
|
||||
return selected.filter((s) => s !== item.value);
|
||||
} else {
|
||||
return [...selected, item.value];
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<CartesianGrid vertical={false} />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
|
||||
<Area
|
||||
hide={!selectedItem.includes('pv')}
|
||||
type="monotone"
|
||||
dataKey="pv"
|
||||
stroke={colors.chart.pv}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorUv)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={getStrokeDasharray('pv')}
|
||||
onAnimationEnd={handleAnimationEnd}
|
||||
/>
|
||||
|
||||
<Area
|
||||
hide={!selectedItem.includes('uv')}
|
||||
type="monotone"
|
||||
dataKey="uv"
|
||||
stroke={colors.chart.uv}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorPv)"
|
||||
strokeWidth={2}
|
||||
strokeDasharray={getStrokeDasharray('uv')}
|
||||
onAnimationEnd={handleAnimationEnd}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
);
|
||||
|
||||
return <Column {...config} />;
|
||||
});
|
||||
TimeEventChart.displayName = 'TimeEventChart';
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getJWT, setJWT } from '../api/auth';
|
||||
import { Loading } from './Loading';
|
||||
import { trpc } from '../api/trpc';
|
||||
import { setUserInfo } from '../store/user';
|
||||
@ -7,26 +6,19 @@ import { setUserInfo } from '../store/user';
|
||||
export const TokenLoginContainer: React.FC<React.PropsWithChildren> =
|
||||
React.memo((props) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const mutation = trpc.user.loginWithToken.useMutation();
|
||||
const trpcUtils = trpc.useUtils();
|
||||
|
||||
useEffect(() => {
|
||||
const token = getJWT();
|
||||
if (token) {
|
||||
mutation
|
||||
.mutateAsync({
|
||||
token,
|
||||
trpcUtils.user.info
|
||||
.fetch()
|
||||
.then((userInfo) => {
|
||||
if (userInfo) {
|
||||
setUserInfo(userInfo);
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
setJWT(res.token);
|
||||
setUserInfo(res.info);
|
||||
})
|
||||
.catch((err) => {})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
|
61
src/client/components/UsageCard.tsx
Normal file
61
src/client/components/UsageCard.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardHeader } from './ui/card';
|
||||
import { formatNumber } from '@/utils/common';
|
||||
import { LuAlertCircle } from 'react-icons/lu';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
interface UsageCardProps {
|
||||
title: string;
|
||||
current: number;
|
||||
limit?: number;
|
||||
}
|
||||
export const UsageCard: React.FC<UsageCardProps> = React.memo((props) => {
|
||||
const { title, current, limit } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card className="relative h-full w-full overflow-hidden">
|
||||
{limit && (
|
||||
<div
|
||||
className="absolute h-full bg-black bg-opacity-5 dark:bg-white dark:bg-opacity-10"
|
||||
style={{ width: `${(current / limit) * 100}%` }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{limit && current > limit && (
|
||||
<div className="absolute right-2 top-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LuAlertCircle stroke={colors.red['500']} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div>
|
||||
{t(
|
||||
'Exceeded the limit, please upgrade your plan or your workspace will be paused soon.'
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-muted-foreground">{title}</CardHeader>
|
||||
<CardContent>
|
||||
{limit && limit >= 0 ? (
|
||||
<div>
|
||||
<span className="text-2xl font-bold">{formatNumber(current)}</span>{' '}
|
||||
/ <span>{formatNumber(limit)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<span className="text-2xl font-bold">{formatNumber(current)}</span>{' '}
|
||||
/ <span>∞</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
UsageCard.displayName = 'UsageCard';
|
227
src/client/components/VirtualizedInfiniteDataTable.tsx
Normal file
227
src/client/components/VirtualizedInfiniteDataTable.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
//3 TanStack Libraries!!!
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
Row,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { InfiniteData } from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from './ui/table';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
|
||||
interface VirtualizedInfiniteDataTableProps<TData> {
|
||||
columns: ColumnDef<TData, any>[];
|
||||
data: InfiniteData<{ items: TData[] }> | undefined;
|
||||
onFetchNextPage: () => void;
|
||||
isFetching: boolean;
|
||||
isLoading: boolean;
|
||||
hasNextPage: boolean | undefined;
|
||||
}
|
||||
|
||||
export function VirtualizedInfiniteDataTable<TData>(
|
||||
props: VirtualizedInfiniteDataTableProps<TData>
|
||||
) {
|
||||
const { columns, data, onFetchNextPage, isFetching, isLoading, hasNextPage } =
|
||||
props;
|
||||
|
||||
//we need a reference to the scrolling element for logic down below
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
//flatten the array of arrays from the useInfiniteQuery hook
|
||||
const flatData = useMemo(
|
||||
() => data?.pages?.flatMap((page) => page.items) ?? [],
|
||||
[data]
|
||||
);
|
||||
|
||||
//called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
|
||||
const fetchMoreOnBottomReached = useCallback(
|
||||
(containerRefElement?: HTMLDivElement | null) => {
|
||||
if (containerRefElement) {
|
||||
const { scrollHeight, scrollTop, clientHeight } = containerRefElement;
|
||||
//once the user has scrolled within 500px of the bottom of the table, fetch more data if we can
|
||||
if (
|
||||
scrollHeight - scrollTop - clientHeight < 500 &&
|
||||
!isFetching &&
|
||||
hasNextPage
|
||||
) {
|
||||
onFetchNextPage();
|
||||
}
|
||||
}
|
||||
},
|
||||
[onFetchNextPage, isFetching, hasNextPage]
|
||||
);
|
||||
|
||||
// a check on mount and after a fetch to see if the table is already scrolled to the bottom and immediately needs to fetch more data
|
||||
useEffect(() => {
|
||||
fetchMoreOnBottomReached(tableContainerRef.current);
|
||||
}, [fetchMoreOnBottomReached]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: flatData,
|
||||
columns,
|
||||
columnResizeMode: 'onChange',
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const { rows } = table.getRowModel();
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: rows.length,
|
||||
estimateSize: () => 40, //estimate row height for accurate scrollbar dragging
|
||||
getScrollElement: () => tableContainerRef.current,
|
||||
//measure dynamic row height, except in firefox because it measures table border height incorrectly
|
||||
measureElement:
|
||||
typeof window !== 'undefined' &&
|
||||
navigator.userAgent.indexOf('Firefox') === -1
|
||||
? (element) => element?.getBoundingClientRect().height
|
||||
: undefined,
|
||||
overscan: 5,
|
||||
});
|
||||
|
||||
/**
|
||||
* Instead of calling `column.getSize()` on every render for every header
|
||||
* and especially every data cell (very expensive),
|
||||
* we will calculate all column sizes at once at the root table level in a useMemo
|
||||
* and pass the column sizes down as CSS variables to the <table> element.
|
||||
*/
|
||||
const columnSizeVars = useMemo(() => {
|
||||
const headers = table.getFlatHeaders();
|
||||
const colSizes: { [key: string]: number } = {};
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const header = headers[i]!;
|
||||
colSizes[`--header-${header.id}-size`] = header.getSize();
|
||||
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
|
||||
}
|
||||
return colSizes;
|
||||
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="virtualized-infinite-data-table relative h-full overflow-auto"
|
||||
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}
|
||||
ref={tableContainerRef}
|
||||
>
|
||||
{/* Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights */}
|
||||
<table style={{ display: 'grid' }}>
|
||||
<TableHeader
|
||||
className="sticky top-0 z-10 grid"
|
||||
style={{
|
||||
...columnSizeVars,
|
||||
}}
|
||||
>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow
|
||||
key={headerGroup.id}
|
||||
className="bg-background hover:bg-background flex w-full"
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className="relative overflow-hidden text-ellipsis text-nowrap pt-2.5"
|
||||
style={{
|
||||
width: `calc(var(--header-${header?.id}-size) * 1px)`,
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
|
||||
<div
|
||||
{...{
|
||||
onDoubleClick: () => header.column.resetSize(),
|
||||
onMouseDown: header.getResizeHandler(),
|
||||
onTouchStart: header.getResizeHandler(),
|
||||
className: `resizer ${
|
||||
header.column.getIsResizing() ? 'isResizing' : ''
|
||||
}`,
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody
|
||||
className="relative grid"
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const row = rows[virtualRow.index] as Row<TData>;
|
||||
return (
|
||||
<TableRow
|
||||
data-index={virtualRow.index} //needed for dynamic row height measurement
|
||||
ref={(node) => rowVirtualizer.measureElement(node)} //measure dynamic row height
|
||||
key={row.id}
|
||||
className="absolute flex w-full"
|
||||
style={{
|
||||
transform: `translateY(${virtualRow.start}px)`, //this should always be a `style` as it changes on scroll
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const content = flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
);
|
||||
const value = cell.getValue();
|
||||
const useSystemTooltip =
|
||||
typeof value === 'string' || typeof value === 'number';
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="flex"
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
}}
|
||||
>
|
||||
{useSystemTooltip ? (
|
||||
<div
|
||||
className="w-full cursor-default overflow-hidden text-ellipsis whitespace-nowrap text-left"
|
||||
title={String(value)}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<div className="w-full cursor-default overflow-hidden text-ellipsis whitespace-nowrap text-left">
|
||||
{content}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{content}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</table>
|
||||
{isFetching && <div>Fetching More...</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
VirtualizedInfiniteDataTable.displayName = 'VirtualizedInfiniteDataTable';
|
230
src/client/components/WebhookPlayground.tsx
Normal file
230
src/client/components/WebhookPlayground.tsx
Normal file
@ -0,0 +1,230 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from './ui/resizable';
|
||||
import { cn } from '@/utils/style';
|
||||
import { ScrollArea } from './ui/scroll-area';
|
||||
import { Trans, useTranslation } from '@i18next-toolkit/react';
|
||||
import { Empty } from 'antd';
|
||||
import { Button } from './ui/button';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { toast } from 'sonner';
|
||||
import { Code } from './Code';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { reverse, toPairs } from 'lodash-es';
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from './ui/dropdown-menu';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { useSocketSubscribeList } from '@/api/socketio';
|
||||
import { Spinner } from './ui/spinner';
|
||||
|
||||
export const WebhookPlayground: React.FC = React.memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
|
||||
const requestList = useSocketSubscribeList(
|
||||
'onReceivePlaygroundWebhookRequest'
|
||||
);
|
||||
|
||||
const selectedRequest = useMemo(() => {
|
||||
return requestList.find((item) => item.id === selectedRequestId);
|
||||
}, [selectedRequestId, requestList]);
|
||||
|
||||
const handleCopyAsCurl = useEvent(() => {
|
||||
if (!selectedRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = selectedRequest.url.startsWith('/')
|
||||
? `${window.location.origin}${selectedRequest.url}`
|
||||
: selectedRequest.url;
|
||||
const command = [
|
||||
'curl',
|
||||
`-X ${selectedRequest.method}`,
|
||||
`${toPairs(selectedRequest.headers)
|
||||
.filter(([key]) => !['content-length'].includes(key.toLowerCase()))
|
||||
.map(([key, value]) => `-H '${key}: ${value}'`)
|
||||
.join(' ')}`,
|
||||
`-d '${JSON.stringify(selectedRequest.body)}'`,
|
||||
`'${url}'`,
|
||||
].join(' ');
|
||||
copy(command);
|
||||
toast.success('Copied into your clipboard!');
|
||||
});
|
||||
|
||||
const list = (
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
{requestList.length === 0 && (
|
||||
<div className="pt-10">
|
||||
<Empty
|
||||
description={t(
|
||||
'Currently waiting for a new request from the remote server'
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="mt-2 flex justify-center text-center">
|
||||
<Spinner size={24} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{reverse([...requestList]).map((item) => {
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all',
|
||||
selectedRequestId === item.id
|
||||
? 'bg-gray-100 dark:!bg-gray-800'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-900'
|
||||
)}
|
||||
onClick={() => {
|
||||
setSelectedRequestId(item.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<Badge>{item.method}</Badge>
|
||||
|
||||
<div className="text-xs opacity-80">
|
||||
{dayjs(item.createdAt).fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between gap-1">
|
||||
<div className="overflow-hidden text-ellipsis font-semibold">
|
||||
{item.url}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
|
||||
const webhookUrl = `${window.location.origin}/open/feed/playground/${workspaceId}`;
|
||||
const emptyContentFallback = (
|
||||
<div className="pt-8">
|
||||
<div>
|
||||
<Trans>
|
||||
Set the webhook URL to <Code children={webhookUrl} />, and keep this
|
||||
window active. Once done, you will start receiving webhook requests
|
||||
here.
|
||||
</Trans>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
copy(webhookUrl);
|
||||
toast.success('Copied into your clipboard!');
|
||||
}}
|
||||
>
|
||||
{t('Copy URL')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const copyBtn = (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">{t('Copy as')}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" side="bottom" align="end">
|
||||
<DropdownMenuItem onClick={handleCopyAsCurl}>cURL</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
const content = selectedRequest ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Badge>{selectedRequest.method}</Badge>
|
||||
<div className="flex w-full flex-1 items-center justify-between gap-1 overflow-hidden text-ellipsis font-semibold">
|
||||
{selectedRequest.url}
|
||||
</div>
|
||||
{copyBtn}
|
||||
</div>
|
||||
<div className="text-right text-xs opacity-80">
|
||||
{dayjs(selectedRequest.createdAt).fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('Request Header')}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{toPairs(selectedRequest.headers).map(([key, value]) => {
|
||||
return (
|
||||
<div key={key} className="flex items-center justify-between">
|
||||
<div className="overflow-hidden text-ellipsis font-semibold">
|
||||
{key}
|
||||
</div>
|
||||
<div className="overflow-hidden text-ellipsis">{value}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('Request Body')}</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<CodeBlock
|
||||
height={600}
|
||||
code={JSON.stringify(selectedRequest.body, null, 2)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
emptyContentFallback
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="h-full items-stretch"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={30}
|
||||
collapsible={false}
|
||||
minSize={20}
|
||||
maxSize={50}
|
||||
className={cn('flex flex-col')}
|
||||
>
|
||||
{list}
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle withHandle />
|
||||
|
||||
<ResizablePanel>
|
||||
<ScrollArea className="h-full px-4 py-2">{content}</ScrollArea>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
WebhookPlayground.displayName = 'WebhookPlayground';
|
@ -1,14 +1,42 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import React, { useState } from 'react';
|
||||
import { cn } from '@/utils/style';
|
||||
import { useUserInfo } from '@/store/user';
|
||||
import { RiRocket2Fill } from 'react-icons/ri';
|
||||
import {
|
||||
changeUserCurrentWorkspace,
|
||||
setUserInfo,
|
||||
useCurrentWorkspace,
|
||||
useCurrentWorkspaceSafe,
|
||||
useUserInfo,
|
||||
} from '@/store/user';
|
||||
import { LuPlusCircle } from 'react-icons/lu';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from './ui/command';
|
||||
import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './ui/dialog';
|
||||
import { Label } from './ui/label';
|
||||
import { Input } from './ui/input';
|
||||
import { useEvent, useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { showErrorToast } from '@/utils/error';
|
||||
import { first, upperCase } from 'lodash-es';
|
||||
import { Empty } from 'antd';
|
||||
|
||||
interface WorkspaceSwitcherProps {
|
||||
isCollapsed: boolean;
|
||||
@ -16,40 +44,220 @@ interface WorkspaceSwitcherProps {
|
||||
export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
|
||||
(props) => {
|
||||
const userInfo = useUserInfo();
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [showNewWorkspaceDialog, setShowNewWorkspaceDialog] = useState(false);
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
||||
const currentWorkspace = useCurrentWorkspaceSafe();
|
||||
const createWorkspaceMutation = trpc.workspace.create.useMutation({
|
||||
onSuccess: (userInfo) => {
|
||||
setUserInfo(userInfo);
|
||||
},
|
||||
});
|
||||
const switchWorkspaceMutation = trpc.workspace.switch.useMutation({
|
||||
onSuccess: (userInfo) => {
|
||||
setUserInfo(userInfo);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSwitchWorkspace = useEvent(
|
||||
async (workspace: { id: string; name: string }) => {
|
||||
setOpen(false);
|
||||
|
||||
if (userInfo?.currentWorkspaceId === workspace.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await switchWorkspaceMutation.mutateAsync({
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
changeUserCurrentWorkspace(workspace.id);
|
||||
} catch (err) {
|
||||
showErrorToast(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const [handleCreateNewWorkspace, isCreateLoading] = useEventWithLoading(
|
||||
async () => {
|
||||
try {
|
||||
await createWorkspaceMutation.mutateAsync({
|
||||
name: newWorkspaceName,
|
||||
});
|
||||
|
||||
setShowNewWorkspaceDialog(false);
|
||||
} catch (err) {
|
||||
showErrorToast(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select value={userInfo.currentWorkspace.id}>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
|
||||
props.isCollapsed &&
|
||||
'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
|
||||
)}
|
||||
aria-label="Select workspace"
|
||||
<Dialog
|
||||
open={showNewWorkspaceDialog}
|
||||
onOpenChange={setShowNewWorkspaceDialog}
|
||||
>
|
||||
<SelectValue placeholder="Select workspace">
|
||||
<RiRocket2Fill />
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
'flex w-full justify-between',
|
||||
props.isCollapsed && 'h-9 w-9 items-center justify-center p-0'
|
||||
)}
|
||||
>
|
||||
{currentWorkspace ? (
|
||||
<>
|
||||
<Avatar
|
||||
className={cn('h-5 w-5', props.isCollapsed ? '' : 'mr-2')}
|
||||
>
|
||||
<AvatarImage
|
||||
src={`https://avatar.vercel.sh/${currentWorkspace.name}.png`}
|
||||
alt={currentWorkspace.name}
|
||||
className="grayscale"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{upperCase(first(currentWorkspace.name))}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<span className={cn('ml-2', props.isCollapsed && 'hidden')}>
|
||||
{userInfo.currentWorkspace.name}
|
||||
<span
|
||||
className={cn(
|
||||
'flex-1 overflow-hidden text-ellipsis text-left',
|
||||
props.isCollapsed && 'hidden'
|
||||
)}
|
||||
>
|
||||
{currentWorkspace.name}
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{userInfo.workspaces.map((w) => (
|
||||
<SelectItem key={w.workspace.id} value={w.workspace.id}>
|
||||
<div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
|
||||
<RiRocket2Fill />
|
||||
{w.workspace.name}
|
||||
</div>
|
||||
</SelectItem>
|
||||
</>
|
||||
) : (
|
||||
<span>{t('Select Workspace')}</span>
|
||||
)}
|
||||
|
||||
<CaretSortIcon
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4 shrink-0 opacity-50',
|
||||
props.isCollapsed && 'hidden'
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t('No workspace found.')}</CommandEmpty>
|
||||
<CommandGroup key="workspace" heading={t('Workspace')}>
|
||||
{userInfo.workspaces.length === 0 && (
|
||||
<Empty
|
||||
imageStyle={{ width: 80, height: 80, margin: 'auto' }}
|
||||
description={t(
|
||||
'Not any workspace has been found, please create first'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{userInfo.workspaces.map(({ workspace }) => (
|
||||
<CommandItem
|
||||
key={workspace.id}
|
||||
onSelect={() => {
|
||||
handleSwitchWorkspace(workspace);
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
<Avatar className="mr-2 h-5 w-5">
|
||||
<AvatarImage
|
||||
src={`https://avatar.vercel.sh/${workspace.name}.png`}
|
||||
alt={workspace.name}
|
||||
className="grayscale"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{upperCase(first(workspace.name))}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<span
|
||||
className="overflow-hidden text-ellipsis"
|
||||
title={workspace.name}
|
||||
>
|
||||
{workspace.name}
|
||||
</span>
|
||||
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4',
|
||||
currentWorkspace?.id === workspace.id
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandList>
|
||||
<CommandGroup key="create">
|
||||
<DialogTrigger asChild>
|
||||
<CommandItem
|
||||
aria-selected="false"
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
setShowNewWorkspaceDialog(true);
|
||||
}}
|
||||
>
|
||||
<LuPlusCircle className="mr-2" size={20} />
|
||||
{t('Create Workspace')}
|
||||
</CommandItem>
|
||||
</DialogTrigger>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('Create Workspace')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('Create a new workspace to cooperate with team members.')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<div className="space-y-4 py-2 pb-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('Workspace Name')}</Label>
|
||||
<Input
|
||||
value={newWorkspaceName}
|
||||
onChange={(e) => setNewWorkspaceName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowNewWorkspaceDialog(false)}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isCreateLoading}
|
||||
onClick={handleCreateNewWorkspace}
|
||||
>
|
||||
{t('Create')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
166
src/client/components/billing/SubscriptionSelection.tsx
Normal file
166
src/client/components/billing/SubscriptionSelection.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import { Check } from 'lucide-react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { defaultErrorHandler, trpc } from '@/api/trpc';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { cn } from '@/utils/style';
|
||||
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
||||
import { LuInfo } from 'react-icons/lu';
|
||||
|
||||
interface SubscriptionSelectionProps {
|
||||
tier: 'FREE' | 'PRO' | 'TEAM' | 'UNLIMITED' | undefined;
|
||||
}
|
||||
export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> =
|
||||
React.memo((props) => {
|
||||
const { tier } = props;
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const checkoutMutation = trpc.billing.checkout.useMutation({
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const handleCheckoutSubscribe = useEvent(
|
||||
async (tier: 'free' | 'pro' | 'team') => {
|
||||
const { url } = await checkoutMutation.mutateAsync({
|
||||
workspaceId,
|
||||
tier,
|
||||
redirectUrl: location.href,
|
||||
});
|
||||
|
||||
location.href = url;
|
||||
}
|
||||
);
|
||||
|
||||
const plans = [
|
||||
{
|
||||
id: 'FREE',
|
||||
name: t('Free'),
|
||||
price: 0,
|
||||
features: [
|
||||
t('Basic trial'),
|
||||
t('Basic Usage'),
|
||||
t('Up to 3 websites'),
|
||||
t('Up to 3 surveys'),
|
||||
t('Up to 3 feed channels'),
|
||||
t('100K website events per month'),
|
||||
t('100K monitor execution per month'),
|
||||
t('10K feed event per month'),
|
||||
t('Discord Community Support'),
|
||||
],
|
||||
onClick: () => handleCheckoutSubscribe('free'),
|
||||
},
|
||||
{
|
||||
id: 'PRO',
|
||||
name: 'Pro',
|
||||
price: 19.99,
|
||||
features: [
|
||||
t('Sufficient for most situations'),
|
||||
t('Priority access to advanced features'),
|
||||
t('Up to 10 websites'),
|
||||
t('Up to 20 surveys'),
|
||||
t('Up to 20 feed channels'),
|
||||
t('1M website events per month'),
|
||||
t('1M monitor execution per month'),
|
||||
t('100K feed events per month'),
|
||||
t('Discord Community Support'),
|
||||
],
|
||||
onClick: () => handleCheckoutSubscribe('pro'),
|
||||
},
|
||||
{
|
||||
id: 'TEAM',
|
||||
name: 'Team',
|
||||
price: 99.99,
|
||||
features: [
|
||||
t('Fully sufficient'),
|
||||
t('Priority access to advanced features'),
|
||||
t('Unlimited websites'),
|
||||
t('Unlimited surveys'),
|
||||
t('Unlimited feed channels'),
|
||||
t('20M website events per month'),
|
||||
t('20M monitor execution per month'),
|
||||
t('1M feed events per month'),
|
||||
t('Priority email support'),
|
||||
],
|
||||
onClick: () => handleCheckoutSubscribe('team'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="mb-8 text-center text-3xl font-bold">
|
||||
{t('Subscription Plan')}
|
||||
</h1>
|
||||
|
||||
<Alert className="mb-4">
|
||||
<LuInfo className="h-4 w-4" />
|
||||
<AlertTitle>{t('Current Plan')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('Your Current Plan is:')}{' '}
|
||||
<span className="font-bold">{tier}</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{plans.map((plan) => {
|
||||
const isCurrent = plan.id === tier;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={plan.name}
|
||||
className={cn('flex flex-col', isCurrent && 'border-primary')}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription>${plan.price} per month</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow">
|
||||
<ul className="space-y-2">
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center">
|
||||
<Check className="mr-2 h-4 w-4 text-green-500" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{isCurrent ? (
|
||||
<Button className="w-full" disabled variant="outline">
|
||||
{t('Current')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={checkoutMutation.isLoading}
|
||||
onClick={plan.onClick}
|
||||
>
|
||||
{t('{{action}} to {{plan}}', {
|
||||
action:
|
||||
plans.indexOf(plan) <
|
||||
plans.findIndex((p) => p.id === tier)
|
||||
? t('Downgrade')
|
||||
: t('Upgrade'),
|
||||
plan: plan.name,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SubscriptionSelection.displayName = 'SubscriptionSelection';
|
@ -1,167 +0,0 @@
|
||||
import { Button, Dropdown, MenuProps, Space } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { trpc } from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import { useDashboardStore } from '../../store/dashboard';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
|
||||
export const DashboardItemAddButton: React.FC = React.memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { data: websites = [], isLoading: isWebsiteLoading } =
|
||||
trpc.website.all.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
const { data: monitors = [], isLoading: isMonitorLoading } =
|
||||
trpc.monitor.all.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
const { addItem } = useDashboardStore();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const isLoading = isWebsiteLoading || isMonitorLoading;
|
||||
|
||||
const menu: MenuProps = {
|
||||
items: [
|
||||
{
|
||||
key: 'website',
|
||||
label: t('Website'),
|
||||
children:
|
||||
websites.length > 0
|
||||
? websites.map((website) => ({
|
||||
key: `website#${website.id}`,
|
||||
label: website.name,
|
||||
children: [
|
||||
{
|
||||
key: `website#${website.id}#overview`,
|
||||
label: t('Overview'),
|
||||
onClick: () => {
|
||||
addItem(
|
||||
'websiteOverview',
|
||||
website.id,
|
||||
`${website.name}'s Overview`
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `website#${website.id}#events`,
|
||||
label: t('Events'),
|
||||
onClick: () => {
|
||||
addItem(
|
||||
'websiteEvents',
|
||||
website.id,
|
||||
`${website.name}'s Events`
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: `website#none`,
|
||||
label: t('(None)'),
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'monitor',
|
||||
label: t('Monitor'),
|
||||
children:
|
||||
monitors.length > 0
|
||||
? monitors.map((monitor) => ({
|
||||
key: `monitor#${monitor.id}`,
|
||||
label: monitor.name,
|
||||
children: [
|
||||
{
|
||||
key: `monitor#${monitor.id}#healthBar`,
|
||||
label: t('Health Bar'),
|
||||
onClick: () => {
|
||||
addItem(
|
||||
'monitorHealthBar',
|
||||
monitor.id,
|
||||
t("{{monitorName}}'s Health", {
|
||||
monitorName: monitor.name,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `monitor#${monitor.id}#metrics`,
|
||||
label: t('Metrics'),
|
||||
onClick: () => {
|
||||
addItem(
|
||||
'monitorMetrics',
|
||||
monitor.id,
|
||||
t("{{monitorName}}'s Metrics", {
|
||||
monitorName: monitor.name,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `monitor#${monitor.id}#chart`,
|
||||
label: t('Chart'),
|
||||
onClick: () => {
|
||||
addItem(
|
||||
'monitorChart',
|
||||
monitor.id,
|
||||
t("{{monitorName}}'s Chart", {
|
||||
monitorName: monitor.name,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `monitor#${monitor.id}#events`,
|
||||
label: t('Events'),
|
||||
onClick: () => {
|
||||
addItem(
|
||||
'monitorEvents',
|
||||
monitor.id,
|
||||
t("{{monitorName}}'s Events", {
|
||||
monitorName: monitor.name,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: `monitor#none`,
|
||||
label: t('(None)'),
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
disabled={isLoading}
|
||||
menu={menu}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<Button type="primary" size="large" className="w-32">
|
||||
<Space>
|
||||
<span>{t('Add')}</span>
|
||||
<DownOutlined
|
||||
className={clsx(
|
||||
'scale-y-75 transition-transform',
|
||||
open && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
DashboardItemAddButton.displayName = 'DashboardItemAddButton';
|
@ -1,102 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { DashboardGrid } from './Grid';
|
||||
import { DashboardItemAddButton } from './AddButton';
|
||||
import { defaultBlankLayouts, useDashboardStore } from '../../store/dashboard';
|
||||
import { useEvent } from '../../hooks/useEvent';
|
||||
import { Layouts } from 'react-grid-layout';
|
||||
import { Button, Empty, message } from 'antd';
|
||||
import { DateFilter } from '../DateFilter';
|
||||
import { trpc } from '../../api/trpc';
|
||||
import { useCurrentWorkspace, useCurrentWorkspaceId } from '../../store/user';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
|
||||
export const Dashboard: React.FC = React.memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { isEditMode, switchEditMode, layouts, items } = useDashboardStore();
|
||||
const mutation = trpc.workspace.saveDashboardLayout.useMutation();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const workspace = useCurrentWorkspace();
|
||||
|
||||
useEffect(() => {
|
||||
// Init on mount
|
||||
const { items = [], layouts = defaultBlankLayouts } =
|
||||
workspace.dashboardLayout ?? {};
|
||||
|
||||
useDashboardStore.setState({
|
||||
items,
|
||||
layouts,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleChangeLayouts = useEvent((layouts: Layouts) => {
|
||||
useDashboardStore.setState({
|
||||
layouts,
|
||||
});
|
||||
});
|
||||
|
||||
const handleSaveDashboardLayout = useEvent(async () => {
|
||||
await mutation.mutateAsync({
|
||||
workspaceId,
|
||||
dashboardLayout: {
|
||||
layouts,
|
||||
items,
|
||||
},
|
||||
});
|
||||
switchEditMode();
|
||||
message.success(t('Layout saved success'));
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<div
|
||||
className={clsx(
|
||||
'flex justify-end gap-2 bg-white py-2 dark:bg-gray-900',
|
||||
isEditMode && 'sticky top-0 z-10'
|
||||
)}
|
||||
>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<DashboardItemAddButton />
|
||||
<Button
|
||||
className="w-32"
|
||||
size="large"
|
||||
loading={mutation.isLoading}
|
||||
disabled={mutation.isLoading}
|
||||
onClick={handleSaveDashboardLayout}
|
||||
>
|
||||
{t('Done')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DateFilter />
|
||||
<Button
|
||||
className="w-32"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={switchEditMode}
|
||||
>
|
||||
{t('Edit')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<DashboardGrid
|
||||
layouts={layouts}
|
||||
onChangeLayouts={handleChangeLayouts}
|
||||
items={items}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
|
||||
{items.length === 0 && (
|
||||
<Empty
|
||||
description={t(
|
||||
'You have not dashboard item yet, please enter edit mode and add you item.'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Dashboard.displayName = 'Dashboard';
|
@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Layouts, Responsive, WidthProvider } from 'react-grid-layout';
|
||||
import clsx from 'clsx';
|
||||
import { DashboardGridItem } from './items';
|
||||
import { DashboardItem } from '../../store/dashboard';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
|
||||
interface DashboardGridProps {
|
||||
isEditMode: boolean;
|
||||
items: DashboardItem[];
|
||||
layouts: Layouts;
|
||||
onChangeLayouts: (layouts: Layouts) => void;
|
||||
}
|
||||
export const DashboardGrid: React.FC<DashboardGridProps> = React.memo(
|
||||
(props) => {
|
||||
const { layouts, onChangeLayouts, items, isEditMode } = props;
|
||||
|
||||
return (
|
||||
<ResponsiveGridLayout
|
||||
className={clsx('layout', isEditMode && 'select-none')}
|
||||
layouts={layouts}
|
||||
rowHeight={50}
|
||||
draggableCancel=".non-draggable"
|
||||
isDraggable={isEditMode}
|
||||
isResizable={isEditMode}
|
||||
breakpoints={{ lg: 1200, md: 768, sm: 0 }}
|
||||
cols={{ lg: 4, md: 3, sm: 2 }}
|
||||
onLayoutChange={(currentLayout, allLayouts) => {
|
||||
onChangeLayouts(allLayouts);
|
||||
}}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div key={item.key}>
|
||||
<DashboardGridItem item={item} />
|
||||
</div>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
);
|
||||
}
|
||||
);
|
||||
DashboardGrid.displayName = 'DashboardGrid';
|
@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
import { MonitorDataChart } from '../../monitor/MonitorDataChart';
|
||||
|
||||
export const MonitorChartItem: React.FC<{
|
||||
monitorId: string;
|
||||
}> = React.memo((props) => {
|
||||
return <MonitorDataChart monitorId={props.monitorId} />;
|
||||
});
|
||||
MonitorChartItem.displayName = 'MonitorChartItem';
|
@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
import { MonitorEventList } from '../../monitor/MonitorEventList';
|
||||
|
||||
export const MonitorEventsItem: React.FC<{
|
||||
monitorId: string;
|
||||
}> = React.memo((props) => {
|
||||
return <MonitorEventList monitorId={props.monitorId} />;
|
||||
});
|
||||
MonitorEventsItem.displayName = 'MonitorEventsItem';
|
@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { MonitorHealthBar } from '../../monitor/MonitorHealthBar';
|
||||
import { useCurrentWorkspaceId } from '../../../store/user';
|
||||
|
||||
export const MonitorHealthBarItem: React.FC<{
|
||||
monitorId: string;
|
||||
}> = React.memo((props) => {
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
|
||||
return (
|
||||
<MonitorHealthBar
|
||||
workspaceId={workspaceId}
|
||||
monitorId={props.monitorId}
|
||||
count={40}
|
||||
size="large"
|
||||
showCurrentStatus={true}
|
||||
/>
|
||||
);
|
||||
});
|
||||
MonitorHealthBarItem.displayName = 'MonitorHealthBarItem';
|
@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import { trpc } from '../../../api/trpc';
|
||||
import { NotFoundTip } from '../../NotFoundTip';
|
||||
import { useCurrentWorkspaceId } from '../../../store/user';
|
||||
import { Loading } from '../../Loading';
|
||||
import { MonitorDataMetrics } from '../../monitor/MonitorDataMetrics';
|
||||
|
||||
export const MonitorMetricsItem: React.FC<{
|
||||
monitorId: string;
|
||||
}> = React.memo((props) => {
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
|
||||
const { data: monitorInfo, isLoading } = trpc.monitor.get.useQuery({
|
||||
workspaceId,
|
||||
monitorId: props.monitorId,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!monitorInfo) {
|
||||
return <NotFoundTip />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MonitorDataMetrics
|
||||
monitorId={monitorInfo.id}
|
||||
monitorType={monitorInfo.type}
|
||||
/>
|
||||
);
|
||||
});
|
||||
MonitorMetricsItem.displayName = 'MonitorMetricsItem';
|
@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { WebsiteMetricsTable } from '../../website/WebsiteMetricsTable';
|
||||
import { useGlobalRangeDate } from '../../../hooks/useGlobalRangeDate';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
|
||||
export const WebsiteEventItem: React.FC<{
|
||||
websiteId: string;
|
||||
}> = React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
const { startDate, endDate } = useGlobalRangeDate();
|
||||
const startAt = startDate.valueOf();
|
||||
const endAt = endDate.valueOf();
|
||||
|
||||
return (
|
||||
<WebsiteMetricsTable
|
||||
websiteId={props.websiteId}
|
||||
type="event"
|
||||
title={[t('Events'), t('Actions')]}
|
||||
startAt={startAt}
|
||||
endAt={endAt}
|
||||
/>
|
||||
);
|
||||
});
|
||||
WebsiteEventItem.displayName = 'WebsiteEventItem';
|
@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { trpc } from '../../../api/trpc';
|
||||
import { useCurrentWorkspaceId } from '../../../store/user';
|
||||
import { Loading } from '../../Loading';
|
||||
import { NotFoundTip } from '../../NotFoundTip';
|
||||
import { WebsiteOverview } from '../../website/WebsiteOverview';
|
||||
import { Button } from 'antd';
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
|
||||
export const WebsiteOverviewItem: React.FC<{
|
||||
websiteId: string;
|
||||
}> = React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
|
||||
const { data: websiteInfo, isLoading } = trpc.website.info.useQuery({
|
||||
workspaceId,
|
||||
websiteId: props.websiteId,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!websiteInfo) {
|
||||
return <NotFoundTip />;
|
||||
}
|
||||
|
||||
return (
|
||||
<WebsiteOverview
|
||||
website={websiteInfo}
|
||||
actions={
|
||||
<>
|
||||
<Link to="/website/$websiteId" params={{ websiteId: websiteInfo.id }}>
|
||||
<Button type="primary" size="large">
|
||||
{t('View Details')} <ArrowRightOutlined />
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
WebsiteOverviewItem.displayName = 'WebsiteOverviewItem';
|
@ -1,77 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { DashboardItem, useDashboardStore } from '../../../store/dashboard';
|
||||
import { WebsiteOverviewItem } from './WebsiteOverviewItem';
|
||||
import { NotFoundTip } from '../../NotFoundTip';
|
||||
import { Button, Card, Input, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { useEvent } from '../../../hooks/useEvent';
|
||||
import { WebsiteEventItem } from './WebsiteEventItem';
|
||||
import { MonitorHealthBarItem } from './MonitorHealthBarItem';
|
||||
import { MonitorMetricsItem } from './MonitorMetricsItem';
|
||||
import { MonitorChartItem } from './MonitorChartItem';
|
||||
import { MonitorEventsItem } from './MonitorEventsItem';
|
||||
import { EditableText } from '../../EditableText';
|
||||
|
||||
interface DashboardGridItemProps {
|
||||
item: DashboardItem;
|
||||
}
|
||||
export const DashboardGridItem: React.FC<DashboardGridItemProps> = React.memo(
|
||||
(props) => {
|
||||
const { isEditMode, removeItem, changeItemTitle } = useDashboardStore();
|
||||
const { key, id, title, type } = props.item;
|
||||
|
||||
const inner = useMemo(() => {
|
||||
if (type === 'websiteOverview') {
|
||||
return <WebsiteOverviewItem websiteId={id} />;
|
||||
} else if (type === 'websiteEvents') {
|
||||
return <WebsiteEventItem websiteId={id} />;
|
||||
} else if (type === 'monitorHealthBar') {
|
||||
return <MonitorHealthBarItem monitorId={id} />;
|
||||
} else if (type === 'monitorMetrics') {
|
||||
return <MonitorMetricsItem monitorId={id} />;
|
||||
} else if (type === 'monitorChart') {
|
||||
return <MonitorChartItem monitorId={id} />;
|
||||
} else if (type === 'monitorEvents') {
|
||||
return <MonitorEventsItem monitorId={id} />;
|
||||
} else {
|
||||
return <NotFoundTip />;
|
||||
}
|
||||
}, [id, type]);
|
||||
|
||||
const handleDelete = useEvent(
|
||||
(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
removeItem(key);
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="h-full w-full overflow-auto"
|
||||
title={
|
||||
<EditableText
|
||||
className="non-draggable"
|
||||
enable={isEditMode}
|
||||
defaultValue={title}
|
||||
onSave={(text) => changeItemTitle(key, text)}
|
||||
/>
|
||||
}
|
||||
headStyle={{ padding: 10, minHeight: 40 }}
|
||||
bodyStyle={{ padding: 10 }}
|
||||
extra={
|
||||
isEditMode && (
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{inner}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
);
|
||||
DashboardGridItem.displayName = 'DashboardGridItem';
|
97
src/client/components/feed/FeedApiGuide.tsx
Normal file
97
src/client/components/feed/FeedApiGuide.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { CodeExample } from '../CodeExample';
|
||||
|
||||
interface FeedApiGuideProps {
|
||||
channelId: string;
|
||||
webhookSignature?: string;
|
||||
}
|
||||
export const FeedApiGuide: React.FC<FeedApiGuideProps> = React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card className="w-full overflow-hidden">
|
||||
<CardHeader>
|
||||
<div>{t('You can send a message to this channel with:')}</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-5 overflow-hidden">
|
||||
<CodeExample
|
||||
example={{
|
||||
curl: {
|
||||
label: 'curl',
|
||||
code: generateCurlCode(props.channelId, props.webhookSignature),
|
||||
},
|
||||
fetch: {
|
||||
label: 'fetch',
|
||||
code: generateFetchCode(props.channelId, props.webhookSignature),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="pl-2 font-bold">{t('OR')}</div>
|
||||
|
||||
<div>{t('Integrate with third party with webhook')}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
FeedApiGuide.displayName = 'FeedApiGuide';
|
||||
|
||||
function generateCurlCode(channelId: string, webhookSignature?: string) {
|
||||
if (webhookSignature) {
|
||||
return `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-Webhook-Signature: ${webhookSignature}" \\
|
||||
-d '{
|
||||
"eventName": "test name",
|
||||
"eventContent": "test content",
|
||||
"tags": ["test"],
|
||||
"source": "custom",
|
||||
"important": false
|
||||
}'`;
|
||||
}
|
||||
|
||||
return `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"eventName": "test name",
|
||||
"eventContent": "test content",
|
||||
"tags": ["test"],
|
||||
"source": "custom",
|
||||
"important": false
|
||||
}'`;
|
||||
}
|
||||
|
||||
function generateFetchCode(channelId: string, webhookSignature?: string) {
|
||||
if (webhookSignature) {
|
||||
return `fetch('${window.location.origin}/open/feed/${channelId}/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Webhook-Signature': '${webhookSignature}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
eventName: 'test name',
|
||||
eventContent: 'test content',
|
||||
tags: ['test'],
|
||||
source: 'custom',
|
||||
important: false,
|
||||
})
|
||||
})`;
|
||||
}
|
||||
|
||||
return `fetch('${window.location.origin}/open/feed/${channelId}/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
eventName: 'test name',
|
||||
eventContent: 'test content',
|
||||
tags: ['test'],
|
||||
source: 'custom',
|
||||
important: false,
|
||||
})
|
||||
})`;
|
||||
}
|
139
src/client/components/feed/FeedArchivePageButton.tsx
Normal file
139
src/client/components/feed/FeedArchivePageButton.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Button } from '../ui/button';
|
||||
import { LuArchive, LuArchiveRestore } from 'react-icons/lu';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
import { AppRouterOutput, trpc } from '@/api/trpc';
|
||||
import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
|
||||
import { DynamicVirtualList } from '../DynamicVirtualList';
|
||||
import { get } from 'lodash-es';
|
||||
import { FeedEventItem } from './FeedEventItem';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { Separator } from '../ui/separator';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { toast } from 'sonner';
|
||||
import { AlertConfirm } from '../AlertConfirm';
|
||||
|
||||
type FeedItem = AppRouterOutput['feed']['fetchEventsByCursor']['items'][number];
|
||||
|
||||
interface FeedArchivePageButtonProps {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
export const FeedArchivePageButton: React.FC<FeedArchivePageButtonProps> =
|
||||
React.memo((props) => {
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const channelId = props.channelId;
|
||||
const { t } = useTranslation();
|
||||
const unarchiveEventMutation = trpc.feed.unarchiveEvent.useMutation();
|
||||
const clearAllArchivedEventsMutation =
|
||||
trpc.feed.clearAllArchivedEvents.useMutation();
|
||||
const trpcUtils = trpc.useUtils();
|
||||
const hasAdminPermission = useHasAdminPermission();
|
||||
|
||||
const {
|
||||
data,
|
||||
isInitialLoading,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = trpc.feed.fetchEventsByCursor.useInfiniteQuery(
|
||||
{
|
||||
workspaceId,
|
||||
channelId,
|
||||
archived: true,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
);
|
||||
|
||||
const handleUnArchive = useEvent(async (event: FeedItem) => {
|
||||
await unarchiveEventMutation.mutateAsync({
|
||||
workspaceId,
|
||||
channelId,
|
||||
eventId: event.id,
|
||||
});
|
||||
trpcUtils.feed.fetchEventsByCursor.refetch();
|
||||
toast.success(t('Event unarchived'));
|
||||
});
|
||||
|
||||
const handleClear = useEvent(async () => {
|
||||
const count = await clearAllArchivedEventsMutation.mutateAsync({
|
||||
workspaceId,
|
||||
channelId,
|
||||
});
|
||||
trpcUtils.feed.fetchEventsByCursor.refetch();
|
||||
toast.success(t('{{num}} events cleared', { num: count }));
|
||||
});
|
||||
|
||||
const fullEvents = useMemo(
|
||||
() => data?.pages.flatMap((p) => p.items) ?? [],
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button size="icon" variant="outline">
|
||||
<LuArchive />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="flex h-[50vh] w-96 flex-col overflow-hidden"
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-lg font-bold">{t('Archived Events')}</h1>
|
||||
|
||||
{hasAdminPermission && (
|
||||
<AlertConfirm onConfirm={handleClear}>
|
||||
<Button size="sm">{t('Clear')}</Button>
|
||||
</AlertConfirm>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
<div className="flex-1">
|
||||
<DynamicVirtualList
|
||||
allData={fullEvents}
|
||||
estimateSize={100}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onFetchNextPage={fetchNextPage}
|
||||
getItemKey={(index) => get(fullEvents, [index, 'id'])}
|
||||
renderItem={(item) => (
|
||||
<FeedEventItem
|
||||
className="animate-fade-in mb-2"
|
||||
event={item}
|
||||
actions={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
className="h-6 w-6 overflow-hidden"
|
||||
onClick={() => handleUnArchive(item)}
|
||||
>
|
||||
<LuArchiveRestore size={12} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
renderEmpty={() => (
|
||||
<div className="w-full overflow-hidden p-4">
|
||||
<div className="text-muted text-center">
|
||||
{isInitialLoading
|
||||
? t('Loading...')
|
||||
: t('No archived events')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
FeedArchivePageButton.displayName = 'FeedArchivePageButton';
|
186
src/client/components/feed/FeedChannelEditForm.tsx
Normal file
186
src/client/components/feed/FeedChannelEditForm.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { Card, CardContent, CardFooter } from '@/components/ui/card';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import React from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../ui/select';
|
||||
import { NotificationPicker } from '../notification/NotificationPicker';
|
||||
import { LuRefreshCcw } from 'react-icons/lu';
|
||||
import md5 from 'md5';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const addFormSchema = z.object({
|
||||
name: z.string(),
|
||||
webhookSignature: z.string().default(''),
|
||||
notificationIds: z.array(z.string()).default([]),
|
||||
notifyFrequency: z.enum(['none', 'event', 'day', 'week', 'month']),
|
||||
});
|
||||
|
||||
export type FeedChannelEditFormValues = z.infer<typeof addFormSchema>;
|
||||
|
||||
interface FeedChannelEditFormProps {
|
||||
defaultValues?: FeedChannelEditFormValues;
|
||||
onSubmit: (values: FeedChannelEditFormValues) => Promise<void>;
|
||||
}
|
||||
export const FeedChannelEditForm: React.FC<FeedChannelEditFormProps> =
|
||||
React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<FeedChannelEditFormValues>({
|
||||
resolver: zodResolver(addFormSchema),
|
||||
defaultValues: props.defaultValues ?? {
|
||||
name: 'New Channel',
|
||||
webhookSignature: '',
|
||||
notificationIds: [],
|
||||
notifyFrequency: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
const [handleSubmit, isLoading] = useEventWithLoading(
|
||||
async (values: FeedChannelEditFormValues) => {
|
||||
await props.onSubmit(values);
|
||||
form.reset();
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-8">
|
||||
<Card>
|
||||
<CardContent className="pt-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Channel Name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('Channel Name to Display')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookSignature"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel optional={true}>
|
||||
{t('Webhook Signature')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex">
|
||||
<Input className="rounded-r-none" {...field} />
|
||||
<Button
|
||||
className="rounded-l-none"
|
||||
type="button"
|
||||
Icon={LuRefreshCcw}
|
||||
onClick={() => {
|
||||
form.setValue(
|
||||
'webhookSignature',
|
||||
md5(dayjs().valueOf().toString())
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('Optional, Webhook Signature for Incoming Webhook')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notificationIds"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Notification')}</FormLabel>
|
||||
<FormControl>
|
||||
<NotificationPicker
|
||||
className="w-full"
|
||||
{...field}
|
||||
allowClear={true}
|
||||
mode="multiple"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('Select Notification for send')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notifyFrequency"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Notification Frequency')}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{t('None')}</SelectItem>
|
||||
<SelectItem value="event">
|
||||
{t('Every Event')}
|
||||
</SelectItem>
|
||||
<SelectItem value="day">{t('Every Day')}</SelectItem>
|
||||
<SelectItem value="week">
|
||||
{t('Every Week')}
|
||||
</SelectItem>
|
||||
<SelectItem value="month">
|
||||
{t('Every Month')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<Button type="submit" loading={isLoading}>
|
||||
{props.defaultValues ? t('Update') : t('Create')}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
FeedChannelEditForm.displayName = 'FeedChannelEditForm';
|
56
src/client/components/feed/FeedChannelPicker.tsx
Normal file
56
src/client/components/feed/FeedChannelPicker.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Button, Empty, Select, SelectProps } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { trpc } from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
|
||||
interface FeedChannelPickerProps extends SelectProps<string | string[]> {}
|
||||
export const FeedChannelPicker: React.FC<FeedChannelPickerProps> = React.memo(
|
||||
(props) => {
|
||||
const { t } = useTranslation();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const navigate = useNavigate();
|
||||
const { data: allChannels = [] } = trpc.feed.channels.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
allChannels.map((m) => ({
|
||||
label: m.name,
|
||||
value: m.id,
|
||||
})),
|
||||
[allChannels]
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
notFoundContent={
|
||||
<Empty
|
||||
description={
|
||||
<div className="py-2">
|
||||
<div className="mb-1">{t('Not found any feed channel')}</div>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/feed/add',
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('Create Now')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
options={options}
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
FeedChannelPicker.displayName = 'FeedChannelPicker';
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user