Compare commits

...

123 Commits

Author SHA1 Message Date
moonrailgun
162954606a feat: add auto language detect for browser 2024-11-11 01:33:06 +08:00
moonrailgun
3bf86b3e6e feat: add audit log clear feature 2024-11-10 06:08:22 +08:00
moonrailgun
843a581d42 feat: add subscription selection page 2024-11-09 20:28:27 +08:00
moonrailgun
fffc989336 chore: remove unused script 2024-11-09 20:27:35 +08:00
moonrailgun
ea75ed7f88 chore: add alert 2024-11-09 20:27:17 +08:00
moonrailgun
34f9fe6957 refactor: add usage limit and update card style 2024-11-08 01:57:35 +08:00
moonrailgun
71f75c27dd feat: add api key and usage to command panel 2024-11-08 01:56:07 +08:00
moonrailgun
a12fa3e6fe feat: add <UsageCard /> component which can render usage data and progress 2024-11-08 01:47:49 +08:00
moonrailgun
ae5f5a97d9 chore: remove passport package 2024-11-08 00:28:04 +08:00
moonrailgun
31ad64cd95 feat: add cronjob to check workspace limit which will pause workspace 2024-11-07 00:06:04 +08:00
tommy141x
1096e9ca9a Fix number casting issue 2024-11-06 12:11:08 +08:00
moonrailgun
b71bf6542e feat: add more usage stats 2024-11-06 01:19:57 +08:00
moonrailgun
e4b98b1c36 feat: add workspace subscription 2024-11-06 01:11:03 +08:00
moonrailgun
fa1ff3b5f6 refactor: move billing mode inside folder 2024-11-06 01:11:03 +08:00
moonrailgun
f0ddf6c5dd refactor: add apikey check before setup 2024-11-06 01:11:03 +08:00
moonrailgun
74d391afc1 feat: add lemonsqueezy subscription 2024-11-06 01:11:03 +08:00
moonrailgun
c70e69879f fix: fix isUser middleware will call twice problem 2024-11-04 00:54:57 +08:00
moonrailgun
6a4bdd324c feat: add api key fe and usage counter 2024-11-03 19:39:59 +08:00
moonrailgun
f7b1d33c5d feat: add user api key backend support 2024-11-03 17:56:47 +08:00
moonrailgun
7aec9e7237 chore: release v1.16.5 2024-11-03 00:02:15 +08:00
moonrailgun
f637ade70f refactor: refactor status header and add typescript and translation support 2024-11-01 07:22:52 +08:00
Tommy Johnston
5207338ac1 change: percent > 95 to percent >= 95 2024-11-01 06:58:22 +08:00
Tommy Johnston
cb476f7361 fix styling hiccup 2024-11-01 06:58:22 +08:00
tommy
f77acf9eac consider 95% or higher to be green 2024-11-01 06:58:22 +08:00
tommy
325ab38fbb use client local time for date display 2024-11-01 06:58:22 +08:00
tommy
9949b973bd fix: add key to Fragment in map for monitor items 2024-11-01 06:58:22 +08:00
tommy
6312ec6eed Add Status Header & Modify Styles 2024-11-01 06:58:22 +08:00
moonrailgun
59b874644f chore: fix ci problem 2024-11-01 00:45:19 +08:00
moonrailgun
266b08f2da refactor: update webhooks signature api guide 2024-10-31 22:20:01 +08:00
tommy
a8a47ed94d fix: retrieve date as string
Apparently prisma can automatically cast dates to UTC, formatting the date to a string in the query can prevent this.
2024-10-31 16:07:07 +08:00
moonrailgun
272505669e refactor: update amount in stripe 2024-10-30 02:57:24 +08:00
moonrailgun
6b3631eae1 feat: add webhookSignature in feed channel 2024-10-29 03:41:45 +08:00
moonrailgun
f592466d62 chore: release v1.16.4 2024-10-27 17:29:50 +08:00
moonrailgun
98298c4367 refactor: update currency symbols in feed 2024-10-27 03:16:06 +08:00
moonrailgun
09d0f02d84 feat: add stripe feed integration 2024-10-26 00:55:51 +08:00
moonrailgun
59d32e0119 chore: release v1.16.3 2024-10-25 00:22:32 +08:00
moonrailgun
1c5737e588 chore: fix ci problem and upgrade version 2024-10-24 02:46:57 +08:00
moonrailgun
ba580dd70b chore: release v1.16.2 2024-10-24 02:36:36 +08:00
moonrailgun
e402ee1688 chore: update openapi document 2024-10-23 23:53:32 +08:00
moonrailgun
1df32dc257 docs: update README 2024-10-22 23:39:56 +08:00
moonrailgun
79667a9644 fix: fix a bug which will match incorrect path #115 2024-10-22 02:26:20 +08:00
moonrailgun
554f902584 chore: fix ci problem 2024-10-22 01:16:23 +08:00
moonrailgun
fcb8f22116 feat: add prometheus report support 2024-10-22 00:37:46 +08:00
moonrailgun
f080830407 chore: release v1.16.1 2024-10-21 01:26:02 +08:00
moonrailgun
c7e20df516 feat: add timezone support #114 2024-10-20 22:47:22 +08:00
moonrailgun
83850f2981 refactor: update cronjob clear time 2024-10-20 22:29:07 +08:00
moonrailgun
3dca8fc27c feat: add workspace settings manage 2024-10-20 22:27:31 +08:00
moonrailgun
4e3fd9db64 feat: add test notify 2024-10-20 16:58:23 +08:00
moonrailgun
b4ab20ad32 chore: release v1.16.0 2024-10-20 01:12:56 +08:00
moonrailgun
7f70557c77 fix: fix reporter memory leak problem #103 2024-10-19 01:37:57 +08:00
moonrailgun
2a503ca250 chore: rename old tsconfig paths 2024-10-16 01:16:21 +08:00
moonrailgun
527f734bc4 refactor: ignore unknown sentry log 2024-10-16 01:10:47 +08:00
moonrailgun
c9f2458775 test: update md5 unit test 2024-10-16 01:09:04 +08:00
moonrailgun
f553f157dd refactor: add border radius in smtp template 2024-10-16 01:02:11 +08:00
moonrailgun
61980b37d3 refactor: let version text more prominent 2024-10-15 01:10:29 +08:00
moonrailgun
820b25baed chore: fix ci problem 2024-10-15 01:08:04 +08:00
moonrailgun
279e616bee feat: add click event for status page item which allow hide/show chart 2024-10-14 00:29:06 +08:00
moonrailgun
dcff57fe69 feat: add daily monitor data display for public 2024-10-14 00:29:06 +08:00
moonrailgun
316b95467d feat: add MonitorLatestResponse and up status summary 2024-10-14 00:29:06 +08:00
moonrailgun
e5e77dbdee refactor: change public summary display logic
old is recent data, now is monthly data
2024-10-14 00:29:06 +08:00
moonrailgun
bbb8d88116 feat: add monitor summary function 2024-10-14 00:29:06 +08:00
moonrailgun
f1513fe3f7 chore: release v1.15.8 2024-10-13 19:54:18 +08:00
moonrailgun
8c5c417a19 docs: change code command line style 2024-10-12 20:07:37 +08:00
moonrailgun
763810e8b7 docs: add shacdn to website 2024-10-12 20:07:11 +08:00
moonrailgun
c0e2ef0fe8 refactor: migrate monitor data chart to recharts and remove @ant-design/charts 2024-10-12 01:23:00 +08:00
moonrailgun
a218c22397 chore: update survey edit form 2024-10-11 01:06:25 +08:00
moonrailgun
f00163b2f1 feat: survey add webhook url field which can send webhook when receive any survey 2024-10-11 01:06:25 +08:00
moonrailgun
de572426eb feat: add survey webhook 2024-10-11 01:06:25 +08:00
moonrailgun
5d54ca1cbc feat: add survey curl example code 2024-10-10 00:09:18 +08:00
moonrailgun
29f184c15d refactor: add CodeExample component 2024-10-09 23:55:01 +08:00
moonrailgun
6474cefd89 feat: add plausible tracking(for testing) 2024-10-09 00:04:32 +08:00
moonrailgun
9b9799ec6f chore: comment sitemap to make sure its can build safe 2024-10-08 23:39:45 +08:00
moonrailgun
e51a88044f chore: use prebuilt rather than deploy build
its better to let website build by sitemap can read git info
2024-10-08 00:05:52 +08:00
moonrailgun
1e57905f32 docs: upgrade openapi 2024-10-07 23:23:00 +08:00
moonrailgun
de38363315 docs: update depenpendency to resolve issue of docusaurus build 2024-10-07 20:18:01 +08:00
moonrailgun
3d9d03296e docs: remove used blog directory 2024-10-07 17:16:27 +08:00
moonrailgun
384224cb62 docs: add sitemap to improve SEO 2024-10-07 17:16:11 +08:00
moonrailgun
a32f3d9824 chore: fix ci problem 2024-10-06 01:17:51 +08:00
moonrailgun
eaffe3ab21 chore: fix version of postman-code-generators 2024-10-05 21:36:29 +08:00
moonrailgun
43b4c9fe37 chore: fix isolated-vm version 2024-10-05 19:28:44 +08:00
moonrailgun
9e6e03117c docs: resolve build problem with update source document content 2024-10-05 18:18:02 +08:00
moonrailgun
064dbe9985 chore: update pnpm lock file to resolve some magic problem 2024-10-05 18:16:34 +08:00
moonrailgun
7bda5420c5 docs: add website more language: de, fr, ja, zh-Hans 2024-10-05 17:52:02 +08:00
moonrailgun
f5933ec054 feat: sdk add send feed function export 2024-10-05 07:58:08 +08:00
moonrailgun
7322ad741d chore: update ci run trigger path 2024-10-05 00:23:24 +08:00
moonrailgun
e46f97097a chore: upgrade @tianji/website docusaurus version 2024-10-04 22:14:25 +08:00
moonrailgun
4b7877155f feat: time event chart legend add some interaction 2024-10-04 22:01:43 +08:00
moonrailgun
f3d8f5543d feat: add realtime datarange which can visit data more easy 2024-10-04 21:40:49 +08:00
moonrailgun
6da0e6f415 feat: add ping animation in website realtime visitor 2024-10-04 21:22:36 +08:00
moonrailgun
9bc8c63fe2 perf: improve display of visitor map if data is too much 2024-10-04 21:19:20 +08:00
moonrailgun
9d559b93d1 chore: upgrade @radix-ui/react-scroll-area version 2024-10-04 03:23:37 +08:00
moonrailgun
572d96babb feat: add payload for feed event integration and send function
for easy to fetch origin data and debug later
2024-10-03 22:16:35 +08:00
moonrailgun
88fa90c2ca chore: release v1.15.7 2024-10-03 10:42:47 +08:00
moonrailgun
7301eeb82a chore: update workspace config and remove unused lock file 2024-10-03 10:37:55 +08:00
moonrailgun
e09d7eef87 chore: split website from monorepo
make sure its can easy to build in vercel
2024-10-03 10:16:52 +08:00
moonrailgun
2d5a09c79c fix: fix a problem which will make request list incorrect 2024-10-03 09:37:49 +08:00
1Luc1
1fe50092ba docs: fix update code to new version 2024-10-02 21:57:01 +08:00
moonrailgun
2cb80584cf chore: release v1.15.6 2024-10-02 19:26:27 +08:00
moonrailgun
1be03ccf53 refactor: improve install package time in docker build static stage 2024-10-02 19:26:00 +08:00
moonrailgun
5eb7696ead chore: update NODE_OPTIONS in static layer to make sure build can pass 2024-10-02 17:43:52 +08:00
moonrailgun
79b75f55e3 chore: add build dependency for build zeromq 2024-10-02 17:32:57 +08:00
moonrailgun
9f7fbafcf0 chore: release v1.15.5 2024-10-02 00:03:20 +08:00
moonrailgun
92196e4e5b feat: add webhook playground entry 2024-10-02 00:02:58 +08:00
moonrailgun
33a0a60eee feat: add webhook playground 2024-10-01 22:14:43 +08:00
moonrailgun
04dc1e98dd perf: improve avatar display timing for non-avatar user 2024-10-01 20:21:53 +08:00
moonrailgun
b778f8c982 refactor: update style of website page card 2024-10-01 20:20:51 +08:00
moonrailgun
1337eaa2c0 refactor: refactor time event chart to recharts 2024-10-01 19:26:03 +08:00
moonrailgun
055f57e087 chore: upgrade shadcn cli and add recharts 2024-10-01 07:53:17 +08:00
moonrailgun
50a35732ff feat: add zeromq to make sure lighthouse can only run one at same time
and add socketio notify to refresh list
2024-10-01 07:37:27 +08:00
moonrailgun
bb0c574893 feat: add error message for lighthouse 2024-10-01 04:58:20 +08:00
moonrailgun
6c2a093842 feat: add lighthouse score in database fields 2024-10-01 01:41:10 +08:00
moonrailgun
9cf46e679c chore: release v1.15.4 2024-10-01 01:24:29 +08:00
moonrailgun
b16a7c3c2c fix: fix login view split incorrect if not any extra login way 2024-10-01 01:23:37 +08:00
moonrailgun
fe432f1332 refactor: try to resolve no screenshot problem by remove single process.
follow https://github.com/GoogleChrome/lighthouse/issues/11537#issuecomment-799895027
2024-10-01 01:14:55 +08:00
moonrailgun
de09059e65 refactor: update dockerfile, carry back auto install dependency 2024-10-01 01:14:55 +08:00
moonrailgun
e942769af2 chore: upgrade puppeteer version to 23.4.1 2024-10-01 01:14:55 +08:00
moonrailgun
d73fa10897 chore: upgrade axios version to latest to resolve vulnerabilities 2024-10-01 01:14:55 +08:00
moonrailgun
13227416c0 chore: upgrade puppeteer usage to fit new version 2024-10-01 01:14:55 +08:00
moonrailgun
f59793d6f1 chore: upgrade puppeteer to make sure can fit with alpine image chromium version 2024-10-01 01:14:55 +08:00
moonrailgun
e6df595af8 chore: downgrade alpine version to 3.19 to avoid issue 2024-10-01 01:14:55 +08:00
moonrailgun
57ebaf6ad3 chore: improve docker build and lighthouse config 2024-10-01 01:14:55 +08:00
moonrailgun
8b6a74033c refactor: add no sandbox args in puppeteer 2024-10-01 01:14:55 +08:00
moonrailgun
23c691541d chore: docker add puppeteer support 2024-10-01 01:14:55 +08:00
284 changed files with 16354 additions and 14431 deletions

View File

@ -6,6 +6,8 @@ on:
- master - master
paths: paths:
- "src/**" - "src/**"
- "package.json"
- "pnpm-lock.yaml"
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View File

@ -1,5 +1,229 @@
## [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) ## [1.15.3](https://github.com/msgbyte/tianji/compare/v1.15.2...v1.15.3) (2024-09-24)

View File

@ -7,12 +7,28 @@ COPY ./reporter/ ./reporter/
RUN apt update RUN apt update
RUN cd reporter && go build . RUN cd reporter && go build .
# # Base ------------------------------ # Base ------------------------------
FROM node:20-alpine AS 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@9.7.1 RUN npm install -g pnpm@9.7.1
# For apprise
RUN apk add --update --no-cache python3 py3-pip g++ make 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 ------------------------------ # Tianji frontend ------------------------------
FROM base AS static FROM base AS static
WORKDIR /app/tianji WORKDIR /app/tianji
@ -22,9 +38,10 @@ ARG VERSION
COPY . . COPY . .
RUN pnpm install --frozen-lockfile RUN pnpm install --filter @tianji/client... --config.dedupe-peer-dependents=false --frozen-lockfile
ENV VITE_VERSION=$VERSION ENV VITE_VERSION=$VERSION
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN pnpm build:static RUN pnpm build:static
@ -32,6 +49,11 @@ RUN pnpm build:static
FROM base AS app FROM base AS app
WORKDIR /app/tianji 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 . . COPY . .
RUN pnpm install --filter @tianji/server... --config.dedupe-peer-dependents=false RUN pnpm install --filter @tianji/server... --config.dedupe-peer-dependents=false

View File

@ -37,9 +37,8 @@ It's good to specialize in one thing, if we are experts in related abilities we
- [x] waitlist - [x] waitlist
- [x] survey - [x] survey
- [ ] survey page - [ ] survey page
- [ ] lighthouse report - [x] lighthouse report
- [x] hooks - [x] hooks
- [ ] links
- [x] helm install support - [x] helm install support
- [x] allow install from public - [x] allow install from public
- [ ] improve monitor reporter usage - [ ] improve monitor reporter usage

View File

@ -2,6 +2,9 @@ version: '3'
services: services:
tianji: tianji:
image: moonrailgun/tianji image: moonrailgun/tianji
build:
context: ./
dockerfile: ./Dockerfile
ports: ports:
- "12345:12345" - "12345:12345"
environment: environment:

View File

@ -1,7 +1,7 @@
{ {
"name": "tianji", "name": "tianji",
"private": true, "private": true,
"version": "1.15.3", "version": "1.16.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently --kill-others npm:dev:server npm:dev:web", "dev": "concurrently --kill-others npm:dev:server npm:dev:web",
@ -52,6 +52,7 @@
"@auth/core": "0.34.1", "@auth/core": "0.34.1",
"dayjs": "1.11.10", "dayjs": "1.11.10",
"esbuild": "0.24.0", "esbuild": "0.24.0",
"postman-code-generators": "1.8.0",
"typescript": "5.5.4" "typescript": "5.5.4"
}, },
"patchedDependencies": { "patchedDependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "tianji-client-sdk", "name": "tianji-client-sdk",
"version": "1.0.3", "version": "1.1.1",
"description": "", "description": "",
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {

View 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;
}

View File

@ -2,3 +2,4 @@ export { initOpenapiSDK } from './config';
export { openApiClient } from './open'; export { openApiClient } from './open';
export * from './tracker'; export * from './tracker';
export * from './survey'; export * from './survey';
export * from './feed';

View File

@ -47,7 +47,7 @@ export const OpenAPI: OpenAPIConfig = {
PASSWORD: undefined, PASSWORD: undefined,
TOKEN: undefined, TOKEN: undefined,
USERNAME: undefined, USERNAME: undefined,
VERSION: '1.13.1', VERSION: '1.16.1',
WITH_CREDENTIALS: false, WITH_CREDENTIALS: false,
interceptors: { interceptors: {
request: new Interceptors(), request: new Interceptors(),

View File

@ -51,6 +51,21 @@ export class UserService {
}); });
} }
/**
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static userRegister(data: $OpenApiTs['/register']['post']['req']): CancelablePromise<$OpenApiTs['/register']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/register',
body: data.requestBody,
mediaType: 'application/json'
});
}
} }
export class WorkspaceService { export class WorkspaceService {
@ -84,16 +99,31 @@ export class WorkspaceService {
}); });
} }
/**
* @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 The data for the request.
* @param data.workspaceId * @param data.workspaceId
* @returns unknown Successful response * @returns unknown Successful response
* @throws ApiError * @throws ApiError
*/ */
public static workspaceDelete(data: $OpenApiTs['/workspace//{workspaceId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}']['delete']['res'][200]> { public static workspaceDelete(data: $OpenApiTs['/workspace//{workspaceId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/del']['delete']['res'][200]> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'DELETE', method: 'DELETE',
url: '/workspace//{workspaceId}', url: '/workspace//{workspaceId}/del',
path: { path: {
workspaceId: data.workspaceId workspaceId: data.workspaceId
} }
@ -116,6 +146,25 @@ export class WorkspaceService {
}); });
} }
/**
* @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 The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -428,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 The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -449,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 { export class MonitorService {
@ -475,28 +604,10 @@ export class MonitorService {
* @returns unknown Successful response * @returns unknown Successful response
* @throws ApiError * @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, { return __request(OpenAPI, {
method: 'GET', method: 'GET',
url: '/workspace/{workspaceId}/monitor/{monitorId}', url: '/workspace/{workspaceId}/monitor/{monitorId}/get',
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}',
path: { path: {
workspaceId: data.workspaceId, workspaceId: data.workspaceId,
monitorId: data.monitorId monitorId: data.monitorId
@ -538,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 The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -605,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 The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -1048,10 +1213,10 @@ export class SurveyService {
* @returns unknown Successful response * @returns unknown Successful response
* @throws ApiError * @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, { return __request(OpenAPI, {
method: 'GET', method: 'GET',
url: '/workspace/{workspaceId}/survey/{surveyId}', url: '/workspace/{workspaceId}/survey/{surveyId}/get',
path: { path: {
workspaceId: data.workspaceId, workspaceId: data.workspaceId,
surveyId: data.surveyId surveyId: data.surveyId
@ -1316,6 +1481,7 @@ export class FeedService {
* @param data.channelId * @param data.channelId
* @param data.limit * @param data.limit
* @param data.cursor * @param data.cursor
* @param data.archived
* @returns unknown Successful response * @returns unknown Successful response
* @throws ApiError * @throws ApiError
*/ */
@ -1329,7 +1495,8 @@ export class FeedService {
}, },
query: { query: {
limit: data.limit, limit: data.limit,
cursor: data.cursor cursor: data.cursor,
archived: data.archived
} }
}); });
} }
@ -1360,10 +1527,10 @@ export class FeedService {
* @returns unknown Successful response * @returns unknown Successful response
* @throws ApiError * @throws ApiError
*/ */
public static feedDeleteChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}']['delete']['res'][200]> { 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, { return __request(OpenAPI, {
method: 'DELETE', method: 'DELETE',
url: '/workspace/{workspaceId}/feed/{channelId}', url: '/workspace/{workspaceId}/feed/{channelId}/del',
path: { path: {
workspaceId: data.workspaceId, workspaceId: data.workspaceId,
channelId: data.channelId channelId: data.channelId
@ -1390,6 +1557,86 @@ export class FeedService {
}); });
} }
/**
* @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 * integrate with github webhook
* @param data The data for the request. * @param data The data for the request.
@ -1426,4 +1673,22 @@ export class FeedService {
}); });
} }
/**
* 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
}
});
}
} }

View File

@ -43,15 +43,15 @@ export type $OpenApiTs = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt: string | null; deletedAt: string | null;
currentWorkspace: { currentWorkspaceId: string | null;
id: string;
name: string;
};
workspaces: Array<{ workspaces: Array<{
role: string; role: string;
workspace: { workspace: {
id: string; id: string;
name: string; name: string;
settings: {
[key: string]: unknown;
};
}; };
}>; }>;
}; };
@ -82,15 +82,55 @@ export type $OpenApiTs = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt: string | null; deletedAt: string | null;
currentWorkspace: { currentWorkspaceId: string | null;
id: string;
name: string;
};
workspaces: Array<{ workspaces: Array<{
role: string; role: string;
workspace: { workspace: {
id: string; id: string;
name: string; name: string;
settings: {
[key: string]: unknown;
};
};
}>;
};
token: string;
};
};
};
};
'/register': {
post: {
req: {
requestBody: {
username: string;
password: string;
};
};
res: {
/**
* Successful response
*/
200: {
info: {
id: string;
role: string;
username: string;
nickname: string | null;
avatar: string | null;
email: string | null;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
currentWorkspaceId: string | null;
workspaces: Array<{
role: string;
workspace: {
id: string;
name: string;
settings: {
[key: string]: unknown;
};
}; };
}>; }>;
}; };
@ -120,15 +160,15 @@ export type $OpenApiTs = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt: string | null; deletedAt: string | null;
currentWorkspace: { currentWorkspaceId: string | null;
id: string;
name: string;
};
workspaces: Array<{ workspaces: Array<{
role: string; role: string;
workspace: { workspace: {
id: string; id: string;
name: string; name: string;
settings: {
[key: string]: unknown;
};
}; };
}>; }>;
}; };
@ -156,22 +196,44 @@ export type $OpenApiTs = {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
deletedAt: string | null; deletedAt: string | null;
currentWorkspace: { currentWorkspaceId: string | null;
id: string;
name: string;
};
workspaces: Array<{ workspaces: Array<{
role: string; role: string;
workspace: { workspace: {
id: string; id: string;
name: string; name: string;
settings: {
[key: string]: unknown;
};
}; };
}>; }>;
}; };
}; };
}; };
}; };
'/workspace//{workspaceId}': { '/workspace//rename': {
patch: {
req: {
requestBody: {
workspaceId: string;
name: string;
};
};
res: {
/**
* Successful response
*/
200: {
id: string;
name: string;
settings: {
[key: string]: unknown;
};
};
};
};
};
'/workspace//{workspaceId}/del': {
delete: { delete: {
req: { req: {
workspaceId: string; workspaceId: string;
@ -209,11 +271,35 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/workspace//{workspaceId}/updateSettings': {
post: {
req: {
requestBody: {
settings: {
[key: string]: unknown;
};
};
workspaceId: string;
};
res: {
/**
* Successful response
*/
200: {
id: string;
name: string;
settings: {
[key: string]: unknown;
};
};
};
};
};
'/workspace//{workspaceId}/invite': { '/workspace//{workspaceId}/invite': {
post: { post: {
req: { req: {
requestBody: { requestBody: {
targetUserEmail: string; emailOrId: string;
}; };
workspaceId: string; workspaceId: string;
}; };
@ -501,6 +587,31 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/workspace/{workspaceId}/website/{websiteId}': {
delete: {
req: {
websiteId: string;
workspaceId: string;
};
res: {
/**
* Successful response
*/
200: {
id: string;
workspaceId: string;
name: string;
domain: string | null;
shareId: string | null;
resetAt: string | null;
monitorId: string | null;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
};
};
};
};
'/workspace/{workspaceId}/website/{websiteId}/update': { '/workspace/{workspaceId}/website/{websiteId}/update': {
put: { put: {
req: { req: {
@ -531,6 +642,68 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport': {
post: {
req: {
requestBody: {
url: string;
};
websiteId: string;
workspaceId: string;
};
res: {
/**
* Error response
*/
200: {
message: string;
code: string;
issues?: Array<{
message: string;
}>;
};
};
};
};
'/workspace/{workspaceId}/website/{websiteId}/getLighthouseReport': {
get: {
req: {
cursor?: string;
limit?: number;
websiteId: string;
workspaceId: string;
};
res: {
/**
* Successful response
*/
200: {
items: Array<{
id: string;
status: 'Pending' | 'Success' | 'Failed';
url: string;
createdAt: string;
}>;
nextCursor?: string;
};
};
};
};
'/lighthouse/{lighthouseId}': {
get: {
req: {
lighthouseId: string;
};
res: {
/**
* Successful response
*/
200: {
[key: string]: unknown;
};
};
};
};
'/workspace/{workspaceId}/monitor/all': { '/workspace/{workspaceId}/monitor/all': {
get: { get: {
req: { req: {
@ -561,7 +734,7 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/workspace/{workspaceId}/monitor/{monitorId}': { '/workspace/{workspaceId}/monitor/{monitorId}/get': {
get: { get: {
req: { req: {
monitorId: string; monitorId: string;
@ -591,9 +764,43 @@ export type $OpenApiTs = {
} | null; } | null;
}; };
}; };
delete: { };
'/monitor/getPublicInfo': {
post: {
req: { req: {
monitorId: string; requestBody: {
monitorIds: Array<(string)>;
};
};
res: {
/**
* Successful response
*/
200: Array<{
id: string;
name: string;
type: string;
trendingMode: boolean;
}>;
};
};
};
'/workspace/{workspaceId}/monitor/upsert': {
post: {
req: {
requestBody: {
id?: string;
name: string;
type: string;
active?: boolean;
interval?: number;
maxRetries?: number;
trendingMode?: boolean;
notificationIds?: Array<(string)>;
payload: {
[key: string]: unknown;
};
};
workspaceId: string; workspaceId: string;
}; };
res: { res: {
@ -618,41 +825,10 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/monitor/getPublicInfo': { '/workspace/{workspaceId}/monitor/{monitorId}/del': {
post: { delete: {
req: { req: {
requestBody: { monitorId: string;
monitorIds: Array<(string)>;
};
};
res: {
/**
* Successful response
*/
200: Array<{
id: string;
name: string;
type: string;
}>;
};
};
};
'/workspace/{workspaceId}/monitor/upsert': {
post: {
req: {
requestBody: {
id?: string;
name: string;
type: string;
active?: boolean;
interval?: number;
maxRetries?: number;
trendingMode?: boolean;
notificationIds?: Array<(string)>;
payload: {
[key: string]: unknown;
};
};
workspaceId: string; workspaceId: string;
}; };
res: { res: {
@ -745,6 +921,42 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/workspace/{workspaceId}/monitor/{monitorId}/publicSummary': {
get: {
req: {
monitorId: string;
workspaceId: string;
};
res: {
/**
* Successful response
*/
200: Array<{
day: string;
totalCount: number;
upCount: number;
upRate: number;
}>;
};
};
};
'/workspace/{workspaceId}/monitor/{monitorId}/publicData': {
get: {
req: {
monitorId: string;
workspaceId: string;
};
res: {
/**
* Successful response
*/
200: Array<{
value: number;
createdAt: string;
}>;
};
};
};
'/workspace/{workspaceId}/monitor/{monitorId}/dataMetrics': { '/workspace/{workspaceId}/monitor/{monitorId}/dataMetrics': {
get: { get: {
req: { req: {
@ -864,6 +1076,9 @@ export type $OpenApiTs = {
slug: string; slug: string;
title: string; title: string;
description: string; description: string;
body: {
[key: string]: unknown;
};
monitorList: Array<{ monitorList: Array<{
id: string; id: string;
showCurrent?: boolean; showCurrent?: boolean;
@ -890,6 +1105,9 @@ export type $OpenApiTs = {
slug: string; slug: string;
title: string; title: string;
description: string; description: string;
body: {
[key: string]: unknown;
};
monitorList: Array<{ monitorList: Array<{
id: string; id: string;
showCurrent?: boolean; showCurrent?: boolean;
@ -908,6 +1126,9 @@ export type $OpenApiTs = {
slug: string; slug: string;
title: string; title: string;
description?: string; description?: string;
body?: {
[key: string]: unknown;
};
monitorList?: Array<{ monitorList?: Array<{
id: string; id: string;
showCurrent?: boolean; showCurrent?: boolean;
@ -926,6 +1147,9 @@ export type $OpenApiTs = {
slug: string; slug: string;
title: string; title: string;
description: string; description: string;
body: {
[key: string]: unknown;
};
monitorList: Array<{ monitorList: Array<{
id: string; id: string;
showCurrent?: boolean; showCurrent?: boolean;
@ -945,6 +1169,9 @@ export type $OpenApiTs = {
slug?: string; slug?: string;
title?: string; title?: string;
description?: string; description?: string;
body?: {
[key: string]: unknown;
};
monitorList?: Array<{ monitorList?: Array<{
id: string; id: string;
showCurrent?: boolean; showCurrent?: boolean;
@ -963,6 +1190,9 @@ export type $OpenApiTs = {
slug: string; slug: string;
title: string; title: string;
description: string; description: string;
body: {
[key: string]: unknown;
};
monitorList: Array<{ monitorList: Array<{
id: string; id: string;
showCurrent?: boolean; showCurrent?: boolean;
@ -990,6 +1220,9 @@ export type $OpenApiTs = {
slug: string; slug: string;
title: string; title: string;
description: string; description: string;
body: {
[key: string]: unknown;
};
monitorList: Array<{ monitorList: Array<{
id: string; id: string;
showCurrent?: boolean; showCurrent?: boolean;
@ -1232,13 +1465,14 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}>; }>;
}; };
}; };
}; };
'/workspace/{workspaceId}/survey/{surveyId}': { '/workspace/{workspaceId}/survey/{surveyId}/get': {
get: { get: {
req: { req: {
surveyId: string; surveyId: string;
@ -1262,6 +1496,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} | null; } | null;
@ -1347,6 +1582,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
}; };
workspaceId: string; workspaceId: string;
}; };
@ -1368,6 +1604,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
@ -1389,6 +1626,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds?: Array<(string)>; feedChannelIds?: Array<(string)>;
feedTemplate?: string; feedTemplate?: string;
webhookUrl?: string;
}; };
surveyId: string; surveyId: string;
workspaceId: string; workspaceId: string;
@ -1411,6 +1649,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
@ -1441,6 +1680,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
@ -1608,6 +1848,7 @@ export type $OpenApiTs = {
'/workspace/{workspaceId}/feed/{channelId}/fetchEventsByCursor': { '/workspace/{workspaceId}/feed/{channelId}/fetchEventsByCursor': {
get: { get: {
req: { req: {
archived?: boolean;
channelId: string; channelId: string;
cursor?: string; cursor?: string;
limit?: number; limit?: number;
@ -1631,6 +1872,10 @@ export type $OpenApiTs = {
senderName?: string | null; senderName?: string | null;
url?: string | null; url?: string | null;
important: boolean; important: boolean;
archived: boolean;
payload?: {
[key: string]: unknown;
} | null;
}>; }>;
nextCursor?: string; nextCursor?: string;
}; };
@ -1663,7 +1908,7 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/workspace/{workspaceId}/feed/{channelId}': { '/workspace/{workspaceId}/feed/{channelId}/del': {
delete: { delete: {
req: { req: {
channelId: string; channelId: string;
@ -1696,6 +1941,9 @@ export type $OpenApiTs = {
senderId?: string | null; senderId?: string | null;
senderName?: string | null; senderName?: string | null;
important: boolean; important: boolean;
payload?: {
[key: string]: unknown;
} | null;
}; };
}; };
res: { res: {
@ -1715,6 +1963,85 @@ export type $OpenApiTs = {
senderName?: string | null; senderName?: string | null;
url?: string | null; url?: string | null;
important: boolean; important: boolean;
archived: boolean;
payload?: {
[key: string]: unknown;
} | null;
};
};
};
};
'/feed/{channelId}/{eventId}/archive': {
patch: {
req: {
channelId: string;
eventId: string;
requestBody: {
workspaceId: string;
};
};
res: {
/**
* Successful response
*/
200: unknown;
};
};
};
'/feed/{channelId}/{eventId}/unarchive': {
patch: {
req: {
channelId: string;
eventId: string;
requestBody: {
workspaceId: string;
};
};
res: {
/**
* Successful response
*/
200: unknown;
};
};
};
'/feed/{channelId}/clearAllArchivedEvents': {
patch: {
req: {
channelId: string;
requestBody: {
workspaceId: string;
};
};
res: {
/**
* Error response
*/
200: {
message: string;
code: string;
issues?: Array<{
message: string;
}>;
};
};
};
};
'/feed/playground/{workspaceId}': {
post: {
req: {
workspaceId: string;
};
res: {
/**
* Error response
*/
200: {
message: string;
code: string;
issues?: Array<{
message: string;
}>;
}; };
}; };
}; };
@ -1757,4 +2084,23 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/feed/{channelId}/sentry': {
post: {
req: {
channelId: string;
};
res: {
/**
* Error response
*/
200: {
message: string;
code: string;
issues?: Array<{
message: string;
}>;
};
};
};
};
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "tianji-client-react", "name": "tianji-client-react",
"version": "1.0.0", "version": "1.0.1",
"description": "", "description": "",
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {

View File

@ -6,7 +6,7 @@ import {
} from 'tianji-client-sdk'; } from 'tianji-client-sdk';
type SurveyInfo = type SurveyInfo =
openApiClient.$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['res']['200']; openApiClient.$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/get']['get']['res']['200'];
interface UseTianjiSurveyOptions { interface UseTianjiSurveyOptions {
baseUrl?: string; baseUrl?: string;

File diff suppressed because it is too large Load Diff

View File

@ -59,7 +59,10 @@ func main() {
interval := *Interval 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("Start reporting...")
log.Println("Mode:", *Mode) log.Println("Mode:", *Mode)
@ -78,10 +81,10 @@ func main() {
if *Mode == "udp" { if *Mode == "udp" {
sendUDPPack(*parsedURL, payload) sendUDPPack(*parsedURL, payload)
} else { } 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 * 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) jsonData, err := jsoniter.Marshal(payload)
if err != nil { if err != nil {
log.Println("Error encoding JSON:", err) 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("Content-Type", "application/json")
req.Header.Set("x-tianji-report-version", version) req.Header.Set("x-tianji-report-version", version)
client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
log.Println("Send request error:", err) log.Println("Send request error:", err)

View File

@ -109,7 +109,24 @@ export function useSocket() {
return { socket, emit, subscribe }; 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, name: keyof SubscribeEventMap,
defaultData: T defaultData: T
): T { ): T {

View File

@ -4,10 +4,10 @@ import { Button } from './ui/button';
import { LuCopy, LuCopyCheck } from 'react-icons/lu'; import { LuCopy, LuCopyCheck } from 'react-icons/lu';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { ScrollBar } from './ui/scroll-area';
export const CodeBlock: React.FC<{ export const CodeBlock: React.FC<{
code: string; code: string;
height?: number;
}> = React.memo((props) => { }> = React.memo((props) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@ -22,7 +22,12 @@ export const CodeBlock: React.FC<{
return ( return (
<div className="group relative w-full overflow-auto"> <div className="group relative w-full overflow-auto">
<pre className="max-h-96 rounded-sm border border-zinc-200 bg-zinc-100 p-3 pr-12 text-sm dark:border-zinc-800 dark:bg-zinc-900"> <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> <code>{props.code}</code>
</pre> </pre>
<Button <Button

View 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';

View File

@ -14,6 +14,8 @@ import {
LuAreaChart, LuAreaChart,
LuBellDot, LuBellDot,
LuFilePieChart, LuFilePieChart,
LuKanbanSquare,
LuKeyRound,
LuMonitorDot, LuMonitorDot,
LuSearch, LuSearch,
LuServer, LuServer,
@ -171,6 +173,22 @@ export const CommandPanel: React.FC<CommandPanelProps> = React.memo((props) => {
<LuBellDot className="mr-2 h-4 w-4" /> <LuBellDot className="mr-2 h-4 w-4" />
<span>{t('Notifications')}</span> <span>{t('Notifications')}</span>
</CommandItem> </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> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command>

View 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';

View File

@ -27,6 +27,15 @@ export const DateFilter: React.FC<DateFilterProps> = React.memo((props) => {
setShowDropdown(false); setShowDropdown(false);
}, },
items: compact([ items: compact([
{
label: t('Realtime'),
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Realtime });
},
},
{
type: 'divider',
},
{ {
label: t('Today'), label: t('Today'),
onClick: () => { onClick: () => {

View File

@ -1,10 +1,9 @@
import { useResizeObserver } from '@/hooks/useResizeObserver'; import { useResizeObserver } from '@/hooks/useResizeObserver';
import { getStatusBgColorClassName, HealthStatus } from '@/utils/health';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import clsx from 'clsx'; import clsx from 'clsx';
import React from 'react'; import React from 'react';
type HealthStatus = 'health' | 'error' | 'warning' | 'none';
export interface HealthBarBeat { export interface HealthBarBeat {
title?: string; title?: string;
status: HealthStatus; status: HealthStatus;
@ -52,12 +51,7 @@ export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
'h-4 w-[5px]': size === 'small', 'h-4 w-[5px]': size === 'small',
'h-8 w-2': size === 'large', 'h-8 w-2': size === 'large',
}, },
{ getStatusBgColorClassName(beat.status)
'bg-green-500': beat.status === 'health',
'bg-red-600': beat.status === 'error',
'bg-yellow-400': beat.status === 'warning',
'bg-gray-400': beat.status === 'none',
}
)} )}
/> />
))} ))}

View File

@ -1,63 +1,115 @@
import { useMemo } from 'react';
import { useTheme } from '../hooks/useTheme'; import { useTheme } from '../hooks/useTheme';
import { DateUnit } from '@tianji/shared'; import { DateUnit } from '@tianji/shared';
import React from 'react'; import React, { useState } from 'react';
import { formatDate, formatDateWithUnit } from '../utils/date'; import { formatDateWithUnit } from '../utils/date';
import { Column, ColumnConfig } from '@ant-design/charts'; import {
import { useIsMobile } from '@/hooks/useIsMobile'; Area,
import { useTranslation } from '@i18next-toolkit/react'; 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<{ export const TimeEventChart: React.FC<{
labelMapping?: Record<string, string>; labelMapping?: Record<string, string>;
data: { x: string; y: number; type: string }[]; data: { date: string; [key: string]: number | string }[];
unit: DateUnit; unit: DateUnit;
}> = React.memo((props) => { }> = React.memo((props) => {
const { colors } = useTheme(); const { colors } = useTheme();
const isMobile = useIsMobile(); const [calcStrokeDasharray, strokes] = useStrokeDasharray({});
const { t } = useTranslation(); const [strokeDasharray, setStrokeDasharray] = React.useState([...strokes]);
const handleAnimationEnd = () => setStrokeDasharray([...strokes]);
const labelMapping = props.labelMapping ?? { const getStrokeDasharray = (name: string) => {
pageview: t('pageview'), const lineDasharray = strokeDasharray.find((s) => s.name === name);
session: t('session'), return lineDasharray ? lineDasharray.strokeDasharray : undefined;
}; };
const [selectedItem, setSelectedItem] = useState<string[]>(['pv', 'uv']);
const config = useMemo( return (
() => <ChartContainer config={chartConfig}>
({ <AreaChart
data: props.data, data={props.data}
isStack: true, margin={{ top: 10, right: 0, left: 0, bottom: 0 }}
xField: 'x', >
yField: 'y', <defs>
seriesField: 'type', <linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
label: { <stop offset="5%" stopColor={colors.chart.pv} stopOpacity={0.8} />
position: 'middle' as const, <stop offset="95%" stopColor={colors.chart.pv} stopOpacity={0} />
style: { </linearGradient>
fill: '#FFFFFF', <linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
opacity: 0.6, <stop offset="5%" stopColor={colors.chart.uv} stopOpacity={0.8} />
}, <stop offset="95%" stopColor={colors.chart.uv} stopOpacity={0} />
}, </linearGradient>
tooltip: { </defs>
title: (t) => formatDate(t), <Customized component={calcStrokeDasharray} />
}, <XAxis
color: [colors.chart.pv, colors.chart.uv], dataKey="date"
legend: isMobile tickFormatter={(text) => formatDateWithUnit(text, props.unit)}
? false />
: { <YAxis mirror />
itemName: { <ChartLegend
formatter: (text) => labelMapping[text] ?? text, content={
}, <ChartLegendContent
}, selectedItem={selectedItem}
xAxis: { onItemClick={(item) => {
label: { setSelectedItem((selected) => {
autoHide: true, if (selected.includes(item.value)) {
autoRotate: false, return selected.filter((s) => s !== item.value);
formatter: (text) => formatDateWithUnit(text, props.unit), } else {
}, return [...selected, item.value];
}, }
}) satisfies ColumnConfig, });
[props.data, props.unit, props.labelMapping] }}
/>
}
/>
<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'; TimeEventChart.displayName = 'TimeEventChart';

View 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';

View 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';

View 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';

View File

@ -1,31 +1,33 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Card, CardContent, CardHeader } from '@/components/ui/card';
import React from 'react'; import React from 'react';
import { CodeBlock } from '../CodeBlock';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; import { CodeExample } from '../CodeExample';
export const FeedApiGuide: React.FC<{ channelId: string }> = React.memo( interface FeedApiGuideProps {
(props) => { channelId: string;
webhookSignature?: string;
}
export const FeedApiGuide: React.FC<FeedApiGuideProps> = React.memo((props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Card className="w-full overflow-hidden"> <Card className="w-full overflow-hidden">
<CardHeader> <CardHeader>
<div>{t('You can send any message into this channel with:')}</div> <div>{t('You can send a message to this channel with:')}</div>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-5 overflow-hidden"> <CardContent className="flex w-full flex-col gap-5 overflow-hidden">
<Tabs defaultValue="curl"> <CodeExample
<TabsList> example={{
<TabsTrigger value="curl">curl</TabsTrigger> curl: {
<TabsTrigger value="fetch">fetch</TabsTrigger> label: 'curl',
</TabsList> code: generateCurlCode(props.channelId, props.webhookSignature),
<TabsContent value="curl"> },
<CodeBlock code={generateCurlCode(props.channelId)} /> fetch: {
</TabsContent> label: 'fetch',
<TabsContent value="fetch"> code: generateFetchCode(props.channelId, props.webhookSignature),
<CodeBlock code={generateFetchCode(props.channelId)} /> },
</TabsContent> }}
</Tabs> />
<div className="pl-2 font-bold">{t('OR')}</div> <div className="pl-2 font-bold">{t('OR')}</div>
@ -33,12 +35,24 @@ export const FeedApiGuide: React.FC<{ channelId: string }> = React.memo(
</CardContent> </CardContent>
</Card> </Card>
); );
} });
);
FeedApiGuide.displayName = 'FeedApiGuide'; FeedApiGuide.displayName = 'FeedApiGuide';
function generateCurlCode(channelId: string) { function generateCurlCode(channelId: string, webhookSignature?: string) {
const code = `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\ 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" \\ -H "Content-Type: application/json" \\
-d '{ -d '{
"eventName": "test name", "eventName": "test name",
@ -47,12 +61,27 @@ function generateCurlCode(channelId: string) {
"source": "custom", "source": "custom",
"important": false "important": false
}'`; }'`;
return code;
} }
function generateFetchCode(channelId: string) { function generateFetchCode(channelId: string, webhookSignature?: string) {
const code = `fetch('${window.location.origin}/open/feed/${channelId}/send', { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -65,6 +94,4 @@ function generateFetchCode(channelId: string) {
important: false, important: false,
}) })
})`; })`;
return code;
} }

View File

@ -24,9 +24,13 @@ import {
SelectValue, SelectValue,
} from '../ui/select'; } from '../ui/select';
import { NotificationPicker } from '../notification/NotificationPicker'; import { NotificationPicker } from '../notification/NotificationPicker';
import { LuRefreshCcw } from 'react-icons/lu';
import md5 from 'md5';
import dayjs from 'dayjs';
const addFormSchema = z.object({ const addFormSchema = z.object({
name: z.string(), name: z.string(),
webhookSignature: z.string().default(''),
notificationIds: z.array(z.string()).default([]), notificationIds: z.array(z.string()).default([]),
notifyFrequency: z.enum(['none', 'event', 'day', 'week', 'month']), notifyFrequency: z.enum(['none', 'event', 'day', 'week', 'month']),
}); });
@ -45,6 +49,7 @@ export const FeedChannelEditForm: React.FC<FeedChannelEditFormProps> =
resolver: zodResolver(addFormSchema), resolver: zodResolver(addFormSchema),
defaultValues: props.defaultValues ?? { defaultValues: props.defaultValues ?? {
name: 'New Channel', name: 'New Channel',
webhookSignature: '',
notificationIds: [], notificationIds: [],
notifyFrequency: 'none', notifyFrequency: 'none',
}, },
@ -79,6 +84,38 @@ export const FeedChannelEditForm: React.FC<FeedChannelEditFormProps> =
)} )}
/> />
<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 <FormField
control={form.control} control={form.control}
name="notificationIds" name="notificationIds"

View File

@ -1,13 +1,14 @@
import React from 'react'; import React from 'react';
import { LuGithub, LuPlug } from 'react-icons/lu'; import { LuGithub, LuPlug, LuTestTube2 } from 'react-icons/lu';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { CodeBlock } from '../CodeBlock'; import { CodeBlock } from '../CodeBlock';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { SiSentry } from 'react-icons/si'; import { SiSentry } from 'react-icons/si';
import { Link } from '@tanstack/react-router'; import { FaStripe } from 'react-icons/fa6';
export const FeedIntegration: React.FC<{ export const FeedIntegration: React.FC<{
feedId: string; feedId: string;
webhookSignature: string;
}> = React.memo((props) => { }> = React.memo((props) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -58,9 +59,32 @@ export const FeedIntegration: React.FC<{
} }
/> />
<FeedIntegrationItem
icon={<FaStripe size={32} />}
label="Stripe"
content={
<div>
<div className="text-lg font-bold">{t('Receive Webhooks')}</div>
<div>{t('Add sentry webhook with url')}:</div>
<CodeBlock
code={`${window.location.origin}/open/feed/${props.feedId}/stripe`}
/>
</div>
}
/>
<div onClick={() => window.open('/feed/playground', '_blank')}>
<FeedIntegrationItemTrigger
icon={<LuTestTube2 size={32} />}
label={t('Playground')}
/>
</div>
<FeedIntegrationItem <FeedIntegrationItem
icon={<LuPlug size={32} />} icon={<LuPlug size={32} />}
label="Custom" label={t('Custom')}
content={ content={
<div> <div>
<div className="text-lg font-bold">{t('Custom Request')}</div> <div className="text-lg font-bold">{t('Custom Request')}</div>
@ -69,7 +93,7 @@ export const FeedIntegration: React.FC<{
<CodeBlock <CodeBlock
code={`POST ${window.location.origin}/open/feed/${props.feedId}/send code={`POST ${window.location.origin}/open/feed/${props.feedId}/send
${props.webhookSignature ? `\nHeader:\nX-Webhook-Signature: ${props.webhookSignature}\n` : ''}
Body Body
{ {
eventName: "", eventName: "",
@ -97,10 +121,7 @@ const FeedIntegrationItem: React.FC<{
return ( return (
<Popover> <Popover>
<PopoverTrigger> <PopoverTrigger>
<div className="border-muted hover:bg-muted flex h-20 w-20 cursor-pointer flex-col items-center justify-center rounded-lg border p-2 text-center"> <FeedIntegrationItemTrigger icon={props.icon} label={props.label} />
<div className="mb-1">{props.icon}</div>
<div>{props.label}</div>
</div>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-screen sm:w-[640px]"> <PopoverContent className="w-screen sm:w-[640px]">
{props.content} {props.content}
@ -109,3 +130,16 @@ const FeedIntegrationItem: React.FC<{
); );
}); });
FeedIntegrationItem.displayName = 'FeedIntegrationItem'; FeedIntegrationItem.displayName = 'FeedIntegrationItem';
const FeedIntegrationItemTrigger: React.FC<{
icon: React.ReactNode;
label: string;
}> = React.memo((props) => {
return (
<div className="border-muted hover:bg-muted flex h-20 w-20 cursor-pointer flex-col items-center justify-center rounded-lg border p-2 text-center">
<div className="mb-1">{props.icon}</div>
<div className="text-sm">{props.label}</div>
</div>
);
});
FeedIntegrationItemTrigger.displayName = 'FeedIntegrationItemTrigger';

View File

@ -23,7 +23,7 @@ import {
useUserInfo, useUserInfo,
useUserStore, useUserStore,
} from '@/store/user'; } from '@/store/user';
import { languages } from '@/utils/constants'; import { languages } from '@/utils/i18n';
import { useTranslation, setLanguage } from '@i18next-toolkit/react'; import { useTranslation, setLanguage } from '@i18next-toolkit/react';
import { useNavigate } from '@tanstack/react-router'; import { useNavigate } from '@tanstack/react-router';
import { version } from '@/utils/env'; import { version } from '@/utils/env';
@ -76,7 +76,7 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
<Avatar size={props.isCollapsed ? 'sm' : 'default'}> <Avatar size={props.isCollapsed ? 'sm' : 'default'}>
{userInfo?.avatar && <AvatarImage src={userInfo.avatar} />} {userInfo?.avatar && <AvatarImage src={userInfo.avatar} />}
<AvatarFallback> <AvatarFallback delayMs={userInfo?.avatar ? undefined : 0}>
{nickname.substring(0, 2).toUpperCase()} {nickname.substring(0, 2).toUpperCase()}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
@ -234,7 +234,7 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
</DropdownMenuPortal> </DropdownMenuPortal>
</DropdownMenuSub> </DropdownMenuSub>
<DropdownMenuLabel className="text-muted-foreground dark:text-muted"> <DropdownMenuLabel className="text-gray-500">
v{version} v{version}
</DropdownMenuLabel> </DropdownMenuLabel>
</DropdownMenuContent> </DropdownMenuContent>

View File

@ -0,0 +1,49 @@
import { useTheme } from '@/hooks/useTheme';
import { get } from 'lodash-es';
import React from 'react';
import { useMemo } from 'react';
import { Rectangle } from 'recharts';
export const CustomizedErrorArea: React.FC = (props) => {
const { colors } = useTheme();
const y = get(props, 'offset.top', 10);
const height = get(props, 'offset.height', 160);
const points = get(props, 'formattedGraphicalItems.0.props.points', []) as {
x: number;
y: number | null;
}[];
const errorArea = useMemo(() => {
const _errorArea: { x: number; width: number }[] = [];
let prevX: number | null = null;
points.forEach((item, i, arr) => {
if (i === 0 && !item.y) {
prevX = 0;
} else if (!item.y && prevX === null && arr[i - 1].y) {
prevX = arr[i - 1].x;
} else if (item.y && prevX !== null) {
_errorArea.push({
x: prevX,
width: item.x - prevX,
});
prevX = null;
}
});
return _errorArea;
}, [points]);
return errorArea.map((area, i) => {
return (
<Rectangle
key={i}
width={area.width}
height={height}
x={area.x}
y={y}
fill={colors.chart.error}
/>
);
});
};
CustomizedErrorArea.displayName = 'CustomizedErrorArea';

View File

@ -1,13 +1,35 @@
import { AreaConfig, Area } from '@ant-design/charts';
import { Select } from 'antd'; import { Select } from 'antd';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { max, min, uniqBy } from 'lodash-es'; import { get, takeRight, uniqBy } from 'lodash-es';
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { useSocketSubscribeList } from '../../api/socketio'; import { useSocketSubscribeList } from '../../api/socketio';
import { trpc } from '../../api/trpc'; import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user'; import { useCurrentWorkspaceId } from '../../store/user';
import { getMonitorProvider, getProviderDisplay } from './provider'; import { getMonitorProvider, getProviderDisplay } from './provider';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '../ui/chart';
import {
Area,
AreaChart,
CartesianGrid,
Customized,
Rectangle,
XAxis,
YAxis,
} from 'recharts';
import { useTheme } from '@/hooks/useTheme';
import { CustomizedErrorArea } from './CustomizedErrorArea';
const chartConfig = {
value: {
label: <span className="text-sm font-bold">Result</span>,
},
} satisfies ChartConfig;
export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo( export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
(props) => { (props) => {
@ -15,6 +37,7 @@ export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const { monitorId } = props; const { monitorId } = props;
const [rangeType, setRangeType] = useState('recent'); const [rangeType, setRangeType] = useState('recent');
const { colors } = useTheme();
const subscribedDataList = useSocketSubscribeList( const subscribedDataList = useSocketSubscribeList(
'onMonitorReceiveNewData', 'onMonitorReceiveNewData',
{ {
@ -61,100 +84,26 @@ export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
const providerInfo = getMonitorProvider(monitorInfo?.type ?? ''); const providerInfo = getMonitorProvider(monitorInfo?.type ?? '');
const { data, annotations } = useMemo(() => { const { data } = useMemo(() => {
const annotations: AreaConfig['annotations'] = [];
let start: number | null = null;
let fetchedData = rangeType === 'recent' ? _recentData : _data; let fetchedData = rangeType === 'recent' ? _recentData : _data;
const data = uniqBy( const data = takeRight(
[...fetchedData, ...subscribedDataList], uniqBy([...fetchedData, ...subscribedDataList], 'createdAt'),
'createdAt' fetchedData.length
).map((d, i, arr) => { ).map((d, i, arr) => {
const value = d.value > 0 ? d.value : null; const value = d.value > 0 ? d.value : null;
const time = dayjs(d.createdAt).valueOf(); const time = dayjs(d.createdAt).valueOf();
if (!value && !start && arr[i - 1]) {
start = dayjs(arr[i - 1]['createdAt']).valueOf();
} else if (value && start) {
annotations.push({
type: 'region',
start: [start, 'min'],
end: [time, 'max'],
style: {
fill: 'red',
fillOpacity: 0.25,
},
});
start = null;
}
return { return {
value, value,
time, time,
}; };
}); });
return { data, annotations }; return { data };
}, [_recentData, _data, subscribedDataList]); }, [_recentData, _data, subscribedDataList]);
const config = useMemo<AreaConfig>(() => {
const values = data.map((d) => d.value);
const maxValue = max(values) ?? 0;
const minValue = min(values) ?? 0;
const isTrendingMode = monitorInfo?.trendingMode ?? false; // if true, y axis not start from 0 const isTrendingMode = monitorInfo?.trendingMode ?? false; // if true, y axis not start from 0
const yMin = isTrendingMode
? Math.max(minValue - (maxValue - minValue) / 10, 0)
: 0;
return {
data,
height: 200,
xField: 'time',
yField: 'value',
smooth: true,
meta: {
value: {
min: yMin,
},
time: {
formatter(value) {
return dayjs(value).format(
rangeType === '1w' ? 'MM-DD HH:mm' : 'HH:mm'
);
},
},
},
// need explore how to display null data
// xAxis: {
// type: 'time',
// },
color: 'rgb(34 197 94 / 0.8)',
areaStyle: () => {
return {
fill: 'l(270) 0:rgb(34 197 94 / 0.2) 0.5:rgb(34 197 94 / 0.5) 1:rgb(34 197 94 / 0.8)',
};
},
annotations,
tooltip: {
title: (title, datum) => {
return dayjs(datum.time).format('YYYY-MM-DD HH:mm');
},
formatter(datum) {
const { name, text } = getProviderDisplay(
datum.value,
providerInfo
);
return {
name,
value: datum.value ? text : 'null',
};
},
},
};
}, [data, rangeType]);
return ( return (
<div> <div>
<div className="mb-4 text-right"> <div className="mb-4 text-right">
@ -172,7 +121,76 @@ export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
</Select> </Select>
</div> </div>
<Area {...config} /> <ChartContainer className="h-[200px] w-full" config={chartConfig}>
<AreaChart
data={data}
margin={{ top: 10, right: 0, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="color" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor={colors.chart.monitor}
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor={colors.chart.monitor}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
dataKey="time"
type="number"
domain={['dataMin', 'dataMax']}
tickFormatter={(date) =>
dayjs(date).format(rangeType === '1w' ? 'MM-DD HH:mm' : 'HH:mm')
}
/>
<YAxis
mirror
domain={[isTrendingMode ? 'dataMin' : 0, 'dataMax']}
/>
<CartesianGrid vertical={false} />
<ChartTooltip
labelFormatter={(label, payload) =>
dayjs(get(payload, [0, 'payload', 'time'])).format(
'YYYY-MM-DD HH:mm:ss'
)
}
formatter={(value, defaultText, item, index, payload) => {
if (typeof value !== 'number') {
return defaultText;
}
const { name, text } = getProviderDisplay(
Number(value),
providerInfo
);
return (
<div>
<span className="mr-2">{name}:</span>
<span>{text}</span>
</div>
);
}}
content={<ChartTooltipContent />}
/>
<Customized component={CustomizedErrorArea} />
<Area
type="monotone"
dataKey="value"
stroke={colors.chart.monitor}
fillOpacity={1}
fill="url(#color)"
strokeWidth={2}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>
</div> </div>
); );
} }

View File

@ -61,6 +61,10 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
onSuccess: defaultSuccessHandler, onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler, onError: defaultErrorHandler,
}); });
const testNotifyScriptMutation = trpc.monitor.testNotifyScript.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const trpcUtils = trpc.useContext(); const trpcUtils = trpc.useContext();
@ -229,6 +233,15 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
label: t('Show Badge'), label: t('Show Badge'),
onClick: () => setShowBadge(true), onClick: () => setShowBadge(true),
}, },
{
key: 'testNotify',
label: t('Test Notify'),
onClick: () =>
testNotifyScriptMutation.mutateAsync({
workspaceId,
monitorId,
}),
},
{ {
type: 'divider', type: 'divider',
}, },

View File

@ -0,0 +1,146 @@
import dayjs from 'dayjs';
import { get } from 'lodash-es';
import React, { useMemo } from 'react';
import { trpc } from '../../api/trpc';
import { getMonitorProvider, getProviderDisplay } from './provider';
import { useTranslation } from '@i18next-toolkit/react';
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '../ui/chart';
import {
Area,
AreaChart,
CartesianGrid,
Customized,
XAxis,
YAxis,
} from 'recharts';
import { useTheme } from '@/hooks/useTheme';
import { CustomizedErrorArea } from './CustomizedErrorArea';
const chartConfig = {
value: {
label: <span className="text-sm font-bold">Result</span>,
},
} satisfies ChartConfig;
interface MonitorPublicDataChartProps {
workspaceId: string;
monitorId: string;
className?: string;
}
export const MonitorPublicDataChart: React.FC<MonitorPublicDataChartProps> =
React.memo((props) => {
const { t } = useTranslation();
const { workspaceId, monitorId } = props;
const { colors } = useTheme();
const { data: monitorInfo } = trpc.monitor.getPublicInfo.useQuery(
{
monitorIds: [monitorId],
},
{
select(data) {
return data[0];
},
}
);
const { data: _data = [] } = trpc.monitor.publicData.useQuery({
workspaceId,
monitorId,
});
const providerInfo = getMonitorProvider(monitorInfo?.type ?? '');
const { data } = useMemo(() => {
const data = _data.map((d, i, arr) => {
const value = d.value > 0 ? d.value : null;
const time = dayjs(d.createdAt).valueOf();
return {
value,
time,
};
});
return { data };
}, [_data]);
const isTrendingMode = monitorInfo?.trendingMode ?? false; // if true, y axis not start from 0
return (
<div>
<ChartContainer className="h-[120px] w-full" config={chartConfig}>
<AreaChart
data={data}
margin={{ top: 10, right: 0, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="color" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor={colors.chart.monitor}
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor={colors.chart.monitor}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
dataKey="time"
type="number"
domain={['dataMin', 'dataMax']}
tickFormatter={(date) => dayjs(date).format('HH:mm')}
/>
<YAxis domain={[isTrendingMode ? 'dataMin' : 0, 'dataMax']} />
<CartesianGrid vertical={false} />
<ChartTooltip
labelFormatter={(label, payload) =>
dayjs(get(payload, [0, 'payload', 'time'])).format(
'YYYY-MM-DD HH:mm:ss'
)
}
formatter={(value, defaultText, item, index, payload) => {
if (typeof value !== 'number') {
return defaultText;
}
const { name, text } = getProviderDisplay(
Number(value),
providerInfo
);
return (
<div>
<span className="mr-2">{name}:</span>
<span>{text}</span>
</div>
);
}}
content={<ChartTooltipContent />}
/>
<Customized component={CustomizedErrorArea} />
<Area
type="monotone"
dataKey="value"
stroke={colors.chart.monitor}
fillOpacity={1}
fill="url(#color)"
strokeWidth={2}
isAnimationActive={false}
/>
</AreaChart>
</ChartContainer>
</div>
);
});
MonitorPublicDataChart.displayName = 'MonitorPublicDataChart';

View File

@ -1,9 +1,22 @@
import { AppRouterOutput, trpc } from '@/api/trpc'; import { AppRouterOutput, trpc } from '@/api/trpc';
import React, { useMemo } from 'react'; import React, { useMemo, useReducer } from 'react';
import { bodySchema } from './schema'; import { bodySchema } from './schema';
import { Empty } from 'antd'; import { Empty } from 'antd';
import { Separator } from '@/components/ui/separator';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { MonitorListItem } from '../MonitorListItem'; import { cn } from '@/utils/style';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { HealthBar } from '@/components/HealthBar';
import { getMonitorProvider, getProviderDisplay } from '../provider';
import {
getStatusBgColorClassName,
parseHealthStatusByPercent,
} from '@/utils/health';
import { MonitorPublicDataChart } from '../MonitorPublicDataChart';
interface StatusPageBodyProps { interface StatusPageBodyProps {
workspaceId: string; workspaceId: string;
@ -24,12 +37,13 @@ export const StatusPageBody: React.FC<StatusPageBodyProps> = React.memo(
}, [info.body]); }, [info.body]);
return ( return (
<div> <div className="rounded-lg border border-gray-200/80 dark:border-gray-700/25">
{body.groups.map((group) => ( {body.groups.map((group) => (
<div key={group.key} className="mb-6"> <div key={group.key} className="m-4 rounded-lg bg-neutral-500/15">
<div className="mb-2 text-lg font-semibold">{group.title}</div> <div className="ml-4 pl-2.5 pt-2.5 text-lg font-semibold">
{group.title}
<div className="flex flex-col gap-4 rounded-md border border-gray-200 p-2.5 dark:border-gray-700"> </div>
<div className="flex flex-col gap-2 rounded-md p-2.5">
{group.children.length === 0 && ( {group.children.length === 0 && (
<Empty description={t('No any monitor has been set')} /> <Empty description={t('No any monitor has been set')} />
)} )}
@ -37,12 +51,14 @@ export const StatusPageBody: React.FC<StatusPageBodyProps> = React.memo(
{group.children.map((item) => { {group.children.map((item) => {
if (item.type === 'monitor') { if (item.type === 'monitor') {
return ( return (
<React.Fragment key={item.key}>
<Separator />
<StatusItemMonitor <StatusItemMonitor
key={item.key}
workspaceId={props.workspaceId} workspaceId={props.workspaceId}
id={item.id} monitorId={item.id}
showCurrent={item.showCurrent ?? false} showCurrent={item.showCurrent ?? false}
/> />
</React.Fragment>
); );
} }
@ -58,33 +74,145 @@ export const StatusPageBody: React.FC<StatusPageBodyProps> = React.memo(
StatusPageBody.displayName = 'StatusPageBody'; StatusPageBody.displayName = 'StatusPageBody';
export const StatusItemMonitor: React.FC<{ export const StatusItemMonitor: React.FC<{
id: string; monitorId: string;
showCurrent: boolean; showCurrent: boolean;
workspaceId: string; workspaceId: string;
}> = React.memo((props) => { }> = React.memo((props) => {
const { data: list = [], isLoading } = trpc.monitor.getPublicInfo.useQuery({ const { data: info } = trpc.monitor.getPublicInfo.useQuery(
monitorIds: [props.id], {
monitorIds: [props.monitorId],
},
{
select: (data) => data[0],
}
);
const { data: list = [], isLoading } = trpc.monitor.publicSummary.useQuery({
workspaceId: props.workspaceId,
monitorId: props.monitorId,
}); });
const [showChart, toggleShowChart] = useReducer((state) => !state, false);
const { summaryStatus, summaryPercent } = useMemo(() => {
let upCount = 0;
let totalCount = 0;
list.forEach((item) => {
upCount += item.upCount;
totalCount += item.totalCount;
});
const percent = Number(((upCount / totalCount) * 100).toFixed(1));
return {
summaryPercent: percent,
summaryStatus: parseHealthStatusByPercent(percent, totalCount),
};
}, [list]);
if (isLoading) { if (isLoading) {
return null; return null;
} }
const item = list[0];
if (!item) {
return null;
}
return ( return (
<MonitorListItem <div>
key={item.id} <div
className={cn(
'mb-1 flex cursor-pointer items-center overflow-hidden rounded-lg bg-green-500 bg-opacity-0 px-4 py-3 hover:bg-opacity-10'
)}
onClick={toggleShowChart}
>
<div>
<span
className={cn(
'text-white inline-block min-w-[62px] rounded-lg p-0.5 text-center font-semibold',
getStatusBgColorClassName(summaryStatus)
)}
>
{summaryPercent}%
</span>
</div>
<div className="flex-1 pl-2">
<div className="text-nowrap text-base">{info?.name}</div>
</div>
{props.showCurrent && info && (
<MonitorLatestResponse
workspaceId={props.workspaceId} workspaceId={props.workspaceId}
monitorId={item.id} monitorId={info.id}
monitorName={item.name} monitorType={info.type}
monitorType={item.type}
showCurrentResponse={props.showCurrent}
/> />
)}
<div className="flex-shrink basis-[250px] items-center overflow-hidden px-1">
<HealthBar
className="justify-end"
size="small"
beats={[...list].reverse().map((item) => {
const status = parseHealthStatusByPercent(
item.upRate,
item.totalCount
);
return {
status,
title: `${item.day} | (${item.upCount}/${item.totalCount}) ${item.upRate}%`,
};
})}
/>
</div>
</div>
{showChart && (
<MonitorPublicDataChart
workspaceId={props.workspaceId}
monitorId={props.monitorId}
/>
)}
</div>
); );
}); });
StatusItemMonitor.displayName = 'StatusItemMonitor'; StatusItemMonitor.displayName = 'StatusItemMonitor';
const MonitorLatestResponse: React.FC<{
workspaceId: string;
monitorId: string;
monitorType: string;
}> = React.memo((props) => {
const { t } = useTranslation();
const { data: recentText } = trpc.monitor.recentData.useQuery(
{
workspaceId: props.workspaceId,
monitorId: props.monitorId,
take: 1,
},
{
select: (data) => {
const provider = getMonitorProvider(props.monitorType);
const value = data[0].value;
if (!value) {
return '';
}
const { text } = getProviderDisplay(value, provider);
return text;
},
}
);
return (
<Tooltip>
<TooltipTrigger asChild={true}>
<div className="px-2 text-sm text-gray-800 dark:text-gray-400">
{recentText}
</div>
</TooltipTrigger>
<TooltipContent>{t('Current')}</TooltipContent>
</Tooltip>
);
});
MonitorLatestResponse.displayName = 'MonitorLatestResponse';

View File

@ -0,0 +1,199 @@
import React, { useMemo } from 'react';
import { cn } from '@/utils/style';
import { bodySchema } from './schema';
import { LuCheckCircle2, LuCircleSlash, LuAlertCircle } from 'react-icons/lu';
import { AppRouterOutput, trpc } from '../../../api/trpc';
import { getMonitorProvider, getProviderDisplay } from '../provider';
import { takeRight, last } from 'lodash-es';
import dayjs from 'dayjs';
import { IconType } from 'react-icons';
import { useTranslation } from '@i18next-toolkit/react';
interface StatusPageHeaderProps {
info: NonNullable<AppRouterOutput['monitor']['getPageInfo']>;
workspaceId: string;
}
interface ContextItem {
id: string;
groupId: string;
groupName: string;
}
type StatusType = 'operational' | 'degraded' | 'offline' | 'unknown';
export const StatusPageHeader: React.FC<StatusPageHeaderProps> = React.memo(
({ info, workspaceId }) => {
const { t } = useTranslation();
const body = useMemo(() => {
const res = bodySchema.safeParse(info.body);
return res.success ? res.data : { groups: [] };
}, [info.body]);
const monitorContexts = useMemo(() => {
const contexts: ContextItem[] = [];
body.groups.forEach((group) => {
group.children.forEach((item) => {
if (item.type === 'monitor') {
contexts.push({
id: item.id,
groupId: group.key,
groupName: group.title,
});
}
});
});
if (Array.isArray(info.monitorList)) {
info.monitorList.forEach((monitor) => {
contexts.push({
id: monitor.id,
groupId: 'deprecated',
groupName: 'Legacy Monitors',
});
});
}
return contexts;
}, [body, info.monitorList]);
const recentDataQueries = monitorContexts.map((context) => {
const { data: recentData = [] } = trpc.monitor.recentData.useQuery({
workspaceId,
monitorId: context.id,
take: 1,
});
const items = useMemo(() => {
return takeRight(
[...Array.from({ length: 1 }).map(() => null), ...recentData],
1
);
}, [recentData]);
const provider = useMemo(
() => getMonitorProvider(context.id),
[context.id]
);
const latestStatus = useMemo(() => {
const latestItem = last(items);
if (!latestItem) {
return 'none';
}
const { value, createdAt } = latestItem;
const { text } = getProviderDisplay(value, provider);
const title = `${dayjs(createdAt).format('YYYY-MM-DD HH:mm')} | ${text}`;
return value < 0
? { status: 'error', title }
: { status: 'health', title };
}, [items, provider]);
return {
id: context.id,
status: latestStatus === 'none' ? undefined : latestStatus.status,
timestamp: dayjs(last(items)?.createdAt).valueOf(),
};
});
const { overallStatus, lastChecked } = useMemo(() => {
let totalCount = 0;
let errorCount = 0;
let latestTimestamp = 0;
recentDataQueries.forEach((query) => {
if (!query) return;
totalCount += 1;
if (query.status != 'health') {
errorCount += 1;
}
if (
!latestTimestamp ||
(query.timestamp && query.timestamp > latestTimestamp)
) {
latestTimestamp = query.timestamp;
}
});
let status: string = 'unknown';
let uprate = ((totalCount - errorCount) / totalCount) * 100;
if (uprate > 90) {
status = 'operational';
} else if (uprate > 50) {
status = 'degraded';
} else if (uprate > 0) {
status = 'offline';
}
return {
overallStatus: status as StatusType,
servicesCount: totalCount,
lastChecked: latestTimestamp,
};
}, [recentDataQueries]);
const statusConfig: Record<
StatusType,
{ text: string; icon: IconType; iconColor: string }
> = {
operational: {
text: t('All Systems Operational'),
icon: LuCheckCircle2,
iconColor: 'text-green-500',
},
degraded: {
text: t('Partial System Outage'),
icon: LuAlertCircle,
iconColor: 'text-yellow-500',
},
offline: {
text: t('Major System Outage'),
icon: LuCircleSlash,
iconColor: 'text-red-500',
},
unknown: {
text: t('Status Unknown'),
icon: LuAlertCircle,
iconColor: 'text-gray-500',
},
};
const config = statusConfig[overallStatus];
const StatusIcon = config.icon;
const formatDate = (date: number) => {
const options: Intl.DateTimeFormatOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: true,
};
const formatted = new Date(date).toLocaleString('en-US', options);
return `${t('Last updated')} ${formatted}`;
};
return (
<div className="flex flex-col items-center space-y-2">
<StatusIcon
className={cn('h-12 w-12', config.iconColor)}
aria-hidden="true"
/>
<h1 className="pb-2 pt-4 text-4xl font-bold">{config.text}</h1>
{lastChecked && (
<p className="text-md text-gray-600 dark:text-gray-400">
{formatDate(lastChecked)}
</p>
)}
</div>
);
}
);
export default StatusPageHeader;

View File

@ -9,6 +9,7 @@ import clsx from 'clsx';
import { useRequest } from '../../../hooks/useRequest'; import { useRequest } from '../../../hooks/useRequest';
import { ColorSchemeSwitcher } from '../../ColorSchemeSwitcher'; import { ColorSchemeSwitcher } from '../../ColorSchemeSwitcher';
import { StatusPageServices } from './Services'; import { StatusPageServices } from './Services';
import { StatusPageHeader } from './StatusHeader';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { Link, useNavigate } from '@tanstack/react-router'; import { Link, useNavigate } from '@tanstack/react-router';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
@ -146,8 +147,14 @@ export const MonitorStatusPage: React.FC<MonitorStatusPageProps> = React.memo(
</div> </div>
)} )}
{info && (
<div className="my-6">
<StatusPageHeader info={info} workspaceId={info.workspaceId} />
</div>
)}
{/* Desc */} {/* Desc */}
<div className="mb-4"> <div className="mb-6 text-center">
<MarkdownViewer value={info?.description ?? ''} /> <MarkdownViewer value={info?.description ?? ''} />
</div> </div>

View File

@ -5,11 +5,11 @@ import { useWatch } from '../../hooks/useWatch';
import { Loading } from '../Loading'; import { Loading } from '../Loading';
import { without } from 'lodash-es'; import { without } from 'lodash-es';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { useSocketSubscribe } from '@/api/socketio'; import { useSocketSubscribeData } from '@/api/socketio';
import { ServerStatusInfo } from '../../../types'; import { ServerStatusInfo } from '../../../types';
function useServerMap(): Record<string, ServerStatusInfo> { function useServerMap(): Record<string, ServerStatusInfo> {
const serverMap = useSocketSubscribe<Record<string, ServerStatusInfo>>( const serverMap = useSocketSubscribeData<Record<string, ServerStatusInfo>>(
'onServerStatusUpdate', 'onServerStatusUpdate',
{} {}
); );

View File

@ -1,10 +1,10 @@
import { useSocket, useSocketSubscribe } from '@/api/socketio'; import { useSocket, useSocketSubscribeData } from '@/api/socketio';
import { ServerStatusInfo } from '../../../types'; import { ServerStatusInfo } from '../../../types';
import { useVisibilityChange } from '@/hooks/useVisibilityChange'; import { useVisibilityChange } from '@/hooks/useVisibilityChange';
export function useServerMap(): Record<string, ServerStatusInfo> { export function useServerMap(): Record<string, ServerStatusInfo> {
const { socket } = useSocket(); const { socket } = useSocket();
const serverMap = useSocketSubscribe<Record<string, ServerStatusInfo>>( const serverMap = useSocketSubscribeData<Record<string, ServerStatusInfo>>(
'onServerStatusUpdate', 'onServerStatusUpdate',
{} {}
); );

View File

@ -1,12 +1,7 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useEvent, useEventWithLoading } from '@/hooks/useEvent'; import { useEventWithLoading } from '@/hooks/useEvent';
import { useCurrentWorkspaceId } from '@/store/user';
import { defaultErrorHandler, trpc } from '@/api/trpc';
import { Card, CardContent, CardFooter } from '@/components/ui/card'; import { Card, CardContent, CardFooter } from '@/components/ui/card';
import { CommonWrapper } from '@/components/CommonWrapper';
import { routeAuthBeforeLoad } from '@/utils/route';
import { z } from 'zod'; import { z } from 'zod';
import { import {
Form, Form,
@ -18,7 +13,7 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { useForm, useFieldArray, useWatch } from 'react-hook-form'; import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { generateRandomString } from '@/utils/common'; import { generateRandomString } from '@/utils/common';
import { LuArrowDown, LuArrowUp, LuMinus, LuPlus } from 'react-icons/lu'; import { LuArrowDown, LuArrowUp, LuMinus, LuPlus } from 'react-icons/lu';
@ -49,6 +44,7 @@ const addFormSchema = z.object({
}), }),
feedChannelIds: z.array(z.string()), feedChannelIds: z.array(z.string()),
feedTemplate: z.string(), feedTemplate: z.string(),
webhookUrl: z.string().url().or(z.literal('')),
}); });
export type SurveyEditFormValues = z.infer<typeof addFormSchema>; export type SurveyEditFormValues = z.infer<typeof addFormSchema>;
@ -80,6 +76,7 @@ export const SurveyEditForm: React.FC<SurveyEditFormProps> = React.memo(
}, },
feedChannelIds: [], feedChannelIds: [],
feedTemplate: '', feedTemplate: '',
webhookUrl: '',
}, },
}); });
@ -87,7 +84,7 @@ export const SurveyEditForm: React.FC<SurveyEditFormProps> = React.memo(
const [handleSubmit, isLoading] = useEventWithLoading( const [handleSubmit, isLoading] = useEventWithLoading(
async (values: SurveyEditFormValues) => { async (values: SurveyEditFormValues) => {
await props.onSubmit(values); await props.onSubmit({ ...values });
form.reset(); form.reset();
} }
); );
@ -297,6 +294,23 @@ export const SurveyEditForm: React.FC<SurveyEditFormProps> = React.memo(
)} )}
/> />
)} )}
<FormField
control={form.control}
name="webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel optional={true}>{t('Webhook Url')}</FormLabel>
<FormControl className="w-full">
<Input {...field} />
</FormControl>
<FormDescription>
{t('Optional, webhook url to send survey payload')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent> </CardContent>
<CardFooter> <CardFooter>

View File

@ -13,7 +13,11 @@ import { LuCode2 } from 'react-icons/lu';
import { trpc } from '@/api/trpc'; import { trpc } from '@/api/trpc';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { CodeBlock } from '../CodeBlock'; import { CodeBlock } from '../CodeBlock';
import { generateSurveyExampleCode } from '@/utils/survey'; import {
generateSurveyExampleCurlCode,
generateSurveyExampleSDKCode,
} from '@/utils/survey';
import { CodeExample } from '../CodeExample';
interface SurveyUsageBtnProps { interface SurveyUsageBtnProps {
surveyId: string; surveyId: string;
@ -29,8 +33,6 @@ export const SurveyUsageBtn: React.FC<SurveyUsageBtnProps> = React.memo(
surveyId, surveyId,
}); });
const exampleCode = generateSurveyExampleCode(window.location.origin, info);
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -46,9 +48,33 @@ export const SurveyUsageBtn: React.FC<SurveyUsageBtnProps> = React.memo(
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<CodeExample
className="overflow-hidden"
example={{
curl: {
label: 'curl',
code: generateSurveyExampleCurlCode(
window.location.origin,
info
),
},
sdk: {
label: 'sdk',
element: (
<div className="flex flex-col gap-1">
<CodeBlock code="npm install tianji-client-sdk" /> <CodeBlock code="npm install tianji-client-sdk" />
<CodeBlock code={exampleCode} /> <CodeBlock
code={generateSurveyExampleSDKCode(
window.location.origin,
info
)}
/>
</div>
),
},
}}
/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -51,10 +51,11 @@ export const TelemetryOverview: React.FC<{
const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit); const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit);
const sessionsArr = getDateArray(sessions, startDate, endDate, unit); const sessionsArr = getDateArray(sessions, startDate, endDate, unit);
return [ return pageviewsArr.map((item, i) => ({
...pageviewsArr.map((item) => ({ ...item, type: 'pageview' })), pv: item.y,
...sessionsArr.map((item) => ({ ...item, type: 'session' })), uv: sessionsArr[i]?.y ?? 0,
]; date: item.x,
}));
}, },
} }
); );

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/utils/style"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -0,0 +1,375 @@
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import {
NameType,
Payload,
ValueType,
} from "recharts/types/component/DefaultTooltipContent"
import { cn } from "@/utils/style"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
type ChartLegendContentItem = NonNullable<RechartsPrimitive.LegendProps['payload']>[number]
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
} & {
selectedItem?: string[]
onItemClick?: (item: ChartLegendContentItem) => void
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey, selectedItem, onItemClick },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
typeof onItemClick === 'function' && 'cursor-pointer',
selectedItem?.includes(item.value) && 'font-bold',
)}
onClick={() => onItemClick?.(item)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

@ -28,6 +28,7 @@ import { useEvent } from '@/hooks/useEvent';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { LuArrowRight, LuPlus } from 'react-icons/lu'; import { LuArrowRight, LuPlus } from 'react-icons/lu';
import { ROLES } from '@tianji/shared'; import { ROLES } from '@tianji/shared';
import { useSocketSubscribe } from '@/api/socketio';
interface WebsiteLighthouseBtnProps { interface WebsiteLighthouseBtnProps {
websiteId: string; websiteId: string;
@ -55,6 +56,16 @@ export const WebsiteLighthouseBtn: React.FC<WebsiteLighthouseBtnProps> =
onError: defaultErrorHandler, onError: defaultErrorHandler,
}); });
useSocketSubscribe('onLighthouseWorkCompleted', (data) => {
const { websiteId } = data;
if (websiteId !== props.websiteId) {
return;
}
refetch();
toast.info(t('Lighthouse report completed!'));
});
const allData = useMemo(() => { const allData = useMemo(() => {
if (!data) { if (!data) {
return []; return [];

View File

@ -17,7 +17,10 @@ export const WebsiteOnlineCount: React.FC<{
if (typeof count === 'number' && count > 0) { if (typeof count === 'number' && count > 0) {
return ( return (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className='w-2.5" relative flex h-2.5'>
<div className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75" />
<div className="h-2.5 w-2.5 flex-shrink-0 rounded-full bg-green-500" /> <div className="h-2.5 w-2.5 flex-shrink-0 rounded-full bg-green-500" />
</div>
<span> <span>
{count} {t('current visitor')} {count} {t('current visitor')}
</span> </span>

View File

@ -50,10 +50,11 @@ export const WebsiteOverview: React.FC<{
const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit); const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit);
const sessionsArr = getDateArray(sessions, startDate, endDate, unit); const sessionsArr = getDateArray(sessions, startDate, endDate, unit);
return [ return pageviewsArr.map((item, i) => ({
...pageviewsArr.map((item) => ({ ...item, type: 'pageview' })), pv: item.y,
...sessionsArr.map((item) => ({ ...item, type: 'session' })), uv: sessionsArr[i]?.y ?? 0,
]; date: item.x,
}));
}, },
} }
); );

View File

@ -5,7 +5,7 @@ import {
PointLayer, PointLayer,
PointLayerProps, PointLayerProps,
} from '@antv/larkmap'; } from '@antv/larkmap';
import React from 'react'; import React, { useMemo } from 'react';
import { AppRouterOutput } from '../../../api/trpc'; import { AppRouterOutput } from '../../../api/trpc';
import { useGlobalConfig } from '../../../hooks/useConfig'; import { useGlobalConfig } from '../../../hooks/useConfig';
import { useSettingsStore } from '../../../store/settings'; import { useSettingsStore } from '../../../store/settings';
@ -76,10 +76,18 @@ export const VisitorLarkMap: React.FC<{
parser: { type: 'json', x: 'longitude', y: 'latitude' }, parser: { type: 'json', x: 'longitude', y: 'latitude' },
}; };
const size = useMemo(() => {
if (props.data.length > 5000) {
return 3;
}
return 5;
}, [props.data.length]);
return ( return (
<LarkMap {...config} style={{ height: '60vh' }}> <LarkMap {...config} style={{ height: '60vh' }}>
<FullscreenControl /> <FullscreenControl />
<PointLayer {...layerOptions} source={source} /> <PointLayer {...layerOptions} size={size} source={source} />
</LarkMap> </LarkMap>
); );
}); });

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useMemo } from 'react';
import { AppRouterOutput } from '../../../api/trpc'; import { AppRouterOutput } from '../../../api/trpc';
import { MapContainer, CircleMarker, Popup, TileLayer } from 'react-leaflet'; import { MapContainer, CircleMarker, Popup, TileLayer } from 'react-leaflet';
import { mapCenter } from './utils'; import { mapCenter } from './utils';
@ -7,11 +7,13 @@ import './VisitorLeafletMap.css';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
export const UserDataPoint: React.FC<{ export const UserDataPoint: React.FC<{
pointRadius?: number;
longitude: number; longitude: number;
latitude: number; latitude: number;
count: number; count: number;
}> = React.memo((props) => { }> = React.memo((props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const pointRadius = props.pointRadius ?? 5;
return ( return (
<CircleMarker <CircleMarker
@ -19,7 +21,7 @@ export const UserDataPoint: React.FC<{
lat: props.latitude, lat: props.latitude,
lng: props.longitude, lng: props.longitude,
}} }}
radius={5} radius={pointRadius}
stroke={false} stroke={false}
fill={true} fill={true}
fillColor="rgb(236,112,20)" fillColor="rgb(236,112,20)"
@ -38,6 +40,22 @@ UserDataPoint.displayName = 'UserDataPoint';
export const VisitorLeafletMap: React.FC<{ export const VisitorLeafletMap: React.FC<{
data: AppRouterOutput['website']['geoStats']; data: AppRouterOutput['website']['geoStats'];
}> = React.memo((props) => { }> = React.memo((props) => {
const pointRadius = useMemo(() => {
if (props.data.length > 20000) {
return 1;
}
if (props.data.length > 5000) {
return 2;
}
if (props.data.length > 1000) {
return 3;
}
return 5;
}, [props.data.length]);
return ( return (
<MapContainer <MapContainer
className="h-[60vh] w-full" className="h-[60vh] w-full"
@ -50,7 +68,11 @@ export const VisitorLeafletMap: React.FC<{
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{props.data.map((item) => ( {props.data.map((item) => (
<UserDataPoint key={`${item.longitude},${item.latitude}`} {...item} /> <UserDataPoint
key={`${item.longitude},${item.latitude}`}
pointRadius={pointRadius}
{...item}
/>
))} ))}
</MapContainer> </MapContainer>
); );

View File

@ -22,6 +22,9 @@ export function useGlobalConfig(): AppRouterOutput['global']['config'] {
{ {
staleTime: 1000 * 60 * 60 * 1, // 1 hour staleTime: 1000 * 60 * 60 * 1, // 1 hour
onSuccess(data) { onSuccess(data) {
/**
* Call anonymous telemetry if not disabled
*/
if (data.disableAnonymousTelemetry !== true) { if (data.disableAnonymousTelemetry !== true) {
callAnonymousTelemetry(); callAnonymousTelemetry();
} }

View File

@ -1,5 +1,5 @@
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { useMemo, useReducer } from 'react'; import { useEffect, useMemo, useReducer } from 'react';
import { getMinimumUnit } from '@tianji/shared'; import { getMinimumUnit } from '@tianji/shared';
import { DateRange, useGlobalStateStore } from '../store/global'; import { DateRange, useGlobalStateStore } from '../store/global';
import { DateUnit } from '../utils/date'; import { DateUnit } from '../utils/date';
@ -45,6 +45,15 @@ export function useGlobalRangeDate(): {
}; };
} }
if (dateRange === DateRange.Realtime) {
return {
label: t('Realtime'),
startDate: dayjs().subtract(1, 'hour').startOf('minute'),
endDate: dayjs().endOf('minute'),
unit: 'minute' as const,
};
}
if (dateRange === DateRange.Today) { if (dateRange === DateRange.Today) {
return { return {
label: t('Today'), label: t('Today'),
@ -126,5 +135,30 @@ export function useGlobalRangeDate(): {
}; };
}, [dateRange, globalStartDate, globalEndDate, updateInc]); }, [dateRange, globalStartDate, globalEndDate, updateInc]);
/**
* Auto refresh if is realtime
* NOTICE: Not cool yet
*/
useEffect(() => {
let timer: number | null = null;
if (dateRange === DateRange.Realtime) {
if (timer) {
window.clearInterval(timer);
}
timer = window.setInterval(() => {
refresh();
}, 60 * 1000);
}
return () => {
if (timer) {
window.clearInterval(timer);
timer = null;
}
};
}, [dateRange]);
return { label, startDate, endDate, unit, refresh }; return { label, startDate, endDate, unit, refresh };
} }

View File

@ -0,0 +1,188 @@
/**
* Reference: https://bit.cloud/teambit/analytics/hooks/use-recharts-line-stroke-dasharray
*/
import { useCallback, useRef } from 'react';
type GraphicalItemPoint = {
/**
* x point coordinate.
*/
x?: number;
/**
* y point coordinate.
*/
y?: number;
};
type GraphicalItemProps = {
/**
* graphical item points.
*/
points?: GraphicalItemPoint[];
};
type ItemProps = {
/**
* item data key.
*/
dataKey?: string;
};
type ItemType = {
/**
* recharts item display name.
*/
displayName?: string;
};
type Item = {
/**
* item props.
*/
props?: ItemProps;
/**
* recharts item class.
*/
type?: ItemType;
};
type GraphicalItem = {
/**
* from recharts internal state and props of chart.
*/
props?: GraphicalItemProps;
/**
* from recharts internal state and props of chart.
*/
item?: Item;
};
type RechartsChartProps = {
/**
* from recharts internal state and props of chart.
*/
formattedGraphicalItems?: GraphicalItem[];
};
type CalculateStrokeDasharray = (props?: any) => any;
type LineStrokeDasharray = {
/**
* line name.
*/
name?: string;
/**
* line strokeDasharray.
*/
strokeDasharray?: string;
};
type LinesStrokeDasharray = LineStrokeDasharray[];
type LineProps = {
/**
* line name.
*/
name?: string;
/**
* specifies the starting index of the first dot in the dash pattern.
*/
dotIndex?: number;
/**
* defines the pattern of dashes and gaps. an array of [gap length, dash length].
*/
strokeDasharray?: [number, number];
/**
* adjusts the percentage correction of the first line segment for better alignment in curved lines.
*/
curveCorrection?: number;
};
export type UseStrokeDasharrayProps = {
/**
* an array of properties to target specific line(s) and override default settings.
*/
linesProps?: LineProps[];
} & LineProps;
export function useStrokeDasharray({
linesProps = [],
dotIndex = -2,
strokeDasharray: restStroke = [5, 3],
curveCorrection = 1,
}: UseStrokeDasharrayProps): [CalculateStrokeDasharray, LinesStrokeDasharray] {
const linesStrokeDasharray = useRef<LinesStrokeDasharray>([]);
const calculateStrokeDasharray = useCallback(
(props: RechartsChartProps): null => {
const items = props?.formattedGraphicalItems;
const getLineWidth = (points: GraphicalItemPoint[]) => {
const width = points?.reduce((acc, point, index) => {
if (!index) return acc;
const prevPoint = points?.[index - 1];
const xAxis = point?.x || 0;
const prevXAxis = prevPoint?.x || 0;
const xWidth = xAxis - prevXAxis;
const yAxis = point?.y || 0;
const prevYAxis = prevPoint?.y || 0;
const yWidth = Math.abs(yAxis - prevYAxis);
const hypotenuse = Math.sqrt(xWidth * xWidth + yWidth * yWidth);
acc += hypotenuse;
return acc;
}, 0);
return width || 0;
};
items?.forEach((line) => {
const linePoints = line?.props?.points ?? [];
const lineWidth = getLineWidth(linePoints);
const name = line?.item?.props?.dataKey;
const targetLine = linesProps?.find((target) => target?.name === name);
const targetIndex = targetLine?.dotIndex ?? dotIndex;
const dashedPoints = linePoints?.slice(targetIndex);
const dashedWidth = getLineWidth(dashedPoints);
if (!lineWidth || !dashedWidth) return;
const firstWidth = lineWidth - dashedWidth;
const targetCurve = targetLine?.curveCorrection ?? curveCorrection;
const correctionWidth = (firstWidth * targetCurve) / 100;
const firstDasharray = firstWidth + correctionWidth;
const targetRestStroke = targetLine?.strokeDasharray || restStroke;
const gapDashWidth = targetRestStroke?.[0] + targetRestStroke?.[1] || 1;
const restDasharrayLength = dashedWidth / gapDashWidth;
const restDasharray = new Array(Math.ceil(restDasharrayLength)).fill(
targetRestStroke.join(' ')
);
const strokeDasharray = `${firstDasharray} ${restDasharray.join(' ')}`;
const lineStrokeDasharray = { name, strokeDasharray };
const dasharrayIndex = linesStrokeDasharray.current.findIndex((d) => {
return d.name === line?.item?.props?.dataKey;
});
if (dasharrayIndex === -1) {
linesStrokeDasharray.current.push(lineStrokeDasharray);
return;
}
linesStrokeDasharray.current[dasharrayIndex] = lineStrokeDasharray;
});
return null;
},
[dotIndex]
);
return [calculateStrokeDasharray, linesStrokeDasharray.current];
}

View File

@ -2,6 +2,7 @@ import { useColorSchema } from '@/store/settings';
import { theme, ThemeConfig } from 'antd'; import { theme, ThemeConfig } from 'antd';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { colord } from 'colord'; import { colord } from 'colord';
import twColors from 'tailwindcss/colors';
const THEME_CONFIG = 'tianji.theme'; const THEME_CONFIG = 'tianji.theme';
@ -19,6 +20,9 @@ const THEME_COLORS = {
gray700: '#6e6e6e', gray700: '#6e6e6e',
gray800: '#4b4b4b', gray800: '#4b4b4b',
gray900: '#2c2c2c', gray900: '#2c2c2c',
green400: twColors.green['400'],
green500: twColors.green['500'],
green600: twColors.green['600'],
}, },
dark: { dark: {
primary: '#2680eb', primary: '#2680eb',
@ -33,6 +37,9 @@ const THEME_COLORS = {
gray700: '#b9b9b9', gray700: '#b9b9b9',
gray800: '#e3e3e3', gray800: '#e3e3e3',
gray900: '#ffffff', gray900: '#ffffff',
green400: twColors.green['600'],
green500: twColors.green['500'],
green600: twColors.green['400'],
}, },
}; };
@ -55,7 +62,14 @@ export function useTheme() {
const customTheme = window.localStorage.getItem(THEME_CONFIG); const customTheme = window.localStorage.getItem(THEME_CONFIG);
const theme = isValidTheme(customTheme) ? customTheme : defaultTheme; const theme = isValidTheme(customTheme) ? customTheme : defaultTheme;
const primaryColor = useMemo(() => colord(THEME_COLORS[theme].primary), []); const primaryColor = useMemo(
() => colord(THEME_COLORS[theme].primary),
[theme]
);
const healthColor = useMemo(
() => colord(THEME_COLORS[theme].green400),
[theme]
);
const colors = useMemo( const colors = useMemo(
() => ({ () => ({
@ -63,10 +77,12 @@ export function useTheme() {
...THEME_COLORS[theme], ...THEME_COLORS[theme],
}, },
chart: { chart: {
error: twColors.red[500],
text: THEME_COLORS[theme].gray700, text: THEME_COLORS[theme].gray700,
line: THEME_COLORS[theme].gray200, line: THEME_COLORS[theme].gray200,
pv: primaryColor.alpha(0.4).toRgbString(), pv: primaryColor.alpha(0.4).toRgbString(),
uv: primaryColor.alpha(0.6).toRgbString(), uv: primaryColor.alpha(0.6).toRgbString(),
monitor: healthColor.alpha(0.8).toRgbString(),
}, },
map: { map: {
baseColor: THEME_COLORS[theme].primary, baseColor: THEME_COLORS[theme].primary,

View File

@ -1,6 +1,6 @@
/** @type {import('@i18next-toolkit/cli').I18nextToolkitConfig} */ /** @type {import('@i18next-toolkit/cli').I18nextToolkitConfig} */
const config = { const config = {
locales: ['en', 'zh', 'jp', 'fr', 'de', 'pl', 'pt', 'ru'], locales: ['en', 'zh-CN', 'ja-JP', 'fr-FR', 'de-DE', 'pl-PL', 'pt-PT', 'ru-RU'],
verbose: true, verbose: true,
namespaces: ['translation'], namespaces: ['translation'],
translator: { translator: {

View File

@ -98,6 +98,11 @@ a {
--input: 240 5.9% 90%; --input: 240 5.9% 90%;
--ring: 240 5.9% 10%; --ring: 240 5.9% 10%;
--radius: 0.5rem; --radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
} }
.dark { .dark {
@ -120,6 +125,11 @@ a {
--border: 240 3.7% 15.9%; --border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%; --input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%; --ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
} }
} }

3
src/client/init.ts Normal file
View File

@ -0,0 +1,3 @@
import { initI18N } from './utils/i18n';
initI18N();

View File

@ -1,5 +1,6 @@
import './index.css'; import './index.css';
import './styles/global.less'; import './styles/global.less';
import './init';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';

View File

@ -7,7 +7,7 @@
"scripts": { "scripts": {
"dev": "vite --port 10000", "dev": "vite --port 10000",
"build": "vite build", "build": "vite build",
"ui:add": "shadcn-ui add", "ui:add": "shadcn add",
"check:type": "tsc --noEmit --skipLibCheck --module esnext", "check:type": "tsc --noEmit --skipLibCheck --module esnext",
"translation:extract": "i18next-toolkit extract", "translation:extract": "i18next-toolkit extract",
"translation:scan": "i18next-toolkit scan", "translation:scan": "i18next-toolkit scan",
@ -17,14 +17,13 @@
"keywords": [], "keywords": [],
"author": "moonrailgun <moonrailgun@gmail.com>", "author": "moonrailgun <moonrailgun@gmail.com>",
"dependencies": { "dependencies": {
"@ant-design/charts": "^1.4.2",
"@ant-design/icons": "^5.3.6", "@ant-design/icons": "^5.3.6",
"@antv/l7": "^2.20.14", "@antv/l7": "^2.20.14",
"@antv/larkmap": "^1.4.13", "@antv/larkmap": "^1.4.13",
"@bytemd/plugin-gfm": "^1.21.0", "@bytemd/plugin-gfm": "^1.21.0",
"@bytemd/react": "^1.21.0", "@bytemd/react": "^1.21.0",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@i18next-toolkit/react": "^1.1.0", "@i18next-toolkit/react": "2.0.0-rc.5",
"@loadable/component": "^5.16.3", "@loadable/component": "^5.16.3",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
@ -37,7 +36,7 @@
"@radix-ui/react-menubar": "^1.0.4", "@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-scroll-area": "1.2.0-rc.7", "@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
@ -55,7 +54,7 @@
"ahooks": "^3.7.10", "ahooks": "^3.7.10",
"antd": "^5.13.1", "antd": "^5.13.1",
"array-move": "^3.0.1", "array-move": "^3.0.1",
"axios": "^1.5.0", "axios": "1.7.7",
"bytemd": "^1.21.0", "bytemd": "^1.21.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
@ -71,6 +70,7 @@
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide-react": "^0.358.0", "lucide-react": "^0.358.0",
"md5": "^2.3.0",
"millify": "^6.1.0", "millify": "^6.1.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"pretty-ms": "^9.0.0", "pretty-ms": "^9.0.0",
@ -88,6 +88,7 @@
"react-resizable-panels": "^2.0.12", "react-resizable-panels": "^2.0.12",
"react-router": "^6.15.0", "react-router": "^6.15.0",
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"recharts": "^2.12.7",
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"socket.io-client": "^4.7.2", "socket.io-client": "^4.7.2",
"sonner": "^1.4.3", "sonner": "^1.4.3",
@ -106,6 +107,7 @@
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/loadable__component": "^5.13.8", "@types/loadable__component": "^5.13.8",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/md5": "^2.3.5",
"@types/react": "^18.2.22", "@types/react": "^18.2.22",
"@types/react-beautiful-dnd": "^13.1.8", "@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
@ -117,7 +119,7 @@
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"less": "^4.2.0", "less": "^4.2.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"shadcn-ui": "^0.8.0", "shadcn": "^2.1.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vite": "^5.0.12", "vite": "^5.0.12",

View File

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

View File

@ -19,11 +19,14 @@
"k17058821": "Website Lighthouse Berichte", "k17058821": "Website Lighthouse Berichte",
"k172a09c3": "Vorschläge", "k172a09c3": "Vorschläge",
"k1777bbf2": "Manuell", "k1777bbf2": "Manuell",
"k1940fd6": "Allgemein",
"k1964b988": "Stopp", "k1964b988": "Stopp",
"k1bd89236": "Reporter mit ausführen", "k1bd89236": "Reporter mit ausführen",
"k1c33c293": "Einstellungen", "k1c33c293": "Einstellungen",
"k1d8f92b4": "Tablet", "k1d8f92b4": "Tablet",
"k1da4ecc2": "Sie können eine Nachricht an diesen Kanal senden mit:",
"k1eb5b3ed": "Übersicht", "k1eb5b3ed": "Übersicht",
"k1ee0c2ca": "Setzen Sie die Webhook-URL auf <1></1> und halten Sie dieses Fenster aktiv. Sobald Sie fertig sind, beginnen Sie, Webhook-Anfragen hier zu empfangen.",
"k1f6dea0": "Kanalname", "k1f6dea0": "Kanalname",
"k2099f2e0": "Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort", "k2099f2e0": "Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort",
"k20edf271": "24 Stunden", "k20edf271": "24 Stunden",
@ -53,12 +56,15 @@
"k2c84fe32": "Feed-Ereigniszähler", "k2c84fe32": "Feed-Ereigniszähler",
"k2cecf817": "Typ", "k2cecf817": "Typ",
"k2dad13e3": "Sprache", "k2dad13e3": "Sprache",
"k2db2c0c5": "Testbenachrichtigung",
"k2e6dbf02": "An E-Mail", "k2e6dbf02": "An E-Mail",
"k2ea8a019": "Überwachen", "k2ea8a019": "Überwachen",
"k30b5f01b": "Arbeitsbereiche", "k30b5f01b": "Arbeitsbereiche",
"k30d33d71": "Webhook-Signatur",
"k310fee": "Letzte 30 Tage", "k310fee": "Letzte 30 Tage",
"k32344f64": "Daten löschen", "k32344f64": "Daten löschen",
"k3260f019": "Abmelden", "k3260f019": "Abmelden",
"k3404b72f": "Neuer Arbeitsbereichsname",
"k340547f0": "Entschuldigung, aber etwas ist schief gelaufen", "k340547f0": "Entschuldigung, aber etwas ist schief gelaufen",
"k3471e956": "Neues Passwort wiederholen", "k3471e956": "Neues Passwort wiederholen",
"k34981fea": "Docker treibt auf See und findet seinen Weg nicht. Bitte starten Sie Docker, um wieder auf Kurs zu kommen.", "k34981fea": "Docker treibt auf See und findet seinen Weg nicht. Bitte starten Sie Docker, um wieder auf Kurs zu kommen.",
@ -85,6 +91,7 @@
"k3e8b13f8": "Discord beitreten", "k3e8b13f8": "Discord beitreten",
"k3eaab921": "ÜberwachungsListe", "k3eaab921": "ÜberwachungsListe",
"k3f36e17e": "Twitter folgen", "k3f36e17e": "Twitter folgen",
"k406089a4": "Aktion",
"k406e9ad8": "Bestätigen", "k406e9ad8": "Bestätigen",
"k41d3ce6c": "Ereignis wiederhergestellt", "k41d3ce6c": "Ereignis wiederhergestellt",
"k42347b91": "Website-Ereigniszählung", "k42347b91": "Website-Ereigniszählung",
@ -93,7 +100,8 @@
"k44186b66": "Zählung", "k44186b66": "Zählung",
"k44cad477": "(Aktuell)", "k44cad477": "(Aktuell)",
"k45f80a27": "Erweitert", "k45f80a27": "Erweitert",
"k4738284": "Sie können jede Nachricht in diesen Kanal mit folgendem senden:", "k4727e4db": "Ablaufdatum",
"k477b7ee4": "Teilweise Systemausfälle",
"k47fe1f95": "Fügen Sie diesen Beispielcode zu Ihrem Projekt hinzu", "k47fe1f95": "Fügen Sie diesen Beispielcode zu Ihrem Projekt hinzu",
"k48186ce": "Zurück zur Startseite", "k48186ce": "Zurück zur Startseite",
"k4905ed7b": "KEINE", "k4905ed7b": "KEINE",
@ -107,6 +115,7 @@
"k4de48e75": "Maximale Wiederholungen", "k4de48e75": "Maximale Wiederholungen",
"k4e08cf58": "Detailnummer anzeigen", "k4e08cf58": "Detailnummer anzeigen",
"k4eea9393": "Profil", "k4eea9393": "Profil",
"k4f182a7c": "Wichtige Systemausfälle",
"k4fc2b5b": "Bild", "k4fc2b5b": "Bild",
"k4fe1b4de": "Telemetrie", "k4fe1b4de": "Telemetrie",
"k505c2733": "Bericht erstellen", "k505c2733": "Bericht erstellen",
@ -123,9 +132,12 @@
"k58267a45": "Quelle", "k58267a45": "Quelle",
"k58f90514": "Bot-Token", "k58f90514": "Bot-Token",
"k593cf342": "Sind Sie sicher, diesen Monitor zu löschen?", "k593cf342": "Sind Sie sicher, diesen Monitor zu löschen?",
"k5a782f4b": "Website-Anzahl",
"k5a839f71": "Betriebszeit", "k5a839f71": "Betriebszeit",
"k5b5be0d4": "Aktuelle Rolle", "k5b5be0d4": "Aktuelle Rolle",
"k5c18db28": "Statusseiteninformationen ändern", "k5c18db28": "Statusseiteninformationen ändern",
"k5d00536d": "Kopiert",
"k5d49d751": "Neuer API-Schlüssel wurde in Ihre Zwischenablage kopiert!",
"k5eb87a8b": "Start", "k5eb87a8b": "Start",
"k5ec0de4": "Für die HTTPS-Überwachung werden bei Zuweisung einer Benachrichtigungsmethode Benachrichtigungen 1, 3, 7 und 14 Tage vor Ablauf gesendet.", "k5ec0de4": "Für die HTTPS-Überwachung werden bei Zuweisung einer Benachrichtigungsmethode Benachrichtigungen 1, 3, 7 und 14 Tage vor Ablauf gesendet.",
"k5ecf04b0": "Ansicht", "k5ecf04b0": "Ansicht",
@ -135,6 +147,7 @@
"k62e19375": "Letzte Aktualisierung: {{date}}", "k62e19375": "Letzte Aktualisierung: {{date}}",
"k6488f302": "Optional", "k6488f302": "Optional",
"k659b065": "Zum Beispiel: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000", "k659b065": "Zum Beispiel: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
"k678e2f90": "Anforderungsinhalt",
"k67c5a895": "Gestern", "k67c5a895": "Gestern",
"k683be220": "Ausführen", "k683be220": "Ausführen",
"k691b7170": "Gestoppt", "k691b7170": "Gestoppt",
@ -147,9 +160,11 @@
"k6e96fc3": "Formularinfo", "k6e96fc3": "Formularinfo",
"k6ea11aff": "Holen!", "k6ea11aff": "Holen!",
"k6f15bcc3": "Host", "k6f15bcc3": "Host",
"k71067412": "Optional, Webhook-Signatur für eingehenden Webhook",
"k721589c1": "Heute", "k721589c1": "Heute",
"k7247683c": "Arbeitsbereich löschen", "k7247683c": "Arbeitsbereich löschen",
"k7350bd93": "Gleichzeitig können wir es auch in einigen Client-Seiten-Anwendungsszenarien verwenden, wie z.B. das Sammeln der Häufigkeit der CLI-Nutzung, das Sammeln der Installation von selbst gehosteten Apps und so weiter.", "k7350bd93": "Gleichzeitig können wir es auch in einigen Client-Seiten-Anwendungsszenarien verwenden, wie z.B. das Sammeln der Häufigkeit der CLI-Nutzung, das Sammeln der Installation von selbst gehosteten Apps und so weiter.",
"k736f3e4c": "Kopieren als",
"k75581e13": "Kreditkarte", "k75581e13": "Kreditkarte",
"k75bfaaa6": "Fügen Sie diesen Code in das Kopf-Skript Ihrer Website ein", "k75bfaaa6": "Fügen Sie diesen Code in das Kopf-Skript Ihrer Website ein",
"k763816ac": "Vorschau", "k763816ac": "Vorschau",
@ -157,16 +172,17 @@
"k78b1ef6a": "Eingeben", "k78b1ef6a": "Eingeben",
"k7927b824": "Sind Sie sicher, alle Offline-Knoten zu löschen?", "k7927b824": "Sind Sie sicher, alle Offline-Knoten zu löschen?",
"k7a132ce8": "Entschuldigung, aber diese Seite wurde nicht gefunden", "k7a132ce8": "Entschuldigung, aber diese Seite wurde nicht gefunden",
"k7a15497a": "Echtzeit",
"k7ac44a6e": "Sitzungsschlüssel", "k7ac44a6e": "Sitzungsschlüssel",
"k7b74a43f": "Besucher", "k7b74a43f": "Besucher",
"k7b75e24c": "Integration", "k7b75e24c": "Integration",
"k7b9aa48c": "Inhalt", "k7b9aa48c": "Inhalt",
"k7cac602a": "Status", "k7cac602a": "Status",
"k7d8cd81c": "URL kopieren",
"k7e0360fd": "Es wurde keine Gruppe erstellt, klicken Sie auf die Schaltfläche, um eine zu erstellen", "k7e0360fd": "Es wurde keine Gruppe erstellt, klicken Sie auf die Schaltfläche, um eine zu erstellen",
"k7e61b1af": "Arbeitsbereich auswählen", "k7e61b1af": "Arbeitsbereich auswählen",
"k7f01b47c": "Prüfprotokoll", "k7f01b47c": "Prüfprotokoll",
"k7f03a704": "Denken Sie daran, keine Daten mit application/json zu senden", "k7f03a704": "Denken Sie daran, keine Daten mit application/json zu senden",
"k7f29bae5": "Seitenaufrufe",
"k8037cc6b": "Server", "k8037cc6b": "Server",
"k816ce026": "Herunterladen", "k816ce026": "Herunterladen",
"k819633bc": "Zur Speicherung verwenden", "k819633bc": "Zur Speicherung verwenden",
@ -176,6 +192,7 @@
"k84ce1618": "(24 Stunden)", "k84ce1618": "(24 Stunden)",
"k84e82947": "{{num}} Ereignisse gelöscht", "k84e82947": "{{num}} Ereignisse gelöscht",
"k85344b23": "Laden", "k85344b23": "Laden",
"k85a116ee": "Webhook-URL",
"k85c5fd4c": "Noch kein Monitor eingerichtet", "k85c5fd4c": "Noch kein Monitor eingerichtet",
"k85db19da": "Noch kein Feed-Kanal vorhanden. Verwenden Sie die Feed-Funktion, um alle Ereignisse aus dem Netzwerk oder Ihrem eigenen Dienst zu empfangen.", "k85db19da": "Noch kein Feed-Kanal vorhanden. Verwenden Sie die Feed-Funktion, um alle Ereignisse aus dem Netzwerk oder Ihrem eigenen Dienst zu empfangen.",
"k873c90e6": "Anzeigelabel", "k873c90e6": "Anzeigelabel",
@ -188,6 +205,7 @@
"k88d2647b": "Webseite", "k88d2647b": "Webseite",
"k89056082": "(30 Tage)", "k89056082": "(30 Tage)",
"k892f84b6": "Aktuelle Benutzerinformationen können nicht abgerufen werden", "k892f84b6": "Aktuelle Benutzerinformationen können nicht abgerufen werden",
"k895cafe1": "Optional, Webhook-URL zum Senden der Umfrage-Payload",
"k899fd0cd": "Ports", "k899fd0cd": "Ports",
"k89d54f7a": "Überwachung der Ausführungszählung", "k89d54f7a": "Überwachung der Ausführungszählung",
"k8a1deb63": "Mitglieder", "k8a1deb63": "Mitglieder",
@ -207,7 +225,10 @@
"k90b603b8": "Duplizieren", "k90b603b8": "Duplizieren",
"k90b668e5": "Letzte 24 Stunden", "k90b668e5": "Letzte 24 Stunden",
"k93374bc9": "Website löschen", "k93374bc9": "Website löschen",
"k93458b98": "Spielplatz",
"k951a939a": "Akzeptierte Zählung der Website", "k951a939a": "Akzeptierte Zählung der Website",
"k95f932a": "Warten derzeit auf eine neue Anfrage vom Remote-Server",
"k97b02874": "Seitenanzahl",
"k98f433ee": "Reporter herunterladen von", "k98f433ee": "Reporter herunterladen von",
"k9991c290": "Gemeinschaft", "k9991c290": "Gemeinschaft",
"k9a272ecf": "Sind das Ihre Server?", "k9a272ecf": "Sind das Ihre Server?",
@ -233,6 +254,7 @@
"ka6ee7455": "Website-ID", "ka6ee7455": "Website-ID",
"ka71c12e1": "Die beiden Passwörter stimmen nicht überein", "ka71c12e1": "Die beiden Passwörter stimmen nicht überein",
"ka765ad32": "Benachrichtigung", "ka765ad32": "Benachrichtigung",
"ka7d8617e": "Feed-Kanalanzahl",
"ka7fe5937": "Festplattenlesen/-schreiben", "ka7fe5937": "Festplattenlesen/-schreiben",
"ka8e41156": "Suche und schneller Sprung", "ka8e41156": "Suche und schneller Sprung",
"ka90bc019": "Deinstallieren", "ka90bc019": "Deinstallieren",
@ -254,6 +276,7 @@
"kb0e351e0": "Aktualisiert", "kb0e351e0": "Aktualisiert",
"kb114a2e8": "Veraltet", "kb114a2e8": "Veraltet",
"kb15a6374": "Sie können Ihre Statusseite in Ihrer eigenen Domain konfigurieren, zum Beispiel: status.beispiel.com", "kb15a6374": "Sie können Ihre Statusseite in Ihrer eigenen Domain konfigurieren, zum Beispiel: status.beispiel.com",
"kb2dded49": "Schlüssel",
"kb320aac4": "Überwacht seit {{dayNum}} Tagen", "kb320aac4": "Überwacht seit {{dayNum}} Tagen",
"kb35cde91": "Suche", "kb35cde91": "Suche",
"kb35d71ed": "ODER", "kb35d71ed": "ODER",
@ -261,6 +284,7 @@
"kb5673707": "Letzte 7 Tage", "kb5673707": "Letzte 7 Tage",
"kb659c1bc": "Zert. Ablauf", "kb659c1bc": "Zert. Ablauf",
"kb6d350b6": "Feed-Kanäle", "kb6d350b6": "Feed-Kanäle",
"kb7bf8869": "API-Schlüssel",
"kb7fa344a": "Wählen Sie einen Feed-Kanal zum Senden aus", "kb7fa344a": "Wählen Sie einen Feed-Kanal zum Senden aus",
"kb8de8c50": "BCC", "kb8de8c50": "BCC",
"kbb31d3db": "Statistikdatum", "kbb31d3db": "Statistikdatum",
@ -296,13 +320,16 @@
"kcc9c1bff": "Jede Woche", "kcc9c1bff": "Jede Woche",
"kccaa732a": "Keine aufeinanderfolgenden Bindestriche", "kccaa732a": "Keine aufeinanderfolgenden Bindestriche",
"kccb42483": "Passwort", "kccb42483": "Passwort",
"kcd56f27b": "Zuletzt aktualisiert",
"kcd643ef3": "Lade...", "kcd643ef3": "Lade...",
"kce77d0c1": "Zeitzone",
"kcff78587": "Zuletzt verwendet am",
"kd005f7a8": "Alle Feeds werden entfernt", "kd005f7a8": "Alle Feeds werden entfernt",
"kd031b383": "Ansichten", "kd031b383": "Ansichten",
"kd044d5d4": "Session",
"kd092de58": "Aktueller Arbeitsbereich:", "kd092de58": "Aktueller Arbeitsbereich:",
"kd1f7e695": "Abmelden bestätigen", "kd1f7e695": "Abmelden bestätigen",
"kd211e2d4": "Versionsseite", "kd211e2d4": "Versionsseite",
"kd25f123a": "Status unbekannt",
"kd2a7ad83": "Feed-Vorlage", "kd2a7ad83": "Feed-Vorlage",
"kd3262a4a": "Konfig", "kd3262a4a": "Konfig",
"kd3396544": "Allgemein werden wir ein ein Pixel großes leeres Bild verwenden, sodass es die normale Nutzung des Benutzers nicht beeinträchtigt.", "kd3396544": "Allgemein werden wir ein ein Pixel großes leeres Bild verwenden, sodass es die normale Nutzung des Benutzers nicht beeinträchtigt.",
@ -311,14 +338,18 @@
"kd7279fa6": "Code", "kd7279fa6": "Code",
"kd7985726": "{{num}} Benutzer", "kd7985726": "{{num}} Benutzer",
"kd92fa3e7": "Host-Name", "kd92fa3e7": "Host-Name",
"kdaa6ae2b": "Überwachungsanzahl",
"kdaff25a6": "Zeige den neuesten Wert", "kdaff25a6": "Zeige den neuesten Wert",
"kdb61adbb": "Offline verbergen", "kdb61adbb": "Offline verbergen",
"kdbadcf43": "Alle Systeme betriebsbereit",
"kdbe222b": "API-Schlüssel",
"kdc10ee1a": "Erstellen Sie einen neuen Arbeitsbereich, um mit Teammitgliedern zusammenzuarbeiten.", "kdc10ee1a": "Erstellen Sie einen neuen Arbeitsbereich, um mit Teammitgliedern zusammenzuarbeiten.",
"kdc15c5d": "Daten", "kdc15c5d": "Daten",
"kdc1bf80e": "Url ist erforderlich", "kdc1bf80e": "Url ist erforderlich",
"kdc51b5db": "Webseiten", "kdc51b5db": "Webseiten",
"kdd44ac01": "Anzuzeigender Telemetrie-Name", "kdd44ac01": "Anzuzeigender Telemetrie-Name",
"kdd55936a": "Resolver-Port", "kdd55936a": "Resolver-Port",
"kde315178": "Umbenennen",
"kde37bc27": "Zurück zum Admin", "kde37bc27": "Zurück zum Admin",
"kdeba7706": "Geräte", "kdeba7706": "Geräte",
"kdeecbfea": "Resolver-Server", "kdeecbfea": "Resolver-Server",
@ -359,6 +390,7 @@
"kf246dd2e": "Es wurde kein Arbeitsbereich gefunden, bitte zuerst erstellen", "kf246dd2e": "Es wurde kein Arbeitsbereich gefunden, bitte zuerst erstellen",
"kf3b749ef": "Unterstützt Direktchat / Gruppe / Kanal-Chat-ID", "kf3b749ef": "Unterstützt Direktchat / Gruppe / Kanal-Chat-ID",
"kf55495e0": "Speichern", "kf55495e0": "Speichern",
"kf5c3b616": "Anforderungsheader",
"kf5c9520e": "Noch keine Statusseite vorhanden, Sie können eine neue erstellen, um den Status Ihres Dienstes der Öffentlichkeit anzuzeigen.", "kf5c9520e": "Noch keine Statusseite vorhanden, Sie können eine neue erstellen, um den Status Ihres Dienstes der Öffentlichkeit anzuzeigen.",
"kf6339d4f": "Verifiziert", "kf6339d4f": "Verifiziert",
"kf6582ba": "Arbeitsbereich", "kf6582ba": "Arbeitsbereich",
@ -374,6 +406,7 @@
"kf97b6f71": "Führen Sie diesen Befehl auf Ihrer Linux-Maschine aus", "kf97b6f71": "Führen Sie diesen Befehl auf Ihrer Linux-Maschine aus",
"kf9877f28": "Details anzeigen", "kf9877f28": "Details anzeigen",
"kf9965c19": "Alle Inhalte in diesem Arbeitsbereich werden zerstört und können nicht wiederhergestellt werden.", "kf9965c19": "Alle Inhalte in diesem Arbeitsbereich werden zerstört und können nicht wiederhergestellt werden.",
"kf9a498c7": "Lighthouse-Bericht abgeschlossen!",
"kfc98929b": "{{num}} Tage", "kfc98929b": "{{num}} Tage",
"kfd33c459": "Kopieren erfolgreich!", "kfd33c459": "Kopieren erfolgreich!",
"kfdaf0bb3": "Zuletzt online: {{time}}", "kfdaf0bb3": "Zuletzt online: {{time}}",

View File

@ -19,11 +19,14 @@
"k17058821": "Website Lighthouse Reports", "k17058821": "Website Lighthouse Reports",
"k172a09c3": "Suggestions", "k172a09c3": "Suggestions",
"k1777bbf2": "Manual", "k1777bbf2": "Manual",
"k1940fd6": "General",
"k1964b988": "Stop", "k1964b988": "Stop",
"k1bd89236": "run reporter with", "k1bd89236": "run reporter with",
"k1c33c293": "Settings", "k1c33c293": "Settings",
"k1d8f92b4": "Tablet", "k1d8f92b4": "Tablet",
"k1da4ecc2": "You can send a message to this channel with:",
"k1eb5b3ed": "Overview", "k1eb5b3ed": "Overview",
"k1ee0c2ca": "Set the webhook URL to <1></1>, and keep this window active. Once done, you will start receiving webhook requests here.",
"k1f6dea0": "Channel Name", "k1f6dea0": "Channel Name",
"k2099f2e0": "Login failed, please check your username and password", "k2099f2e0": "Login failed, please check your username and password",
"k20edf271": "24h", "k20edf271": "24h",
@ -53,12 +56,15 @@
"k2c84fe32": "Feed Event Count", "k2c84fe32": "Feed Event Count",
"k2cecf817": "Type", "k2cecf817": "Type",
"k2dad13e3": "Language", "k2dad13e3": "Language",
"k2db2c0c5": "Test Notify",
"k2e6dbf02": "To Email", "k2e6dbf02": "To Email",
"k2ea8a019": "Monitor", "k2ea8a019": "Monitor",
"k30b5f01b": "Workspaces", "k30b5f01b": "Workspaces",
"k30d33d71": "Webhook Signature",
"k310fee": "Last 30 days", "k310fee": "Last 30 days",
"k32344f64": "Clear Data", "k32344f64": "Clear Data",
"k3260f019": "Logout", "k3260f019": "Logout",
"k3404b72f": "New Workspace Name",
"k340547f0": "Sorry, but something went wrong", "k340547f0": "Sorry, but something went wrong",
"k3471e956": "Repaet New Password", "k3471e956": "Repaet New Password",
"k34981fea": "Docker is adrift at sea, unable to find its way. Please start Docker to get back on course.", "k34981fea": "Docker is adrift at sea, unable to find its way. Please start Docker to get back on course.",
@ -85,6 +91,7 @@
"k3e8b13f8": "Join Discord", "k3e8b13f8": "Join Discord",
"k3eaab921": "Monitor List", "k3eaab921": "Monitor List",
"k3f36e17e": "Follow Twitter", "k3f36e17e": "Follow Twitter",
"k406089a4": "Action",
"k406e9ad8": "Confirm", "k406e9ad8": "Confirm",
"k41d3ce6c": "Event unarchived", "k41d3ce6c": "Event unarchived",
"k42347b91": "Website Event Count", "k42347b91": "Website Event Count",
@ -93,7 +100,8 @@
"k44186b66": "Count", "k44186b66": "Count",
"k44cad477": "(Current)", "k44cad477": "(Current)",
"k45f80a27": "Advanced", "k45f80a27": "Advanced",
"k4738284": "You can send a message to this channel with:", "k4727e4db": "Expired At",
"k477b7ee4": "Partial System Outage",
"k47fe1f95": "Add this example code into your project", "k47fe1f95": "Add this example code into your project",
"k48186ce": "Back to Homepage", "k48186ce": "Back to Homepage",
"k4905ed7b": "NONE", "k4905ed7b": "NONE",
@ -107,6 +115,7 @@
"k4de48e75": "Max Retries", "k4de48e75": "Max Retries",
"k4e08cf58": "Show Detail Number", "k4e08cf58": "Show Detail Number",
"k4eea9393": "Profile", "k4eea9393": "Profile",
"k4f182a7c": "Major System Outage",
"k4fc2b5b": "Image", "k4fc2b5b": "Image",
"k4fe1b4de": "Telemetry", "k4fe1b4de": "Telemetry",
"k505c2733": "Create Report", "k505c2733": "Create Report",
@ -123,9 +132,12 @@
"k58267a45": "Source", "k58267a45": "Source",
"k58f90514": "Bot Token", "k58f90514": "Bot Token",
"k593cf342": "Are you sure you want to delete this monitor?", "k593cf342": "Are you sure you want to delete this monitor?",
"k5a782f4b": "Website Count",
"k5a839f71": "Uptime", "k5a839f71": "Uptime",
"k5b5be0d4": "Current Role", "k5b5be0d4": "Current Role",
"k5c18db28": "Modify Status Page Info", "k5c18db28": "Modify Status Page Info",
"k5d00536d": "Copied",
"k5d49d751": "New api key has been copied into your clipboard!",
"k5eb87a8b": "Start", "k5eb87a8b": "Start",
"k5ec0de4": "For HTTPS monitoring, if any notification method is assigned, notifications will be sent at 1, 3, 7 and 14 days before expiration.", "k5ec0de4": "For HTTPS monitoring, if any notification method is assigned, notifications will be sent at 1, 3, 7 and 14 days before expiration.",
"k5ecf04b0": "View", "k5ecf04b0": "View",
@ -135,6 +147,7 @@
"k62e19375": "Last updated at: {{date}}", "k62e19375": "Last updated at: {{date}}",
"k6488f302": "Optional", "k6488f302": "Optional",
"k659b065": "For example: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000", "k659b065": "For example: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
"k678e2f90": "Request Body",
"k67c5a895": "Yesterday", "k67c5a895": "Yesterday",
"k683be220": "Run", "k683be220": "Run",
"k691b7170": "Stopped", "k691b7170": "Stopped",
@ -147,9 +160,11 @@
"k6e96fc3": "Form Info", "k6e96fc3": "Form Info",
"k6ea11aff": "Get!", "k6ea11aff": "Get!",
"k6f15bcc3": "Host", "k6f15bcc3": "Host",
"k71067412": "Optional, Webhook Signature for Incoming Webhook",
"k721589c1": "Today", "k721589c1": "Today",
"k7247683c": "Delete Workspace", "k7247683c": "Delete Workspace",
"k7350bd93": "At the same time, we can also use it in some client-side application scenarios, such as collecting the frequency of cli usage or installation of selfhosted apps, and so on.", "k7350bd93": "At the same time, we can also use it in some client-side application scenarios, such as collecting the frequency of cli usage or installation of selfhosted apps, and so on.",
"k736f3e4c": "Copy as",
"k75581e13": "CC", "k75581e13": "CC",
"k75bfaaa6": "Add this code into your website head script", "k75bfaaa6": "Add this code into your website head script",
"k763816ac": "Preview", "k763816ac": "Preview",
@ -157,16 +172,17 @@
"k78b1ef6a": "Enter", "k78b1ef6a": "Enter",
"k7927b824": "Are you sure to clear all offline nodes?", "k7927b824": "Are you sure to clear all offline nodes?",
"k7a132ce8": "Sorry, but this page is not found", "k7a132ce8": "Sorry, but this page is not found",
"k7a15497a": "Realtime",
"k7ac44a6e": "Session Key", "k7ac44a6e": "Session Key",
"k7b74a43f": "visitors", "k7b74a43f": "visitors",
"k7b75e24c": "Integration", "k7b75e24c": "Integration",
"k7b9aa48c": "Body", "k7b9aa48c": "Body",
"k7cac602a": "Status", "k7cac602a": "Status",
"k7d8cd81c": "Copy URL",
"k7e0360fd": "No group has been created yet, click button to create one", "k7e0360fd": "No group has been created yet, click button to create one",
"k7e61b1af": "Select Workspace", "k7e61b1af": "Select Workspace",
"k7f01b47c": "Audit Log", "k7f01b47c": "Audit Log",
"k7f03a704": "Dont remember send data with application/json", "k7f03a704": "Dont remember send data with application/json",
"k7f29bae5": "pageview",
"k8037cc6b": "Servers", "k8037cc6b": "Servers",
"k816ce026": "Download", "k816ce026": "Download",
"k819633bc": "Use for storage", "k819633bc": "Use for storage",
@ -176,6 +192,7 @@
"k84ce1618": "(24 hour)", "k84ce1618": "(24 hour)",
"k84e82947": "{{num}} events cleared", "k84e82947": "{{num}} events cleared",
"k85344b23": "Load", "k85344b23": "Load",
"k85a116ee": "Webhook Url",
"k85c5fd4c": "No monitor has been set", "k85c5fd4c": "No monitor has been set",
"k85db19da": "No feed channel yet. Use feed feature to receive any event from your network or service.", "k85db19da": "No feed channel yet. Use feed feature to receive any event from your network or service.",
"k873c90e6": "Display Label", "k873c90e6": "Display Label",
@ -188,6 +205,7 @@
"k88d2647b": "Website", "k88d2647b": "Website",
"k89056082": "(30 days)", "k89056082": "(30 days)",
"k892f84b6": "Can not get current user info", "k892f84b6": "Can not get current user info",
"k895cafe1": "Optional, webhook url to send survey payload",
"k899fd0cd": "ports", "k899fd0cd": "ports",
"k89d54f7a": "Monitor Execution Count", "k89d54f7a": "Monitor Execution Count",
"k8a1deb63": "Members", "k8a1deb63": "Members",
@ -207,7 +225,10 @@
"k90b603b8": "Duplicate", "k90b603b8": "Duplicate",
"k90b668e5": "Last 24 Hours", "k90b668e5": "Last 24 Hours",
"k93374bc9": "Delete Website", "k93374bc9": "Delete Website",
"k93458b98": "Playground",
"k951a939a": "Website Accepted Count", "k951a939a": "Website Accepted Count",
"k95f932a": "Currently waiting for a new request from the remote server",
"k97b02874": "Page Count",
"k98f433ee": "Download reporter from", "k98f433ee": "Download reporter from",
"k9991c290": "Community", "k9991c290": "Community",
"k9a272ecf": "Is this your servers?", "k9a272ecf": "Is this your servers?",
@ -233,6 +254,7 @@
"ka6ee7455": "Website ID", "ka6ee7455": "Website ID",
"ka71c12e1": "The two passwords are not consistent", "ka71c12e1": "The two passwords are not consistent",
"ka765ad32": "Notification", "ka765ad32": "Notification",
"ka7d8617e": "Feed Channel Count",
"ka7fe5937": "Disk read/write", "ka7fe5937": "Disk read/write",
"ka8e41156": "Search and quick jump", "ka8e41156": "Search and quick jump",
"ka90bc019": "Uninstall", "ka90bc019": "Uninstall",
@ -254,6 +276,7 @@
"kb0e351e0": "Refreshed", "kb0e351e0": "Refreshed",
"kb114a2e8": "Deprecated", "kb114a2e8": "Deprecated",
"kb15a6374": "You can config your status page in your own domain, for example: status.example.com", "kb15a6374": "You can config your status page in your own domain, for example: status.example.com",
"kb2dded49": "Key",
"kb320aac4": "Monitored for {{dayNum}} days", "kb320aac4": "Monitored for {{dayNum}} days",
"kb35cde91": "Search", "kb35cde91": "Search",
"kb35d71ed": "OR", "kb35d71ed": "OR",
@ -261,6 +284,7 @@
"kb5673707": "Last 7 days", "kb5673707": "Last 7 days",
"kb659c1bc": "Cert Exp.", "kb659c1bc": "Cert Exp.",
"kb6d350b6": "Feed Channels", "kb6d350b6": "Feed Channels",
"kb7bf8869": "Api Keys",
"kb7fa344a": "Select Feed Channel for send", "kb7fa344a": "Select Feed Channel for send",
"kb8de8c50": "BCC", "kb8de8c50": "BCC",
"kbb31d3db": "Statistic Date", "kbb31d3db": "Statistic Date",
@ -296,13 +320,16 @@
"kcc9c1bff": "Every Week", "kcc9c1bff": "Every Week",
"kccaa732a": "No consecutive dashes", "kccaa732a": "No consecutive dashes",
"kccb42483": "Password", "kccb42483": "Password",
"kcd56f27b": "Last updated",
"kcd643ef3": "Loading...", "kcd643ef3": "Loading...",
"kce77d0c1": "Timezone",
"kcff78587": "Last Use At",
"kd005f7a8": "All feed will be remove", "kd005f7a8": "All feed will be remove",
"kd031b383": "Views", "kd031b383": "Views",
"kd044d5d4": "session",
"kd092de58": "Current Workspace:", "kd092de58": "Current Workspace:",
"kd1f7e695": "Confirm to logout", "kd1f7e695": "Confirm to logout",
"kd211e2d4": "Releases Page", "kd211e2d4": "Releases Page",
"kd25f123a": "Status Unknown",
"kd2a7ad83": "Feed Template", "kd2a7ad83": "Feed Template",
"kd3262a4a": "Config", "kd3262a4a": "Config",
"kd3396544": "Generally, we will use a one-pixel blank image so that it will not affect the user's normal use.", "kd3396544": "Generally, we will use a one-pixel blank image so that it will not affect the user's normal use.",
@ -311,14 +338,18 @@
"kd7279fa6": "Code", "kd7279fa6": "Code",
"kd7985726": "{{num}} users", "kd7985726": "{{num}} users",
"kd92fa3e7": "Host Name", "kd92fa3e7": "Host Name",
"kdaa6ae2b": "Monitor Count",
"kdaff25a6": "Show Latest Value", "kdaff25a6": "Show Latest Value",
"kdb61adbb": "Hide Offline", "kdb61adbb": "Hide Offline",
"kdbadcf43": "All Systems Operational",
"kdbe222b": "Api Key",
"kdc10ee1a": "Create a new workspace to cooperate with team members.", "kdc10ee1a": "Create a new workspace to cooperate with team members.",
"kdc15c5d": "Data", "kdc15c5d": "Data",
"kdc1bf80e": "Url is required", "kdc1bf80e": "Url is required",
"kdc51b5db": "Websites", "kdc51b5db": "Websites",
"kdd44ac01": "Telemetry Name to Display", "kdd44ac01": "Telemetry Name to Display",
"kdd55936a": "Resolver Port", "kdd55936a": "Resolver Port",
"kde315178": "Rename",
"kde37bc27": "Back to Admin", "kde37bc27": "Back to Admin",
"kdeba7706": "Devices", "kdeba7706": "Devices",
"kdeecbfea": "Resolver Server", "kdeecbfea": "Resolver Server",
@ -359,6 +390,7 @@
"kf246dd2e": "Not any workspace has been found, please create first", "kf246dd2e": "Not any workspace has been found, please create first",
"kf3b749ef": "Support Direct Chat / Group / Channel's Chat ID", "kf3b749ef": "Support Direct Chat / Group / Channel's Chat ID",
"kf55495e0": "Save", "kf55495e0": "Save",
"kf5c3b616": "Request Header",
"kf5c9520e": "No any status page yet, you can create a new one to show your service status to public.", "kf5c9520e": "No any status page yet, you can create a new one to show your service status to public.",
"kf6339d4f": "Verified", "kf6339d4f": "Verified",
"kf6582ba": "Workspace", "kf6582ba": "Workspace",
@ -374,6 +406,7 @@
"kf97b6f71": "Run this command in your linux machine", "kf97b6f71": "Run this command in your linux machine",
"kf9877f28": "View Details", "kf9877f28": "View Details",
"kf9965c19": "All content in this workspace will be destory and can not recover.", "kf9965c19": "All content in this workspace will be destory and can not recover.",
"kf9a498c7": "Lighthouse report completed!",
"kfc98929b": "{{num}} days", "kfc98929b": "{{num}} days",
"kfd33c459": "Copy success!", "kfd33c459": "Copy success!",
"kfdaf0bb3": "Last online: {{time}}", "kfdaf0bb3": "Last online: {{time}}",

View File

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

View File

@ -19,11 +19,14 @@
"k17058821": "Rapports Lighthouse du site Web", "k17058821": "Rapports Lighthouse du site Web",
"k172a09c3": "Suggestions", "k172a09c3": "Suggestions",
"k1777bbf2": "Manuel", "k1777bbf2": "Manuel",
"k1940fd6": "Général",
"k1964b988": "Arrêter", "k1964b988": "Arrêter",
"k1bd89236": "exécuter le rapporteur avec", "k1bd89236": "exécuter le rapporteur avec",
"k1c33c293": "Paramètres", "k1c33c293": "Paramètres",
"k1d8f92b4": "Tablette", "k1d8f92b4": "Tablette",
"k1da4ecc2": "Vous pouvez envoyer un message à ce canal avec :",
"k1eb5b3ed": "Aperçu", "k1eb5b3ed": "Aperçu",
"k1ee0c2ca": "Définissez l'URL du webhook sur <1></1>, et gardez cette fenêtre active. Une fois terminé, vous commencerez à recevoir des requêtes webhook ici.",
"k1f6dea0": "Nom du canal", "k1f6dea0": "Nom du canal",
"k2099f2e0": "Échec de la connexion, veuillez vérifier votre nom d'utilisateur et votre mot de passe", "k2099f2e0": "Échec de la connexion, veuillez vérifier votre nom d'utilisateur et votre mot de passe",
"k20edf271": "24h", "k20edf271": "24h",
@ -53,12 +56,15 @@
"k2c84fe32": "Nombre d'événements de flux", "k2c84fe32": "Nombre d'événements de flux",
"k2cecf817": "Type", "k2cecf817": "Type",
"k2dad13e3": "Langue", "k2dad13e3": "Langue",
"k2db2c0c5": "Test Notify",
"k2e6dbf02": "À l'email", "k2e6dbf02": "À l'email",
"k2ea8a019": "Moniteur", "k2ea8a019": "Moniteur",
"k30b5f01b": "Espaces de travail", "k30b5f01b": "Espaces de travail",
"k30d33d71": "Signature du Webhook",
"k310fee": "30 derniers jours", "k310fee": "30 derniers jours",
"k32344f64": "Effacer les données", "k32344f64": "Effacer les données",
"k3260f019": "Déconnexion", "k3260f019": "Déconnexion",
"k3404b72f": "Nouveau nom d'espace de travail",
"k340547f0": "Désolé, mais quelque chose s'est mal passé", "k340547f0": "Désolé, mais quelque chose s'est mal passé",
"k3471e956": "Répéter le nouveau mot de passe", "k3471e956": "Répéter le nouveau mot de passe",
"k34981fea": "Docker est à la dérive en mer, incapable de trouver son chemin. Veuillez démarrer Docker pour revenir sur la bonne voie.", "k34981fea": "Docker est à la dérive en mer, incapable de trouver son chemin. Veuillez démarrer Docker pour revenir sur la bonne voie.",
@ -85,6 +91,7 @@
"k3e8b13f8": "Rejoindre Discord", "k3e8b13f8": "Rejoindre Discord",
"k3eaab921": "Liste de surveillance", "k3eaab921": "Liste de surveillance",
"k3f36e17e": "Suivre Twitter", "k3f36e17e": "Suivre Twitter",
"k406089a4": "Action",
"k406e9ad8": "Confirmer", "k406e9ad8": "Confirmer",
"k41d3ce6c": "Événement désarchivé", "k41d3ce6c": "Événement désarchivé",
"k42347b91": "Nombre d'événements sur le site Web", "k42347b91": "Nombre d'événements sur le site Web",
@ -93,7 +100,8 @@
"k44186b66": "Compte", "k44186b66": "Compte",
"k44cad477": "(Actuel)", "k44cad477": "(Actuel)",
"k45f80a27": "Avancé", "k45f80a27": "Avancé",
"k4738284": "Vous pouvez envoyer n'importe quel message dans ce canal avec :", "k4727e4db": "Expiré À",
"k477b7ee4": "Panne partielle du système",
"k47fe1f95": "Ajoutez ce code d'exemple à votre projet", "k47fe1f95": "Ajoutez ce code d'exemple à votre projet",
"k48186ce": "Retour à la page d'accueil", "k48186ce": "Retour à la page d'accueil",
"k4905ed7b": "AUCUN", "k4905ed7b": "AUCUN",
@ -107,6 +115,7 @@
"k4de48e75": "Nombre maximum de tentatives", "k4de48e75": "Nombre maximum de tentatives",
"k4e08cf58": "Afficher le numéro de détail", "k4e08cf58": "Afficher le numéro de détail",
"k4eea9393": "Profil", "k4eea9393": "Profil",
"k4f182a7c": "Panne majeure du système",
"k4fc2b5b": "Image", "k4fc2b5b": "Image",
"k4fe1b4de": "Télémétrie", "k4fe1b4de": "Télémétrie",
"k505c2733": "Créer un rapport", "k505c2733": "Créer un rapport",
@ -123,9 +132,12 @@
"k58267a45": "Source", "k58267a45": "Source",
"k58f90514": "Jeton de bot", "k58f90514": "Jeton de bot",
"k593cf342": "Êtes-vous sûr de vouloir supprimer ce moniteur ?", "k593cf342": "Êtes-vous sûr de vouloir supprimer ce moniteur ?",
"k5a782f4b": "Nombre de Sites Web",
"k5a839f71": "Disponibilité", "k5a839f71": "Disponibilité",
"k5b5be0d4": "Rôle actuel", "k5b5be0d4": "Rôle actuel",
"k5c18db28": "Modifier les informations de la page d'état", "k5c18db28": "Modifier les informations de la page d'état",
"k5d00536d": "Copié",
"k5d49d751": "La nouvelle clé API a été copiée dans votre presse-papiers !",
"k5eb87a8b": "Démarrer", "k5eb87a8b": "Démarrer",
"k5ec0de4": "Pour la surveillance HTTPS, si une méthode de notification est assignée, des notifications seront envoyées à 1, 3, 7 et 14 jours avant l'expiration.", "k5ec0de4": "Pour la surveillance HTTPS, si une méthode de notification est assignée, des notifications seront envoyées à 1, 3, 7 et 14 jours avant l'expiration.",
"k5ecf04b0": "Vue", "k5ecf04b0": "Vue",
@ -135,6 +147,7 @@
"k62e19375": "Dernière mise à jour : {{date}}", "k62e19375": "Dernière mise à jour : {{date}}",
"k6488f302": "Optionnel", "k6488f302": "Optionnel",
"k659b065": "Par exemple : https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000", "k659b065": "Par exemple : https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
"k678e2f90": "Corps de la requête",
"k67c5a895": "Hier", "k67c5a895": "Hier",
"k683be220": "Exécuter", "k683be220": "Exécuter",
"k691b7170": "Arrêté", "k691b7170": "Arrêté",
@ -147,9 +160,11 @@
"k6e96fc3": "Informations sur le formulaire", "k6e96fc3": "Informations sur le formulaire",
"k6ea11aff": "Obtenir !", "k6ea11aff": "Obtenir !",
"k6f15bcc3": "Hôte", "k6f15bcc3": "Hôte",
"k71067412": "Optionnel, signature du webhook pour le webhook entrant",
"k721589c1": "Aujourd'hui", "k721589c1": "Aujourd'hui",
"k7247683c": "Supprimer l'espace de travail", "k7247683c": "Supprimer l'espace de travail",
"k7350bd93": "En même temps, nous pouvons également l'utiliser dans certains scénarios d'application côté client, comme la collecte de la fréquence d'utilisation du CLI, la collecte de l'installation d'applications auto-hébergées, etc.", "k7350bd93": "En même temps, nous pouvons également l'utiliser dans certains scénarios d'application côté client, comme la collecte de la fréquence d'utilisation du CLI, la collecte de l'installation d'applications auto-hébergées, etc.",
"k736f3e4c": "Copier en tant que",
"k75581e13": "Sous-titres", "k75581e13": "Sous-titres",
"k75bfaaa6": "Ajoutez ce code dans le script de tête de votre site web", "k75bfaaa6": "Ajoutez ce code dans le script de tête de votre site web",
"k763816ac": "Aperçu", "k763816ac": "Aperçu",
@ -157,16 +172,17 @@
"k78b1ef6a": "Entrer", "k78b1ef6a": "Entrer",
"k7927b824": "Êtes-vous sûr de vouloir effacer tous les nœuds hors ligne ?", "k7927b824": "Êtes-vous sûr de vouloir effacer tous les nœuds hors ligne ?",
"k7a132ce8": "Désolé, mais cette page est introuvable", "k7a132ce8": "Désolé, mais cette page est introuvable",
"k7a15497a": "Temps réel",
"k7ac44a6e": "Clé de session", "k7ac44a6e": "Clé de session",
"k7b74a43f": "visiteurs", "k7b74a43f": "visiteurs",
"k7b75e24c": "Intégration", "k7b75e24c": "Intégration",
"k7b9aa48c": "Corps", "k7b9aa48c": "Corps",
"k7cac602a": "Statut", "k7cac602a": "Statut",
"k7d8cd81c": "Copier l'URL",
"k7e0360fd": "Aucun groupe n'a été créé, cliquez sur le bouton pour en créer un", "k7e0360fd": "Aucun groupe n'a été créé, cliquez sur le bouton pour en créer un",
"k7e61b1af": "Sélectionner l'espace de travail", "k7e61b1af": "Sélectionner l'espace de travail",
"k7f01b47c": "Journal d'audit", "k7f01b47c": "Journal d'audit",
"k7f03a704": "N'oubliez pas de ne pas envoyer de données avec application/json", "k7f03a704": "N'oubliez pas de ne pas envoyer de données avec application/json",
"k7f29bae5": "Vue de la page",
"k8037cc6b": "Serveurs", "k8037cc6b": "Serveurs",
"k816ce026": "Télécharger", "k816ce026": "Télécharger",
"k819633bc": "Utiliser pour le stockage", "k819633bc": "Utiliser pour le stockage",
@ -176,6 +192,7 @@
"k84ce1618": "(24 heures)", "k84ce1618": "(24 heures)",
"k84e82947": "{{num}} événements effacés", "k84e82947": "{{num}} événements effacés",
"k85344b23": "Charge", "k85344b23": "Charge",
"k85a116ee": "URL du Webhook",
"k85c5fd4c": "Aucun moniteur n'a été défini", "k85c5fd4c": "Aucun moniteur n'a été défini",
"k85db19da": "Pas encore de canal de flux. Utilisez la fonctionnalité de flux pour recevoir tous les événements du réseau ou de votre propre service.", "k85db19da": "Pas encore de canal de flux. Utilisez la fonctionnalité de flux pour recevoir tous les événements du réseau ou de votre propre service.",
"k873c90e6": "Étiquette d'affichage", "k873c90e6": "Étiquette d'affichage",
@ -188,6 +205,7 @@
"k88d2647b": "Site Web", "k88d2647b": "Site Web",
"k89056082": "(30 jours)", "k89056082": "(30 jours)",
"k892f84b6": "Impossible d'obtenir les informations de l'utilisateur actuel", "k892f84b6": "Impossible d'obtenir les informations de l'utilisateur actuel",
"k895cafe1": "Optionnel, URL du webhook pour envoyer la charge utile de l'enquête",
"k899fd0cd": "Ports", "k899fd0cd": "Ports",
"k89d54f7a": "Compte des exécutions surveillées", "k89d54f7a": "Compte des exécutions surveillées",
"k8a1deb63": "Membres", "k8a1deb63": "Membres",
@ -207,7 +225,10 @@
"k90b603b8": "Dupliquer", "k90b603b8": "Dupliquer",
"k90b668e5": "24 dernières heures", "k90b668e5": "24 dernières heures",
"k93374bc9": "Supprimer le site Web", "k93374bc9": "Supprimer le site Web",
"k93458b98": "Terrain de jeu",
"k951a939a": "Compte accepté par le site Web", "k951a939a": "Compte accepté par le site Web",
"k95f932a": "En attente d'une nouvelle requête du serveur distant",
"k97b02874": "Nombre de Pages",
"k98f433ee": "Télécharger le rapporteur de", "k98f433ee": "Télécharger le rapporteur de",
"k9991c290": "Communauté", "k9991c290": "Communauté",
"k9a272ecf": "S'agit-il de vos serveurs ?", "k9a272ecf": "S'agit-il de vos serveurs ?",
@ -233,6 +254,7 @@
"ka6ee7455": "ID du site Web", "ka6ee7455": "ID du site Web",
"ka71c12e1": "Les deux mots de passe ne sont pas cohérents", "ka71c12e1": "Les deux mots de passe ne sont pas cohérents",
"ka765ad32": "Notification", "ka765ad32": "Notification",
"ka7d8617e": "Nombre de Canaux de Flux",
"ka7fe5937": "Lecture/écriture de disque", "ka7fe5937": "Lecture/écriture de disque",
"ka8e41156": "Rechercher et sauter rapidement", "ka8e41156": "Rechercher et sauter rapidement",
"ka90bc019": "Désinstaller", "ka90bc019": "Désinstaller",
@ -254,6 +276,7 @@
"kb0e351e0": "Rafraîchi", "kb0e351e0": "Rafraîchi",
"kb114a2e8": "Obsolète", "kb114a2e8": "Obsolète",
"kb15a6374": "Vous pouvez configurer votre page de statut sur votre propre domaine, par exemple : status.example.com", "kb15a6374": "Vous pouvez configurer votre page de statut sur votre propre domaine, par exemple : status.example.com",
"kb2dded49": "Clé",
"kb320aac4": "Surveillé pendant {{dayNum}} jours", "kb320aac4": "Surveillé pendant {{dayNum}} jours",
"kb35cde91": "Recherche", "kb35cde91": "Recherche",
"kb35d71ed": "OU", "kb35d71ed": "OU",
@ -261,6 +284,7 @@
"kb5673707": "7 derniers jours", "kb5673707": "7 derniers jours",
"kb659c1bc": "Expiration du cert.", "kb659c1bc": "Expiration du cert.",
"kb6d350b6": "Canaux de flux", "kb6d350b6": "Canaux de flux",
"kb7bf8869": "Clés API",
"kb7fa344a": "Sélectionner le canal de flux à envoyer", "kb7fa344a": "Sélectionner le canal de flux à envoyer",
"kb8de8c50": "CCI", "kb8de8c50": "CCI",
"kbb31d3db": "Date de statistique", "kbb31d3db": "Date de statistique",
@ -296,13 +320,16 @@
"kcc9c1bff": "Toutes les semaines", "kcc9c1bff": "Toutes les semaines",
"kccaa732a": "Pas de tirets consécutifs", "kccaa732a": "Pas de tirets consécutifs",
"kccb42483": "Mot de passe", "kccb42483": "Mot de passe",
"kcd56f27b": "Dernière mise à jour",
"kcd643ef3": "Chargement...", "kcd643ef3": "Chargement...",
"kce77d0c1": "Fuseau horaire",
"kcff78587": "Dernière Utilisation À",
"kd005f7a8": "Tous les flux seront supprimés", "kd005f7a8": "Tous les flux seront supprimés",
"kd031b383": "Vues", "kd031b383": "Vues",
"kd044d5d4": "Session",
"kd092de58": "Espace de travail actuel :", "kd092de58": "Espace de travail actuel :",
"kd1f7e695": "Confirmer la déconnexion", "kd1f7e695": "Confirmer la déconnexion",
"kd211e2d4": "Page des versions", "kd211e2d4": "Page des versions",
"kd25f123a": "Statut inconnu",
"kd2a7ad83": "Modèle de flux", "kd2a7ad83": "Modèle de flux",
"kd3262a4a": "Configuration", "kd3262a4a": "Configuration",
"kd3396544": "Généralement, nous utiliserons une image vide d'un pixel de sorte qu'elle n'affecte pas l'utilisation normale de l'utilisateur.", "kd3396544": "Généralement, nous utiliserons une image vide d'un pixel de sorte qu'elle n'affecte pas l'utilisation normale de l'utilisateur.",
@ -311,14 +338,18 @@
"kd7279fa6": "Code", "kd7279fa6": "Code",
"kd7985726": "{{num}} utilisateurs", "kd7985726": "{{num}} utilisateurs",
"kd92fa3e7": "Nom de l'hôte", "kd92fa3e7": "Nom de l'hôte",
"kdaa6ae2b": "Nombre de Moniteurs",
"kdaff25a6": "Afficher la dernière valeur", "kdaff25a6": "Afficher la dernière valeur",
"kdb61adbb": "Masquer hors ligne", "kdb61adbb": "Masquer hors ligne",
"kdbadcf43": "Tous les systèmes opérationnels",
"kdbe222b": "Clé API",
"kdc10ee1a": "Créer un nouvel espace de travail pour coopérer avec les membres de l'équipe.", "kdc10ee1a": "Créer un nouvel espace de travail pour coopérer avec les membres de l'équipe.",
"kdc15c5d": "Données", "kdc15c5d": "Données",
"kdc1bf80e": "L'URL est requise", "kdc1bf80e": "L'URL est requise",
"kdc51b5db": "Sites Web", "kdc51b5db": "Sites Web",
"kdd44ac01": "Nom de la télémétrie à afficher", "kdd44ac01": "Nom de la télémétrie à afficher",
"kdd55936a": "Port de résolveur", "kdd55936a": "Port de résolveur",
"kde315178": "Renommer",
"kde37bc27": "Retour à l'administrateur", "kde37bc27": "Retour à l'administrateur",
"kdeba7706": "Appareils", "kdeba7706": "Appareils",
"kdeecbfea": "Serveur de résolveur", "kdeecbfea": "Serveur de résolveur",
@ -359,6 +390,7 @@
"kf246dd2e": "Aucun espace de travail n'a été trouvé, veuillez d'abord en créer un", "kf246dd2e": "Aucun espace de travail n'a été trouvé, veuillez d'abord en créer un",
"kf3b749ef": "Prend en charge le chat direct / groupe / ID de chat de canal", "kf3b749ef": "Prend en charge le chat direct / groupe / ID de chat de canal",
"kf55495e0": "Sauvegarder", "kf55495e0": "Sauvegarder",
"kf5c3b616": "En-tête de la requête",
"kf5c9520e": "Pas encore de page de statut, vous pouvez en créer une nouvelle pour afficher l'état de votre service au public.", "kf5c9520e": "Pas encore de page de statut, vous pouvez en créer une nouvelle pour afficher l'état de votre service au public.",
"kf6339d4f": "Vérifié", "kf6339d4f": "Vérifié",
"kf6582ba": "Espace de travail", "kf6582ba": "Espace de travail",
@ -374,6 +406,7 @@
"kf97b6f71": "Exécutez cette commande sur votre machine Linux", "kf97b6f71": "Exécutez cette commande sur votre machine Linux",
"kf9877f28": "Voir les détails", "kf9877f28": "Voir les détails",
"kf9965c19": "Tout le contenu de cet espace de travail sera détruit et ne pourra pas être récupéré.", "kf9965c19": "Tout le contenu de cet espace de travail sera détruit et ne pourra pas être récupéré.",
"kf9a498c7": "Rapport Lighthouse terminé !",
"kfc98929b": "{{num}} jours", "kfc98929b": "{{num}} jours",
"kfd33c459": "Copie réussie !", "kfd33c459": "Copie réussie !",
"kfdaf0bb3": "Dernière connexion : {{time}}", "kfdaf0bb3": "Dernière connexion : {{time}}",

View File

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 450 B

View File

@ -19,11 +19,14 @@
"k17058821": "ウェブサイト ライトハウス レポート", "k17058821": "ウェブサイト ライトハウス レポート",
"k172a09c3": "提案", "k172a09c3": "提案",
"k1777bbf2": "マニュアル", "k1777bbf2": "マニュアル",
"k1940fd6": "一般",
"k1964b988": "停止", "k1964b988": "停止",
"k1bd89236": "レポーターを実行する", "k1bd89236": "レポーターを実行する",
"k1c33c293": "設定", "k1c33c293": "設定",
"k1d8f92b4": "タブレット", "k1d8f92b4": "タブレット",
"k1da4ecc2": "このチャンネルにメッセージを送信できます:",
"k1eb5b3ed": "概要", "k1eb5b3ed": "概要",
"k1ee0c2ca": "Webhook URLを<1></1>に設定し、このウィンドウをアクティブに保ってください。完了すると、ここでWebhookリクエストを受信し始めます。",
"k1f6dea0": "チャンネル名", "k1f6dea0": "チャンネル名",
"k2099f2e0": "ログインに失敗しました。ユーザー名とパスワードを確認してください", "k2099f2e0": "ログインに失敗しました。ユーザー名とパスワードを確認してください",
"k20edf271": "24時間", "k20edf271": "24時間",
@ -53,12 +56,15 @@
"k2c84fe32": "フィードイベント数", "k2c84fe32": "フィードイベント数",
"k2cecf817": "タイプ", "k2cecf817": "タイプ",
"k2dad13e3": "言語", "k2dad13e3": "言語",
"k2db2c0c5": "テスト通知",
"k2e6dbf02": "メールアドレスへ", "k2e6dbf02": "メールアドレスへ",
"k2ea8a019": "モニター", "k2ea8a019": "モニター",
"k30b5f01b": "ワークスペース", "k30b5f01b": "ワークスペース",
"k30d33d71": "Webhook署名",
"k310fee": "過去30日間", "k310fee": "過去30日間",
"k32344f64": "データクリア", "k32344f64": "データクリア",
"k3260f019": "ログアウト", "k3260f019": "ログアウト",
"k3404b72f": "新しいワークスペース名",
"k340547f0": "申し訳ありませんが、何か問題が発生しました", "k340547f0": "申し訳ありませんが、何か問題が発生しました",
"k3471e956": "新しいパスワードの再入力", "k3471e956": "新しいパスワードの再入力",
"k34981fea": "Dockerは海上で漂流しており、方向を見失っています。Dockerを起動して進路を修正してください。", "k34981fea": "Dockerは海上で漂流しており、方向を見失っています。Dockerを起動して進路を修正してください。",
@ -85,6 +91,7 @@
"k3e8b13f8": "Discordに参加", "k3e8b13f8": "Discordに参加",
"k3eaab921": "モニターリスト", "k3eaab921": "モニターリスト",
"k3f36e17e": "Twitterをフォロー", "k3f36e17e": "Twitterをフォロー",
"k406089a4": "アクション",
"k406e9ad8": "確認", "k406e9ad8": "確認",
"k41d3ce6c": "イベントがアーカイブ解除されました", "k41d3ce6c": "イベントがアーカイブ解除されました",
"k42347b91": "ウェブサイトイベント数", "k42347b91": "ウェブサイトイベント数",
@ -93,7 +100,8 @@
"k44186b66": "カウント", "k44186b66": "カウント",
"k44cad477": "(現在)", "k44cad477": "(現在)",
"k45f80a27": "詳細", "k45f80a27": "詳細",
"k4738284": "次の方法でこのチャンネルにメッセージを送信できます:", "k4727e4db": "期限切れ",
"k477b7ee4": "部分的なシステム障害",
"k47fe1f95": "このサンプルコードをプロジェクトに追加してください", "k47fe1f95": "このサンプルコードをプロジェクトに追加してください",
"k48186ce": "ホームページに戻る", "k48186ce": "ホームページに戻る",
"k4905ed7b": "なし", "k4905ed7b": "なし",
@ -107,6 +115,7 @@
"k4de48e75": "最大リトライ回数", "k4de48e75": "最大リトライ回数",
"k4e08cf58": "詳細番号を表示", "k4e08cf58": "詳細番号を表示",
"k4eea9393": "プロファイル", "k4eea9393": "プロファイル",
"k4f182a7c": "重大なシステム障害",
"k4fc2b5b": "画像", "k4fc2b5b": "画像",
"k4fe1b4de": "テレメトリー", "k4fe1b4de": "テレメトリー",
"k505c2733": "レポートを作成", "k505c2733": "レポートを作成",
@ -123,9 +132,12 @@
"k58267a45": "ソース", "k58267a45": "ソース",
"k58f90514": "ボットトークン", "k58f90514": "ボットトークン",
"k593cf342": "このモニターを削除してもよろしいですか?", "k593cf342": "このモニターを削除してもよろしいですか?",
"k5a782f4b": "ウェブサイト数",
"k5a839f71": "アップタイム", "k5a839f71": "アップタイム",
"k5b5be0d4": "現在の役割", "k5b5be0d4": "現在の役割",
"k5c18db28": "ステータスページ情報を変更", "k5c18db28": "ステータスページ情報を変更",
"k5d00536d": "コピー済み",
"k5d49d751": "新しいAPIキーがクリップボードにコピーされました",
"k5eb87a8b": "開始", "k5eb87a8b": "開始",
"k5ec0de4": "HTTPSモニタリングの場合、通知方法が割り当てられている場合、有効期限の1、3、7、14日前に通知が送信されます。", "k5ec0de4": "HTTPSモニタリングの場合、通知方法が割り当てられている場合、有効期限の1、3、7、14日前に通知が送信されます。",
"k5ecf04b0": "ビュー", "k5ecf04b0": "ビュー",
@ -135,6 +147,7 @@
"k62e19375": "最終更新:{{date}}", "k62e19375": "最終更新:{{date}}",
"k6488f302": "オプション", "k6488f302": "オプション",
"k659b065": "例https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000", "k659b065": "例https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
"k678e2f90": "リクエストボディ",
"k67c5a895": "昨日", "k67c5a895": "昨日",
"k683be220": "実行", "k683be220": "実行",
"k691b7170": "停止済み", "k691b7170": "停止済み",
@ -147,9 +160,11 @@
"k6e96fc3": "フォーム情報", "k6e96fc3": "フォーム情報",
"k6ea11aff": "取得!", "k6ea11aff": "取得!",
"k6f15bcc3": "ホスト", "k6f15bcc3": "ホスト",
"k71067412": "オプション、受信WebhookのWebhook署名",
"k721589c1": "今日", "k721589c1": "今日",
"k7247683c": "ワークスペースを削除", "k7247683c": "ワークスペースを削除",
"k7350bd93": "同時に、CLIの使用頻度の収集、自己ホスト型アプリのインストールの収集など、クライアントサイドのアプリケーションシナリオでも使用することができます。", "k7350bd93": "同時に、CLIの使用頻度の収集、自己ホスト型アプリのインストールの収集など、クライアントサイドのアプリケーションシナリオでも使用することができます。",
"k736f3e4c": "コピーとして",
"k75581e13": "CC", "k75581e13": "CC",
"k75bfaaa6": "このコードをウェブサイトのヘッドスクリプトに追加してください", "k75bfaaa6": "このコードをウェブサイトのヘッドスクリプトに追加してください",
"k763816ac": "プレビュー", "k763816ac": "プレビュー",
@ -157,16 +172,17 @@
"k78b1ef6a": "入力", "k78b1ef6a": "入力",
"k7927b824": "すべてのオフラインノードをクリアしてもよろしいですか?", "k7927b824": "すべてのオフラインノードをクリアしてもよろしいですか?",
"k7a132ce8": "申し訳ありませんが、このページは見つかりません", "k7a132ce8": "申し訳ありませんが、このページは見つかりません",
"k7a15497a": "リアルタイム",
"k7ac44a6e": "セッションキー", "k7ac44a6e": "セッションキー",
"k7b74a43f": "訪問者", "k7b74a43f": "訪問者",
"k7b75e24c": "統合", "k7b75e24c": "統合",
"k7b9aa48c": "ボディ", "k7b9aa48c": "ボディ",
"k7cac602a": "ステータス", "k7cac602a": "ステータス",
"k7d8cd81c": "URLをコピー",
"k7e0360fd": "グループが作成されていません。ボタンをクリックして作成してください", "k7e0360fd": "グループが作成されていません。ボタンをクリックして作成してください",
"k7e61b1af": "ワークスペースを選択", "k7e61b1af": "ワークスペースを選択",
"k7f01b47c": "監査ログ", "k7f01b47c": "監査ログ",
"k7f03a704": "application/json でデータを送信しないことを忘れないでください", "k7f03a704": "application/json でデータを送信しないことを忘れないでください",
"k7f29bae5": "ページビュー",
"k8037cc6b": "サーバー", "k8037cc6b": "サーバー",
"k816ce026": "ダウンロード", "k816ce026": "ダウンロード",
"k819633bc": "ストレージ用", "k819633bc": "ストレージ用",
@ -176,6 +192,7 @@
"k84ce1618": "24時間", "k84ce1618": "24時間",
"k84e82947": "{{num}} イベントがクリアされました", "k84e82947": "{{num}} イベントがクリアされました",
"k85344b23": "ロード", "k85344b23": "ロード",
"k85a116ee": "Webhook URL",
"k85c5fd4c": "まだモニターが設定されていません", "k85c5fd4c": "まだモニターが設定されていません",
"k85db19da": "まだフィードチャンネルがありません。ネットワークや自分のサービスからのすべてのイベントを受信するには、フィード機能を使用してください。", "k85db19da": "まだフィードチャンネルがありません。ネットワークや自分のサービスからのすべてのイベントを受信するには、フィード機能を使用してください。",
"k873c90e6": "表示ラベル", "k873c90e6": "表示ラベル",
@ -188,6 +205,7 @@
"k88d2647b": "ウェブサイト", "k88d2647b": "ウェブサイト",
"k89056082": "30日間", "k89056082": "30日間",
"k892f84b6": "現在のユーザー情報を取得できません", "k892f84b6": "現在のユーザー情報を取得できません",
"k895cafe1": "オプション、調査ペイロードを送信するためのWebhook URL",
"k899fd0cd": "ポート", "k899fd0cd": "ポート",
"k89d54f7a": "実行カウントの監視", "k89d54f7a": "実行カウントの監視",
"k8a1deb63": "メンバー", "k8a1deb63": "メンバー",
@ -207,7 +225,10 @@
"k90b603b8": "重複", "k90b603b8": "重複",
"k90b668e5": "過去24時間", "k90b668e5": "過去24時間",
"k93374bc9": "ウェブサイトを削除", "k93374bc9": "ウェブサイトを削除",
"k93458b98": "プレイグラウンド",
"k951a939a": "ウェブサイト承認カウント", "k951a939a": "ウェブサイト承認カウント",
"k95f932a": "現在、リモートサーバーからの新しいリクエストを待機中です",
"k97b02874": "ページ数",
"k98f433ee": "からレポーターをダウンロード", "k98f433ee": "からレポーターをダウンロード",
"k9991c290": "コミュニティ", "k9991c290": "コミュニティ",
"k9a272ecf": "これはあなたのサーバーですか?", "k9a272ecf": "これはあなたのサーバーですか?",
@ -233,6 +254,7 @@
"ka6ee7455": "ウェブサイトID", "ka6ee7455": "ウェブサイトID",
"ka71c12e1": "2つのパスワードが一致しません", "ka71c12e1": "2つのパスワードが一致しません",
"ka765ad32": "通知", "ka765ad32": "通知",
"ka7d8617e": "フィードチャンネル数",
"ka7fe5937": "ディスク読み取り/書き込み", "ka7fe5937": "ディスク読み取り/書き込み",
"ka8e41156": "検索して素早く移動", "ka8e41156": "検索して素早く移動",
"ka90bc019": "アンインストール", "ka90bc019": "アンインストール",
@ -254,6 +276,7 @@
"kb0e351e0": "更新されました", "kb0e351e0": "更新されました",
"kb114a2e8": "非推奨", "kb114a2e8": "非推奨",
"kb15a6374": "自分のドメインでステータスページを設定できます。たとえば、status.example.com", "kb15a6374": "自分のドメインでステータスページを設定できます。たとえば、status.example.com",
"kb2dded49": "キー",
"kb320aac4": "{{dayNum}}日間監視", "kb320aac4": "{{dayNum}}日間監視",
"kb35cde91": "検索", "kb35cde91": "検索",
"kb35d71ed": "または", "kb35d71ed": "または",
@ -261,6 +284,7 @@
"kb5673707": "過去7日間", "kb5673707": "過去7日間",
"kb659c1bc": "証明書の有効期限", "kb659c1bc": "証明書の有効期限",
"kb6d350b6": "フィードチャンネル", "kb6d350b6": "フィードチャンネル",
"kb7bf8869": "APIキー",
"kb7fa344a": "送信するフィードチャンネルを選択", "kb7fa344a": "送信するフィードチャンネルを選択",
"kb8de8c50": "BCC", "kb8de8c50": "BCC",
"kbb31d3db": "統計日", "kbb31d3db": "統計日",
@ -296,13 +320,16 @@
"kcc9c1bff": "毎週", "kcc9c1bff": "毎週",
"kccaa732a": "連続ダッシュなし", "kccaa732a": "連続ダッシュなし",
"kccb42483": "パスワード", "kccb42483": "パスワード",
"kcd56f27b": "最終更新",
"kcd643ef3": "読み込み中...", "kcd643ef3": "読み込み中...",
"kce77d0c1": "タイムゾーン",
"kcff78587": "最終使用日時",
"kd005f7a8": "すべてのフィードが削除されます", "kd005f7a8": "すべてのフィードが削除されます",
"kd031b383": "ビュー", "kd031b383": "ビュー",
"kd044d5d4": "セッション",
"kd092de58": "現在のワークスペース:", "kd092de58": "現在のワークスペース:",
"kd1f7e695": "ログアウトを確認", "kd1f7e695": "ログアウトを確認",
"kd211e2d4": "リリースページ", "kd211e2d4": "リリースページ",
"kd25f123a": "ステータス不明",
"kd2a7ad83": "フィードテンプレート", "kd2a7ad83": "フィードテンプレート",
"kd3262a4a": "設定", "kd3262a4a": "設定",
"kd3396544": "一般的に、ユーザーの通常の使用に影響を与えないように、1ピクセルの空白画像を使用します。", "kd3396544": "一般的に、ユーザーの通常の使用に影響を与えないように、1ピクセルの空白画像を使用します。",
@ -311,14 +338,18 @@
"kd7279fa6": "コード", "kd7279fa6": "コード",
"kd7985726": "{{num}}人のユーザー", "kd7985726": "{{num}}人のユーザー",
"kd92fa3e7": "ホスト名", "kd92fa3e7": "ホスト名",
"kdaa6ae2b": "モニター数",
"kdaff25a6": "最新値を表示", "kdaff25a6": "最新値を表示",
"kdb61adbb": "オフラインを隠す", "kdb61adbb": "オフラインを隠す",
"kdbadcf43": "すべてのシステムが稼働中",
"kdbe222b": "APIキー",
"kdc10ee1a": "チームメンバーと協力するために新しいワークスペースを作成します。", "kdc10ee1a": "チームメンバーと協力するために新しいワークスペースを作成します。",
"kdc15c5d": "データ", "kdc15c5d": "データ",
"kdc1bf80e": "URLは必須です", "kdc1bf80e": "URLは必須です",
"kdc51b5db": "ウェブサイト", "kdc51b5db": "ウェブサイト",
"kdd44ac01": "表示するテレメトリー名", "kdd44ac01": "表示するテレメトリー名",
"kdd55936a": "リゾルバーポート", "kdd55936a": "リゾルバーポート",
"kde315178": "名前を変更",
"kde37bc27": "管理者に戻る", "kde37bc27": "管理者に戻る",
"kdeba7706": "デバイス", "kdeba7706": "デバイス",
"kdeecbfea": "リゾルバーサーバー", "kdeecbfea": "リゾルバーサーバー",
@ -359,6 +390,7 @@
"kf246dd2e": "ワークスペースが見つかりません。最初に作成してください。", "kf246dd2e": "ワークスペースが見つかりません。最初に作成してください。",
"kf3b749ef": "ダイレクトチャット/グループ/チャネルのチャットIDをサポート", "kf3b749ef": "ダイレクトチャット/グループ/チャネルのチャットIDをサポート",
"kf55495e0": "保存", "kf55495e0": "保存",
"kf5c3b616": "リクエストヘッダー",
"kf5c9520e": "まだステータスページがありません。新しいステータスページを作成して、サービスのステータスを公開することができます。", "kf5c9520e": "まだステータスページがありません。新しいステータスページを作成して、サービスのステータスを公開することができます。",
"kf6339d4f": "確認済み", "kf6339d4f": "確認済み",
"kf6582ba": "ワークスペース", "kf6582ba": "ワークスペース",
@ -374,6 +406,7 @@
"kf97b6f71": "Linuxマシンでこのコマンドを実行してください", "kf97b6f71": "Linuxマシンでこのコマンドを実行してください",
"kf9877f28": "詳細を見る", "kf9877f28": "詳細を見る",
"kf9965c19": "このワークスペース内のすべてのコンテンツは破壊され、復元できません。", "kf9965c19": "このワークスペース内のすべてのコンテンツは破壊され、復元できません。",
"kf9a498c7": "Lighthouseレポートが完了しました",
"kfc98929b": "{{num}}日", "kfc98929b": "{{num}}日",
"kfd33c459": "コピーに成功しました!", "kfd33c459": "コピーに成功しました!",
"kfdaf0bb3": "最後のオンライン:{{time}}", "kfdaf0bb3": "最後のオンライン:{{time}}",

View File

Before

Width:  |  Height:  |  Size: 259 B

After

Width:  |  Height:  |  Size: 259 B

View File

@ -19,11 +19,14 @@
"k17058821": "Raporty Lighthouse Strony", "k17058821": "Raporty Lighthouse Strony",
"k172a09c3": "Sugestie", "k172a09c3": "Sugestie",
"k1777bbf2": "Instrukcja obsługi", "k1777bbf2": "Instrukcja obsługi",
"k1940fd6": "Ogólne",
"k1964b988": "Zatrzymaj", "k1964b988": "Zatrzymaj",
"k1bd89236": "uruchom raportera z", "k1bd89236": "uruchom raportera z",
"k1c33c293": "Ustawienia", "k1c33c293": "Ustawienia",
"k1d8f92b4": "Tablet", "k1d8f92b4": "Tablet",
"k1da4ecc2": "Możesz wysłać wiadomość do tego kanału za pomocą:",
"k1eb5b3ed": "Przegląd", "k1eb5b3ed": "Przegląd",
"k1ee0c2ca": "Ustaw adres URL webhooka na <1></1> i utrzymuj to okno aktywne. Po zakończeniu zaczniesz otrzymywać żądania webhooka tutaj.",
"k1f6dea0": "Nazwa kanału", "k1f6dea0": "Nazwa kanału",
"k2099f2e0": "Logowanie nie powiodło się, sprawdź swoją nazwę użytkownika i hasło", "k2099f2e0": "Logowanie nie powiodło się, sprawdź swoją nazwę użytkownika i hasło",
"k20edf271": "24h", "k20edf271": "24h",
@ -53,12 +56,15 @@
"k2c84fe32": "Liczba zdarzeń w kanale", "k2c84fe32": "Liczba zdarzeń w kanale",
"k2cecf817": "Typ", "k2cecf817": "Typ",
"k2dad13e3": "Język", "k2dad13e3": "Język",
"k2db2c0c5": "Test Powiadomienie",
"k2e6dbf02": "Do e-maila", "k2e6dbf02": "Do e-maila",
"k2ea8a019": "Monitorować", "k2ea8a019": "Monitorować",
"k30b5f01b": "Obszary robocze", "k30b5f01b": "Obszary robocze",
"k30d33d71": "Podpis Webhooka",
"k310fee": "Ostatnie 30 dni", "k310fee": "Ostatnie 30 dni",
"k32344f64": "Wyczyść dane", "k32344f64": "Wyczyść dane",
"k3260f019": "Wyloguj", "k3260f019": "Wyloguj",
"k3404b72f": "Nowa nazwa przestrzeni roboczej",
"k340547f0": "Przepraszamy, ale coś poszło nie tak", "k340547f0": "Przepraszamy, ale coś poszło nie tak",
"k3471e956": "Powtórz nowe hasło", "k3471e956": "Powtórz nowe hasło",
"k34981fea": "Docker dryfuje po morzu, nie mogąc znaleźć drogi. Uruchom Docker, aby wrócić na właściwy kurs.", "k34981fea": "Docker dryfuje po morzu, nie mogąc znaleźć drogi. Uruchom Docker, aby wrócić na właściwy kurs.",
@ -85,6 +91,7 @@
"k3e8b13f8": "Dołącz do Discorda", "k3e8b13f8": "Dołącz do Discorda",
"k3eaab921": "Lista monitorów", "k3eaab921": "Lista monitorów",
"k3f36e17e": "Śledź na Twitterze", "k3f36e17e": "Śledź na Twitterze",
"k406089a4": "Akcja",
"k406e9ad8": "Potwierdź", "k406e9ad8": "Potwierdź",
"k41d3ce6c": "Wydarzenie odarchiwizowane", "k41d3ce6c": "Wydarzenie odarchiwizowane",
"k42347b91": "Liczba zdarzeń na stronie internetowej", "k42347b91": "Liczba zdarzeń na stronie internetowej",
@ -93,7 +100,8 @@
"k44186b66": "Liczba", "k44186b66": "Liczba",
"k44cad477": "(Obecny)", "k44cad477": "(Obecny)",
"k45f80a27": "Zaawansowane", "k45f80a27": "Zaawansowane",
"k4738284": "Możesz wysłać dowolną wiadomość do tego kanału za pomocą:", "k4727e4db": "Wygasło",
"k477b7ee4": "Częściowa awaria systemu",
"k47fe1f95": "Dodaj ten przykładowy kod do swojego projektu", "k47fe1f95": "Dodaj ten przykładowy kod do swojego projektu",
"k48186ce": "Powrót do strony głównej", "k48186ce": "Powrót do strony głównej",
"k4905ed7b": "BRAK", "k4905ed7b": "BRAK",
@ -107,6 +115,7 @@
"k4de48e75": "Maksymalna liczba prób", "k4de48e75": "Maksymalna liczba prób",
"k4e08cf58": "Pokaż liczbę szczegółów", "k4e08cf58": "Pokaż liczbę szczegółów",
"k4eea9393": "Profil", "k4eea9393": "Profil",
"k4f182a7c": "Poważna awaria systemu",
"k4fc2b5b": "Obraz", "k4fc2b5b": "Obraz",
"k4fe1b4de": "Telemetria", "k4fe1b4de": "Telemetria",
"k505c2733": "Utwórz Raport", "k505c2733": "Utwórz Raport",
@ -123,9 +132,12 @@
"k58267a45": "Źródło", "k58267a45": "Źródło",
"k58f90514": "Token Bota", "k58f90514": "Token Bota",
"k593cf342": "Czy na pewno chcesz usunąć ten monitor?", "k593cf342": "Czy na pewno chcesz usunąć ten monitor?",
"k5a782f4b": "Liczba stron internetowych",
"k5a839f71": "Czas działania", "k5a839f71": "Czas działania",
"k5b5be0d4": "Aktualna Rola", "k5b5be0d4": "Aktualna Rola",
"k5c18db28": "Zmień informacje na stronie statusu", "k5c18db28": "Zmień informacje na stronie statusu",
"k5d00536d": "Skopiowane",
"k5d49d751": "Nowy klucz API został skopiowany do schowka!",
"k5eb87a8b": "Wznów", "k5eb87a8b": "Wznów",
"k5ec0de4": "Dla monitorowania HTTPS, jeśli przypisana jest jakakolwiek metoda powiadamiania, powiadomienia zostaną wysłane 1, 3, 7 i 14 dni przed wygaśnięciem.", "k5ec0de4": "Dla monitorowania HTTPS, jeśli przypisana jest jakakolwiek metoda powiadamiania, powiadomienia zostaną wysłane 1, 3, 7 i 14 dni przed wygaśnięciem.",
"k5ecf04b0": "Widok", "k5ecf04b0": "Widok",
@ -135,6 +147,7 @@
"k62e19375": "Ostatnia aktualizacja: {{date}}", "k62e19375": "Ostatnia aktualizacja: {{date}}",
"k6488f302": "Opcjonalne", "k6488f302": "Opcjonalne",
"k659b065": "Na przykład: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000", "k659b065": "Na przykład: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
"k678e2f90": "Treść żądania",
"k67c5a895": "Wczoraj", "k67c5a895": "Wczoraj",
"k683be220": "Uruchom", "k683be220": "Uruchom",
"k691b7170": "Zatrzymany", "k691b7170": "Zatrzymany",
@ -147,9 +160,11 @@
"k6e96fc3": "Informacje o formularzu", "k6e96fc3": "Informacje o formularzu",
"k6ea11aff": "OK!", "k6ea11aff": "OK!",
"k6f15bcc3": "Host", "k6f15bcc3": "Host",
"k71067412": "Opcjonalne, podpis webhooka dla przychodzącego webhooka",
"k721589c1": "Dziś", "k721589c1": "Dziś",
"k7247683c": "Usuń przestrzeń roboczą", "k7247683c": "Usuń przestrzeń roboczą",
"k7350bd93": "W tym samym czasie możemy go również użyć w niektórych scenariuszach aplikacji po stronie klienta, takich jak zbieranie częstotliwości używania wiersza poleceń, takie jak zbieranie instalacji aplikacji selfhosted itp.", "k7350bd93": "W tym samym czasie możemy go również użyć w niektórych scenariuszach aplikacji po stronie klienta, takich jak zbieranie częstotliwości używania wiersza poleceń, takie jak zbieranie instalacji aplikacji selfhosted itp.",
"k736f3e4c": "Kopiuj jako",
"k75581e13": "DW", "k75581e13": "DW",
"k75bfaaa6": "Dodaj ten kod do sekcji head na swojej stronie internetowej", "k75bfaaa6": "Dodaj ten kod do sekcji head na swojej stronie internetowej",
"k763816ac": "Podgląd", "k763816ac": "Podgląd",
@ -157,16 +172,17 @@
"k78b1ef6a": "Wprowadź", "k78b1ef6a": "Wprowadź",
"k7927b824": "Czy na pewno chcesz wyczyścić wszystkie wyłączone węzły?", "k7927b824": "Czy na pewno chcesz wyczyścić wszystkie wyłączone węzły?",
"k7a132ce8": "Przepraszamy, ale ta strona nie została znaleziona", "k7a132ce8": "Przepraszamy, ale ta strona nie została znaleziona",
"k7a15497a": "Na żywo",
"k7ac44a6e": "Klucz sesji", "k7ac44a6e": "Klucz sesji",
"k7b74a43f": "odwiedzający", "k7b74a43f": "odwiedzający",
"k7b75e24c": "Integracja", "k7b75e24c": "Integracja",
"k7b9aa48c": "Treść", "k7b9aa48c": "Treść",
"k7cac602a": "Status", "k7cac602a": "Status",
"k7d8cd81c": "Kopiuj URL",
"k7e0360fd": "Nie utworzono żadnej grupy, kliknij przycisk, aby utworzyć jedną", "k7e0360fd": "Nie utworzono żadnej grupy, kliknij przycisk, aby utworzyć jedną",
"k7e61b1af": "Wybierz przestrzeń roboczą", "k7e61b1af": "Wybierz przestrzeń roboczą",
"k7f01b47c": "Dziennik audytu", "k7f01b47c": "Dziennik audytu",
"k7f03a704": "Pamiętaj, aby nie wysyłać danych za pomocą application/json", "k7f03a704": "Pamiętaj, aby nie wysyłać danych za pomocą application/json",
"k7f29bae5": "wyświetlenia strony",
"k8037cc6b": "Serwery", "k8037cc6b": "Serwery",
"k816ce026": "Pobierz", "k816ce026": "Pobierz",
"k819633bc": "Użyj do przechowywania", "k819633bc": "Użyj do przechowywania",
@ -176,6 +192,7 @@
"k84ce1618": "(24 godziny)", "k84ce1618": "(24 godziny)",
"k84e82947": "{{num}} zdarzeń usuniętych", "k84e82947": "{{num}} zdarzeń usuniętych",
"k85344b23": "Obciążenie", "k85344b23": "Obciążenie",
"k85a116ee": "Adres URL Webhooka",
"k85c5fd4c": "Nie ustawiono żadnego monitora", "k85c5fd4c": "Nie ustawiono żadnego monitora",
"k85db19da": "Brak kanałów informacyjnych. Użyj funkcji kanałów, aby otrzymywać wszystkie zdarzenia z sieci lub własnej usługi.", "k85db19da": "Brak kanałów informacyjnych. Użyj funkcji kanałów, aby otrzymywać wszystkie zdarzenia z sieci lub własnej usługi.",
"k873c90e6": "Etykieta wyświetlania", "k873c90e6": "Etykieta wyświetlania",
@ -188,6 +205,7 @@
"k88d2647b": "Strona internetowa", "k88d2647b": "Strona internetowa",
"k89056082": "(30 dni)", "k89056082": "(30 dni)",
"k892f84b6": "Nie można uzyskać informacji o bieżącym użytkowniku", "k892f84b6": "Nie można uzyskać informacji o bieżącym użytkowniku",
"k895cafe1": "Opcjonalne, adres url webhooka do wysyłania ładunku ankiety",
"k899fd0cd": "Porty", "k899fd0cd": "Porty",
"k89d54f7a": "Liczba wykonań monitora", "k89d54f7a": "Liczba wykonań monitora",
"k8a1deb63": "Członkowie", "k8a1deb63": "Członkowie",
@ -207,7 +225,10 @@
"k90b603b8": "Duplikat", "k90b603b8": "Duplikat",
"k90b668e5": "Ostatnie 24 godziny", "k90b668e5": "Ostatnie 24 godziny",
"k93374bc9": "Usuń stronę internetową", "k93374bc9": "Usuń stronę internetową",
"k93458b98": "Plac zabaw",
"k951a939a": "Liczba zaakceptowanych stron internetowych", "k951a939a": "Liczba zaakceptowanych stron internetowych",
"k95f932a": "Obecnie czekam na nowe żądanie z zdalnego serwera",
"k97b02874": "Liczba stron",
"k98f433ee": "Pobierz reporter z", "k98f433ee": "Pobierz reporter z",
"k9991c290": "Społeczność", "k9991c290": "Społeczność",
"k9a272ecf": "Czy to twoje serwery?", "k9a272ecf": "Czy to twoje serwery?",
@ -233,6 +254,7 @@
"ka6ee7455": "ID strony internetowej", "ka6ee7455": "ID strony internetowej",
"ka71c12e1": "Dwa hasła nie są zgodne", "ka71c12e1": "Dwa hasła nie są zgodne",
"ka765ad32": "Powiadomienie", "ka765ad32": "Powiadomienie",
"ka7d8617e": "Liczba kanałów feed",
"ka7fe5937": "Odczyt/zapis dysku", "ka7fe5937": "Odczyt/zapis dysku",
"ka8e41156": "Wyszukiwanie i szybkie przeskakiwanie", "ka8e41156": "Wyszukiwanie i szybkie przeskakiwanie",
"ka90bc019": "Odinstaluj", "ka90bc019": "Odinstaluj",
@ -254,6 +276,7 @@
"kb0e351e0": "Odświeżone", "kb0e351e0": "Odświeżone",
"kb114a2e8": "Przestarzałe", "kb114a2e8": "Przestarzałe",
"kb15a6374": "Możesz skonfigurować swoją stronę statusu pod własną domeną, na przykład: status.example.com", "kb15a6374": "Możesz skonfigurować swoją stronę statusu pod własną domeną, na przykład: status.example.com",
"kb2dded49": "Klucz",
"kb320aac4": "Monitorowane przez {{dayNum}} dni", "kb320aac4": "Monitorowane przez {{dayNum}} dni",
"kb35cde91": "Szukaj", "kb35cde91": "Szukaj",
"kb35d71ed": "LUB", "kb35d71ed": "LUB",
@ -261,6 +284,7 @@
"kb5673707": "Ostatnie 7 dni", "kb5673707": "Ostatnie 7 dni",
"kb659c1bc": "Wygaśnięcie certyfikatu", "kb659c1bc": "Wygaśnięcie certyfikatu",
"kb6d350b6": "Kanały feedu", "kb6d350b6": "Kanały feedu",
"kb7bf8869": "Klucze API",
"kb7fa344a": "Wybierz kanał feedu do wysłania", "kb7fa344a": "Wybierz kanał feedu do wysłania",
"kb8de8c50": "DWU", "kb8de8c50": "DWU",
"kbb31d3db": "Data statystyk", "kbb31d3db": "Data statystyk",
@ -284,7 +308,7 @@
"kc5f82d53": "Na przykład: pushdeer://pushKey", "kc5f82d53": "Na przykład: pushdeer://pushKey",
"kc6888ac4": "Automatyczny", "kc6888ac4": "Automatyczny",
"kc6cac621": "(Brak)", "kc6cac621": "(Brak)",
"kc6dc3c38": "Desktop", "kc6dc3c38": "Komputer stacjonarny",
"kc70d69ad": "Odpowiedź", "kc70d69ad": "Odpowiedź",
"kc9b446d1": "Zakończono uruchamianie", "kc9b446d1": "Zakończono uruchamianie",
"kcacbfde1": "Utwórz teraz", "kcacbfde1": "Utwórz teraz",
@ -296,13 +320,16 @@
"kcc9c1bff": "Każdy tydzień", "kcc9c1bff": "Każdy tydzień",
"kccaa732a": "Brak kolejnych myślników", "kccaa732a": "Brak kolejnych myślników",
"kccb42483": "Hasło", "kccb42483": "Hasło",
"kcd56f27b": "Ostatnia aktualizacja",
"kcd643ef3": "Ładowanie...", "kcd643ef3": "Ładowanie...",
"kce77d0c1": "Strefa czasowa",
"kcff78587": "Ostatnie użycie",
"kd005f7a8": "Wszystkie kanały informacyjne zostaną usunięte", "kd005f7a8": "Wszystkie kanały informacyjne zostaną usunięte",
"kd031b383": "Odsłony", "kd031b383": "Odsłony",
"kd044d5d4": "sesja",
"kd092de58": "Aktualna przestrzeń robocza:", "kd092de58": "Aktualna przestrzeń robocza:",
"kd1f7e695": "Potwierdź wylogowanie", "kd1f7e695": "Potwierdź wylogowanie",
"kd211e2d4": "Strona wydań", "kd211e2d4": "Strona wydań",
"kd25f123a": "Status nieznany",
"kd2a7ad83": "Szablon feedu", "kd2a7ad83": "Szablon feedu",
"kd3262a4a": "Konfiguracja", "kd3262a4a": "Konfiguracja",
"kd3396544": "Zazwyczaj użyjemy pustego obrazu o rozmiarze jednego piksela, aby nie wpływał na normalne użytkowanie użytkownika.", "kd3396544": "Zazwyczaj użyjemy pustego obrazu o rozmiarze jednego piksela, aby nie wpływał na normalne użytkowanie użytkownika.",
@ -311,14 +338,18 @@
"kd7279fa6": "Kod", "kd7279fa6": "Kod",
"kd7985726": "{{num}} użytkowników", "kd7985726": "{{num}} użytkowników",
"kd92fa3e7": "Nazwa hosta", "kd92fa3e7": "Nazwa hosta",
"kdaa6ae2b": "Liczba monitorów",
"kdaff25a6": "Pokaż najnowszą wartość", "kdaff25a6": "Pokaż najnowszą wartość",
"kdb61adbb": "Ukryj wyłączone", "kdb61adbb": "Ukryj wyłączone",
"kdbadcf43": "Wszystkie systemy działają",
"kdbe222b": "Klucz API",
"kdc10ee1a": "Utwórz nową przestrzeń roboczą, aby współpracować z członkami zespołu.", "kdc10ee1a": "Utwórz nową przestrzeń roboczą, aby współpracować z członkami zespołu.",
"kdc15c5d": "Dane", "kdc15c5d": "Dane",
"kdc1bf80e": "Url jest wymagany", "kdc1bf80e": "Url jest wymagany",
"kdc51b5db": "Strony internetowe", "kdc51b5db": "Strony internetowe",
"kdd44ac01": "Nazwa telemetrii do wyświetlenia", "kdd44ac01": "Nazwa telemetrii do wyświetlenia",
"kdd55936a": "Port resolvera", "kdd55936a": "Port resolvera",
"kde315178": "Zmień nazwę",
"kde37bc27": "Powrót do panelu administratora", "kde37bc27": "Powrót do panelu administratora",
"kdeba7706": "Urządzenia", "kdeba7706": "Urządzenia",
"kdeecbfea": "Serwer resolvera", "kdeecbfea": "Serwer resolvera",
@ -359,6 +390,7 @@
"kf246dd2e": "Nie znaleziono żadnej przestrzeni roboczej, proszę najpierw utworzyć", "kf246dd2e": "Nie znaleziono żadnej przestrzeni roboczej, proszę najpierw utworzyć",
"kf3b749ef": "Wsparcie dla czatu bezpośredniego / grupy / czatu kanału", "kf3b749ef": "Wsparcie dla czatu bezpośredniego / grupy / czatu kanału",
"kf55495e0": "Zapisz", "kf55495e0": "Zapisz",
"kf5c3b616": "Nagłówek żądania",
"kf5c9520e": "Brak stron statusu, możesz utworzyć nową, aby pokazać stan swojej usługi publicznie.", "kf5c9520e": "Brak stron statusu, możesz utworzyć nową, aby pokazać stan swojej usługi publicznie.",
"kf6339d4f": "Zweryfikowane", "kf6339d4f": "Zweryfikowane",
"kf6582ba": "Przestrzeń robocza", "kf6582ba": "Przestrzeń robocza",
@ -374,6 +406,7 @@
"kf97b6f71": "Uruchom to polecenie na swojej maszynie z systemem Linux", "kf97b6f71": "Uruchom to polecenie na swojej maszynie z systemem Linux",
"kf9877f28": "Pokaż szczegóły", "kf9877f28": "Pokaż szczegóły",
"kf9965c19": "Cała zawartość w tej przestrzeni roboczej zostanie zniszczona i nie można jej odzyskać.", "kf9965c19": "Cała zawartość w tej przestrzeni roboczej zostanie zniszczona i nie można jej odzyskać.",
"kf9a498c7": "Raport Lighthouse zakończony!",
"kfc98929b": "{{num}} dni", "kfc98929b": "{{num}} dni",
"kfd33c459": "Kopiowanie powiodło się!", "kfd33c459": "Kopiowanie powiodło się!",
"kfdaf0bb3": "O na pewno chcesz usunąć wszystkie zdarzenia dla tego monitora?", "kfdaf0bb3": "O na pewno chcesz usunąć wszystkie zdarzenia dla tego monitora?",

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -19,11 +19,14 @@
"k17058821": "Relatórios do Website Lighthouse", "k17058821": "Relatórios do Website Lighthouse",
"k172a09c3": "Sugestões", "k172a09c3": "Sugestões",
"k1777bbf2": "Manual", "k1777bbf2": "Manual",
"k1940fd6": "Geral",
"k1964b988": "Parar", "k1964b988": "Parar",
"k1bd89236": "correr repórter com", "k1bd89236": "correr repórter com",
"k1c33c293": "Definições", "k1c33c293": "Definições",
"k1d8f92b4": "Tablet", "k1d8f92b4": "Tablet",
"k1da4ecc2": "Você pode enviar uma mensagem para este canal com:",
"k1eb5b3ed": "Visão geral", "k1eb5b3ed": "Visão geral",
"k1ee0c2ca": "Defina a URL do webhook para <1></1> e mantenha esta janela ativa. Uma vez feito, você começará a receber solicitações de webhook aqui.",
"k1f6dea0": "Nome do Canal", "k1f6dea0": "Nome do Canal",
"k2099f2e0": "Falha no login, verifique seu nome de usuário e senha", "k2099f2e0": "Falha no login, verifique seu nome de usuário e senha",
"k20edf271": "24 horas", "k20edf271": "24 horas",
@ -53,12 +56,15 @@
"k2c84fe32": "Contagem de eventos de feed", "k2c84fe32": "Contagem de eventos de feed",
"k2cecf817": "Tipo", "k2cecf817": "Tipo",
"k2dad13e3": "Idioma", "k2dad13e3": "Idioma",
"k2db2c0c5": "Notificação de Teste",
"k2e6dbf02": "Para o e-mail", "k2e6dbf02": "Para o e-mail",
"k2ea8a019": "Monitorar", "k2ea8a019": "Monitorar",
"k30b5f01b": "Áreas de trabalho", "k30b5f01b": "Áreas de trabalho",
"k30d33d71": "Assinatura do Webhook",
"k310fee": "Últimos 30 dias", "k310fee": "Últimos 30 dias",
"k32344f64": "Limpar dados", "k32344f64": "Limpar dados",
"k3260f019": "Terminar sessão", "k3260f019": "Terminar sessão",
"k3404b72f": "Novo Nome do Espaço de Trabalho",
"k340547f0": "Desculpe, mas algo correu mal", "k340547f0": "Desculpe, mas algo correu mal",
"k3471e956": "Repetir nova palavra-passe", "k3471e956": "Repetir nova palavra-passe",
"k34981fea": "O Docker está à deriva no mar, incapaz de encontrar seu caminho. Por favor, inicie o Docker para voltar ao curso.", "k34981fea": "O Docker está à deriva no mar, incapaz de encontrar seu caminho. Por favor, inicie o Docker para voltar ao curso.",
@ -85,6 +91,7 @@
"k3e8b13f8": "Aderir ao Discord", "k3e8b13f8": "Aderir ao Discord",
"k3eaab921": "Lista de Monitoramento", "k3eaab921": "Lista de Monitoramento",
"k3f36e17e": "Seguir o Twitter", "k3f36e17e": "Seguir o Twitter",
"k406089a4": "Ação",
"k406e9ad8": "Confirmar", "k406e9ad8": "Confirmar",
"k41d3ce6c": "Evento desarquivado", "k41d3ce6c": "Evento desarquivado",
"k42347b91": "Contagem de eventos do sítio Web", "k42347b91": "Contagem de eventos do sítio Web",
@ -93,7 +100,8 @@
"k44186b66": "Contar", "k44186b66": "Contar",
"k44cad477": "(Atual)", "k44cad477": "(Atual)",
"k45f80a27": "Avançado", "k45f80a27": "Avançado",
"k4738284": "Você pode enviar qualquer mensagem para este canal com:", "k4727e4db": "Expirado Em",
"k477b7ee4": "Interrupção Parcial do Sistema",
"k47fe1f95": "Adicione este código de exemplo ao seu projeto", "k47fe1f95": "Adicione este código de exemplo ao seu projeto",
"k48186ce": "Voltar à página inicial", "k48186ce": "Voltar à página inicial",
"k4905ed7b": "NENHUM", "k4905ed7b": "NENHUM",
@ -107,6 +115,7 @@
"k4de48e75": "Máximo de tentativas", "k4de48e75": "Máximo de tentativas",
"k4e08cf58": "Mostrar número de pormenor", "k4e08cf58": "Mostrar número de pormenor",
"k4eea9393": "Perfil", "k4eea9393": "Perfil",
"k4f182a7c": "Interrupção Maior do Sistema",
"k4fc2b5b": "Imagem", "k4fc2b5b": "Imagem",
"k4fe1b4de": "Telemetria", "k4fe1b4de": "Telemetria",
"k505c2733": "Criar Relatório", "k505c2733": "Criar Relatório",
@ -123,9 +132,12 @@
"k58267a45": "Tipo de Letra", "k58267a45": "Tipo de Letra",
"k58f90514": "Token de Bot", "k58f90514": "Token de Bot",
"k593cf342": "De certeza que eliminou este monitor?", "k593cf342": "De certeza que eliminou este monitor?",
"k5a782f4b": "Contagem de Sites",
"k5a839f71": "Tempo de atividade", "k5a839f71": "Tempo de atividade",
"k5b5be0d4": "Função Atual", "k5b5be0d4": "Função Atual",
"k5c18db28": "Modificar Informações da Página de Status", "k5c18db28": "Modificar Informações da Página de Status",
"k5d00536d": "Copiado",
"k5d49d751": "Nova chave de API foi copiada para sua área de transferência!",
"k5eb87a8b": "Início", "k5eb87a8b": "Início",
"k5ec0de4": "Para monitoramento HTTPS, se algum método de notificação estiver atribuído, notificações serão enviadas com 1, 3, 7 e 14 dias antes do vencimento.", "k5ec0de4": "Para monitoramento HTTPS, se algum método de notificação estiver atribuído, notificações serão enviadas com 1, 3, 7 e 14 dias antes do vencimento.",
"k5ecf04b0": "Ver", "k5ecf04b0": "Ver",
@ -135,6 +147,7 @@
"k62e19375": "Última atualização em: {{date}}", "k62e19375": "Última atualização em: {{date}}",
"k6488f302": "Opcional", "k6488f302": "Opcional",
"k659b065": "Por exemplo: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000", "k659b065": "Por exemplo: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
"k678e2f90": "Corpo da Solicitação",
"k67c5a895": "Ontem", "k67c5a895": "Ontem",
"k683be220": "Correr", "k683be220": "Correr",
"k691b7170": "Parado", "k691b7170": "Parado",
@ -147,9 +160,11 @@
"k6e96fc3": "Informações do formulário", "k6e96fc3": "Informações do formulário",
"k6ea11aff": "Obter!", "k6ea11aff": "Obter!",
"k6f15bcc3": "Anfitrião", "k6f15bcc3": "Anfitrião",
"k71067412": "Opcional, Assinatura do Webhook para Webhook de Entrada",
"k721589c1": "Hoje", "k721589c1": "Hoje",
"k7247683c": "Excluir Espaço de Trabalho", "k7247683c": "Excluir Espaço de Trabalho",
"k7350bd93": "Ao mesmo tempo, também podemos utilizá-lo em alguns cenários de aplicações do lado do cliente, como a recolha da frequência de utilização do cli, como a recolha da instalação de aplicações auto-hospedadas, etc.", "k7350bd93": "Ao mesmo tempo, também podemos utilizá-lo em alguns cenários de aplicações do lado do cliente, como a recolha da frequência de utilização do cli, como a recolha da instalação de aplicações auto-hospedadas, etc.",
"k736f3e4c": "Copiar como",
"k75581e13": "CC", "k75581e13": "CC",
"k75bfaaa6": "Adicionar este código ao script principal do seu sítio Web", "k75bfaaa6": "Adicionar este código ao script principal do seu sítio Web",
"k763816ac": "Pré-visualização", "k763816ac": "Pré-visualização",
@ -157,16 +172,17 @@
"k78b1ef6a": "Entrar", "k78b1ef6a": "Entrar",
"k7927b824": "Tem a certeza de que pretende limpar todos os nós offline?", "k7927b824": "Tem a certeza de que pretende limpar todos os nós offline?",
"k7a132ce8": "Desculpe, mas esta página não foi encontrada", "k7a132ce8": "Desculpe, mas esta página não foi encontrada",
"k7a15497a": "Em Tempo Real",
"k7ac44a6e": "Chave de sessão", "k7ac44a6e": "Chave de sessão",
"k7b74a43f": "visitantes", "k7b74a43f": "visitantes",
"k7b75e24c": "Integração", "k7b75e24c": "Integração",
"k7b9aa48c": "Corpo", "k7b9aa48c": "Corpo",
"k7cac602a": "Estado", "k7cac602a": "Estado",
"k7d8cd81c": "Copiar URL",
"k7e0360fd": "Nenhum grupo foi criado, clique no botão para criar um", "k7e0360fd": "Nenhum grupo foi criado, clique no botão para criar um",
"k7e61b1af": "Selecionar Espaço de Trabalho", "k7e61b1af": "Selecionar Espaço de Trabalho",
"k7f01b47c": "Registo de auditoria", "k7f01b47c": "Registo de auditoria",
"k7f03a704": "Não se esqueça de não enviar dados com application/json", "k7f03a704": "Não se esqueça de não enviar dados com application/json",
"k7f29bae5": "visualização de página",
"k8037cc6b": "Servidores", "k8037cc6b": "Servidores",
"k816ce026": "Baixar", "k816ce026": "Baixar",
"k819633bc": "Usar para armazenamento", "k819633bc": "Usar para armazenamento",
@ -176,6 +192,7 @@
"k84ce1618": "(24 horas)", "k84ce1618": "(24 horas)",
"k84e82947": "{{num}} eventos limpos", "k84e82947": "{{num}} eventos limpos",
"k85344b23": "Carregar", "k85344b23": "Carregar",
"k85a116ee": "URL do Webhook",
"k85c5fd4c": "Não foi definido qualquer monitor", "k85c5fd4c": "Não foi definido qualquer monitor",
"k85db19da": "Ainda não há nenhum canal de feed. Use o recurso de feed para receber todos os eventos da rede ou do seu próprio serviço.", "k85db19da": "Ainda não há nenhum canal de feed. Use o recurso de feed para receber todos os eventos da rede ou do seu próprio serviço.",
"k873c90e6": "Etiqueta de Exibição", "k873c90e6": "Etiqueta de Exibição",
@ -188,6 +205,7 @@
"k88d2647b": "Sítio Web", "k88d2647b": "Sítio Web",
"k89056082": "(30 dias)", "k89056082": "(30 dias)",
"k892f84b6": "Não é possível obter as informações do usuário atual", "k892f84b6": "Não é possível obter as informações do usuário atual",
"k895cafe1": "Opcional, url do webhook para enviar carga de pesquisa",
"k899fd0cd": "Portos", "k899fd0cd": "Portos",
"k89d54f7a": "Contagem de Execução do Monitor", "k89d54f7a": "Contagem de Execução do Monitor",
"k8a1deb63": "Membros", "k8a1deb63": "Membros",
@ -207,7 +225,10 @@
"k90b603b8": "Duplicar", "k90b603b8": "Duplicar",
"k90b668e5": "Últimas 24 horas", "k90b668e5": "Últimas 24 horas",
"k93374bc9": "Eliminar sítio Web", "k93374bc9": "Eliminar sítio Web",
"k93458b98": "Playground",
"k951a939a": "Contagem de sites aceites", "k951a939a": "Contagem de sites aceites",
"k95f932a": "Aguardando atualmente uma nova solicitação do servidor remoto",
"k97b02874": "Contagem de Páginas",
"k98f433ee": "Descarregar repórter de", "k98f433ee": "Descarregar repórter de",
"k9991c290": "Comunidade", "k9991c290": "Comunidade",
"k9a272ecf": "Estes são os vossos servidores?", "k9a272ecf": "Estes são os vossos servidores?",
@ -233,6 +254,7 @@
"ka6ee7455": "ID do sítio Web", "ka6ee7455": "ID do sítio Web",
"ka71c12e1": "As duas palavras-passe não são consistentes", "ka71c12e1": "As duas palavras-passe não são consistentes",
"ka765ad32": "Notificação", "ka765ad32": "Notificação",
"ka7d8617e": "Contagem de Canais de Feed",
"ka7fe5937": "Leitura/escrita de disco", "ka7fe5937": "Leitura/escrita de disco",
"ka8e41156": "Pesquisa e salto rápido", "ka8e41156": "Pesquisa e salto rápido",
"ka90bc019": "Desinstalar", "ka90bc019": "Desinstalar",
@ -254,6 +276,7 @@
"kb0e351e0": "Atualizado", "kb0e351e0": "Atualizado",
"kb114a2e8": "Obsoleto", "kb114a2e8": "Obsoleto",
"kb15a6374": "Você pode configurar sua página de status em seu próprio domínio, por exemplo: status.example.com", "kb15a6374": "Você pode configurar sua página de status em seu próprio domínio, por exemplo: status.example.com",
"kb2dded49": "Chave",
"kb320aac4": "Monitorizado durante {{dayNum}} dias", "kb320aac4": "Monitorizado durante {{dayNum}} dias",
"kb35cde91": "Pesquisar", "kb35cde91": "Pesquisar",
"kb35d71ed": "OU", "kb35d71ed": "OU",
@ -261,6 +284,7 @@
"kb5673707": "Últimos 7 dias", "kb5673707": "Últimos 7 dias",
"kb659c1bc": "Exp. do certificado", "kb659c1bc": "Exp. do certificado",
"kb6d350b6": "Canais de Feed", "kb6d350b6": "Canais de Feed",
"kb7bf8869": "Chaves de API",
"kb7fa344a": "Selecione o Canal de Feed para enviar", "kb7fa344a": "Selecione o Canal de Feed para enviar",
"kb8de8c50": "CCO", "kb8de8c50": "CCO",
"kbb31d3db": "Data da estatística", "kbb31d3db": "Data da estatística",
@ -296,13 +320,16 @@
"kcc9c1bff": "Toda semana", "kcc9c1bff": "Toda semana",
"kccaa732a": "Sem traços consecutivos", "kccaa732a": "Sem traços consecutivos",
"kccb42483": "Palavra-passe", "kccb42483": "Palavra-passe",
"kcd56f27b": "Última atualização",
"kcd643ef3": "Carregando...", "kcd643ef3": "Carregando...",
"kce77d0c1": "Fuso Horário",
"kcff78587": "Último Uso Em",
"kd005f7a8": "Todos os feeds serão removidos", "kd005f7a8": "Todos os feeds serão removidos",
"kd031b383": "Vistas", "kd031b383": "Vistas",
"kd044d5d4": "sessão",
"kd092de58": "Espaço de Trabalho Atual:", "kd092de58": "Espaço de Trabalho Atual:",
"kd1f7e695": "Confirmar para terminar a sessão", "kd1f7e695": "Confirmar para terminar a sessão",
"kd211e2d4": "Página de lançamentos", "kd211e2d4": "Página de lançamentos",
"kd25f123a": "Status Desconhecido",
"kd2a7ad83": "Modelo de Feed", "kd2a7ad83": "Modelo de Feed",
"kd3262a4a": "Configuração", "kd3262a4a": "Configuração",
"kd3396544": "Geralmente, utilizamos uma imagem em branco de um pixel para que não afecte a utilização normal do utilizador.", "kd3396544": "Geralmente, utilizamos uma imagem em branco de um pixel para que não afecte a utilização normal do utilizador.",
@ -311,14 +338,18 @@
"kd7279fa6": "Código", "kd7279fa6": "Código",
"kd7985726": "{{num}} utilizadores", "kd7985726": "{{num}} utilizadores",
"kd92fa3e7": "Nome do anfitrião", "kd92fa3e7": "Nome do anfitrião",
"kdaa6ae2b": "Contagem de Monitores",
"kdaff25a6": "Mostrar valor mais recente", "kdaff25a6": "Mostrar valor mais recente",
"kdb61adbb": "Ocultar offline", "kdb61adbb": "Ocultar offline",
"kdbadcf43": "Todos os Sistemas Operacionais",
"kdbe222b": "Chave de API",
"kdc10ee1a": "Crie um novo espaço de trabalho para cooperar com os membros da equipe.", "kdc10ee1a": "Crie um novo espaço de trabalho para cooperar com os membros da equipe.",
"kdc15c5d": "Dados", "kdc15c5d": "Dados",
"kdc1bf80e": "Url é obrigatório", "kdc1bf80e": "Url é obrigatório",
"kdc51b5db": "Sites", "kdc51b5db": "Sites",
"kdd44ac01": "Nome de telemetria a apresentar", "kdd44ac01": "Nome de telemetria a apresentar",
"kdd55936a": "Porta do resolvedor", "kdd55936a": "Porta do resolvedor",
"kde315178": "Renomear",
"kde37bc27": "Voltar ao Administrador", "kde37bc27": "Voltar ao Administrador",
"kdeba7706": "Dispositivos", "kdeba7706": "Dispositivos",
"kdeecbfea": "Servidor de resolução", "kdeecbfea": "Servidor de resolução",
@ -359,6 +390,7 @@
"kf246dd2e": "Nenhum espaço de trabalho foi encontrado, por favor crie primeiro", "kf246dd2e": "Nenhum espaço de trabalho foi encontrado, por favor crie primeiro",
"kf3b749ef": "ID de Chat Direto do Suporte / Grupo / Canal", "kf3b749ef": "ID de Chat Direto do Suporte / Grupo / Canal",
"kf55495e0": "Guardar", "kf55495e0": "Guardar",
"kf5c3b616": "Cabeçalho da Solicitação",
"kf5c9520e": "Ainda não há nenhuma página de status, você pode criar uma nova para mostrar o status do seu serviço ao público.", "kf5c9520e": "Ainda não há nenhuma página de status, você pode criar uma nova para mostrar o status do seu serviço ao público.",
"kf6339d4f": "Verificado", "kf6339d4f": "Verificado",
"kf6582ba": "Espaço de Trabalho", "kf6582ba": "Espaço de Trabalho",
@ -374,6 +406,7 @@
"kf97b6f71": "Executar este comando na sua máquina linux", "kf97b6f71": "Executar este comando na sua máquina linux",
"kf9877f28": "Ver detalhes", "kf9877f28": "Ver detalhes",
"kf9965c19": "Todo o conteúdo neste espaço de trabalho será destruído e não poderá ser recuperado.", "kf9965c19": "Todo o conteúdo neste espaço de trabalho será destruído e não poderá ser recuperado.",
"kf9a498c7": "Relatório do Lighthouse concluído!",
"kfc98929b": "{{num}} dias", "kfc98929b": "{{num}} dias",
"kfd33c459": "Cópia bem sucedida!", "kfd33c459": "Cópia bem sucedida!",
"kfdaf0bb3": "Última vez online: {{tempo}}", "kfdaf0bb3": "Última vez online: {{tempo}}",

View File

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 268 B

View File

@ -19,11 +19,14 @@
"k17058821": "Отчеты Lighthouse для веб-сайтов", "k17058821": "Отчеты Lighthouse для веб-сайтов",
"k172a09c3": "Предложения", "k172a09c3": "Предложения",
"k1777bbf2": "Вручную", "k1777bbf2": "Вручную",
"k1940fd6": "Общее",
"k1964b988": "Остановить", "k1964b988": "Остановить",
"k1bd89236": "запустить репортер с", "k1bd89236": "запустить репортер с",
"k1c33c293": "Настройки", "k1c33c293": "Настройки",
"k1d8f92b4": "Планшет", "k1d8f92b4": "Планшет",
"k1da4ecc2": "Вы можете отправить сообщение в этот канал с помощью:",
"k1eb5b3ed": "Обзор", "k1eb5b3ed": "Обзор",
"k1ee0c2ca": "Установите URL вебхука на <1></1> и оставьте это окно активным. После завершения вы начнете получать запросы вебхука здесь.",
"k1f6dea0": "Название канала", "k1f6dea0": "Название канала",
"k2099f2e0": "Ошибка входа, проверьте имя пользователя и пароль", "k2099f2e0": "Ошибка входа, проверьте имя пользователя и пароль",
"k20edf271": "24ч", "k20edf271": "24ч",
@ -53,12 +56,15 @@
"k2c84fe32": "Количество событий ленты", "k2c84fe32": "Количество событий ленты",
"k2cecf817": "Тип", "k2cecf817": "Тип",
"k2dad13e3": "Язык", "k2dad13e3": "Язык",
"k2db2c0c5": "Тестовое уведомление",
"k2e6dbf02": "На Email", "k2e6dbf02": "На Email",
"k2ea8a019": "Монитор", "k2ea8a019": "Монитор",
"k30b5f01b": "Рабочие области", "k30b5f01b": "Рабочие области",
"k30d33d71": "Подпись вебхука",
"k310fee": "Последние 30 дней", "k310fee": "Последние 30 дней",
"k32344f64": "Очистить данные", "k32344f64": "Очистить данные",
"k3260f019": "Выйти", "k3260f019": "Выйти",
"k3404b72f": "Новое имя рабочего пространства",
"k340547f0": "Извините, но что-то пошло не так", "k340547f0": "Извините, но что-то пошло не так",
"k3471e956": "Повтор нового пароля", "k3471e956": "Повтор нового пароля",
"k34981fea": "Docker дрейфует в море, не может найти свой путь. Пожалуйста, запустите Docker, чтобы вернуться на правильный курс.", "k34981fea": "Docker дрейфует в море, не может найти свой путь. Пожалуйста, запустите Docker, чтобы вернуться на правильный курс.",
@ -85,6 +91,7 @@
"k3e8b13f8": "Присоединяйтесь к Discord", "k3e8b13f8": "Присоединяйтесь к Discord",
"k3eaab921": "Список мониторинга", "k3eaab921": "Список мониторинга",
"k3f36e17e": "Подписаться на Twitter", "k3f36e17e": "Подписаться на Twitter",
"k406089a4": "Действие",
"k406e9ad8": "Подтвердить", "k406e9ad8": "Подтвердить",
"k41d3ce6c": "Событие восстановлено", "k41d3ce6c": "Событие восстановлено",
"k42347b91": "Количество событий на сайте", "k42347b91": "Количество событий на сайте",
@ -93,7 +100,8 @@
"k44186b66": "Количество", "k44186b66": "Количество",
"k44cad477": "(Текущий)", "k44cad477": "(Текущий)",
"k45f80a27": "Расширенный", "k45f80a27": "Расширенный",
"k4738284": "Вы можете отправить любое сообщение в этот канал с помощью:", "k4727e4db": "Истекает",
"k477b7ee4": "Частичный сбой системы",
"k47fe1f95": "Добавьте этот пример кода в ваш проект", "k47fe1f95": "Добавьте этот пример кода в ваш проект",
"k48186ce": "Вернуться на главную страницу", "k48186ce": "Вернуться на главную страницу",
"k4905ed7b": "НИКАКОЙ", "k4905ed7b": "НИКАКОЙ",
@ -107,6 +115,7 @@
"k4de48e75": "Макс. попыток", "k4de48e75": "Макс. попыток",
"k4e08cf58": "Показать подробное количество", "k4e08cf58": "Показать подробное количество",
"k4eea9393": "Профиль", "k4eea9393": "Профиль",
"k4f182a7c": "Крупный сбой системы",
"k4fc2b5b": "Изображение", "k4fc2b5b": "Изображение",
"k4fe1b4de": "Телеметрия", "k4fe1b4de": "Телеметрия",
"k505c2733": "Создать отчет", "k505c2733": "Создать отчет",
@ -123,9 +132,12 @@
"k58267a45": "Источник", "k58267a45": "Источник",
"k58f90514": "Токен бота", "k58f90514": "Токен бота",
"k593cf342": "Вы уверены, что хотите удалить этот монитор?", "k593cf342": "Вы уверены, что хотите удалить этот монитор?",
"k5a782f4b": "Количество сайтов",
"k5a839f71": "Время работы", "k5a839f71": "Время работы",
"k5b5be0d4": "Текущая роль", "k5b5be0d4": "Текущая роль",
"k5c18db28": "Изменить информацию на странице статуса", "k5c18db28": "Изменить информацию на странице статуса",
"k5d00536d": "Скопировано",
"k5d49d751": "Новый API-ключ скопирован в буфер обмена!",
"k5eb87a8b": "Старт", "k5eb87a8b": "Старт",
"k5ec0de4": "Для мониторинга HTTPS, если назначен любой метод уведомления, уведомления будут отправлены за 1, 3, 7 и 14 дней до истечения срока действия.", "k5ec0de4": "Для мониторинга HTTPS, если назначен любой метод уведомления, уведомления будут отправлены за 1, 3, 7 и 14 дней до истечения срока действия.",
"k5ecf04b0": "Просмотр", "k5ecf04b0": "Просмотр",
@ -135,6 +147,7 @@
"k62e19375": "Последнее обновление: {{date}}", "k62e19375": "Последнее обновление: {{date}}",
"k6488f302": "Необязательно", "k6488f302": "Необязательно",
"k659b065": "Например: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000", "k659b065": "Например: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
"k678e2f90": "Тело запроса",
"k67c5a895": "Вчера", "k67c5a895": "Вчера",
"k683be220": "Запустить", "k683be220": "Запустить",
"k691b7170": "Остановлено", "k691b7170": "Остановлено",
@ -147,9 +160,11 @@
"k6e96fc3": "Информация формы", "k6e96fc3": "Информация формы",
"k6ea11aff": "Получить!", "k6ea11aff": "Получить!",
"k6f15bcc3": "Хост", "k6f15bcc3": "Хост",
"k71067412": "Необязательно, подпись вебхука для входящего вебхука",
"k721589c1": "Сегодня", "k721589c1": "Сегодня",
"k7247683c": "Удалить рабочее пространство", "k7247683c": "Удалить рабочее пространство",
"k7350bd93": "В то же время, мы также можем использовать это в некоторых сценариях клиентского приложения, таких как сбор частоты использования cli, сбор установок самостоятельно размещенных приложений и так далее.", "k7350bd93": "В то же время, мы также можем использовать это в некоторых сценариях клиентского приложения, таких как сбор частоты использования cli, сбор установок самостоятельно размещенных приложений и так далее.",
"k736f3e4c": "Копировать как",
"k75581e13": "Копия", "k75581e13": "Копия",
"k75bfaaa6": "Добавьте этот код в скрипт заголовка вашего веб-сайта", "k75bfaaa6": "Добавьте этот код в скрипт заголовка вашего веб-сайта",
"k763816ac": "Предварительный просмотр", "k763816ac": "Предварительный просмотр",
@ -157,16 +172,17 @@
"k78b1ef6a": "Ввод", "k78b1ef6a": "Ввод",
"k7927b824": "Вы уверены, что хотите очистить все офлайн узлы?", "k7927b824": "Вы уверены, что хотите очистить все офлайн узлы?",
"k7a132ce8": "Извините, но эта страница не найдена", "k7a132ce8": "Извините, но эта страница не найдена",
"k7a15497a": "В реальном времени",
"k7ac44a6e": "Ключ сессии", "k7ac44a6e": "Ключ сессии",
"k7b74a43f": "посетители", "k7b74a43f": "посетители",
"k7b75e24c": "Интеграция", "k7b75e24c": "Интеграция",
"k7b9aa48c": "Тело", "k7b9aa48c": "Тело",
"k7cac602a": "Статус", "k7cac602a": "Статус",
"k7d8cd81c": "Копировать URL",
"k7e0360fd": "Не создано ни одной группы, нажмите кнопку, чтобы создать одну", "k7e0360fd": "Не создано ни одной группы, нажмите кнопку, чтобы создать одну",
"k7e61b1af": "Выбрать рабочее пространство", "k7e61b1af": "Выбрать рабочее пространство",
"k7f01b47c": "Журнал аудита", "k7f01b47c": "Журнал аудита",
"k7f03a704": "Не забудьте не отправлять данные с application/json", "k7f03a704": "Не забудьте не отправлять данные с application/json",
"k7f29bae5": "Просмотр страницы",
"k8037cc6b": "Серверы", "k8037cc6b": "Серверы",
"k816ce026": "Скачать", "k816ce026": "Скачать",
"k819633bc": "Использовать для хранения", "k819633bc": "Использовать для хранения",
@ -176,6 +192,7 @@
"k84ce1618": "(24 часа)", "k84ce1618": "(24 часа)",
"k84e82947": "{{num}} события очищены", "k84e82947": "{{num}} события очищены",
"k85344b23": "Нагрузка", "k85344b23": "Нагрузка",
"k85a116ee": "URL вебхука",
"k85c5fd4c": "Мониторы еще не настроены", "k85c5fd4c": "Мониторы еще не настроены",
"k85db19da": "Пока нет ни одного канала. Используйте функцию канала для получения всех событий из сети или вашей собственной службы.", "k85db19da": "Пока нет ни одного канала. Используйте функцию канала для получения всех событий из сети или вашей собственной службы.",
"k873c90e6": "Метка отображения", "k873c90e6": "Метка отображения",
@ -188,6 +205,7 @@
"k88d2647b": "Веб-сайт", "k88d2647b": "Веб-сайт",
"k89056082": "(30 дней)", "k89056082": "(30 дней)",
"k892f84b6": "Не удается получить информацию о текущем пользователе", "k892f84b6": "Не удается получить информацию о текущем пользователе",
"k895cafe1": "Необязательно, URL вебхука для отправки полезной нагрузки опроса",
"k899fd0cd": "Порты", "k899fd0cd": "Порты",
"k89d54f7a": "Количество выполнений мониторинга", "k89d54f7a": "Количество выполнений мониторинга",
"k8a1deb63": "Участники", "k8a1deb63": "Участники",
@ -207,7 +225,10 @@
"k90b603b8": "Дублировать", "k90b603b8": "Дублировать",
"k90b668e5": "Последние 24 часа", "k90b668e5": "Последние 24 часа",
"k93374bc9": "Удалить веб-сайт", "k93374bc9": "Удалить веб-сайт",
"k93458b98": "Площадка",
"k951a939a": "Количество принятых сайтом", "k951a939a": "Количество принятых сайтом",
"k95f932a": "В настоящее время ожидает нового запроса от удаленного сервера",
"k97b02874": "Количество страниц",
"k98f433ee": "Скачать репортер с", "k98f433ee": "Скачать репортер с",
"k9991c290": "Сообщество", "k9991c290": "Сообщество",
"k9a272ecf": "Это ваши серверы?", "k9a272ecf": "Это ваши серверы?",
@ -233,6 +254,7 @@
"ka6ee7455": "ID веб-сайта", "ka6ee7455": "ID веб-сайта",
"ka71c12e1": "Два пароля не совпадают", "ka71c12e1": "Два пароля не совпадают",
"ka765ad32": "Уведомления", "ka765ad32": "Уведомления",
"ka7d8617e": "Количество каналов ленты",
"ka7fe5937": "Чтение/запись на диск", "ka7fe5937": "Чтение/запись на диск",
"ka8e41156": "Поиск и быстрый переход", "ka8e41156": "Поиск и быстрый переход",
"ka90bc019": "Удалить", "ka90bc019": "Удалить",
@ -254,6 +276,7 @@
"kb0e351e0": "Обновлено", "kb0e351e0": "Обновлено",
"kb114a2e8": "Устаревший", "kb114a2e8": "Устаревший",
"kb15a6374": "Вы можете настроить свою страницу статуса на своем собственном домене, например: status.example.com", "kb15a6374": "Вы можете настроить свою страницу статуса на своем собственном домене, например: status.example.com",
"kb2dded49": "Ключ",
"kb320aac4": "Мониторинг в течение {{dayNum}} дней", "kb320aac4": "Мониторинг в течение {{dayNum}} дней",
"kb35cde91": "Поиск", "kb35cde91": "Поиск",
"kb35d71ed": "ИЛИ", "kb35d71ed": "ИЛИ",
@ -261,6 +284,7 @@
"kb5673707": "Последние 7 дней", "kb5673707": "Последние 7 дней",
"kb659c1bc": "Истечение серт.", "kb659c1bc": "Истечение серт.",
"kb6d350b6": "Каналы обратной связи", "kb6d350b6": "Каналы обратной связи",
"kb7bf8869": "API-ключи",
"kb7fa344a": "Выберите канал обратной связи для отправки", "kb7fa344a": "Выберите канал обратной связи для отправки",
"kb8de8c50": "Скрытая копия", "kb8de8c50": "Скрытая копия",
"kbb31d3db": "Дата статистики", "kbb31d3db": "Дата статистики",
@ -296,13 +320,16 @@
"kcc9c1bff": "Каждую неделю", "kcc9c1bff": "Каждую неделю",
"kccaa732a": "Без последовательных тире", "kccaa732a": "Без последовательных тире",
"kccb42483": "Пароль", "kccb42483": "Пароль",
"kcd56f27b": "Последнее обновление",
"kcd643ef3": "Загрузка...", "kcd643ef3": "Загрузка...",
"kce77d0c1": "Часовой пояс",
"kcff78587": "Последнее использование",
"kd005f7a8": "Все ленты будут удалены", "kd005f7a8": "Все ленты будут удалены",
"kd031b383": "Просмотры", "kd031b383": "Просмотры",
"kd044d5d4": "Сессия",
"kd092de58": "Текущее рабочее пространство:", "kd092de58": "Текущее рабочее пространство:",
"kd1f7e695": "Подтвердить выход", "kd1f7e695": "Подтвердить выход",
"kd211e2d4": "Страница релизов", "kd211e2d4": "Страница релизов",
"kd25f123a": "Статус неизвестен",
"kd2a7ad83": "Шаблон обратной связи", "kd2a7ad83": "Шаблон обратной связи",
"kd3262a4a": "Настройка", "kd3262a4a": "Настройка",
"kd3396544": "Обычно мы будем использовать однопиксельное пустое изображение, так что это не повлияет на нормальное использование пользователя.", "kd3396544": "Обычно мы будем использовать однопиксельное пустое изображение, так что это не повлияет на нормальное использование пользователя.",
@ -311,14 +338,18 @@
"kd7279fa6": "Код", "kd7279fa6": "Код",
"kd7985726": "{{num}} пользователей", "kd7985726": "{{num}} пользователей",
"kd92fa3e7": "Имя хоста", "kd92fa3e7": "Имя хоста",
"kdaa6ae2b": "Количество мониторов",
"kdaff25a6": "Показать последнее значение", "kdaff25a6": "Показать последнее значение",
"kdb61adbb": "Скрыть офлайн", "kdb61adbb": "Скрыть офлайн",
"kdbadcf43": "Все системы работают",
"kdbe222b": "API-ключ",
"kdc10ee1a": "Создайте новое рабочее пространство для сотрудничества с членами команды.", "kdc10ee1a": "Создайте новое рабочее пространство для сотрудничества с членами команды.",
"kdc15c5d": "Данные", "kdc15c5d": "Данные",
"kdc1bf80e": "URL обязателен", "kdc1bf80e": "URL обязателен",
"kdc51b5db": "Веб-сайты", "kdc51b5db": "Веб-сайты",
"kdd44ac01": "Отображаемое имя телеметрии", "kdd44ac01": "Отображаемое имя телеметрии",
"kdd55936a": "Порт разрешителя", "kdd55936a": "Порт разрешителя",
"kde315178": "Переименовать",
"kde37bc27": "Вернуться к администратору", "kde37bc27": "Вернуться к администратору",
"kdeba7706": "Устройства", "kdeba7706": "Устройства",
"kdeecbfea": "Сервер разрешителя", "kdeecbfea": "Сервер разрешителя",
@ -359,6 +390,7 @@
"kf246dd2e": "Рабочее пространство не найдено, пожалуйста, создайте его сначала", "kf246dd2e": "Рабочее пространство не найдено, пожалуйста, создайте его сначала",
"kf3b749ef": "Поддержка прямого чата / группы / ID чата канала", "kf3b749ef": "Поддержка прямого чата / группы / ID чата канала",
"kf55495e0": "Сохранить", "kf55495e0": "Сохранить",
"kf5c3b616": "Заголовок запроса",
"kf5c9520e": "Пока нет страницы состояния, вы можете создать новую, чтобы показать статус вашего сервиса общественности.", "kf5c9520e": "Пока нет страницы состояния, вы можете создать новую, чтобы показать статус вашего сервиса общественности.",
"kf6339d4f": "Подтверждено", "kf6339d4f": "Подтверждено",
"kf6582ba": "Рабочее пространство", "kf6582ba": "Рабочее пространство",
@ -374,6 +406,7 @@
"kf97b6f71": "Запустите эту команду на вашем Linux-машине", "kf97b6f71": "Запустите эту команду на вашем Linux-машине",
"kf9877f28": "Посмотреть детали", "kf9877f28": "Посмотреть детали",
"kf9965c19": "Всё содержимое в этом рабочем пространстве будет уничтожено и не может быть восстановлено.", "kf9965c19": "Всё содержимое в этом рабочем пространстве будет уничтожено и не может быть восстановлено.",
"kf9a498c7": "Отчет Lighthouse завершен!",
"kfc98929b": "{{num}} дней", "kfc98929b": "{{num}} дней",
"kfd33c459": "Копирование успешно!", "kfd33c459": "Копирование успешно!",
"kfdaf0bb3": "Последний онлайн: {{time}}", "kfdaf0bb3": "Последний онлайн: {{time}}",

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -19,11 +19,14 @@
"k17058821": "网站灯塔报告", "k17058821": "网站灯塔报告",
"k172a09c3": "建议", "k172a09c3": "建议",
"k1777bbf2": "手动", "k1777bbf2": "手动",
"k1940fd6": "常规",
"k1964b988": "停止", "k1964b988": "停止",
"k1bd89236": "运行报告器", "k1bd89236": "运行报告器",
"k1c33c293": "设置", "k1c33c293": "设置",
"k1d8f92b4": "平板电脑", "k1d8f92b4": "平板电脑",
"k1da4ecc2": "您可以通过以下方式向此频道发送消息:",
"k1eb5b3ed": "概览", "k1eb5b3ed": "概览",
"k1ee0c2ca": "将 webhook URL 设置为 <1></1>,并保持此窗口处于活动状态。完成后,您将开始在此接收 webhook 请求。",
"k1f6dea0": "频道名称", "k1f6dea0": "频道名称",
"k2099f2e0": "登录失败,请检查您的用户名和密码", "k2099f2e0": "登录失败,请检查您的用户名和密码",
"k20edf271": "24小时", "k20edf271": "24小时",
@ -53,12 +56,15 @@
"k2c84fe32": "事件计数", "k2c84fe32": "事件计数",
"k2cecf817": "类型", "k2cecf817": "类型",
"k2dad13e3": "语言", "k2dad13e3": "语言",
"k2db2c0c5": "测试通知",
"k2e6dbf02": "发邮件到", "k2e6dbf02": "发邮件到",
"k2ea8a019": "监控器", "k2ea8a019": "监控器",
"k30b5f01b": "工作区", "k30b5f01b": "工作区",
"k30d33d71": "Webhook 签名",
"k310fee": "最近30天", "k310fee": "最近30天",
"k32344f64": "清除数据", "k32344f64": "清除数据",
"k3260f019": "登出", "k3260f019": "登出",
"k3404b72f": "新工作区名称",
"k340547f0": "抱歉,出了点问题", "k340547f0": "抱歉,出了点问题",
"k3471e956": "重复新密码", "k3471e956": "重复新密码",
"k34981fea": "Docker在海上漂流无法找到方向。请启动Docker以重新导航。", "k34981fea": "Docker在海上漂流无法找到方向。请启动Docker以重新导航。",
@ -85,6 +91,7 @@
"k3e8b13f8": "加入 Discord", "k3e8b13f8": "加入 Discord",
"k3eaab921": "监控列表", "k3eaab921": "监控列表",
"k3f36e17e": "关注 Twitter", "k3f36e17e": "关注 Twitter",
"k406089a4": "操作",
"k406e9ad8": "确认", "k406e9ad8": "确认",
"k41d3ce6c": "事件已取消归档", "k41d3ce6c": "事件已取消归档",
"k42347b91": "网站事件计数", "k42347b91": "网站事件计数",
@ -93,7 +100,8 @@
"k44186b66": "计数", "k44186b66": "计数",
"k44cad477": "(当前)", "k44cad477": "(当前)",
"k45f80a27": "高级", "k45f80a27": "高级",
"k4738284": "你可以通过以下方式向此频道发送任何消息:", "k4727e4db": "到期时间",
"k477b7ee4": "部分系统故障",
"k47fe1f95": "将此示例代码添加到您的项目中", "k47fe1f95": "将此示例代码添加到您的项目中",
"k48186ce": "返回首页", "k48186ce": "返回首页",
"k4905ed7b": "无", "k4905ed7b": "无",
@ -107,6 +115,7 @@
"k4de48e75": "最大重试次数", "k4de48e75": "最大重试次数",
"k4e08cf58": "显示详细数字", "k4e08cf58": "显示详细数字",
"k4eea9393": "个人资料", "k4eea9393": "个人资料",
"k4f182a7c": "重大系统故障",
"k4fc2b5b": "图片", "k4fc2b5b": "图片",
"k4fe1b4de": "遥测", "k4fe1b4de": "遥测",
"k505c2733": "创建报告", "k505c2733": "创建报告",
@ -123,9 +132,12 @@
"k58267a45": "源", "k58267a45": "源",
"k58f90514": "机器人令牌", "k58f90514": "机器人令牌",
"k593cf342": "您确定要删除这个监控器吗?", "k593cf342": "您确定要删除这个监控器吗?",
"k5a782f4b": "网站数量",
"k5a839f71": "正常运行时间", "k5a839f71": "正常运行时间",
"k5b5be0d4": "当前角色", "k5b5be0d4": "当前角色",
"k5c18db28": "修改状态页面信息", "k5c18db28": "修改状态页面信息",
"k5d00536d": "已复制",
"k5d49d751": "新的 API 密钥已复制到您的剪贴板!",
"k5eb87a8b": "开始", "k5eb87a8b": "开始",
"k5ec0de4": "对于 HTTPS 监控,如果分配了任何通知方法,则将在到期前 1、3、7 和 14 天发送通知。", "k5ec0de4": "对于 HTTPS 监控,如果分配了任何通知方法,则将在到期前 1、3、7 和 14 天发送通知。",
"k5ecf04b0": "查看", "k5ecf04b0": "查看",
@ -135,6 +147,7 @@
"k62e19375": "最后更新时间:{{date}}", "k62e19375": "最后更新时间:{{date}}",
"k6488f302": "可选", "k6488f302": "可选",
"k659b065": "示例https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000", "k659b065": "示例https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
"k678e2f90": "请求体",
"k67c5a895": "昨天", "k67c5a895": "昨天",
"k683be220": "运行", "k683be220": "运行",
"k691b7170": "已停止", "k691b7170": "已停止",
@ -147,9 +160,11 @@
"k6e96fc3": "表单信息", "k6e96fc3": "表单信息",
"k6ea11aff": "获取!", "k6ea11aff": "获取!",
"k6f15bcc3": "主机", "k6f15bcc3": "主机",
"k71067412": "可选,传入 webhook 的 webhook 签名",
"k721589c1": "今天", "k721589c1": "今天",
"k7247683c": "删除工作区", "k7247683c": "删除工作区",
"k7350bd93": "同时我们也可以在一些客户端应用场景中使用它比如收集cli使用频率比如收集自托管应用的安装情况等。", "k7350bd93": "同时我们也可以在一些客户端应用场景中使用它比如收集cli使用频率比如收集自托管应用的安装情况等。",
"k736f3e4c": "复制为",
"k75581e13": "抄送", "k75581e13": "抄送",
"k75bfaaa6": "将此代码添加到您的网站头部脚本中", "k75bfaaa6": "将此代码添加到您的网站头部脚本中",
"k763816ac": "预览", "k763816ac": "预览",
@ -157,16 +172,17 @@
"k78b1ef6a": "输入", "k78b1ef6a": "输入",
"k7927b824": "您确定要清除所有离线节点吗?", "k7927b824": "您确定要清除所有离线节点吗?",
"k7a132ce8": "抱歉,找不到此页面", "k7a132ce8": "抱歉,找不到此页面",
"k7a15497a": "实时",
"k7ac44a6e": "会话密钥", "k7ac44a6e": "会话密钥",
"k7b74a43f": "访客", "k7b74a43f": "访客",
"k7b75e24c": "集成", "k7b75e24c": "集成",
"k7b9aa48c": "正文", "k7b9aa48c": "正文",
"k7cac602a": "状态", "k7cac602a": "状态",
"k7d8cd81c": "复制 URL",
"k7e0360fd": "尚未创建任何组,点击按钮创建一个", "k7e0360fd": "尚未创建任何组,点击按钮创建一个",
"k7e61b1af": "选择工作区", "k7e61b1af": "选择工作区",
"k7f01b47c": "审计日志", "k7f01b47c": "审计日志",
"k7f03a704": "记得不要使用 application/json 发送数据", "k7f03a704": "记得不要使用 application/json 发送数据",
"k7f29bae5": "页面查看",
"k8037cc6b": "服务器", "k8037cc6b": "服务器",
"k816ce026": "下载", "k816ce026": "下载",
"k819633bc": "用于存储", "k819633bc": "用于存储",
@ -176,6 +192,7 @@
"k84ce1618": "24小时", "k84ce1618": "24小时",
"k84e82947": "{{num}} 事件已清除", "k84e82947": "{{num}} 事件已清除",
"k85344b23": "负载", "k85344b23": "负载",
"k85a116ee": "Webhook Url",
"k85c5fd4c": "还没有设置任何监控器", "k85c5fd4c": "还没有设置任何监控器",
"k85db19da": "还没有任何订阅频道。使用订阅功能接收来自网络或您自己服务的所有事件。", "k85db19da": "还没有任何订阅频道。使用订阅功能接收来自网络或您自己服务的所有事件。",
"k873c90e6": "显示标签", "k873c90e6": "显示标签",
@ -188,6 +205,7 @@
"k88d2647b": "网站", "k88d2647b": "网站",
"k89056082": "30天", "k89056082": "30天",
"k892f84b6": "无法获取当前用户信息", "k892f84b6": "无法获取当前用户信息",
"k895cafe1": "可选,发送调查有效负载的 webhook url",
"k899fd0cd": "端口", "k899fd0cd": "端口",
"k89d54f7a": "监控执行计数", "k89d54f7a": "监控执行计数",
"k8a1deb63": "成员", "k8a1deb63": "成员",
@ -207,7 +225,10 @@
"k90b603b8": "重复", "k90b603b8": "重复",
"k90b668e5": "最近24小时", "k90b668e5": "最近24小时",
"k93374bc9": "删除网站", "k93374bc9": "删除网站",
"k93458b98": "游乐场",
"k951a939a": "网站接受计数", "k951a939a": "网站接受计数",
"k95f932a": "当前正在等待来自远程服务器的新请求",
"k97b02874": "页面数量",
"k98f433ee": "从这里下载报告器", "k98f433ee": "从这里下载报告器",
"k9991c290": "社区", "k9991c290": "社区",
"k9a272ecf": "这是您的服务器吗?", "k9a272ecf": "这是您的服务器吗?",
@ -233,6 +254,7 @@
"ka6ee7455": "网站ID", "ka6ee7455": "网站ID",
"ka71c12e1": "两次密码不一致", "ka71c12e1": "两次密码不一致",
"ka765ad32": "通知", "ka765ad32": "通知",
"ka7d8617e": "Feed 渠道数量",
"ka7fe5937": "磁盘读/写", "ka7fe5937": "磁盘读/写",
"ka8e41156": "搜索和快速跳转", "ka8e41156": "搜索和快速跳转",
"ka90bc019": "卸载", "ka90bc019": "卸载",
@ -254,6 +276,7 @@
"kb0e351e0": "已刷新", "kb0e351e0": "已刷新",
"kb114a2e8": "已弃用", "kb114a2e8": "已弃用",
"kb15a6374": "您可以在自己的域名中配置您的状态页面例如status.example.com", "kb15a6374": "您可以在自己的域名中配置您的状态页面例如status.example.com",
"kb2dded49": "密钥",
"kb320aac4": "已监控{{dayNum}}天", "kb320aac4": "已监控{{dayNum}}天",
"kb35cde91": "搜索", "kb35cde91": "搜索",
"kb35d71ed": "或", "kb35d71ed": "或",
@ -261,6 +284,7 @@
"kb5673707": "最近7天", "kb5673707": "最近7天",
"kb659c1bc": "证书到期", "kb659c1bc": "证书到期",
"kb6d350b6": "馈送频道", "kb6d350b6": "馈送频道",
"kb7bf8869": "API 密钥",
"kb7fa344a": "选择要发送的馈送频道", "kb7fa344a": "选择要发送的馈送频道",
"kb8de8c50": "密送", "kb8de8c50": "密送",
"kbb31d3db": "统计日期", "kbb31d3db": "统计日期",
@ -296,13 +320,16 @@
"kcc9c1bff": "每周", "kcc9c1bff": "每周",
"kccaa732a": "无连续破折号", "kccaa732a": "无连续破折号",
"kccb42483": "密码", "kccb42483": "密码",
"kcd56f27b": "最后更新",
"kcd643ef3": "加载中...", "kcd643ef3": "加载中...",
"kce77d0c1": "时区",
"kcff78587": "最后使用时间",
"kd005f7a8": "所有订阅将被删除", "kd005f7a8": "所有订阅将被删除",
"kd031b383": "视图", "kd031b383": "视图",
"kd044d5d4": "会话",
"kd092de58": "当前工作区:", "kd092de58": "当前工作区:",
"kd1f7e695": "确认注销", "kd1f7e695": "确认注销",
"kd211e2d4": "发布页面", "kd211e2d4": "发布页面",
"kd25f123a": "状态未知",
"kd2a7ad83": "馈送模板", "kd2a7ad83": "馈送模板",
"kd3262a4a": "配置", "kd3262a4a": "配置",
"kd3396544": "通常,我们会使用一个 1x1 像素的空白图片,这样不会影响用户的正常使用。", "kd3396544": "通常,我们会使用一个 1x1 像素的空白图片,这样不会影响用户的正常使用。",
@ -311,14 +338,18 @@
"kd7279fa6": "代码", "kd7279fa6": "代码",
"kd7985726": "{{num}}个用户", "kd7985726": "{{num}}个用户",
"kd92fa3e7": "主机名", "kd92fa3e7": "主机名",
"kdaa6ae2b": "监控数量",
"kdaff25a6": "显示最新值", "kdaff25a6": "显示最新值",
"kdb61adbb": "隐藏离线", "kdb61adbb": "隐藏离线",
"kdbadcf43": "所有系统正常运行",
"kdbe222b": "API 密钥",
"kdc10ee1a": "创建一个新的工作区以与团队成员合作。", "kdc10ee1a": "创建一个新的工作区以与团队成员合作。",
"kdc15c5d": "数据", "kdc15c5d": "数据",
"kdc1bf80e": "网址是必需的", "kdc1bf80e": "网址是必需的",
"kdc51b5db": "网站", "kdc51b5db": "网站",
"kdd44ac01": "显示的遥测名称", "kdd44ac01": "显示的遥测名称",
"kdd55936a": "解析器端口", "kdd55936a": "解析器端口",
"kde315178": "重命名",
"kde37bc27": "返回管理员", "kde37bc27": "返回管理员",
"kdeba7706": "设备", "kdeba7706": "设备",
"kdeecbfea": "解析器服务器", "kdeecbfea": "解析器服务器",
@ -359,6 +390,7 @@
"kf246dd2e": "未找到任何工作区,请先创建", "kf246dd2e": "未找到任何工作区,请先创建",
"kf3b749ef": "支持直接聊天/群组/频道的聊天ID", "kf3b749ef": "支持直接聊天/群组/频道的聊天ID",
"kf55495e0": "保存", "kf55495e0": "保存",
"kf5c3b616": "请求头",
"kf5c9520e": "还没有任何状态页面,您可以创建一个新的状态页面向公众展示您的服务状态。", "kf5c9520e": "还没有任何状态页面,您可以创建一个新的状态页面向公众展示您的服务状态。",
"kf6339d4f": "已验证", "kf6339d4f": "已验证",
"kf6582ba": "工作区", "kf6582ba": "工作区",
@ -374,6 +406,7 @@
"kf97b6f71": "在您的Linux机器上运行此命令", "kf97b6f71": "在您的Linux机器上运行此命令",
"kf9877f28": "查看详情", "kf9877f28": "查看详情",
"kf9965c19": "此工作区中的所有内容将被销毁,无法恢复。", "kf9965c19": "此工作区中的所有内容将被销毁,无法恢复。",
"kf9a498c7": "灯塔报告已完成!",
"kfc98929b": "{{num}}天", "kfc98929b": "{{num}}天",
"kfd33c459": "复制成功!", "kfd33c459": "复制成功!",
"kfdaf0bb3": "最后在线时间:{{time}}", "kfdaf0bb3": "最后在线时间:{{time}}",

View File

@ -34,10 +34,13 @@ import { Route as SettingsWorkspaceImport } from './routes/settings/workspace'
import { Route as SettingsUsageImport } from './routes/settings/usage' import { Route as SettingsUsageImport } from './routes/settings/usage'
import { Route as SettingsProfileImport } from './routes/settings/profile' import { Route as SettingsProfileImport } from './routes/settings/profile'
import { Route as SettingsNotificationsImport } from './routes/settings/notifications' import { Route as SettingsNotificationsImport } from './routes/settings/notifications'
import { Route as SettingsBillingImport } from './routes/settings/billing'
import { Route as SettingsAuditLogImport } from './routes/settings/auditLog' import { Route as SettingsAuditLogImport } from './routes/settings/auditLog'
import { Route as SettingsApiKeyImport } from './routes/settings/apiKey'
import { Route as PageAddImport } from './routes/page/add' import { Route as PageAddImport } from './routes/page/add'
import { Route as PageSlugImport } from './routes/page/$slug' import { Route as PageSlugImport } from './routes/page/$slug'
import { Route as MonitorAddImport } from './routes/monitor/add' import { Route as MonitorAddImport } from './routes/monitor/add'
import { Route as FeedPlaygroundImport } from './routes/feed_/playground'
import { Route as FeedAddImport } from './routes/feed/add' import { Route as FeedAddImport } from './routes/feed/add'
import { Route as WebsiteWebsiteIdIndexImport } from './routes/website/$websiteId/index' import { Route as WebsiteWebsiteIdIndexImport } from './routes/website/$websiteId/index'
import { Route as SurveySurveyIdIndexImport } from './routes/survey/$surveyId/index' import { Route as SurveySurveyIdIndexImport } from './routes/survey/$surveyId/index'
@ -165,11 +168,21 @@ const SettingsNotificationsRoute = SettingsNotificationsImport.update({
getParentRoute: () => SettingsRoute, getParentRoute: () => SettingsRoute,
} as any) } as any)
const SettingsBillingRoute = SettingsBillingImport.update({
path: '/billing',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsAuditLogRoute = SettingsAuditLogImport.update({ const SettingsAuditLogRoute = SettingsAuditLogImport.update({
path: '/auditLog', path: '/auditLog',
getParentRoute: () => SettingsRoute, getParentRoute: () => SettingsRoute,
} as any) } as any)
const SettingsApiKeyRoute = SettingsApiKeyImport.update({
path: '/apiKey',
getParentRoute: () => SettingsRoute,
} as any)
const PageAddRoute = PageAddImport.update({ const PageAddRoute = PageAddImport.update({
path: '/add', path: '/add',
getParentRoute: () => PageRoute, getParentRoute: () => PageRoute,
@ -185,6 +198,11 @@ const MonitorAddRoute = MonitorAddImport.update({
getParentRoute: () => MonitorRoute, getParentRoute: () => MonitorRoute,
} as any) } as any)
const FeedPlaygroundRoute = FeedPlaygroundImport.update({
path: '/feed/playground',
getParentRoute: () => rootRoute,
} as any)
const FeedAddRoute = FeedAddImport.update({ const FeedAddRoute = FeedAddImport.update({
path: '/add', path: '/add',
getParentRoute: () => FeedRoute, getParentRoute: () => FeedRoute,
@ -290,6 +308,10 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof FeedAddImport preLoaderRoute: typeof FeedAddImport
parentRoute: typeof FeedImport parentRoute: typeof FeedImport
} }
'/feed/playground': {
preLoaderRoute: typeof FeedPlaygroundImport
parentRoute: typeof rootRoute
}
'/monitor/add': { '/monitor/add': {
preLoaderRoute: typeof MonitorAddImport preLoaderRoute: typeof MonitorAddImport
parentRoute: typeof MonitorImport parentRoute: typeof MonitorImport
@ -302,10 +324,18 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PageAddImport preLoaderRoute: typeof PageAddImport
parentRoute: typeof PageImport parentRoute: typeof PageImport
} }
'/settings/apiKey': {
preLoaderRoute: typeof SettingsApiKeyImport
parentRoute: typeof SettingsImport
}
'/settings/auditLog': { '/settings/auditLog': {
preLoaderRoute: typeof SettingsAuditLogImport preLoaderRoute: typeof SettingsAuditLogImport
parentRoute: typeof SettingsImport parentRoute: typeof SettingsImport
} }
'/settings/billing': {
preLoaderRoute: typeof SettingsBillingImport
parentRoute: typeof SettingsImport
}
'/settings/notifications': { '/settings/notifications': {
preLoaderRoute: typeof SettingsNotificationsImport preLoaderRoute: typeof SettingsNotificationsImport
parentRoute: typeof SettingsImport parentRoute: typeof SettingsImport
@ -401,7 +431,9 @@ export const routeTree = rootRoute.addChildren([
RegisterRoute, RegisterRoute,
ServerRoute, ServerRoute,
SettingsRoute.addChildren([ SettingsRoute.addChildren([
SettingsApiKeyRoute,
SettingsAuditLogRoute, SettingsAuditLogRoute,
SettingsBillingRoute,
SettingsNotificationsRoute, SettingsNotificationsRoute,
SettingsProfileRoute, SettingsProfileRoute,
SettingsUsageRoute, SettingsUsageRoute,
@ -420,6 +452,7 @@ export const routeTree = rootRoute.addChildren([
WebsiteWebsiteIdConfigRoute, WebsiteWebsiteIdConfigRoute,
WebsiteWebsiteIdIndexRoute, WebsiteWebsiteIdIndexRoute,
]), ]),
FeedPlaygroundRoute,
StatusSlugRoute, StatusSlugRoute,
]) ])

View File

@ -114,7 +114,12 @@ function PageComponent() {
{info?.id && ( {info?.id && (
<DialogWrapper <DialogWrapper
title={t('Integration')} title={t('Integration')}
content={<FeedIntegration feedId={info.id} />} content={
<FeedIntegration
feedId={info.id}
webhookSignature={info.webhookSignature}
/>
}
> >
<Button variant="default" size="icon" Icon={LuWebhook} /> <Button variant="default" size="icon" Icon={LuWebhook} />
</DialogWrapper> </DialogWrapper>
@ -194,7 +199,12 @@ function PageComponent() {
)} )}
renderEmpty={() => ( renderEmpty={() => (
<div className="w-full overflow-hidden p-4"> <div className="w-full overflow-hidden p-4">
{!isInitialLoading && <FeedApiGuide channelId={channelId} />} {!isInitialLoading && (
<FeedApiGuide
channelId={channelId}
webhookSignature={info?.webhookSignature}
/>
)}
</div> </div>
)} )}
/> />

View File

@ -0,0 +1,12 @@
import { createFileRoute } from '@tanstack/react-router';
import { routeAuthBeforeLoad } from '@/utils/route';
import { WebhookPlayground } from '@/components/WebhookPlayground';
export const Route = createFileRoute('/feed/playground')({
beforeLoad: routeAuthBeforeLoad,
component: PageComponent,
});
function PageComponent() {
return <WebhookPlayground />;
}

View File

@ -9,6 +9,7 @@ import { Input } from '@/components/ui/input';
import { useAuth } from '@/api/authjs/useAuth'; import { useAuth } from '@/api/authjs/useAuth';
import { useEventWithLoading } from '@/hooks/useEvent'; import { useEventWithLoading } from '@/hooks/useEvent';
import { LuGithub, LuLayers } from 'react-icons/lu'; import { LuGithub, LuLayers } from 'react-icons/lu';
import { compact } from 'lodash-es';
export const Route = createFileRoute('/login')({ export const Route = createFileRoute('/login')({
validateSearch: z.object({ validateSearch: z.object({
@ -44,6 +45,27 @@ function LoginComponent() {
}); });
const { allowRegister, authProvider } = useGlobalConfig(); const { allowRegister, authProvider } = useGlobalConfig();
const authProviderEl = compact([
authProvider.includes('github') && (
<Button
variant="secondary"
className="h-12 w-12 p-3"
onClick={() => loginWithOAuth('github')}
>
<LuGithub size={24} />
</Button>
),
authProvider.includes('custom') && (
<Button
variant="secondary"
className="h-12 w-12 p-3"
onClick={() => loginWithOAuth('custom')}
>
<LuLayers size={24} />
</Button>
),
]);
return ( return (
<div className="flex h-full w-full items-center justify-center dark:bg-gray-900"> <div className="flex h-full w-full items-center justify-center dark:bg-gray-900">
<div className="w-80 -translate-y-1/4"> <div className="w-80 -translate-y-1/4">
@ -98,31 +120,11 @@ function LoginComponent() {
)} )}
</Form> </Form>
{authProvider.length > 0 && ( {authProviderEl.length > 0 && (
<> <>
<Divider>{t('Or')}</Divider> <Divider>{t('Or')}</Divider>
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">{authProviderEl}</div>
{authProvider.includes('github') && (
<Button
variant="secondary"
className="h-12 w-12 p-3"
onClick={() => loginWithOAuth('github')}
>
<LuGithub size={24} />
</Button>
)}
{authProvider.includes('custom') && (
<Button
variant="secondary"
className="h-12 w-12 p-3"
onClick={() => loginWithOAuth('custom')}
>
<LuLayers size={24} />
</Button>
)}
</div>
</> </>
)} )}
</div> </div>

View File

@ -6,6 +6,14 @@ import {
} from '@/components/monitor/StatusPage/ServiceList'; } from '@/components/monitor/StatusPage/ServiceList';
import { useState } from 'react'; import { useState } from 'react';
import { EditableText } from '@/components/EditableText'; import { EditableText } from '@/components/EditableText';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { WebhookPlayground } from '@/components/WebhookPlayground';
import React from 'react';
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
import { Button } from '@/components/ui/button';
import { useEvent } from '@/hooks/useEvent';
import { useCurrentWorkspaceId } from '@/store/user';
import { Separator } from '@/components/ui/separator';
export const Route = createFileRoute('/playground')({ export const Route = createFileRoute('/playground')({
beforeLoad: () => { beforeLoad: () => {
@ -45,6 +53,23 @@ function PageComponent() {
]); ]);
return ( return (
<div className="h-full w-full p-4">
<Tabs defaultValue="billing" className="flex h-full flex-col">
<div>
<TabsList>
<TabsTrigger value="billing">Billing</TabsTrigger>
<TabsTrigger value="webhook">Webhook</TabsTrigger>
<TabsTrigger value="misc">Misc</TabsTrigger>
</TabsList>
</div>
<TabsContent value="billing">
<BillingPlayground />
</TabsContent>
<TabsContent value="webhook" className="flex-1 overflow-hidden">
<WebhookPlayground />
</TabsContent>
<TabsContent value="misc">
<div> <div>
<EditableText <EditableText
defaultValue="fooooooooo" defaultValue="fooooooooo"
@ -53,5 +78,119 @@ function PageComponent() {
<MonitorStatusPageServiceList value={list} onChange={setList} /> <MonitorStatusPageServiceList value={list} onChange={setList} />
</div> </div>
</TabsContent>
</Tabs>
</div>
); );
} }
export const BillingPlayground: React.FC = React.memo(() => {
const checkoutMutation = trpc.billing.checkout.useMutation({
onError: defaultErrorHandler,
});
const changePlanMutation = trpc.billing.changePlan.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const cancelSubscriptionMutation =
trpc.billing.cancelSubscription.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const workspaceId = useCurrentWorkspaceId();
const { data, refetch, isInitialLoading, isLoading } =
trpc.billing.currentSubscription.useQuery({
workspaceId,
});
const handleCheckoutSubscribe = useEvent(async (tier: 'pro' | 'team') => {
const { url } = await checkoutMutation.mutateAsync({
workspaceId,
tier,
redirectUrl: location.href,
});
location.href = url;
});
const handleChangeSubscribe = useEvent(
async (tier: 'free' | 'pro' | 'team') => {
await changePlanMutation.mutateAsync({
workspaceId,
tier,
});
refetch();
}
);
const plan = data ? (
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<Button
loading={changePlanMutation.isLoading}
onClick={() => handleChangeSubscribe('free')}
>
Change plan to Free
</Button>
<Button
loading={changePlanMutation.isLoading}
onClick={() => handleChangeSubscribe('pro')}
>
Change plan to Pro
</Button>
<Button
loading={changePlanMutation.isLoading}
onClick={() => handleChangeSubscribe('team')}
>
Change plan to Team
</Button>
</div>
<div>
<Button
loading={cancelSubscriptionMutation.isLoading}
onClick={() =>
cancelSubscriptionMutation.mutateAsync({
workspaceId,
})
}
>
Cancel
</Button>
</div>
</div>
) : (
<div className="flex gap-2">
<Button
loading={checkoutMutation.isLoading}
onClick={() => handleCheckoutSubscribe('pro')}
>
Upgrade to Pro
</Button>
<Button
loading={checkoutMutation.isLoading}
onClick={() => handleCheckoutSubscribe('team')}
>
Upgrade to Team
</Button>
</div>
);
return (
<div className="flex flex-col gap-2">
<div>
<div>Current: {JSON.stringify(data)}</div>
<Button loading={isLoading} onClick={() => refetch()}>
Refresh
</Button>
</div>
<Separator className="my-2" />
{isInitialLoading === false && plan}
</div>
);
});
BillingPlayground.displayName = 'BillingPlayground';

View File

@ -2,6 +2,7 @@ import { CommonHeader } from '@/components/CommonHeader';
import { CommonList } from '@/components/CommonList'; import { CommonList } from '@/components/CommonList';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { Layout } from '@/components/layout'; import { Layout } from '@/components/layout';
import { useGlobalConfig } from '@/hooks/useConfig';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { import {
@ -9,6 +10,7 @@ import {
useNavigate, useNavigate,
useRouterState, useRouterState,
} from '@tanstack/react-router'; } from '@tanstack/react-router';
import { compact } from 'lodash-es';
import { useEffect } from 'react'; import { useEffect } from 'react';
export const Route = createFileRoute('/settings')({ export const Route = createFileRoute('/settings')({
@ -22,8 +24,9 @@ function PageComponent() {
const pathname = useRouterState({ const pathname = useRouterState({
select: (state) => state.location.pathname, select: (state) => state.location.pathname,
}); });
const { enableBilling } = useGlobalConfig();
const items = [ const items = compact([
{ {
id: 'profile', id: 'profile',
title: t('Profile'), title: t('Profile'),
@ -39,6 +42,11 @@ function PageComponent() {
title: t('Workspace'), title: t('Workspace'),
href: '/settings/workspace', href: '/settings/workspace',
}, },
{
id: 'apiKey',
title: t('Api Key'),
href: '/settings/apiKey',
},
{ {
id: 'auditLog', id: 'auditLog',
title: t('Audit Log'), title: t('Audit Log'),
@ -49,7 +57,12 @@ function PageComponent() {
title: t('Usage'), title: t('Usage'),
href: '/settings/usage', href: '/settings/usage',
}, },
]; enableBilling && {
id: 'billing',
title: t('Billing'),
href: '/settings/billing',
},
]);
useEffect(() => { useEffect(() => {
if (pathname === Route.fullPath) { if (pathname === Route.fullPath) {

View File

@ -0,0 +1,157 @@
import { routeAuthBeforeLoad } from '@/utils/route';
import { createFileRoute } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react';
import { CommonWrapper } from '@/components/CommonWrapper';
import { ScrollArea } from '@/components/ui/scroll-area';
import { CommonHeader } from '@/components/CommonHeader';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { AppRouterOutput, defaultErrorHandler, trpc } from '@/api/trpc';
import { createColumnHelper, DataTable } from '@/components/DataTable';
import { useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { useEvent } from '@/hooks/useEvent';
import dayjs from 'dayjs';
import { LuPlus, LuTrash } from 'react-icons/lu';
import copy from 'copy-to-clipboard';
import { toast } from 'sonner';
import { CopyableText } from '@/components/CopyableText';
import { AlertConfirm } from '@/components/AlertConfirm';
import { formatNumber } from '@/utils/common';
export const Route = createFileRoute('/settings/apiKey')({
beforeLoad: routeAuthBeforeLoad,
component: PageComponent,
});
type ApiKeyInfo = AppRouterOutput['user']['allApiKeys'][number];
const columnHelper = createColumnHelper<ApiKeyInfo>();
function PageComponent() {
const { t } = useTranslation();
const { data: apiKeys = [], refetch: refetchApiKeys } =
trpc.user.allApiKeys.useQuery();
const generateApiKeyMutation = trpc.user.generateApiKey.useMutation({
onError: defaultErrorHandler,
});
const deleteApiKeyMutation = trpc.user.deleteApiKey.useMutation({
onError: defaultErrorHandler,
});
const columns = useMemo(() => {
return [
columnHelper.accessor('apiKey', {
header: t('Key'),
size: 300,
cell: (props) => {
return (
<CopyableText text={props.getValue()}>
{props.getValue().slice(0, 20)}...
</CopyableText>
);
},
}),
columnHelper.accessor('usage', {
header: t('Usage'),
size: 80,
cell: (props) => {
return (
<div className="text-right">
{formatNumber(Number(props.getValue()))}
</div>
);
},
}),
columnHelper.accessor('createdAt', {
header: t('Created At'),
size: 130,
cell: (props) => {
const date = props.getValue();
return (
<span>
{date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'}
</span>
);
},
}),
columnHelper.accessor('updatedAt', {
header: t('Last Use At'),
size: 130,
cell: (props) => {
const date = props.getValue();
return (
<span>
{date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'}
</span>
);
},
}),
columnHelper.accessor('expiredAt', {
header: t('Expired At'),
size: 130,
cell: (props) => {
const date = props.getValue();
return (
<span>
{date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'}
</span>
);
},
}),
columnHelper.display({
id: 'action',
header: t('Action'),
size: 130,
cell: (props) => {
return (
<div>
<AlertConfirm
onConfirm={async () => {
await deleteApiKeyMutation.mutateAsync({
apiKey: props.row.original.apiKey,
});
refetchApiKeys();
}}
>
<Button variant="outline" size="icon" Icon={LuTrash} />
</AlertConfirm>
</div>
);
},
}),
];
}, [t]);
const handleGenerateApiKey = useEvent(async () => {
const apiKey = await generateApiKeyMutation.mutateAsync();
copy(apiKey);
toast.success(t('New api key has been copied into your clipboard!'));
refetchApiKeys();
});
return (
<CommonWrapper header={<CommonHeader title={t('Api Keys')} />}>
<ScrollArea className="h-full overflow-hidden p-4">
<div className="flex flex-col gap-4">
<Card>
<CardHeader className="text-lg font-bold">
<div className="flex items-center justify-between gap-2">
<div>{t('Api Keys')}</div>
<Button
Icon={LuPlus}
size="icon"
variant="outline"
onClick={handleGenerateApiKey}
/>
</div>
</CardHeader>
<CardContent>
<DataTable columns={columns} data={apiKeys} />
</CardContent>
</Card>
</div>
</ScrollArea>
</CommonWrapper>
);
}

View File

@ -2,11 +2,13 @@ import { routeAuthBeforeLoad } from '@/utils/route';
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Empty, List } from 'antd';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { trpc } from '../../api/trpc'; import {
import { useCurrentWorkspaceId } from '../../store/user'; defaultErrorHandler,
defaultSuccessHandler,
trpc,
} from '../../api/trpc';
import { useCurrentWorkspaceId, useHasAdminPermission } from '../../store/user';
import { CommonHeader } from '@/components/CommonHeader'; import { CommonHeader } from '@/components/CommonHeader';
import { last } from 'lodash-es'; import { last } from 'lodash-es';
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
@ -14,6 +16,9 @@ import { useWatch } from '@/hooks/useWatch';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { ColorTag } from '@/components/ColorTag'; import { ColorTag } from '@/components/ColorTag';
import { SimpleVirtualList } from '@/components/SimpleVirtualList'; import { SimpleVirtualList } from '@/components/SimpleVirtualList';
import { Button } from '@/components/ui/button';
import { LuTrash2 } from 'react-icons/lu';
import { AlertConfirm } from '@/components/AlertConfirm';
export const Route = createFileRoute('/settings/auditLog')({ export const Route = createFileRoute('/settings/auditLog')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
@ -24,8 +29,9 @@ function PageComponent() {
const { t } = useTranslation(); const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const parentRef = useRef<HTMLDivElement>(null); const parentRef = useRef<HTMLDivElement>(null);
const hasAdminPermission = useHasAdminPermission();
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = const { data, hasNextPage, fetchNextPage, isFetchingNextPage, refetch } =
trpc.auditLog.fetchByCursor.useInfiniteQuery( trpc.auditLog.fetchByCursor.useInfiniteQuery(
{ {
workspaceId, workspaceId,
@ -35,6 +41,11 @@ function PageComponent() {
} }
); );
const clearMutation = trpc.auditLog.clear.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const allData = useMemo(() => { const allData = useMemo(() => {
if (!data) { if (!data) {
return []; return [];
@ -69,7 +80,27 @@ function PageComponent() {
}); });
return ( return (
<CommonWrapper header={<CommonHeader title={t('Audit Log')} />}> <CommonWrapper
header={
<CommonHeader
title={t('Audit Log')}
actions={
<>
{hasAdminPermission && (
<AlertConfirm
onConfirm={() => {
clearMutation.mutateAsync({ workspaceId });
refetch();
}}
>
<Button variant="outline" size="icon" Icon={LuTrash2} />
</AlertConfirm>
)}
</>
}
/>
}
>
<div className="h-full overflow-hidden p-4"> <div className="h-full overflow-hidden p-4">
<SimpleVirtualList <SimpleVirtualList
allData={allData} allData={allData}

View File

@ -0,0 +1,124 @@
import { routeAuthBeforeLoad } from '@/utils/route';
import { createFileRoute } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react';
import { CommonWrapper } from '@/components/CommonWrapper';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useMemo } from 'react';
import {
defaultErrorHandler,
defaultSuccessHandler,
trpc,
} from '../../api/trpc';
import { useCurrentWorkspace, useCurrentWorkspaceId } from '../../store/user';
import { CommonHeader } from '@/components/CommonHeader';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import dayjs from 'dayjs';
import { formatNumber } from '@/utils/common';
import { UsageCard } from '@/components/UsageCard';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { useEvent } from '@/hooks/useEvent';
import { SubscriptionSelection } from '@/components/billing/SubscriptionSelection';
export const Route = createFileRoute('/settings/billing')({
beforeLoad: routeAuthBeforeLoad,
component: PageComponent,
});
function PageComponent() {
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const { data: currentTier } = trpc.billing.currentTier.useQuery({
workspaceId,
});
const checkoutMutation = trpc.billing.checkout.useMutation({
onError: defaultErrorHandler,
});
const changePlanMutation = trpc.billing.changePlan.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const cancelSubscriptionMutation =
trpc.billing.cancelSubscription.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const { data, refetch, isInitialLoading, isLoading } =
trpc.billing.currentSubscription.useQuery({
workspaceId,
});
const handleChangeSubscribe = useEvent(
async (tier: 'free' | 'pro' | 'team') => {
await changePlanMutation.mutateAsync({
workspaceId,
tier,
});
refetch();
}
);
const plan = data ? (
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<Button
loading={changePlanMutation.isLoading}
onClick={() => handleChangeSubscribe('free')}
>
Change plan to Free
</Button>
<Button
loading={changePlanMutation.isLoading}
onClick={() => handleChangeSubscribe('pro')}
>
Change plan to Pro
</Button>
<Button
loading={changePlanMutation.isLoading}
onClick={() => handleChangeSubscribe('team')}
>
Change plan to Team
</Button>
</div>
<div>
<Button
loading={cancelSubscriptionMutation.isLoading}
onClick={() =>
cancelSubscriptionMutation.mutateAsync({
workspaceId,
})
}
>
Cancel
</Button>
</div>
</div>
) : (
<div className="flex gap-2">
<SubscriptionSelection tier={currentTier} />
</div>
);
return (
<CommonWrapper header={<CommonHeader title={t('Billing')} />}>
<ScrollArea className="h-full overflow-hidden p-4">
<div className="flex flex-col gap-2">
<div>
<div>Current: {JSON.stringify(data)}</div>
<Button loading={isLoading} onClick={() => refetch()}>
Refresh
</Button>
</div>
<Separator className="my-2" />
{isInitialLoading === false && plan}
</div>
</ScrollArea>
</CommonWrapper>
);
}

View File

@ -10,6 +10,7 @@ import { CommonHeader } from '@/components/CommonHeader';
import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Card, CardContent, CardHeader } from '@/components/ui/card';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { formatNumber } from '@/utils/common'; import { formatNumber } from '@/utils/common';
import { UsageCard } from '@/components/UsageCard';
export const Route = createFileRoute('/settings/usage')({ export const Route = createFileRoute('/settings/usage')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
@ -24,12 +25,20 @@ function PageComponent() {
[] []
); );
const { data } = trpc.billing.usage.useQuery({ const { data: serviceCountData } = trpc.workspace.getServiceCount.useQuery({
workspaceId,
});
const { data: billingUsageData } = trpc.billing.usage.useQuery({
workspaceId, workspaceId,
startAt: startDate.valueOf(), startAt: startDate.valueOf(),
endAt: endDate.valueOf(), endAt: endDate.valueOf(),
}); });
const { data: limit } = trpc.billing.limit.useQuery({
workspaceId,
});
return ( return (
<CommonWrapper header={<CommonHeader title={t('Usage')} />}> <CommonWrapper header={<CommonHeader title={t('Usage')} />}>
<ScrollArea className="h-full overflow-hidden p-4"> <ScrollArea className="h-full overflow-hidden p-4">
@ -45,50 +54,61 @@ function PageComponent() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3"> <div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
<Card className="flex-1"> <UsageCard
<CardHeader className="text-muted-foreground"> title={t('Website Count')}
{t('Website Accepted Count')} current={serviceCountData?.website ?? 0}
</CardHeader> limit={limit?.maxWebsiteCount}
<CardContent> />
{formatNumber(data?.websiteAcceptedCount ?? 0)}
</CardContent>
</Card>
<Card className="flex-1"> <UsageCard
<CardHeader className="text-muted-foreground"> title={t('Monitor Count')}
{t('Website Event Count')} current={serviceCountData?.monitor ?? 0}
</CardHeader> />
<CardContent>
{formatNumber(data?.websiteEventCount ?? 0)}
</CardContent>
</Card>
<Card className="flex-1"> <UsageCard
<CardHeader className="text-muted-foreground"> title={t('Survey Count')}
{t('Monitor Execution Count')} current={serviceCountData?.survey ?? 0}
</CardHeader> />
<CardContent>
{formatNumber(data?.monitorExecutionCount ?? 0)}
</CardContent>
</Card>
<Card className="flex-1"> <UsageCard
<CardHeader className="text-muted-foreground"> title={t('Page Count')}
{t('Survey Count')} current={serviceCountData?.page ?? 0}
</CardHeader> />
<CardContent>
{formatNumber(data?.surveyCount ?? 0)}
</CardContent>
</Card>
<Card className="flex-1"> <UsageCard
<CardHeader className="text-muted-foreground"> title={t('Feed Channel Count')}
{t('Feed Event Count')} current={serviceCountData?.feed ?? 0}
</CardHeader> limit={limit?.maxFeedChannelCount}
<CardContent> />
{formatNumber(data?.feedEventCount ?? 0)}
</CardContent> <UsageCard
</Card> title={t('Website Accepted Count')}
current={billingUsageData?.websiteAcceptedCount ?? 0}
/>
<UsageCard
title={t('Website Event Count')}
current={billingUsageData?.websiteEventCount ?? 0}
limit={limit?.maxWebsiteEventCount}
/>
<UsageCard
title={t('Monitor Execution Count')}
current={billingUsageData?.monitorExecutionCount ?? 0}
limit={limit?.maxMonitorExecutionCount}
/>
<UsageCard
title={t('Survey Count')}
current={billingUsageData?.surveyCount ?? 0}
limit={limit?.maxSurveyCount}
/>
<UsageCard
title={t('Feed Event Count')}
current={billingUsageData?.feedEventCount ?? 0}
limit={limit?.maxFeedEventCount}
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -38,13 +38,22 @@ import {
} from '@/components/ui/form'; } from '@/components/ui/form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useEventWithLoading } from '@/hooks/useEvent'; import { useEvent, useEventWithLoading } from '@/hooks/useEvent';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
import { AlertConfirm } from '@/components/AlertConfirm'; import { AlertConfirm } from '@/components/AlertConfirm';
import { ROLES } from '@tianji/shared'; import { ROLES } from '@tianji/shared';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import dayjs from 'dayjs';
import { getTimezoneList } from '@/utils/date';
export const Route = createFileRoute('/settings/workspace')({ export const Route = createFileRoute('/settings/workspace')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
@ -62,7 +71,7 @@ const columnHelper = createColumnHelper<MemberInfo>();
function PageComponent() { function PageComponent() {
const { t } = useTranslation(); const { t } = useTranslation();
const { id: workspaceId, name, role } = useCurrentWorkspace(); const { id: workspaceId, name, role, settings } = useCurrentWorkspace();
const hasAdminPermission = useHasAdminPermission(); const hasAdminPermission = useHasAdminPermission();
const { data: members = [], refetch: refetchMembers } = const { data: members = [], refetch: refetchMembers } =
trpc.workspace.members.useQuery({ trpc.workspace.members.useQuery({
@ -71,6 +80,9 @@ function PageComponent() {
const updateCurrentWorkspaceName = useUserStore( const updateCurrentWorkspaceName = useUserStore(
(state) => state.updateCurrentWorkspaceName (state) => state.updateCurrentWorkspaceName
); );
const updateCurrentWorkspaceSettings = useUserStore(
(state) => state.updateCurrentWorkspaceSettings
);
const form = useForm<InviteFormValues>({ const form = useForm<InviteFormValues>({
resolver: zodResolver(inviteFormSchema), resolver: zodResolver(inviteFormSchema),
defaultValues: { defaultValues: {
@ -89,6 +101,10 @@ function PageComponent() {
onSuccess: defaultSuccessHandler, onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler, onError: defaultErrorHandler,
}); });
const updateSettings = trpc.workspace.updateSettings.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const [renameWorkspaceName, setRenameWorkspaceName] = useState(''); const [renameWorkspaceName, setRenameWorkspaceName] = useState('');
const [handleRename, isRenameLoading] = useEventWithLoading(async () => { const [handleRename, isRenameLoading] = useEventWithLoading(async () => {
@ -112,6 +128,19 @@ function PageComponent() {
} }
); );
const handleUpdateSettings = useEvent(async (key: string, value: string) => {
const { settings } = await updateSettings.mutateAsync({
workspaceId,
settings: {
[key]: value,
},
});
updateCurrentWorkspaceSettings(settings);
});
const timezoneList = useMemo(() => getTimezoneList(), []);
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
columnHelper.accessor( columnHelper.accessor(
@ -167,6 +196,36 @@ function PageComponent() {
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader className="text-lg font-bold">
{t('General')}
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<div className="flex-1">{t('Timezone')}</div>
<div>
<Select
value={settings['timezone'] ?? dayjs.tz.guess()}
onValueChange={(value) =>
handleUpdateSettings('timezone', value)
}
>
<SelectTrigger className="w-[240px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{timezoneList.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(handleInvite)} onSubmit={form.handleSubmit(handleInvite)}

View File

@ -2,7 +2,7 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { useCurrentWorkspaceId } from '@/store/user'; import { useCurrentWorkspaceId } from '@/store/user';
import { trpc } from '@/api/trpc'; import { defaultErrorHandler, trpc } from '@/api/trpc';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { CommonWrapper } from '@/components/CommonWrapper'; import { CommonWrapper } from '@/components/CommonWrapper';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
@ -25,7 +25,9 @@ function PageComponent() {
const { surveyId } = Route.useParams<{ surveyId: string }>(); const { surveyId } = Route.useParams<{ surveyId: string }>();
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const navigate = useNavigate(); const navigate = useNavigate();
const mutation = trpc.survey.update.useMutation(); const mutation = trpc.survey.update.useMutation({
onError: defaultErrorHandler,
});
const { data: survey, isLoading } = trpc.survey.get.useQuery({ const { data: survey, isLoading } = trpc.survey.get.useQuery({
workspaceId, workspaceId,
surveyId, surveyId,

View File

@ -13,10 +13,10 @@ import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/website/$websiteId/config')({ export const Route = createFileRoute('/website/$websiteId/config')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
component: WebsiteDetailComponent, component: PageComponent,
}); });
function WebsiteDetailComponent() { function PageComponent() {
const { websiteId } = Route.useParams<{ websiteId: string }>(); const { websiteId } = Route.useParams<{ websiteId: string }>();
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const { data: website, isLoading } = trpc.website.info.useQuery({ const { data: website, isLoading } = trpc.website.info.useQuery({

View File

@ -12,11 +12,7 @@ import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable';
import { WebsiteOverview } from '@/components/website/WebsiteOverview'; import { WebsiteOverview } from '@/components/website/WebsiteOverview';
import { WebsiteVisitorMapBtn } from '@/components/website/WebsiteVisitorMapBtn'; import { WebsiteVisitorMapBtn } from '@/components/website/WebsiteVisitorMapBtn';
import { useGlobalRangeDate } from '@/hooks/useGlobalRangeDate'; import { useGlobalRangeDate } from '@/hooks/useGlobalRangeDate';
import { import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
useCurrentWorkspaceId,
useHasAdminPermission,
useHasPermission,
} from '@/store/user';
import { routeAuthBeforeLoad } from '@/utils/route'; import { routeAuthBeforeLoad } from '@/utils/route';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
@ -25,10 +21,10 @@ import { LuSettings } from 'react-icons/lu';
export const Route = createFileRoute('/website/$websiteId/')({ export const Route = createFileRoute('/website/$websiteId/')({
beforeLoad: routeAuthBeforeLoad, beforeLoad: routeAuthBeforeLoad,
component: WebsiteDetailComponent, component: PageComponent,
}); });
function WebsiteDetailComponent() { function PageComponent() {
const { websiteId } = Route.useParams<{ websiteId: string }>(); const { websiteId } = Route.useParams<{ websiteId: string }>();
const { t } = useTranslation(); const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
@ -87,10 +83,10 @@ function WebsiteDetailComponent() {
/> />
} }
> >
<ScrollArea className="h-full overflow-hidden p-4"> <ScrollArea className="h-full overflow-hidden">
<ScrollBar orientation="horizontal" /> <ScrollBar orientation="horizontal" />
<Card> <Card bordered={false} className="bg-transparent">
<Card.Grid hoverable={false} className="!w-full"> <Card.Grid hoverable={false} className="!w-full">
<WebsiteOverview website={website} showDateFilter={true} /> <WebsiteOverview website={website} showDateFilter={true} />
</Card.Grid> </Card.Grid>
@ -120,7 +116,7 @@ function WebsiteDetailComponent() {
</Card.Grid> </Card.Grid>
<Card.Grid <Card.Grid
hoverable={false} hoverable={false}
className="!w-full sm:min-h-[470px] sm:!w-1/3" className="!w-full sm:min-h-[470px] sm:!w-1/2 md:!w-1/3"
> >
<WebsiteMetricsTable <WebsiteMetricsTable
websiteId={websiteId} websiteId={websiteId}
@ -132,7 +128,7 @@ function WebsiteDetailComponent() {
</Card.Grid> </Card.Grid>
<Card.Grid <Card.Grid
hoverable={false} hoverable={false}
className="!w-full sm:min-h-[470px] sm:!w-1/3" className="!w-full sm:min-h-[470px] sm:!w-1/2 md:!w-1/3"
> >
<WebsiteMetricsTable <WebsiteMetricsTable
websiteId={websiteId} websiteId={websiteId}
@ -144,7 +140,7 @@ function WebsiteDetailComponent() {
</Card.Grid> </Card.Grid>
<Card.Grid <Card.Grid
hoverable={false} hoverable={false}
className="!w-full sm:min-h-[470px] sm:!w-1/3" className="!w-full sm:min-h-[470px] sm:!w-1/2 md:!w-1/3"
> >
<WebsiteMetricsTable <WebsiteMetricsTable
websiteId={websiteId} websiteId={websiteId}
@ -156,7 +152,7 @@ function WebsiteDetailComponent() {
</Card.Grid> </Card.Grid>
<Card.Grid <Card.Grid
hoverable={false} hoverable={false}
className="!w-full sm:min-h-[470px] sm:!w-1/3" className="!w-full sm:min-h-[470px] sm:!w-1/2 md:!w-1/3"
> >
<WebsiteMetricsTable <WebsiteMetricsTable
websiteId={websiteId} websiteId={websiteId}
@ -168,7 +164,7 @@ function WebsiteDetailComponent() {
</Card.Grid> </Card.Grid>
<Card.Grid <Card.Grid
hoverable={false} hoverable={false}
className="!w-full sm:min-h-[470px] sm:!w-1/3" className="!w-full sm:min-h-[470px] sm:!w-1/2 md:!w-1/3"
> >
<WebsiteMetricsTable <WebsiteMetricsTable
websiteId={websiteId} websiteId={websiteId}
@ -184,7 +180,7 @@ function WebsiteDetailComponent() {
</Card.Grid> </Card.Grid>
<Card.Grid <Card.Grid
hoverable={false} hoverable={false}
className="!w-full sm:min-h-[470px] sm:!w-1/3" className="!w-full sm:min-h-[470px] sm:!w-1/2 md:!w-1/3"
> >
<WebsiteMetricsTable <WebsiteMetricsTable
websiteId={websiteId} websiteId={websiteId}

View File

@ -2,6 +2,7 @@ import { Dayjs } from 'dayjs';
import { create } from 'zustand'; import { create } from 'zustand';
export enum DateRange { export enum DateRange {
Realtime,
Last24Hours, Last24Hours,
Today, Today,
Yesterday, Yesterday,

View File

@ -10,6 +10,7 @@ export type UserLoginInfo = NonNullable<AppRouterOutput['user']['info']>;
interface UserState { interface UserState {
info: UserLoginInfo | null; info: UserLoginInfo | null;
updateCurrentWorkspaceName: (name: string) => void; updateCurrentWorkspaceName: (name: string) => void;
updateCurrentWorkspaceSettings: (settings: Record<string, any>) => void;
} }
export const useUserStore = createWithEqualityFn<UserState>()( export const useUserStore = createWithEqualityFn<UserState>()(
@ -27,6 +28,21 @@ export const useUserStore = createWithEqualityFn<UserState>()(
} }
}); });
}, },
updateCurrentWorkspaceSettings: (settings) => {
set((state) => {
const currentUserInfo = useUserStore.getState().info;
if (!currentUserInfo) {
return;
}
for (const workspace of state.info?.workspaces ?? []) {
workspace.workspace.settings = {
...workspace.workspace.settings,
...settings,
};
}
});
},
})), })),
shallow shallow
); );
@ -88,6 +104,7 @@ export function useCurrentWorkspaceSafe() {
id: currentWorkspace.workspace.id, id: currentWorkspace.workspace.id,
name: currentWorkspace.workspace.name, name: currentWorkspace.workspace.name,
role: currentWorkspace.role, role: currentWorkspace.role,
settings: currentWorkspace.workspace.settings,
}; };
}); });

View File

@ -13,6 +13,7 @@ module.exports = {
'./components/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}',
'./pages/**/*.{js,jsx,ts,tsx}', './pages/**/*.{js,jsx,ts,tsx}',
'./routes/**/*.{js,jsx,ts,tsx}', './routes/**/*.{js,jsx,ts,tsx}',
'./utils/health.ts',
], ],
}, },
theme: { theme: {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,16 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`survey > example code 1`] = ` exports[`survey > example curl code 1`] = `
"curl -X POST https://example.com/open/workspace/<workspaceId>/survey/<surveyId>/submit \\
-H "Content-Type: application/json" \\
-d '{
"payload": {
"textField": "Text"
}
}'"
`;
exports[`survey > example sdk code 1`] = `
"import { submitSurvey, initOpenapiSDK } from 'tianji-client-sdk'; "import { submitSurvey, initOpenapiSDK } from 'tianji-client-sdk';
initOpenapiSDK('https://example.com'); initOpenapiSDK('https://example.com');

View File

@ -0,0 +1,10 @@
import { describe, expect, test } from 'vitest';
import { getTimezoneList } from './date';
describe('getTimezoneList', () => {
test('should return timezone list with correct labels and values', () => {
const result = getTimezoneList();
expect(result).toMatchSnapshot();
});
});

View File

@ -68,3 +68,25 @@ export function formatDateWithUnit(val: dayjs.ConfigType, unit: DateUnit) {
return formatDate(val); return formatDate(val);
} }
function formatOffset(offset: number) {
const sign = offset >= 0 ? '+' : '-';
const absOffset = Math.abs(offset);
const hours = String(Math.floor(absOffset / 60)).padStart(2, '0');
const minutes = String(absOffset % 60).padStart(2, '0');
return `${sign}${hours}:${minutes}`;
}
export function getTimezoneList() {
const timezones = Intl.supportedValuesOf('timeZone');
return timezones.map((timezone) => {
const offset = dayjs().tz(timezone).utcOffset();
return {
label: `${timezone} (${formatOffset(offset)})`,
value: timezone,
};
});
}

View File

@ -0,0 +1,45 @@
import { describe, test, expect } from 'vitest';
import {
parseHealthStatusByPercent,
getStatusBgColorClassName,
} from './health';
describe('parseHealthStatusByPercent', () => {
test('should return "health" when percent is 100', () => {
expect(parseHealthStatusByPercent(100, 0)).toEqual('health');
});
test('should return "none" when percent is 0 and count is 0', () => {
expect(parseHealthStatusByPercent(0, 0)).toEqual('none');
});
test('should return "error" when percent is 0 and count is not 0', () => {
expect(parseHealthStatusByPercent(0, 1)).toEqual('error');
});
test('should return "warning" for other cases', () => {
expect(parseHealthStatusByPercent(50, 1)).toEqual('warning');
});
});
describe('getStatusBgColorClassName', () => {
test('should return bg-green-500 for health status', () => {
expect(getStatusBgColorClassName('health')).toEqual('bg-green-500');
});
test('should return bg-red-600 for error status', () => {
expect(getStatusBgColorClassName('error')).toEqual('bg-red-600');
});
test('should return bg-yellow-400 for warning status', () => {
expect(getStatusBgColorClassName('warning')).toEqual('bg-yellow-400');
});
test('should return bg-gray-400 for none status', () => {
expect(getStatusBgColorClassName('none')).toEqual('bg-gray-400');
});
test('should return empty string for other status', () => {
expect(getStatusBgColorClassName('other' as any)).toEqual('');
});
});

View File

@ -0,0 +1,36 @@
export type HealthStatus = 'health' | 'error' | 'warning' | 'none';
/**
*
* @param percent 0 - 100
* @param count
* @returns
*/
export function parseHealthStatusByPercent(
percent: number,
count: number
): HealthStatus {
if (percent >= 95) {
return 'health';
} else if (percent === 0 && count === 0) {
return 'none';
} else if (percent === 0 && count !== 0) {
return 'error';
} else {
return 'warning';
}
}
export function getStatusBgColorClassName(status: HealthStatus): string {
if (status === 'health') {
return 'bg-green-500';
} else if (status === 'error') {
return 'bg-red-600';
} else if (status === 'warning') {
return 'bg-yellow-400';
} else if (status === 'none') {
return 'bg-gray-400';
} else {
return '';
}
}

View File

@ -1,3 +1,5 @@
import { setupI18nInstance } from '@i18next-toolkit/react';
export const languages = [ export const languages = [
{ {
label: 'English', label: 'English',
@ -5,31 +7,36 @@ export const languages = [
}, },
{ {
label: 'Deutsch', label: 'Deutsch',
key: 'de', key: 'de-DE',
}, },
{ {
label: 'Français', label: 'Français',
key: 'fr', key: 'fr-FR',
}, },
{ {
label: '日本語', label: '日本語',
key: 'jp', key: 'ja-JP',
}, },
{ {
label: 'Polski', label: 'Polski',
key: 'pl', key: 'pl-PL',
}, },
{ {
label: 'Português', label: 'Português',
key: 'pt', key: 'pt-PT',
}, },
{ {
label: 'Русский', label: 'Русский',
key: 'ru', key: 'ru-RU',
}, },
{ {
label: '简体中文', label: '简体中文',
key: 'zh', key: 'zh-CN',
}, },
]; ];
export function initI18N() {
setupI18nInstance({
supportedLngs: languages.map((l) => l.key),
});
}

View File

@ -1,10 +1,32 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { generateSurveyExampleCode } from './survey'; import {
generateSurveyExampleCurlCode,
generateSurveyExampleSDKCode,
} from './survey';
describe('survey', () => { describe('survey', () => {
test('example code', () => { test('example sdk code', () => {
expect( expect(
generateSurveyExampleCode('https://example.com', { generateSurveyExampleSDKCode('https://example.com', {
id: '<surveyId>',
workspaceId: '<workspaceId>',
name: 'Test',
payload: {
items: [
{
name: 'textField',
label: 'Text',
type: 'text',
},
],
},
})
).matchSnapshot();
});
test('example curl code', () => {
expect(
generateSurveyExampleCurlCode('https://example.com', {
id: '<surveyId>', id: '<surveyId>',
workspaceId: '<workspaceId>', workspaceId: '<workspaceId>',
name: 'Test', name: 'Test',

View File

@ -1,9 +1,9 @@
import { AppRouterOutput } from '@/api/trpc'; import { AppRouterOutput } from '@/api/trpc';
/** /**
* Generate survey example code * Generate survey example sdk code
*/ */
export function generateSurveyExampleCode( export function generateSurveyExampleSDKCode(
host: string, host: string,
info: info:
| Pick< | Pick<
@ -42,3 +42,29 @@ async function submitForm(${fields.map((field) => field.name).join(', ')}) {
return exampleCode; return exampleCode;
} }
/**
* Generate survey example curl code
*/
export function generateSurveyExampleCurlCode(
host: string,
info:
| Pick<
NonNullable<AppRouterOutput['survey']['get']>,
'id' | 'name' | 'workspaceId' | 'payload'
>
| null
| undefined
): string {
const fields = info?.payload.items ?? [];
const exampleCode = `curl -X POST ${host}/open/workspace/${info?.workspaceId}/survey/${info?.id}/submit \\
-H "Content-Type: application/json" \\
-d '{
"payload": {
${fields.map((field) => `"${field.name}": "${field.label}"`).join(',\n ')}
}
}'`;
return exampleCode;
}

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