Compare commits

...

389 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
moonrailgun
bcc215ca5d chore: unity esbuild version to resolve vulnerabilities which cause by esbuild 2024-09-30 16:07:59 +08:00
moonrailgun
b44e57dde8 refactor: improve websocket log 2024-09-30 14:50:37 +08:00
moonrailgun
ee72f74e2c chore: add ignore in docker build 2024-09-29 01:40:08 +08:00
moonrailgun
9d3e9d89db chore: add logger for lighthouse 2024-09-28 00:19:21 +08:00
moonrailgun
63e6bfe0d1 feat: allow rename workspace 2024-09-27 02:13:52 +08:00
moonrailgun
7c271dc3c1 fix: remove workspace name validation 2024-09-27 01:54:46 +08:00
moonrailgun
9c35bca685 chore: update translation 2024-09-26 01:09:28 +08:00
Amitash
d48af2b8fe Update translation.json 2024-09-26 01:01:51 +08:00
moonrailgun
3011304b9e chore: release v1.15.3 2024-09-25 00:15:21 +08:00
moonrailgun
4f2c1129a0 feat: add fixed server list 2024-09-25 00:15:03 +08:00
moonrailgun
cdc3ce1223 chore: clear unused code 2024-09-24 23:51:36 +08:00
moonrailgun
31e8ce4ab9 refactor: add language fallback to make sure its can be display correct 2024-09-24 00:12:09 +08:00
moonrailgun
f2ce1fb10c perf: improve monitor detail style, enhance style difference 2024-09-24 00:11:47 +08:00
moonrailgun
22d76a0f87 chore: release v1.15.2 2024-09-23 21:49:01 +08:00
moonrailgun
205864720c refactor: change default workspace name 2024-09-22 20:15:39 +08:00
moonrailgun
f16ccb5689 feat: add label map for device type in website 2024-09-22 20:03:03 +08:00
moonrailgun
6bf65cb529 refactor: update translation 2024-09-22 19:44:41 +08:00
moonrailgun
79ed059d99 feat: add admin role and change most owner permission to admin
because only a little action real need owner permission.
2024-09-22 01:10:25 +08:00
moonrailgun
4f4f9b5d3f feat: add workspace role permission check, hide non permission action 2024-09-22 01:05:16 +08:00
moonrailgun
d29785a311 feat: add lighthouse reporter generate in website 2024-09-22 00:14:33 +08:00
moonrailgun
fb75a8b654 refactor: fix light mode color issues 2024-09-22 00:13:57 +08:00
moonrailgun
d1820416f4 feat: add status page incident model 2024-09-21 21:05:48 +08:00
moonrailgun
6a1f413a38 chore: invite add id support 2024-09-21 01:10:43 +08:00
moonrailgun
4a1d704fbb refactor: update README roadmap 2024-09-21 00:09:06 +08:00
moonrailgun
5cd383895f chore: release v1.15.1 2024-09-20 00:02:25 +08:00
moonrailgun
84e4722f2f chore: add account provider 2024-09-20 00:01:47 +08:00
moonrailgun
63484d0db5 chore: fix ci problem 2024-09-20 00:01:30 +08:00
moonrailgun
d0afdf5c91 feat: add custom oidc/oauth provider support 2024-09-19 23:33:22 +08:00
moonrailgun
90953e490c fix: fix website cannot delete problem #91 2024-09-19 23:00:32 +08:00
moonrailgun
32adb75e9a chore: release v1.15.0 2024-09-19 00:24:21 +08:00
moonrailgun
12b8ba95b7 refactor: add body spaces 2024-09-19 00:23:41 +08:00
moonrailgun
ef3d34423b refactor: update translation 2024-09-19 00:21:05 +08:00
moonrailgun
8b86dcdcea refactor: update translation 2024-09-18 01:12:31 +08:00
moonrailgun
4d39cb5ef4 feat: add group feature in backend 2024-09-17 19:08:58 +08:00
moonrailgun
e323e104e0 refactor: add new editable text component which allow to change group title 2024-09-16 21:50:22 +08:00
moonrailgun
946ecaf9f9 feat: refactor sortable group component and add edit body component 2024-09-16 21:25:31 +08:00
moonrailgun
72a1e7b024 refactor: change edit style and logic, create new MonitorPicker component 2024-09-16 17:08:58 +08:00
moonrailgun
ed2141af22 refactor: improve admin style in status page 2024-09-16 16:41:39 +08:00
moonrailgun
42f41cdbcb chore: update translation 2024-09-16 16:38:10 +08:00
moonrailgun
f5151aa2a4 refactor: remove unused code and improve display view in status page 2024-09-16 00:18:28 +08:00
moonrailgun
427e9e3eb7 refactor: improve some style in server status page 2024-09-15 23:48:50 +08:00
moonrailgun
6160d7bcb9 refactor: refactor server status edit form with react-hook-form 2024-09-15 23:36:34 +08:00
moonrailgun
ef30750802 feat: add sortable group component 2024-09-15 21:55:23 +08:00
moonrailgun
fc1e67e005 chore: upgrade @radix-ui/react-scroll-area version 2024-09-15 00:40:11 +08:00
moonrailgun
95b51ca2e1 chore: fix ci problem and remove unused code 2024-09-14 22:24:21 +08:00
moonrailgun
6ce2f7fd4d feat: add logout button in switch workspace page 2024-09-14 22:14:11 +08:00
moonrailgun
2b9a14c969 feat: add delete workspace feature #96 2024-09-14 22:09:45 +08:00
moonrailgun
943f7f594b feat: add lighthouse html report endpoint 2024-09-14 16:52:49 +08:00
moonrailgun
28d982e497 feat: add lighthouse endpoint 2024-09-14 01:14:22 +08:00
moonrailgun
91ade2ab55 chore: add sortable group component which using react-beautiful-dnd 2024-09-13 00:31:12 +08:00
moonrailgun
f309000a0c chore: remove sender name in notification 2024-09-12 00:47:04 +08:00
moonrailgun
1a39f00aef chore: release v1.14.7 2024-09-11 02:27:21 +08:00
moonrailgun
f74289ff05 docs: add document and website entry in app 2024-09-11 02:26:00 +08:00
moonrailgun
1895ac772c refactor: update sentry feed content 2024-09-10 23:24:28 +08:00
moonrailgun
e770e42893 perf: improve notification and feed channel filter logic
support label filter
2024-09-10 00:03:59 +08:00
moonrailgun
2e609452b5 perf: improve data table resizer width to make it more easy to use 2024-09-10 00:00:38 +08:00
moonrailgun
583ff227fd chore: release v1.14.6 2024-09-09 21:29:38 +08:00
moonrailgun
d2afa54301 feat: add unknown integration log 2024-09-09 21:29:12 +08:00
moonrailgun
ae3f5fce2d chore: release v1.14.5 2024-09-07 22:05:14 +08:00
moonrailgun
cf4531c5dd fix: fix row header style issue 2024-09-05 00:15:00 +08:00
moonrailgun
983bcd37cf chore: release v1.14.4 2024-09-04 00:32:58 +08:00
moonrailgun
c4211c270f chore: fix ci problem which cause build failed 2024-09-04 00:31:42 +08:00
1Luc1
8ccace127b docs: fix edit page url 2024-09-03 00:09:53 +08:00
1Luc1
2cc098a5f1 docs: update manual install to include code update 2024-09-03 00:09:23 +08:00
moonrailgun
791141ee5a chore: release v1.14.3 2024-09-03 00:06:48 +08:00
moonrailgun
546055e555 feat: add sentry webhook integration 2024-09-03 00:06:29 +08:00
moonrailgun
8534ab7ba0 feat: add feed event url support 2024-09-02 23:42:34 +08:00
moonrailgun
01d774d395 fix: fix health bar style problem in page 2024-09-02 22:34:02 +08:00
moonrailgun
921dd53a50 chore: release v1.14.2 2024-09-02 21:23:16 +08:00
moonrailgun
88f47db118 perf: improve feed event report style 2024-09-02 21:21:30 +08:00
moonrailgun
9966c1277c refactor: update translations 2024-09-02 21:20:54 +08:00
moonrailgun
e095a081b9 feat: add socket state 2024-09-01 01:16:51 +08:00
moonrailgun
5588aca522 feat: add curl feed api guide 2024-08-31 01:57:32 +08:00
moonrailgun
87b4000c47 feat: add feed archive page 2024-08-31 01:45:19 +08:00
moonrailgun
3270164710 feat: add archive feature 2024-08-31 00:58:44 +08:00
moonrailgun
33de808f3e feat: feishu add markdown syntax support 2024-08-31 00:15:06 +08:00
甜力怕
9fcc6dda60 docs: update Chinese translation
QWQ
2024-08-30 12:08:47 +08:00
moonrailgun
be444d76c9 chore: release v1.14.1 2024-08-30 01:03:35 +08:00
moonrailgun
b9f5582a02 chore: update pnpm version 2024-08-30 01:03:23 +08:00
moonrailgun
17c6a7fa22 chore: release v1.14.0 2024-08-30 00:10:05 +08:00
moonrailgun
5f47831f8e refactor: workspace switcher style and submit form reset 2024-08-29 04:43:44 +08:00
moonrailgun
b64ca8b300 fix: fix some case(maybe) can not key problem 2024-08-27 22:50:31 +08:00
moonrailgun
3cc678f09e refactor: add default error style problem 2024-08-27 22:46:55 +08:00
moonrailgun
d136460e39 docs: add private-policy page 2024-08-26 01:23:33 +08:00
moonrailgun
79a7a923d2 docs: update openapi files 2024-08-25 03:18:36 +08:00
moonrailgun
40df49e1db chore: fix tsconfig problem in tsx 2024-08-25 03:18:20 +08:00
moonrailgun
fa328fb0bf chore: upgrade package manager 2024-08-24 01:42:47 +08:00
moonrailgun
446ddafa0a feat: add unstar feed 2024-08-23 00:06:47 +08:00
moonrailgun
cbdb1c4a07 fix: fix a style issue which workspace switch style broken with long name 2024-08-22 01:04:18 +08:00
moonrailgun
e0e044945f feat: add invite user form 2024-08-22 01:03:11 +08:00
moonrailgun
7f33e2de0d feat: add tick trpc endpoint 2024-08-22 01:02:31 +08:00
moonrailgun
8c8b960f61 feat: add invite endpoint 2024-08-22 00:59:35 +08:00
moonrailgun
bb84661612 fix: fix virtualize table loading and column style problem 2024-08-21 23:44:11 +08:00
moonrailgun
491807165c feat: add workspace page 2024-08-21 00:18:07 +08:00
moonrailgun
b862dd7427 chore: upgrade @radix-ui/react-scroll-area to resolve scroll problem 2024-08-20 00:13:57 +08:00
moonrailgun
f7e1c8114b chore: fix ci problem 2024-08-18 22:41:06 +08:00
moonrailgun
e983092037 refactor: update translation 2024-08-18 22:32:27 +08:00
moonrailgun
3990b0a872 perf: improve healthbar display, will responsive with container size 2024-08-18 03:58:10 +08:00
moonrailgun
6fecde0caa feat: add delete workspace endpoint 2024-08-18 01:27:18 +08:00
moonrailgun
fac0838d8c feat: add create workspace and switch workspace 2024-08-18 01:12:47 +08:00
moonrailgun
ebd1e5eb66 docs: update website feed feature list 2024-08-17 23:34:04 +08:00
moonrailgun
552835800e chore: release v1.13.1 2024-08-17 00:27:32 +08:00
moonrailgun
3e3dc4c22d chore: fix ci problem 2024-08-16 01:19:13 +08:00
moonrailgun
49d0da3a6d refactor: remove lodash 2024-08-16 00:22:24 +08:00
moonrailgun
22fc5f98f8 feat: add feed template string of survey 2024-08-15 01:33:23 +08:00
moonrailgun
d9862105ed feat: add survey result send to feed channel feature 2024-08-14 01:03:26 +08:00
moonrailgun
5f6147e3b6 feat: add FeedChannelPicker component 2024-08-13 01:00:03 +08:00
moonrailgun
5447f53b30 docs: remove unmaintained readme 2024-08-12 01:21:20 +08:00
moonrailgun
59840b5f7b chore: upgrade i18next-toolkit version 2024-08-12 01:19:05 +08:00
moonrailgun
9d2ba6cc55 chore: release v1.13.0 2024-08-11 19:06:45 +08:00
moonrailgun
914046aefa fix: fix tencentCloudAlarmMetricSchema incorrect problem 2024-08-11 03:01:34 +08:00
moonrailgun
f1aaa7040e feat: add virtualized data table resizer 2024-08-08 00:51:22 +08:00
moonrailgun
ad18666851 refactor: add more log for tencent cloud alarm 2024-08-06 19:37:56 +08:00
moonrailgun
bb76c8e895 docs: update README preview images 2024-08-06 01:50:04 +08:00
moonrailgun
b04ddd40ad chore: remove ts-node and change to tsx 2024-08-05 01:07:41 +08:00
moonrailgun
7e38e327bf refactor: update translation file 2024-08-05 00:45:00 +08:00
moonrailgun
f1496429d3 feat: add survey count and feed event count 2024-08-04 19:44:35 +08:00
moonrailgun
0a0a27549a feat: add email restrict 2024-08-04 17:28:20 +08:00
moonrailgun
7f7c95b11c feat: add github auth integrate 2024-08-03 03:44:48 +08:00
moonrailgun
03bc9b5125 feat: add avatar and nickname display in user info scope 2024-08-03 03:41:41 +08:00
moonrailgun
20e95ef973 chore: fix react-router version 2024-08-03 01:01:28 +08:00
moonrailgun
c7ff3666a7 chore: fix ci problem 2024-08-03 00:04:54 +08:00
moonrailgun
e5c2b9484f chore: fix ci issue of typescript type check 2024-08-02 02:13:09 +08:00
moonrailgun
af4792024f refactor: add error log for tencent alarm 2024-08-02 00:43:45 +08:00
moonrailgun
e9c64c57e7 feat: add logout and socketio auth 2024-08-02 00:41:15 +08:00
moonrailgun
3afac062c4 feat: add support for legacy traditional login methods 2024-08-02 00:41:15 +08:00
moonrailgun
06d6ecd2a3 feat: add authjs backend support 2024-08-02 00:41:15 +08:00
moonrailgun
d5d04468cb refactor: change all import with .js suffix, which will help nodejs(esm) to import code clear. 2024-08-02 00:41:15 +08:00
moonrailgun
5dca262482 refactor: translate server side code into esm 2024-08-02 00:41:15 +08:00
moonrailgun
37757f6563 feat: add prisma migrate 2024-08-02 00:41:15 +08:00
moonrailgun
3cf3cfa427 refactor: wip: add auth.js 2024-08-02 00:41:15 +08:00
moonrailgun
275f30f048 feat: add feed channel into search command panel 2024-07-28 00:05:52 +08:00
moonrailgun
b2dccec283 feat: add VirtualizedInfiniteDataTable and refactor survey result list 2024-07-28 00:03:47 +08:00
moonrailgun
827cf07c2a feat: add duplicate feature for monitor 2024-07-27 22:11:08 +08:00
moonrailgun
05c358b2e5 refactor: add more translations 2024-07-27 21:36:18 +08:00
moonrailgun
73dd8c25b7 feat: add none in feed channel 2024-07-27 21:29:37 +08:00
moonrailgun
e4eee420ea docs: add custom example for match text 2024-07-26 22:18:36 +08:00
moonrailgun
7542d88b5a chore: release v1.12.1 2024-07-26 01:30:05 +08:00
moonrailgun
0835fc588b fix: fix code block not display well in light mode #80 2024-07-24 02:42:56 +08:00
moonrailgun
f112adc696 refactor: change website config tabs to shadcn ui and improve ui 2024-07-24 02:42:20 +08:00
moonrailgun
35a6e20717 refactor: add fade in animation 2024-07-24 00:35:04 +08:00
moonrailgun
8585ea4196 feat: add tencent cloud integration 2024-07-24 00:34:47 +08:00
moonrailgun
9a7afed08c docs: update pnpm version in manual install document 2024-07-23 02:53:11 +08:00
moonrailgun
e7189c6395 chore: release v1.12.0 2024-07-23 00:24:47 +08:00
moonrailgun
f2b20c5ef9 feat: add colorized text for server status which help user find problem 2024-07-22 00:16:36 +08:00
moonrailgun
f814691538 refactor: skip event report if not have any events 2024-07-20 18:01:27 +08:00
moonrailgun
1b89c3b5a8 chore: upgrade pnpm version in dockerfile 2024-07-20 17:56:17 +08:00
moonrailgun
9796d42846 refactor: improve logger and test case 2024-07-20 02:55:55 +08:00
moonrailgun
6e68a8044d feat: add date range and improve report display 2024-07-20 01:41:58 +08:00
moonrailgun
7736bf89dc feat: add list content token 2024-07-20 01:39:10 +08:00
moonrailgun
03904d26e0 feat: add weekly and monthly cron job 2024-07-17 01:00:00 +08:00
moonrailgun
82bb2ad267 fix: fix problem of send notification 2024-07-17 00:38:21 +08:00
moonrailgun
a2cb8b0538 chore: upgrade pnpm version in ci 2024-07-17 00:06:34 +08:00
moonrailgun
7bfd92be0b feat: add feed event notification with event and daily 2024-07-16 00:55:05 +08:00
moonrailgun
ab179e9af6 refactor: change create feed event to local 2024-07-16 00:55:05 +08:00
moonrailgun
b6bca6c250 feat: add more clear job 2024-07-15 01:02:56 +08:00
moonrailgun
63de6d7aa5 chore: upgrade pnpm version to v9.5.0 2024-07-14 14:39:24 +08:00
moonrailgun
1d4aecff95 refactor: change push message in github event 2024-07-14 00:19:10 +08:00
moonrailgun
01d81f3929 refactor: add dynamic virtual list 2024-07-14 00:17:58 +08:00
moonrailgun
15c6290587 refactor: improve display in feed channel list 2024-07-13 16:41:41 +08:00
moonrailgun
2ce5597dfe refactor: change feed channel notifyFrequency type to enum 2024-07-13 16:32:03 +08:00
moonrailgun
b355a677d3 docs: update webhooks document 2024-07-13 16:17:39 +08:00
moonrailgun
b2480b0ed5 docs: update category order 2024-07-13 16:16:01 +08:00
moonrailgun
616a623e40 refactor: update openapi base url and regenerate openapi document 2024-07-13 16:08:26 +08:00
moonrailgun
67bfda30bc feat: add channel feed notification 2024-07-13 15:49:34 +08:00
moonrailgun
2f6e92d166 chore: remove unused code 2024-07-12 21:39:18 +08:00
moonrailgun
e10cdfdf26 docs: update roadmap 2024-07-11 01:30:50 +08:00
moonrailgun
537503f288 chore: add document for endpoint 2024-07-10 01:38:23 +08:00
moonrailgun
503df4546d docs: update wechat qrcode 2024-07-08 22:08:10 +08:00
moonrailgun
685d05074b refactor: add env openapi default value 2024-07-08 00:29:50 +08:00
moonrailgun
c34b0124fa refactor: update feed guide 2024-07-06 03:52:37 +08:00
moonrailgun
865e56f40e chore: fix ci problem 2024-07-05 23:28:28 +08:00
moonrailgun
6ccd0ede7b refactor: remove unused size changer 2024-07-05 23:08:38 +08:00
moonrailgun
caf7e9ca72 feat: add VirtualList support for feed events 2024-07-03 00:12:50 +08:00
moonrailgun
1b859e3176 fix: fix auditlog cannot fetch more data problem 2024-07-03 00:02:24 +08:00
moonrailgun
b7670da7db refactor: add simple virtual list 2024-07-02 22:53:58 +08:00
moonrailgun
66ec94fd08 refactor: add empty description message 2024-07-02 01:37:45 +08:00
moonrailgun
17f87c191a refactor: improve feed event item display 2024-06-30 20:09:08 +08:00
moonrailgun
56bbe09005 feat: feed add markdown support 2024-06-30 16:53:09 +08:00
moonrailgun
4d15cccd1b chore: fix ci problem 2024-06-30 15:56:53 +08:00
moonrailgun
85a2a598d7 refactor: update tag and content 2024-06-30 15:45:01 +08:00
moonrailgun
a4c31fe2da refactor: split integration route from feed route 2024-06-29 09:13:50 +08:00
moonrailgun
765cc41c06 feat: add custom feed integration 2024-06-29 09:04:19 +08:00
moonrailgun
478d0c2af3 feat: add realtime feed event and desc feed list 2024-06-29 08:46:11 +08:00
moonrailgun
adb1cc3919 feat: add dialog wrapper and improve display of webhook modal 2024-06-29 06:36:03 +08:00
moonrailgun
fc6ee73366 refactor: update translation 2024-06-28 23:47:30 +08:00
moonrailgun
a7688f02af feat: add feed channel count 2024-06-27 20:34:01 +08:00
moonrailgun
926ea980ff feat: add feed event item created time 2024-06-27 20:33:44 +08:00
moonrailgun
af5f6ad9f5 feat: add integration modal 2024-06-27 20:27:27 +08:00
moonrailgun
3d9b67a430 refactor: add preview text 2024-06-27 00:53:32 +08:00
moonrailgun
29939b6709 feat: github feed add star and issue support 2024-06-25 22:56:20 +08:00
moonrailgun
12fe9f0384 feat: add github integration support 2024-06-24 23:13:27 +08:00
moonrailgun
ac930cd05e refactor: add feed event url 2024-06-24 23:12:48 +08:00
moonrailgun
96a5a33ad6 feat: add feed page 2024-06-24 22:39:36 +08:00
moonrailgun
f459c6beea feat: add feed endpoint 2024-06-22 20:02:07 +08:00
moonrailgun
c6747073b1 chore: release v1.11.4 2024-06-21 21:05:04 +08:00
moonrailgun
3bfd11a7b6 chore: update release it 2024-06-21 21:03:47 +08:00
moonrailgun
0d2c4f97f9 docs: update wechat qrcode 2024-06-20 10:23:03 +08:00
moonrailgun
a91d1ffffe feat: webhook add title and time 2024-06-20 00:08:30 +08:00
moonrailgun
61c1b0e065 docs: add webhook document 2024-06-19 23:42:49 +08:00
moonrailgun
90df8e8e36 feat: add webhook notification 2024-06-18 22:35:13 +08:00
moonrailgun
ee16e6cd76 docs: update changelog 2024-06-17 21:12:18 +08:00
moonrailgun
4943d2dd8e feat: add server install script usage guide 2024-06-16 00:18:09 +08:00
moonrailgun
f06e788f45 docs: add document about server status page custom domain 2024-06-16 00:17:28 +08:00
moonrailgun
3d11d84545 chore: release v1.11.3 2024-06-15 13:16:07 +08:00
moonrailgun
fdce6b42f1 fix: fix setting page not display correct problem 2024-06-14 00:07:05 +08:00
moonrailgun
e9a1b61a7f perf: improve mobile display for tianji 2024-06-13 21:03:35 +08:00
moonrailgun
cae0c1d6c0 perf: improve sidebar hide logic 2024-06-13 20:39:21 +08:00
moonrailgun
0deec1fc55 docs: update changelog 2024-06-12 21:26:46 +08:00
moonrailgun
95a8e9968b docs: #68 add document to how to install with helm 2024-06-10 15:39:09 +08:00
moonrailgun
caab72dac5 docs: add improve monitor reporter usage roadmap #75 2024-06-09 14:51:15 +08:00
moonrailgun
f91110b313 chore: update cr config 2024-06-09 14:24:15 +08:00
moonrailgun
14d1923908 chore: add helm deps registry 2024-06-09 14:10:47 +08:00
moonrailgun
4485e68b1e chore: update helm deps 2024-06-09 14:08:42 +08:00
moonrailgun
f6f104773b chore: update chart dependency 2024-06-09 14:04:55 +08:00
moonrailgun
b90ef89c8a chore: update config 2024-06-09 14:03:01 +08:00
moonrailgun
d250178cc6 chore: add helm charts release action 2024-06-09 13:58:49 +08:00
moonrailgun
94343b0392 chore: add helm support #68 2024-06-09 13:48:06 +08:00
moonrailgun
bc376ed4af docs: add helm support roadmap #68 2024-06-08 16:04:24 +08:00
moonrailgun
27aa22077a refactor: add user name overflow behavior 2024-06-08 15:54:47 +08:00
moonrailgun
1c98cf5c7d fix: fix a style bug which survey table too width and broke style 2024-06-08 14:14:26 +08:00
moonrailgun
ef49d9ebd2 chore: release v1.11.2 2024-06-07 22:06:22 +08:00
moonrailgun
8ac5b11d49 feat: add website view count in website list 2024-06-06 20:55:57 +08:00
moonrailgun
4e8d7613a4 refactor: increase timeout factor of interval 2024-06-05 21:01:34 +08:00
moonrailgun
80713e0fce chore: update env example 2024-06-03 22:51:42 +08:00
moonrailgun
4564347698 docs: update manual install faq 2024-06-03 22:45:08 +08:00
moonrailgun
1dafea61c7 docs: update manual document 2024-06-02 15:20:23 +08:00
moonrailgun
618aedf196 feat: add createdAt field in survey download csv 2024-06-01 22:11:34 +08:00
moonrailgun
98a887825f docs: add prepare markdown 2024-05-31 22:43:39 +08:00
moonrailgun
0c5c993236 docs: update changelog 2024-05-27 00:15:52 +08:00
moonrailgun
a0ab1da6b6 chore: upgrade prisma version to 5.14.0 2024-05-26 00:49:49 +08:00
moonrailgun
d74ba8d283 refactor: update translation 2024-05-26 00:36:09 +08:00
moonrailgun
26da461368 docs: update wechat qrcode 2024-05-25 20:32:16 +08:00
moonrailgun
ad4b67ca45 feat: add document entry 2024-05-25 01:07:03 +08:00
moonrailgun
328a4e856c refactor: remove unused code 2024-05-25 00:56:49 +08:00
moonrailgun
52a89276c8 docs: update environment document 2024-05-24 01:08:00 +08:00
moonrailgun
bffb9d6729 docs: add install script uninstall document 2024-05-23 00:45:37 +08:00
moonrailgun
ed0c2e9d1d refactor: update sdk publish file module type 2024-05-22 22:00:58 +08:00
moonrailgun
154b8b4b64 docs: update manual install document #56 2024-05-22 21:18:42 +08:00
moonrailgun
58445f9249 docs: update readme roadmap 2024-05-22 00:26:03 +08:00
moonrailgun
a05db11518 chore: release v1.11.1 2024-05-22 00:24:28 +08:00
moonrailgun
e3d0555c45 fix: fix display problem in docker panel 2024-05-21 23:04:19 +08:00
moonrailgun
3ecd7aa171 refactor: add survey add state 2024-05-21 20:09:00 +08:00
moonrailgun
8f37c47a57 chore: release v1.11.0 2024-05-20 20:48:05 +08:00
moonrailgun
dec6a8b7c5 chore: move dependency place 2024-05-19 01:08:12 +08:00
moonrailgun
8e96c06d94 docs: update website style
- improve border with docs page
- update feature website icon
- update join text
2024-05-19 01:08:02 +08:00
moonrailgun
7c94caf0ed chore: upgrade trpc version to 10.45.2 2024-05-18 00:54:13 +08:00
moonrailgun
a307b6f2f9 test: fix ci problem 2024-05-18 00:45:51 +08:00
moonrailgun
74bd9ef3d9 refactor: change datatable expend icon and add transition 2024-05-18 00:33:55 +08:00
moonrailgun
c6433f310b feat: add server docker expend view 2024-05-18 00:33:55 +08:00
moonrailgun
1dfa24df1b feat: add reporter send docker info 2024-05-18 00:33:55 +08:00
moonrailgun
a20396ad97 docs: new homepage 2024-05-18 00:33:10 +08:00
moonrailgun
0ea7515ad2 refactor: update survey icon 2024-05-17 22:21:07 +08:00
moonrailgun
00d40c8410 refactor: add loading state for common list 2024-05-17 19:36:46 +08:00
moonrailgun
eacf7fc56f docs: uprade docs website to v3.3.2 2024-05-16 21:25:38 +08:00
moonrailgun
9a0a1eacb6 chore: upgrade tianji-client-sdk version 2024-05-16 01:04:48 +08:00
moonrailgun
ecabfe166f chore: release v1.10.0 2024-05-16 00:10:49 +08:00
moonrailgun
342c076966 refactor: remove unused code 2024-05-15 20:50:29 +08:00
moonrailgun
2b75a8edad feat: add survey usage button 2024-05-15 20:32:25 +08:00
moonrailgun
cc0bd73ed1 refactor: change weight of commit list search 2024-05-14 20:31:04 +08:00
moonrailgun
9d3c0344ee refactor: add tooltip and time display on table 2024-05-14 00:06:40 +08:00
moonrailgun
9780affebc refactor: common list add loading state 2024-05-13 20:25:00 +08:00
moonrailgun
b2fb1832e1 perf: improve mobile display 2024-05-12 17:02:03 +08:00
moonrailgun
6606b253d8 feat: add tianji event for pricing page 2024-05-12 01:54:05 +08:00
moonrailgun
6865a7ca0b docs: reduce homepage image size to improve user network 2024-05-12 01:41:58 +08:00
moonrailgun
3d16d8edd8 docs: clear email input when submit success in price page 2024-05-12 01:35:26 +08:00
598 changed files with 46189 additions and 32782 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -2,8 +2,8 @@
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public" DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
# Whether allow feature # Whether allow feature
ALLOW_REGISTER= ALLOW_REGISTER=false
ALLOW_OPENAPI= ALLOW_OPENAPI=true
# For analyze tianji self # For analyze tianji self
WEBSITE_ID= WEBSITE_ID=

View File

@ -6,6 +6,8 @@ on:
- master - master
paths: paths:
- "src/**" - "src/**"
- "package.json"
- "pnpm-lock.yaml"
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -17,11 +19,11 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
with: with:
version: 8 version: 9.7.1
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
package-manager-strict=true
package-manager-strict-version=true

View File

@ -3,7 +3,9 @@
"release": true "release": true
}, },
"git": { "git": {
"commitMessage": "chore: release v${version}" "commitMessage": "chore: release v${version}",
"tag": true,
"tagName": "v${version}"
}, },
"npm": { "npm": {
"publish": false "publish": false

View File

@ -1,5 +1,744 @@
## [1.16.5](https://github.com/msgbyte/tianji/compare/v1.16.4...v1.16.5) (2024-11-02)
### Features
* add webhookSignature in feed channel ([6b3631e](https://github.com/msgbyte/tianji/commit/6b3631eae186b9cacf64d0ddcfbb66378e041281))
### Bug Fixes
* add key to Fragment in map for monitor items ([9949b97](https://github.com/msgbyte/tianji/commit/9949b973bd63b4ad6b5e71f7b819442f505c09a6))
* retrieve date as string ([a8a47ed](https://github.com/msgbyte/tianji/commit/a8a47ed94dda87c3fe4cdecc0acb9a31f53f00a5))
### Others
* fix ci problem ([59b8746](https://github.com/msgbyte/tianji/commit/59b874644fd3427bc86cd2a7e948054e827de080))
* refactor status header and add typescript and translation support ([f637ade](https://github.com/msgbyte/tianji/commit/f637ade70f230fbf472bdee84105c9b284d6b8d4))
* update amount in stripe ([2725056](https://github.com/msgbyte/tianji/commit/272505669e450d882930cbf594dac39a879b2072))
* update webhooks signature api guide ([266b08f](https://github.com/msgbyte/tianji/commit/266b08f2da16d0457a5a44b4a7a251d28502abc9))
## [1.16.4](https://github.com/msgbyte/tianji/compare/v1.16.3...v1.16.4) (2024-10-27)
### Features
* add stripe feed integration ([09d0f02](https://github.com/msgbyte/tianji/commit/09d0f02d844159565e97bb64f076e0bbe218ce98))
### Others
* update currency symbols in feed ([98298c4](https://github.com/msgbyte/tianji/commit/98298c43670326b4e2300a6bbdeee3daa53f0eb3))
## [1.16.3](https://github.com/msgbyte/tianji/compare/v1.16.2...v1.16.3) (2024-10-24)
### Others
* fix ci problem and upgrade version ([1c5737e](https://github.com/msgbyte/tianji/commit/1c5737e588d19e0657be6437792cf4484b6fdddb))
## [1.16.2](https://github.com/msgbyte/tianji/compare/v1.16.1...v1.16.2) (2024-10-23)
### Features
* add prometheus report support ([fcb8f22](https://github.com/msgbyte/tianji/commit/fcb8f221168281ab710d3d3f12064a99d17b39e7))
### Bug Fixes
* fix a bug which will match incorrect path [#115](https://github.com/msgbyte/tianji/issues/115) ([79667a9](https://github.com/msgbyte/tianji/commit/79667a9644b78451400acb6a6bbf07b6ca61e6e0))
### Document
* update README ([1df32dc](https://github.com/msgbyte/tianji/commit/1df32dc2579f32649afd6c008512c1190a45fd9e))
### Others
* fix ci problem ([554f902](https://github.com/msgbyte/tianji/commit/554f9025847defe0b05492cf07a5dc8acc6c3685))
* update openapi document ([e402ee1](https://github.com/msgbyte/tianji/commit/e402ee1688bb77d83463ce70c5e730c97f68a695))
## [1.16.1](https://github.com/msgbyte/tianji/compare/v1.16.0...v1.16.1) (2024-10-20)
### Features
* add test notify ([4e3fd9d](https://github.com/msgbyte/tianji/commit/4e3fd9db64629f7721e6092b86b06144c47f521d))
* add timezone support [#114](https://github.com/msgbyte/tianji/issues/114) ([c7e20df](https://github.com/msgbyte/tianji/commit/c7e20df516bf3a991ce46c937223948bcdb6b8f0))
* add workspace settings manage ([3dca8fc](https://github.com/msgbyte/tianji/commit/3dca8fc27c82bd96dbab423b111e4de57f3b4bd8))
### Others
* update cronjob clear time ([83850f2](https://github.com/msgbyte/tianji/commit/83850f2981ded0b6624556ee3430f684752b8ea3))
## [1.16.0](https://github.com/msgbyte/tianji/compare/v1.15.8...v1.16.0) (2024-10-19)
### Features
* add click event for status page item which allow hide/show chart ([279e616](https://github.com/msgbyte/tianji/commit/279e616bee510ee5b0c5a3c9a3705a79efd5d3cb))
* add daily monitor data display for public ([dcff57f](https://github.com/msgbyte/tianji/commit/dcff57fe69273c7f9b3dd9c28e8acc9cb6e430a9))
* add monitor summary function ([bbb8d88](https://github.com/msgbyte/tianji/commit/bbb8d881168df695ccc70743f46320b39c1d7718))
* add MonitorLatestResponse and up status summary ([316b954](https://github.com/msgbyte/tianji/commit/316b95467d49b3ebe93d03006d4b90f9ca482262))
### Bug Fixes
* fix reporter memory leak problem [#103](https://github.com/msgbyte/tianji/issues/103) ([7f70557](https://github.com/msgbyte/tianji/commit/7f70557c776c35e4e01a5533d2c05cecc711e113))
### Others
* add border radius in smtp template ([f553f15](https://github.com/msgbyte/tianji/commit/f553f157dd9708d553c9d6cfca4d119a62d849c3))
* change public summary display logic ([e5e77db](https://github.com/msgbyte/tianji/commit/e5e77dbdeeeecb773237b84e3c671dd16e61d458))
* fix ci problem ([820b25b](https://github.com/msgbyte/tianji/commit/820b25baedc6fec02010ca19b43e4da99bf4b820))
* ignore unknown sentry log ([527f734](https://github.com/msgbyte/tianji/commit/527f734bc442458018d86df9a7e750a8e8de4495))
* let version text more prominent ([61980b3](https://github.com/msgbyte/tianji/commit/61980b37d3cecce32fa87b2b9810f4c715990a71))
* rename old tsconfig paths ([2a503ca](https://github.com/msgbyte/tianji/commit/2a503ca2501e705430c0c35cb7c8279927c1d4d5))
## [1.15.8](https://github.com/msgbyte/tianji/compare/v1.15.7...v1.15.8) (2024-10-13)
### Features
* add payload for feed event integration and send function ([572d96b](https://github.com/msgbyte/tianji/commit/572d96babb348858911105659bfe304e869915e4))
* add ping animation in website realtime visitor ([6da0e6f](https://github.com/msgbyte/tianji/commit/6da0e6f415e863448cd36246eb16e1f09dcd8a79))
* add plausible tracking(for testing) ([6474cef](https://github.com/msgbyte/tianji/commit/6474cefd896b36b872460786b11005b8deaf4436))
* add realtime datarange which can visit data more easy ([f3d8f55](https://github.com/msgbyte/tianji/commit/f3d8f5543d4e277fe34940ce98cd552dee45f2a8))
* add survey curl example code ([5d54ca1](https://github.com/msgbyte/tianji/commit/5d54ca1cbc01b69b9bd03d167edd0db242df350e))
* add survey webhook ([de57242](https://github.com/msgbyte/tianji/commit/de572426ebf99e99ff97ce49caf0b8ac13b68154))
* sdk add send feed function export ([f5933ec](https://github.com/msgbyte/tianji/commit/f5933ec0548fb4ac327152b6d7afe7ca2978bade))
* survey add webhook url field which can send webhook when receive any survey ([f00163b](https://github.com/msgbyte/tianji/commit/f00163b2f107bcf08d4ff398fa5dbd92ac36fda8))
* time event chart legend add some interaction ([4b78771](https://github.com/msgbyte/tianji/commit/4b7877155fd54416fb9231a02aca0e868aec97d2))
### Document
* add shacdn to website ([763810e](https://github.com/msgbyte/tianji/commit/763810e8b7e6cd41fdc0d83d28262dc7d747e4bf))
* add sitemap to improve SEO ([384224c](https://github.com/msgbyte/tianji/commit/384224cb624030522057df22527312957665d8e9))
* add website more language: de, fr, ja, zh-Hans ([7bda542](https://github.com/msgbyte/tianji/commit/7bda5420c5f22f6205d2dbd5086dd0bdbbc7f558))
* change code command line style ([8c5c417](https://github.com/msgbyte/tianji/commit/8c5c417a197531c187452fc4937d034d3bdc05a7))
* remove used blog directory ([3d9d032](https://github.com/msgbyte/tianji/commit/3d9d03296e9fb26eb363b2dba2f830f2eb9d58f3))
* resolve build problem with update source document content ([9e6e031](https://github.com/msgbyte/tianji/commit/9e6e03117cc3bd37058563e31817034876d35450))
* update depenpendency to resolve issue of docusaurus build ([de38363](https://github.com/msgbyte/tianji/commit/de38363315275ecbc7384c935c313940fca1d4fc))
* upgrade openapi ([1e57905](https://github.com/msgbyte/tianji/commit/1e57905f3239cc4fb3a949b469161dc9c8d2b40c))
### Others
* add CodeExample component ([29f184c](https://github.com/msgbyte/tianji/commit/29f184c15d36e42af750261f81ad67b49fe58c0b))
* comment sitemap to make sure its can build safe ([9b9799e](https://github.com/msgbyte/tianji/commit/9b9799ec6f846eb40762f5ca8cc0aa6c27fb7e02))
* fix ci problem ([a32f3d9](https://github.com/msgbyte/tianji/commit/a32f3d9824a09a2d8c9e97ba211ee5d44c8e2763))
* fix isolated-vm version ([43b4c9f](https://github.com/msgbyte/tianji/commit/43b4c9fe3763673dc54b61b01e50bc5b3d24a371))
* fix version of postman-code-generators ([eaffe3a](https://github.com/msgbyte/tianji/commit/eaffe3ab21022215a76c3441ef0b9b2c37386227))
* improve display of visitor map if data is too much ([9bc8c63](https://github.com/msgbyte/tianji/commit/9bc8c63fe2ea2ab080e7db3cf7d0c7636fabf8d1))
* migrate monitor data chart to recharts and remove @ant-design/charts ([c0e2ef0](https://github.com/msgbyte/tianji/commit/c0e2ef0fe8f5520a7b935eeeb44f6be9224e56a4))
* update ci run trigger path ([7322ad7](https://github.com/msgbyte/tianji/commit/7322ad741dcfc5c033b5057e1862e91d27244f7f))
* update pnpm lock file to resolve some magic problem ([064dbe9](https://github.com/msgbyte/tianji/commit/064dbe9985767b32492de4264d08c26206318cd4))
* update survey edit form ([a218c22](https://github.com/msgbyte/tianji/commit/a218c2239725deb5bcdee2e8d2de377e04dec941))
* upgrade @radix-ui/react-scroll-area version ([9d559b9](https://github.com/msgbyte/tianji/commit/9d559b93d16c130cf58649e0f12edf9e795ba8a5))
* upgrade @tianji/website docusaurus version ([e46f970](https://github.com/msgbyte/tianji/commit/e46f97097a593fe4bd5a8946237fd2f46fea69f6))
* use prebuilt rather than deploy build ([e51a880](https://github.com/msgbyte/tianji/commit/e51a88044fcef7727b2e7f17c5dd9eff08329cdc))
## [1.15.7](https://github.com/msgbyte/tianji/compare/v1.15.6...v1.15.7) (2024-10-03)
### Bug Fixes
* fix a problem which will make request list incorrect ([2d5a09c](https://github.com/msgbyte/tianji/commit/2d5a09c79cae48f62029b8767bae552376e68639))
### Document
* fix update code to new version ([1fe5009](https://github.com/msgbyte/tianji/commit/1fe50092bab5e0824c1b96b70d40d33f751f4135))
### Others
* split website from monorepo ([e09d7ee](https://github.com/msgbyte/tianji/commit/e09d7eef8788e27ac651f18dcf4d04de895a35ee))
* update workspace config and remove unused lock file ([7301eeb](https://github.com/msgbyte/tianji/commit/7301eeb82a4fdc4ae6853dbe98c1f1d7b9b79bca))
## [1.15.6](https://github.com/msgbyte/tianji/compare/v1.15.5...v1.15.6) (2024-10-02)
### Others
* add build dependency for build zeromq ([79b75f5](https://github.com/msgbyte/tianji/commit/79b75f55e39c057c330bc1fb0b3b15dc10e28a78))
* improve install package time in docker build static stage ([1be03cc](https://github.com/msgbyte/tianji/commit/1be03ccf532a7dd6d23334a5666dec34e2e68d77))
* update NODE_OPTIONS in static layer to make sure build can pass ([5eb7696](https://github.com/msgbyte/tianji/commit/5eb7696ead2da961d6ac5223a2badb48502142ba))
## [1.15.5](https://github.com/msgbyte/tianji/compare/v1.15.4...v1.15.5) (2024-10-01)
### Features
* add error message for lighthouse ([bb0c574](https://github.com/msgbyte/tianji/commit/bb0c57489347242300c6153ed3908d1822bb692c))
* add lighthouse score in database fields ([6c2a093](https://github.com/msgbyte/tianji/commit/6c2a0938423385d67309deefa67a3d971bf8d7c8))
* add webhook playground ([33a0a60](https://github.com/msgbyte/tianji/commit/33a0a60eee53d1ac08cc9accc2e96f06e56ebb52))
* add webhook playground entry ([92196e4](https://github.com/msgbyte/tianji/commit/92196e4e5bb9b183cfe85aad876115c0e17f824e))
* add zeromq to make sure lighthouse can only run one at same time ([50a3573](https://github.com/msgbyte/tianji/commit/50a35732ff202f2452b344c2df17aba677426ec3))
### Others
* improve avatar display timing for non-avatar user ([04dc1e9](https://github.com/msgbyte/tianji/commit/04dc1e98dd448c0fd6661559722cf594ab2e751e))
* refactor time event chart to recharts ([1337eaa](https://github.com/msgbyte/tianji/commit/1337eaa2c0ff55651a05878edd722ac1b46a5067))
* update style of website page card ([b778f8c](https://github.com/msgbyte/tianji/commit/b778f8c982f7df8328c2fffe67783b15cde51c15))
* upgrade shadcn cli and add recharts ([055f57e](https://github.com/msgbyte/tianji/commit/055f57e087f002b8f891053509e3cad865f1d52b))
## [1.15.4](https://github.com/msgbyte/tianji/compare/v1.15.3...v1.15.4) (2024-09-30)
### Features
* allow rename workspace ([63e6bfe](https://github.com/msgbyte/tianji/commit/63e6bfe0d1a989479a6c4658d01ea9d84fc84b45))
### Bug Fixes
* fix login view split incorrect if not any extra login way ([b16a7c3](https://github.com/msgbyte/tianji/commit/b16a7c3c2c203394c94ccfee8e829bc7685a2457))
* remove workspace name validation ([7c271dc](https://github.com/msgbyte/tianji/commit/7c271dc3c14fc6c751fb69b16adf6f08bfd5ac7b))
### Others
* add ignore in docker build ([ee72f74](https://github.com/msgbyte/tianji/commit/ee72f74e2c68c9baec500b78a3b994c8083abeed))
* add logger for lighthouse ([9d3e9d8](https://github.com/msgbyte/tianji/commit/9d3e9d89db40aad4a78df8b64ad3d7bfccb94d2e))
* add no sandbox args in puppeteer ([8b6a740](https://github.com/msgbyte/tianji/commit/8b6a74033c2838a6921c56990e13eedfbc8a559a))
* docker add puppeteer support ([23c6915](https://github.com/msgbyte/tianji/commit/23c691541db0a51b4765dd0943488def7741c0f4))
* downgrade alpine version to 3.19 to avoid issue ([e6df595](https://github.com/msgbyte/tianji/commit/e6df595af8ecddf734fe7e72a797921d84dad2c3))
* improve docker build and lighthouse config ([57ebaf6](https://github.com/msgbyte/tianji/commit/57ebaf6ad361cae3403263009b94566cc7de2293))
* improve websocket log ([b44e57d](https://github.com/msgbyte/tianji/commit/b44e57dde8d027eb05b7e8db20d490d2b62607cc))
* try to resolve no screenshot problem by remove single process. ([fe432f1](https://github.com/msgbyte/tianji/commit/fe432f13325adf5fb4cde3dbb2f4f1218cb789e7)), closes [/github.com/GoogleChrome/lighthouse/issues/11537#issuecomment-799895027](https://github.com/msgbyte//github.com/GoogleChrome/lighthouse/issues/11537/issues/issuecomment-799895027)
* unity esbuild version to resolve vulnerabilities which cause by esbuild ([bcc215c](https://github.com/msgbyte/tianji/commit/bcc215ca5d33126b368b58a9056d02fd93d5a99a))
* update dockerfile, carry back auto install dependency ([de09059](https://github.com/msgbyte/tianji/commit/de09059e6561a27e160f0e39e7987da8ad05edaa))
* update translation ([9c35bca](https://github.com/msgbyte/tianji/commit/9c35bca68508f2009434ebf578f0488a948e6b75))
* upgrade axios version to latest to resolve vulnerabilities ([d73fa10](https://github.com/msgbyte/tianji/commit/d73fa108978b3c965b07dda725fbe6ae20bc4140))
* upgrade puppeteer to make sure can fit with alpine image chromium version ([f59793d](https://github.com/msgbyte/tianji/commit/f59793d6f18625ad66b26d0a74aaa14c604ea812))
* upgrade puppeteer usage to fit new version ([1322741](https://github.com/msgbyte/tianji/commit/13227416c05e2eae5a1a99e5d5f3396679e83d89))
* upgrade puppeteer version to 23.4.1 ([e942769](https://github.com/msgbyte/tianji/commit/e942769af2e2570da71eb23f3ad489e8dcb72e95))
## [1.15.3](https://github.com/msgbyte/tianji/compare/v1.15.2...v1.15.3) (2024-09-24)
### Features
* add fixed server list ([4f2c112](https://github.com/msgbyte/tianji/commit/4f2c1129a0421934b43d3b6e02b17d629d275614))
### Others
* add language fallback to make sure its can be display correct ([31e8ce4](https://github.com/msgbyte/tianji/commit/31e8ce4ab9beec4af730b9302c0c3b861234123e))
* clear unused code ([cdc3ce1](https://github.com/msgbyte/tianji/commit/cdc3ce122386e632f6b4350bc3dd4bdf4c17e0ed))
* improve monitor detail style, enhance style difference ([f2ce1fb](https://github.com/msgbyte/tianji/commit/f2ce1fb10c92a2e2583ec3b36866308954958e1e))
## [1.15.2](https://github.com/msgbyte/tianji/compare/v1.15.1...v1.15.2) (2024-09-23)
### Features
* add admin role and change most owner permission to admin ([79ed059](https://github.com/msgbyte/tianji/commit/79ed059d995da6eaabc452a0844b9acb69dc981c))
* add label map for device type in website ([f16ccb5](https://github.com/msgbyte/tianji/commit/f16ccb56895f65dea530be295b19f04a03c8ed99))
* add lighthouse reporter generate in website ([d29785a](https://github.com/msgbyte/tianji/commit/d29785a31184fe48913f7c49833c2d35a92c244a))
* add status page incident model ([d182041](https://github.com/msgbyte/tianji/commit/d1820416f4924b2fc1920383b2d22b042f6e0381))
* add workspace role permission check, hide non permission action ([4f4f9b5](https://github.com/msgbyte/tianji/commit/4f4f9b5d3f36192ea0f416997a43691674aa79fd))
### Others
* change default workspace name ([2058647](https://github.com/msgbyte/tianji/commit/205864720cdcbb5f46bee92fa6c577769a05f167))
* fix light mode color issues ([fb75a8b](https://github.com/msgbyte/tianji/commit/fb75a8b6545a507c81acafa9cb526afccd39cd35))
* invite add id support ([6a1f413](https://github.com/msgbyte/tianji/commit/6a1f413a384021d3c91e136be36a8c6375c74f99))
* update README roadmap ([4a1d704](https://github.com/msgbyte/tianji/commit/4a1d704fbb88bb87f7e9db61f3da1364fb7543c0))
* update translation ([6bf65cb](https://github.com/msgbyte/tianji/commit/6bf65cb529a2b0c52204063f2a10ad33e7b39aa5))
## [1.15.1](https://github.com/msgbyte/tianji/compare/v1.15.0...v1.15.1) (2024-09-19)
### Features
* add custom oidc/oauth provider support ([d0afdf5](https://github.com/msgbyte/tianji/commit/d0afdf5c91d2112d177ab7bb0315586cb64ad8d7))
### Bug Fixes
* fix website cannot delete problem [#91](https://github.com/msgbyte/tianji/issues/91) ([90953e4](https://github.com/msgbyte/tianji/commit/90953e490ceea8e5256fd564e4d220b2e7da50b3))
### Others
* add account provider ([84e4722](https://github.com/msgbyte/tianji/commit/84e4722f2fc8026de25fa33d23e17db61a6d4437))
* fix ci problem ([63484d0](https://github.com/msgbyte/tianji/commit/63484d0db59e1ccc178c7427ec7d60fd7f1484b0))
## [1.15.0](https://github.com/msgbyte/tianji/compare/v1.14.7...v1.15.0) (2024-09-18)
### Features
* add delete workspace feature [#96](https://github.com/msgbyte/tianji/issues/96) ([2b9a14c](https://github.com/msgbyte/tianji/commit/2b9a14c969c824d630452e0e4e30834f2a9a1b47))
* add group feature in backend ([4d39cb5](https://github.com/msgbyte/tianji/commit/4d39cb5ef4d95626e600289a1e949e78ccd7906f))
* add lighthouse endpoint ([28d982e](https://github.com/msgbyte/tianji/commit/28d982e497bfd04351c8495ec9ddd58fc205e771))
* add lighthouse html report endpoint ([943f7f5](https://github.com/msgbyte/tianji/commit/943f7f594ba90aa037ab5cb1b057f496e8a5fcb2))
* add logout button in switch workspace page ([6ce2f7f](https://github.com/msgbyte/tianji/commit/6ce2f7fd4dbcd56b5a5f493108138e8c8447619b))
* add sortable group component ([ef30750](https://github.com/msgbyte/tianji/commit/ef307508026bf516d321da933c298d87efd0b902))
* refactor sortable group component and add edit body component ([946ecaf](https://github.com/msgbyte/tianji/commit/946ecaf9f946dfb2d85541170a7441ba6e782e5a))
### Others
* add body spaces ([12b8ba9](https://github.com/msgbyte/tianji/commit/12b8ba95b7720d384f8ae607cea0f4133d5f4fc4))
* add new editable text component which allow to change group title ([e323e10](https://github.com/msgbyte/tianji/commit/e323e104e03569b7131dc1aac1e15c548ebe8485))
* add sortable group component which using react-beautiful-dnd ([91ade2a](https://github.com/msgbyte/tianji/commit/91ade2ab555e43dc36ec32c7e9cb2856ffccd5ae))
* change edit style and logic, create new MonitorPicker component ([72a1e7b](https://github.com/msgbyte/tianji/commit/72a1e7b0249c69510af7650e19bf939ed88cf550))
* fix ci problem and remove unused code ([95b51ca](https://github.com/msgbyte/tianji/commit/95b51ca2e160bf835aafb770e0a11b8ad0fc5858))
* improve admin style in status page ([ed2141a](https://github.com/msgbyte/tianji/commit/ed2141af22a6103ea5be850eff57be7f24d9011b))
* improve some style in server status page ([427e9e3](https://github.com/msgbyte/tianji/commit/427e9e3eb7684a58a4c2cb597283c5a5319d9dd3))
* refactor server status edit form with react-hook-form ([6160d7b](https://github.com/msgbyte/tianji/commit/6160d7bcb9d3bf2a8b617b422e80fe7df8839967))
* remove sender name in notification ([f309000](https://github.com/msgbyte/tianji/commit/f309000a0c3a58a4e1df9c44803ea6a0299fe9ac))
* remove unused code and improve display view in status page ([f5151aa](https://github.com/msgbyte/tianji/commit/f5151aa2a4185714f1f988a2bad336b90f410b69))
* update translation ([ef3d344](https://github.com/msgbyte/tianji/commit/ef3d34423b71969f9a9afee0f64394e17a66143f))
* update translation ([8b86dcd](https://github.com/msgbyte/tianji/commit/8b86dcdceaf738cd27bbe6253825c9ef967c675f))
* update translation ([42f41cd](https://github.com/msgbyte/tianji/commit/42f41cdbcb2a4452fbdcf6013451d6008d6d97f1))
* upgrade @radix-ui/react-scroll-area version ([fc1e67e](https://github.com/msgbyte/tianji/commit/fc1e67e005fa7f4502807df1ab02c49b863bc4e4))
## [1.14.7](https://github.com/msgbyte/tianji/compare/v1.14.6...v1.14.7) (2024-09-10)
### Document
* add document and website entry in app ([f74289f](https://github.com/msgbyte/tianji/commit/f74289ff0539b4cf4141eafbc6fc6cec12529357))
### Others
* improve data table resizer width to make it more easy to use ([2e60945](https://github.com/msgbyte/tianji/commit/2e609452b55c6aa957fdff3dd96b6b617bda13ed))
* improve notification and feed channel filter logic ([e770e42](https://github.com/msgbyte/tianji/commit/e770e428936aa84c4ce0811c820ef4e43c2cdea3))
* update sentry feed content ([1895ac7](https://github.com/msgbyte/tianji/commit/1895ac772cec64c92c490a1a06d78df2eae05191))
## [1.14.6](https://github.com/msgbyte/tianji/compare/v1.14.5...v1.14.6) (2024-09-09)
### Features
* add unknown integration log ([d2afa54](https://github.com/msgbyte/tianji/commit/d2afa54301bcdd6a40fe5116b8191196c9b7bb33))
## [1.14.5](https://github.com/msgbyte/tianji/compare/v1.14.4...v1.14.5) (2024-09-07)
### Bug Fixes
* fix row header style issue ([cf4531c](https://github.com/msgbyte/tianji/commit/cf4531c5ddc1758a2d11776f058559622771b311))
## [1.14.4](https://github.com/msgbyte/tianji/compare/v1.14.3...v1.14.4) (2024-09-03)
### Document
* fix edit page url ([8ccace1](https://github.com/msgbyte/tianji/commit/8ccace127ba6aae012e5c164dbcaa42ee299196c))
* update manual install to include code update ([2cc098a](https://github.com/msgbyte/tianji/commit/2cc098a5f1f184fa8e627e3d8d65a7910d9967c6))
### Others
* fix ci problem which cause build failed ([c4211c2](https://github.com/msgbyte/tianji/commit/c4211c270ffd6b1a6355a871676f0abed0f1e24f))
## [1.14.3](https://github.com/msgbyte/tianji/compare/v1.14.2...v1.14.3) (2024-09-02)
### Features
* add feed event url support ([8534ab7](https://github.com/msgbyte/tianji/commit/8534ab7ba029e4c98a642f4c927902455e97d4a9))
* add sentry webhook integration ([546055e](https://github.com/msgbyte/tianji/commit/546055e5559cf460e7d0f1dcce835e905baafc1e))
### Bug Fixes
* fix health bar style problem in page ([01d774d](https://github.com/msgbyte/tianji/commit/01d774d3958abd5ee15631eb771e22d5771f405a))
## [1.14.2](https://github.com/msgbyte/tianji/compare/v1.14.1...v1.14.2) (2024-09-02)
### Features
* add archive feature ([3270164](https://github.com/msgbyte/tianji/commit/3270164710179a534692eabca77285dd28d887a7))
* add curl feed api guide ([5588aca](https://github.com/msgbyte/tianji/commit/5588aca522646d88b7c9ddb5bff98e23c1d3bc15))
* add feed archive page ([87b4000](https://github.com/msgbyte/tianji/commit/87b4000c4791a935a0f9247d1c219529105d0801))
* add socket state ([e095a08](https://github.com/msgbyte/tianji/commit/e095a081b949880a088fd3e2512005d71d024769))
* feishu add markdown syntax support ([33de808](https://github.com/msgbyte/tianji/commit/33de808f3e4e6a763fc36de96f24e01055907aba))
### Document
* update Chinese translation ([9fcc6dd](https://github.com/msgbyte/tianji/commit/9fcc6dda60a6496fb1909cdc5facd0ebb60e9448))
### Others
* improve feed event report style ([88f47db](https://github.com/msgbyte/tianji/commit/88f47db118968aa323b6ee0eac6b14e4fe9aa608))
* update translations ([9966c12](https://github.com/msgbyte/tianji/commit/9966c1277c6bc8df74d9c3ca742b9e98c7577087))
## [1.14.1](https://github.com/msgbyte/tianji/compare/v1.14.0...v1.14.1) (2024-08-29)
### Others
* update pnpm version ([b9f5582](https://github.com/msgbyte/tianji/commit/b9f5582a02afffaff5777c85126e91e243ef82aa))
## [1.14.0](https://github.com/msgbyte/tianji/compare/v1.13.1...v1.14.0) (2024-08-29)
### Features
* add create workspace and switch workspace ([fac0838](https://github.com/msgbyte/tianji/commit/fac0838d8c7b14c7940170b733db0a33ca297b73))
* add delete workspace endpoint ([6fecde0](https://github.com/msgbyte/tianji/commit/6fecde0caa422c25ddbb9ec564afb44031b761da))
* add invite endpoint ([8c8b960](https://github.com/msgbyte/tianji/commit/8c8b960f61926aae415954dbef772e263d738ec5))
* add invite user form ([e0e0449](https://github.com/msgbyte/tianji/commit/e0e044945f02451ad2b1db8041e91a738589cd5d))
* add tick trpc endpoint ([7f33e2d](https://github.com/msgbyte/tianji/commit/7f33e2de0d0e0e1b4c2ff5d172f5d6c89e8dcd15))
* add unstar feed ([446ddaf](https://github.com/msgbyte/tianji/commit/446ddafa0afb534e02f767457973a1594a190f8f))
* add workspace page ([4918071](https://github.com/msgbyte/tianji/commit/491807165c7a4bd27b30961fdb3d525187d05ca3))
### Bug Fixes
* fix a style issue which workspace switch style broken with long name ([cbdb1c4](https://github.com/msgbyte/tianji/commit/cbdb1c4a079fcd19f03750dc3379f3e1aaaeb772))
* fix some case(maybe) can not key problem ([b64ca8b](https://github.com/msgbyte/tianji/commit/b64ca8b300f2bcbbbcbdf95b4e0d9780c1f64b1b))
* fix virtualize table loading and column style problem ([bb84661](https://github.com/msgbyte/tianji/commit/bb846616127e8ef823441b945390327a87b2f689))
### Document
* add private-policy page ([d136460](https://github.com/msgbyte/tianji/commit/d136460e39a69510e66952e76d42bca4016337a4))
* update openapi files ([79a7a92](https://github.com/msgbyte/tianji/commit/79a7a923d247a2581cca5b0e912bcbe35a892851))
* update website feed feature list ([ebd1e5e](https://github.com/msgbyte/tianji/commit/ebd1e5eb6648a424f078047125f31ce3a93bff03))
### Others
* add default error style problem ([3cc678f](https://github.com/msgbyte/tianji/commit/3cc678f09ec8d743f17b7b2f296475b2d37c8841))
* fix ci problem ([f7e1c81](https://github.com/msgbyte/tianji/commit/f7e1c8114b38c740f37e69125bd31fb26e1c2a1f))
* fix tsconfig problem in tsx ([40df49e](https://github.com/msgbyte/tianji/commit/40df49e1dbb5afda4a2d01b94e298e4f2dfaa2d5))
* improve healthbar display, will responsive with container size ([3990b0a](https://github.com/msgbyte/tianji/commit/3990b0a872d963900f52a97e869e7f26227c8107))
* update translation ([e983092](https://github.com/msgbyte/tianji/commit/e9830920378c71371946d526ef20335176cc8f18))
* upgrade @radix-ui/react-scroll-area to resolve scroll problem ([b862dd7](https://github.com/msgbyte/tianji/commit/b862dd74273faa78c65de916dce0a8fdafe9e834))
* upgrade package manager ([fa328fb](https://github.com/msgbyte/tianji/commit/fa328fb0bfe9ef47cce1d44ae16aee2628921e30))
* workspace switcher style and submit form reset ([5f47831](https://github.com/msgbyte/tianji/commit/5f47831f8e3f8df48fd287f6c4a23cb65270c21b))
## [1.13.1](https://github.com/msgbyte/tianji/compare/v1.13.0...v1.13.1) (2024-08-16)
### Features
* add feed template string of survey ([22fc5f9](https://github.com/msgbyte/tianji/commit/22fc5f98f8b646d6505a7a518074f5ce3f40215f))
* add FeedChannelPicker component ([5f6147e](https://github.com/msgbyte/tianji/commit/5f6147e3b6329c6fbeb7f8b1ac981f38fbe3e97a))
* add survey result send to feed channel feature ([d986210](https://github.com/msgbyte/tianji/commit/d9862105edd0f528dcf91d29142eaca9d78a8001))
### Document
* remove unmaintained readme ([5447f53](https://github.com/msgbyte/tianji/commit/5447f53b303fd43f8121fe9cccf5efc0326b7ace))
### Others
* fix ci problem ([3e3dc4c](https://github.com/msgbyte/tianji/commit/3e3dc4c22d765d7eea1d38e9f85c913982c656b6))
* remove lodash ([49d0da3](https://github.com/msgbyte/tianji/commit/49d0da3a6d54a65db7e11b1e9bb2e45fee228bdc))
* upgrade i18next-toolkit version ([59840b5](https://github.com/msgbyte/tianji/commit/59840b5f7b1cde4e6173dc1fa3b1bf39d3f701a7))
## [1.13.0](https://github.com/msgbyte/tianji/compare/v1.12.1...v1.13.0) (2024-08-11)
### Features
* add authjs backend support ([06d6ecd](https://github.com/msgbyte/tianji/commit/06d6ecd2a3384056be017c5608df282968802196))
* add avatar and nickname display in user info scope ([03bc9b5](https://github.com/msgbyte/tianji/commit/03bc9b5125070d2675b422723404c96fd2ac95ad))
* add duplicate feature for monitor ([827cf07](https://github.com/msgbyte/tianji/commit/827cf07c2a70b3437a8381ab3ee16838a348fd91))
* add email restrict ([0a0a275](https://github.com/msgbyte/tianji/commit/0a0a27549ace51bf0b8c9ef135c50fd859980525))
* add feed channel into search command panel ([275f30f](https://github.com/msgbyte/tianji/commit/275f30f0487a02e96289d8df5ea107bdd591212e))
* add github auth integrate ([7f7c95b](https://github.com/msgbyte/tianji/commit/7f7c95b11c664a15732f18b28ea1d154f289fca9))
* add logout and socketio auth ([e9c64c5](https://github.com/msgbyte/tianji/commit/e9c64c57e7b8bd8912669aef93d3958bb754a057))
* add none in feed channel ([73dd8c2](https://github.com/msgbyte/tianji/commit/73dd8c25b7f782a882682039a3cba94526af9906))
* add prisma migrate ([37757f6](https://github.com/msgbyte/tianji/commit/37757f6563d6de71a59aa1b021e7e29e9235eb3b))
* add support for legacy traditional login methods ([3afac06](https://github.com/msgbyte/tianji/commit/3afac062c417bfeef0536ee49a459de96ac7ae72))
* add survey count and feed event count ([f149642](https://github.com/msgbyte/tianji/commit/f1496429d30e13af8e810ae1dbc7ba74707d621d))
* add virtualized data table resizer ([f1aaa70](https://github.com/msgbyte/tianji/commit/f1aaa7040e7953d85b956f398df65873d9104205))
* add VirtualizedInfiniteDataTable and refactor survey result list ([b2dccec](https://github.com/msgbyte/tianji/commit/b2dccec2834a486dc018ac7b1d267bb327d48422))
### Bug Fixes
* fix tencentCloudAlarmMetricSchema incorrect problem ([914046a](https://github.com/msgbyte/tianji/commit/914046aefacafc0500585d55f8a2119685777ac3))
### Document
* add custom example for match text ([e4eee42](https://github.com/msgbyte/tianji/commit/e4eee420ea013068bc3d5fc0d9de436a6f69d65f))
* update README preview images ([bb76c8e](https://github.com/msgbyte/tianji/commit/bb76c8e895d5cb2ba5c8e5889d1a8ae15a605b48))
### Others
* add error log for tencent alarm ([af47920](https://github.com/msgbyte/tianji/commit/af4792024f3adc0b152f101e066f84077c98ecf7))
* add more log for tencent cloud alarm ([ad18666](https://github.com/msgbyte/tianji/commit/ad186668515c40e4127108a689accacc5b782760))
* add more translations ([05c358b](https://github.com/msgbyte/tianji/commit/05c358b2e5b18d74596fac8bb7ef2a0caf06b527))
* change all import with .js suffix, which will help nodejs(esm) to import code clear. ([d5d0446](https://github.com/msgbyte/tianji/commit/d5d04468cb210d0e2313ab66d494e09a8337a9d0))
* fix ci issue of typescript type check ([e5c2b94](https://github.com/msgbyte/tianji/commit/e5c2b9484fb761a2dff2f1e9f3dff3368f371712))
* fix ci problem ([c7ff366](https://github.com/msgbyte/tianji/commit/c7ff3666a7814936d8e5266df8e183fafbec6f96))
* fix react-router version ([20e95ef](https://github.com/msgbyte/tianji/commit/20e95ef97328fa1c0a3caab6a0ae20d54480eea0))
* remove ts-node and change to tsx ([b04ddd4](https://github.com/msgbyte/tianji/commit/b04ddd40ad483b773eeba0141b21ce80bfb4edbe))
* translate server side code into esm ([5dca262](https://github.com/msgbyte/tianji/commit/5dca262482adaadf6e25385bba0b1061ed9d33a4))
* update translation file ([7e38e32](https://github.com/msgbyte/tianji/commit/7e38e327bf3d8662e86a9c4320589f15d122ecd1))
* wip: add auth.js ([3cf3cfa](https://github.com/msgbyte/tianji/commit/3cf3cfa427b1d3ff6704e8839b60781eb7c15b32))
## [1.12.1](https://github.com/msgbyte/tianji/compare/v1.12.0...v1.12.1) (2024-07-25)
### Features
* add tencent cloud integration ([8585ea4](https://github.com/msgbyte/tianji/commit/8585ea4196e61934508f33e5120df8d854d6b18f))
### Bug Fixes
* fix code block not display well in light mode [#80](https://github.com/msgbyte/tianji/issues/80) ([0835fc5](https://github.com/msgbyte/tianji/commit/0835fc588bd0967d578abe34af9596fea2f29390))
### Document
* update pnpm version in manual install document ([9a7afed](https://github.com/msgbyte/tianji/commit/9a7afed08cb4aa80839fb83328a8ff300ac2141e))
### Others
* add fade in animation ([35a6e20](https://github.com/msgbyte/tianji/commit/35a6e20717d42ed4719b7dd441a89b079973d30e))
* change website config tabs to shadcn ui and improve ui ([f112adc](https://github.com/msgbyte/tianji/commit/f112adc696f7d2ded5d7619cf900e8156ea92d37))
## [1.12.0](https://github.com/msgbyte/tianji/compare/v1.11.4...v1.12.0) (2024-07-22)
### Features
* add channel feed notification ([67bfda3](https://github.com/msgbyte/tianji/commit/67bfda30bc95b5b8d11ff3994c6d097106e2c248))
* add colorized text for server status which help user find problem ([f2b20c5](https://github.com/msgbyte/tianji/commit/f2b20c5ef9a8d46aecb51b7c33720831c4e1ddf6))
* add custom feed integration ([765cc41](https://github.com/msgbyte/tianji/commit/765cc41c0637879c08f0cbb882d0decd03fc6e66))
* add date range and improve report display ([6e68a80](https://github.com/msgbyte/tianji/commit/6e68a8044dbda7a391800589ebb4cc2c828a8dbc))
* add dialog wrapper and improve display of webhook modal ([adb1cc3](https://github.com/msgbyte/tianji/commit/adb1cc391926b9fcea71f2db780e387b980b0b1d))
* add feed channel count ([a7688f0](https://github.com/msgbyte/tianji/commit/a7688f02af6a51d3786d4c91114a0e398c880fab))
* add feed endpoint ([f459c6b](https://github.com/msgbyte/tianji/commit/f459c6beeadaea6368e07375efa4845fe994305b))
* add feed event item created time ([926ea98](https://github.com/msgbyte/tianji/commit/926ea980ff130ba93c491fce9445b668f88175aa))
* add feed event notification with event and daily ([7bfd92b](https://github.com/msgbyte/tianji/commit/7bfd92be0b90cc5b9556a91f38627bdf015492d9))
* add feed page ([96a5a33](https://github.com/msgbyte/tianji/commit/96a5a33ad61c8b7bb4e4eae535acc659632aa914))
* add github integration support ([12fe9f0](https://github.com/msgbyte/tianji/commit/12fe9f0384ab08bc9013b22eb29140b14dae559f))
* add integration modal ([af5f6ad](https://github.com/msgbyte/tianji/commit/af5f6ad9f5853c344ceefe7a5e34730f364bfeae))
* add list content token ([7736bf8](https://github.com/msgbyte/tianji/commit/7736bf89dc94524d0cce6dc0b42a6ef430ecab2e))
* add more clear job ([b6bca6c](https://github.com/msgbyte/tianji/commit/b6bca6c250ded389b863e6e11adb77e5aa1b2911))
* add realtime feed event and desc feed list ([478d0c2](https://github.com/msgbyte/tianji/commit/478d0c2af3dd65d743a1ab36c0099a8449dc5224))
* add VirtualList support for feed events ([caf7e9c](https://github.com/msgbyte/tianji/commit/caf7e9ca72358772f3e47b593e7553b78578b226))
* add weekly and monthly cron job ([03904d2](https://github.com/msgbyte/tianji/commit/03904d26e08fbb755983568ed4dec7667f52982a))
* feed add markdown support ([56bbe09](https://github.com/msgbyte/tianji/commit/56bbe09005013276c0ac7324354cb63533ecd18e))
* github feed add star and issue support ([29939b6](https://github.com/msgbyte/tianji/commit/29939b6709e143ab9f68d008b79be38b3f13a6e7))
### Bug Fixes
* fix auditlog cannot fetch more data problem ([1b859e3](https://github.com/msgbyte/tianji/commit/1b859e31768b0f5b1a844989745e37e30d6ed478))
* fix problem of send notification ([82bb2ad](https://github.com/msgbyte/tianji/commit/82bb2ad267ae1f2922924fddb986b9f4e619eb1e))
### Document
* update category order ([b2480b0](https://github.com/msgbyte/tianji/commit/b2480b0ed57eccbc741de0652edbda8548b68db9))
* update roadmap ([e10cdfd](https://github.com/msgbyte/tianji/commit/e10cdfdf2612448938711b42fab2b8f160c3cab2))
* update webhooks document ([b355a67](https://github.com/msgbyte/tianji/commit/b355a677d3e36de4f53a73c59e23ab1aa0cb690e))
* update wechat qrcode ([503df45](https://github.com/msgbyte/tianji/commit/503df4546da7d11035f301db18650b4552d484f0))
### Others
* add document for endpoint ([537503f](https://github.com/msgbyte/tianji/commit/537503f288735de3c73f759291338ca74ce8d5d1))
* add dynamic virtual list ([01d81f3](https://github.com/msgbyte/tianji/commit/01d81f39296b80899c4fcaadda8dffa3f3f28803))
* add empty description message ([66ec94f](https://github.com/msgbyte/tianji/commit/66ec94fd08c24b901a0b8814cec5e246b6f6454f))
* add env openapi default value ([685d050](https://github.com/msgbyte/tianji/commit/685d05074b9a994081eade85cceb5f3b001c74df))
* add feed event url ([ac930cd](https://github.com/msgbyte/tianji/commit/ac930cd05e19e69e5ec9a7b059e56043685426ee))
* add preview text ([3d9b67a](https://github.com/msgbyte/tianji/commit/3d9b67a430536d04adaab035e29bb21d4f2b0051))
* add simple virtual list ([b7670da](https://github.com/msgbyte/tianji/commit/b7670da7db231d7f46f0cafc4ed30c2d46981c0a))
* change create feed event to local ([ab179e9](https://github.com/msgbyte/tianji/commit/ab179e9af6f2f17f2ac93be8e046a71b67f90eb1))
* change feed channel notifyFrequency type to enum ([2ce5597](https://github.com/msgbyte/tianji/commit/2ce5597dfe1c51cbec86c13b1b6a3fe5eecf2e53))
* change push message in github event ([1d4aecf](https://github.com/msgbyte/tianji/commit/1d4aecff9559e30d5274073f8c64002cce54aaef))
* fix ci problem ([865e56f](https://github.com/msgbyte/tianji/commit/865e56f40e7351d21c995f7a9c0fafbcc7b75993))
* fix ci problem ([4d15ccc](https://github.com/msgbyte/tianji/commit/4d15cccd1b8718f862a482564703eb905f1839c2))
* improve display in feed channel list ([15c6290](https://github.com/msgbyte/tianji/commit/15c6290587521abd6ce4a6325fc13456d1b10f42))
* improve feed event item display ([17f87c1](https://github.com/msgbyte/tianji/commit/17f87c191a9b1fcbcb39e73e1830ccb29b6e634c))
* improve logger and test case ([9796d42](https://github.com/msgbyte/tianji/commit/9796d428466f21adcfa42e04f04cbfe2d15aff3c))
* remove unused code ([2f6e92d](https://github.com/msgbyte/tianji/commit/2f6e92d166ac1635b87151147c12f13146136d38))
* remove unused size changer ([6ccd0ed](https://github.com/msgbyte/tianji/commit/6ccd0ede7b4c1bd8caad0697f66e61db8ea1feb9))
* skip event report if not have any events ([f814691](https://github.com/msgbyte/tianji/commit/f814691538578972bdeefe574e74a8dacb261a59))
* split integration route from feed route ([a4c31fe](https://github.com/msgbyte/tianji/commit/a4c31fe2da2b4213726579042ac93aa0547f0cc7))
* update feed guide ([c34b012](https://github.com/msgbyte/tianji/commit/c34b0124fac27fa2e48d2705cab8f07935d7fa02))
* update openapi base url and regenerate openapi document ([616a623](https://github.com/msgbyte/tianji/commit/616a623e40ea86e68595449f7a9d290630a5b116))
* update tag and content ([85a2a59](https://github.com/msgbyte/tianji/commit/85a2a598d76a5835e32772f04b6b2cf0e0d6dccf))
* update translation ([fc6ee73](https://github.com/msgbyte/tianji/commit/fc6ee733663231347a22aa7df83e4f4a454f4bd6))
* upgrade pnpm version in ci ([a2cb8b0](https://github.com/msgbyte/tianji/commit/a2cb8b0538d69d2209faa9574c509e49ec7d55ee))
* upgrade pnpm version in dockerfile ([1b89c3b](https://github.com/msgbyte/tianji/commit/1b89c3b5a808b57f1a695b86a8f35e4199e0ed7c))
* upgrade pnpm version to v9.5.0 ([63de6d7](https://github.com/msgbyte/tianji/commit/63de6d7aa514a5e1a2e206785a8426336cb323fa))
## [1.11.4](https://github.com/msgbyte/tianji/compare/v1.11.3...v1.11.4) (2024-06-21)
### Features
* add server install script usage guide ([4943d2d](https://github.com/msgbyte/tianji/commit/4943d2dd8e4495f3e03631d7f332b9a630e79b49))
* add webhook notification ([90df8e8](https://github.com/msgbyte/tianji/commit/90df8e8e36c618868110c3b9a0119b1b69184546))
* webhook add title and time ([a91d1ff](https://github.com/msgbyte/tianji/commit/a91d1ffffe41b61b9792d92865e8d8f65db27b0f))
### Document
* add document about server status page custom domain ([f06e788](https://github.com/msgbyte/tianji/commit/f06e788f454df877d7030e603aff5b882fcf7a82))
* add webhook document ([61c1b0e](https://github.com/msgbyte/tianji/commit/61c1b0e06504fca8fc35c41a7bb15130e7c08f24))
* update changelog ([ee16e6c](https://github.com/msgbyte/tianji/commit/ee16e6cd76c9138ce343b208f6c8d0026f0ee6c9))
* update wechat qrcode ([0d2c4f9](https://github.com/msgbyte/tianji/commit/0d2c4f97f96494a4874c4b3a30bd633202f35b2f))
### Others
* update release it ([3bfd11a](https://github.com/msgbyte/tianji/commit/3bfd11a7b6b2395cbe24653fea52c415c14bc1ca))
## [1.11.3](https://github.com/msgbyte/tianji/compare/tianji-0.1.17...1.11.3) (2024-06-15)
### Bug Fixes
* fix setting page not display correct problem ([fdce6b4](https://github.com/msgbyte/tianji/commit/fdce6b42f1e9817dba76072adaf732040bf3f8d3))
### Document
* [#68](https://github.com/msgbyte/tianji/issues/68) add document to how to install with helm ([95a8e99](https://github.com/msgbyte/tianji/commit/95a8e9968ba72f6e13db227c0b5695f6d12e388a))
* add improve monitor reporter usage roadmap [#75](https://github.com/msgbyte/tianji/issues/75) ([caab72d](https://github.com/msgbyte/tianji/commit/caab72dac58f2f6131d195f3bdbb29e41fa8bb0f))
* update changelog ([0deec1f](https://github.com/msgbyte/tianji/commit/0deec1fc55e30dcb1a71f835ba51b48b46310e3d))
### Others
* improve mobile display for tianji ([e9a1b61](https://github.com/msgbyte/tianji/commit/e9a1b61a7f3eec1050df9cf7e4ad3644f787091b))
* improve sidebar hide logic ([cae0c1d](https://github.com/msgbyte/tianji/commit/cae0c1d6c094a1662e1e390962ed10b8eabe73ea))
* update cr config ([f91110b](https://github.com/msgbyte/tianji/commit/f91110b313fb7f874813d2f76919476a4cf24631))
## [1.11.2](https://github.com/msgbyte/tianji/compare/v1.11.1...v1.11.2) (2024-06-07)
### Features
* add createdAt field in survey download csv ([618aedf](https://github.com/msgbyte/tianji/commit/618aedf1963559c07af696fb3483d4c073ba7c29))
* add document entry ([ad4b67c](https://github.com/msgbyte/tianji/commit/ad4b67ca459837cabcb1f274c7e10ec03bf128f5))
* add website view count in website list ([8ac5b11](https://github.com/msgbyte/tianji/commit/8ac5b11d4962de05cefe3d5be7c014f4f8bb7c9a))
### Document
* add install script uninstall document ([bffb9d6](https://github.com/msgbyte/tianji/commit/bffb9d6729adba7fd66468e9a618b68c68d09366))
* add prepare markdown ([98a8878](https://github.com/msgbyte/tianji/commit/98a887825f5df8385dc14c45e7e8ce2bc49c4b87))
* update changelog ([0c5c993](https://github.com/msgbyte/tianji/commit/0c5c993236ba51dfb0242af01459f4024e2038a6))
* update environment document ([52a8927](https://github.com/msgbyte/tianji/commit/52a89276c8ef707e5283161336296c3f040354d2))
* update manual document ([1dafea6](https://github.com/msgbyte/tianji/commit/1dafea61c78e01ed28e665ee9048afde433415a9))
* update manual install document [#56](https://github.com/msgbyte/tianji/issues/56) ([154b8b4](https://github.com/msgbyte/tianji/commit/154b8b4b6405c721342a681f366d3914536dc62a))
* update manual install faq ([4564347](https://github.com/msgbyte/tianji/commit/45643476985f65e730c4906a719a3931849cd9bb))
* update readme roadmap ([58445f9](https://github.com/msgbyte/tianji/commit/58445f9249eb8785003d381cc4527835edba485c))
* update wechat qrcode ([26da461](https://github.com/msgbyte/tianji/commit/26da4613683394bb628f9af444bf0f620d4b563a))
### Others
* increase timeout factor of interval ([4e8d761](https://github.com/msgbyte/tianji/commit/4e8d7613a40ba20e425caa2308846f663029ffe2))
* remove unused code ([328a4e8](https://github.com/msgbyte/tianji/commit/328a4e856cee0cf38c0beb1611ac2bc643bbd981))
* update env example ([80713e0](https://github.com/msgbyte/tianji/commit/80713e0fceac5ab07045532cd5759ff3a54522db))
* update sdk publish file module type ([ed0c2e9](https://github.com/msgbyte/tianji/commit/ed0c2e9d1da882acaa55854229336aa05476fd06))
* update translation ([d74ba8d](https://github.com/msgbyte/tianji/commit/d74ba8d283cb804cd7947ffc3804608ef24c41f7))
* upgrade prisma version to 5.14.0 ([a0ab1da](https://github.com/msgbyte/tianji/commit/a0ab1da6b60b3c608fb72b24f36b80e6ef954fe9))
## [1.11.1](https://github.com/msgbyte/tianji/compare/v1.11.0...v1.11.1) (2024-05-21)
### Bug Fixes
* fix display problem in docker panel ([e3d0555](https://github.com/msgbyte/tianji/commit/e3d0555c454cf7e49a9301a28f65cf863fc50573))
### Others
* add survey add state ([3ecd7aa](https://github.com/msgbyte/tianji/commit/3ecd7aa171f7be0b8c7dfdeff7d140294d8819bc))
## [1.11.0](https://github.com/msgbyte/tianji/compare/v1.10.0...v1.11.0) (2024-05-20)
### Features
* add reporter send docker info ([1dfa24d](https://github.com/msgbyte/tianji/commit/1dfa24df1b52544bee134cc6dcc94f744026bc03))
* add server docker expend view ([c6433f3](https://github.com/msgbyte/tianji/commit/c6433f310b821b4e8b3cb55df1e9ccadda7d97f4))
### Document
* new homepage ([a20396a](https://github.com/msgbyte/tianji/commit/a20396ad97cec9a54461411e8e79af9fc6571c6b))
* update website style ([8e96c06](https://github.com/msgbyte/tianji/commit/8e96c06d94b71c74235769eb5ff691c951cc2064))
* uprade docs website to v3.3.2 ([eacf7fc](https://github.com/msgbyte/tianji/commit/eacf7fc56f2f23b301470eca51747d52fe1d78e4))
### Others
* add loading state for common list ([00d40c8](https://github.com/msgbyte/tianji/commit/00d40c8410c0c7c94438518344cd7c946cc64879))
* change datatable expend icon and add transition ([74bd9ef](https://github.com/msgbyte/tianji/commit/74bd9ef3d96c1f2940c0717b61466edc7d0b44ca))
* move dependency place ([dec6a8b](https://github.com/msgbyte/tianji/commit/dec6a8b7c59deac561e68abed46334e7a072f8c5))
* update survey icon ([0ea7515](https://github.com/msgbyte/tianji/commit/0ea7515ad21e8d4e3fd798bf3e1341f0caf56821))
* upgrade tianji-client-sdk version ([9a0a1ea](https://github.com/msgbyte/tianji/commit/9a0a1eacb693dd816ee68db2e217f8f6e48528c6))
* upgrade trpc version to 10.45.2 ([7c94caf](https://github.com/msgbyte/tianji/commit/7c94caf0ed777f9558bbdc84c26eba32d60105a1))
## [1.10.0](https://github.com/msgbyte/tianji/compare/v1.9.4...v1.10.0) (2024-05-15)
### Features
* [#62](https://github.com/msgbyte/tianji/issues/62) add title section in website ([d5895dc](https://github.com/msgbyte/tianji/commit/d5895dc4a9d9d161fd17ebeb7d55b0304101b3aa))
* add survey backend endpoint ([2764262](https://github.com/msgbyte/tianji/commit/27642625ac9612ceeb1329d0aa3db8004af87844))
* add survey command panel ([c9bf016](https://github.com/msgbyte/tianji/commit/c9bf016fbf0c7216aa944118efda6b6f0acde350))
* add survey delete action ([12cd54e](https://github.com/msgbyte/tianji/commit/12cd54eafe63446471b0366d18cdaaea4ac95532))
* add survey download feature ([eebf00f](https://github.com/msgbyte/tianji/commit/eebf00f882cd8906aff2316936da9b2496018a53))
* add survey usage button ([2b75a8e](https://github.com/msgbyte/tianji/commit/2b75a8edad4285c524846438c79acfae7b013923))
* add tianji event for pricing page ([6606b25](https://github.com/msgbyte/tianji/commit/6606b253d84da09e1d821c00bd97bd7b2abf7452))
* add tianji-client-react package and useTianjiSurvey hooks which can easy to get survey info ([0fc112f](https://github.com/msgbyte/tianji/commit/0fc112fc329f0364914044f6f2727765307023ef))
* add website pricing page ([6674c19](https://github.com/msgbyte/tianji/commit/6674c19e87d3a628a2a305f9d5db48e365189b0c))
* survey basic fe framework and add new form ([010fd00](https://github.com/msgbyte/tianji/commit/010fd00be348cb5de0207cf1aea58fb43117130a))
* survey detail and edit ([a596011](https://github.com/msgbyte/tianji/commit/a596011960db230a8241e4f59bac5d33597cdc9a))
* survey sdk and openapi client ([f7f191a](https://github.com/msgbyte/tianji/commit/f7f191a53da03334b8b08844c7c51b19727828d1))
### Document
* Add one-click deploy on sealos ([#64](https://github.com/msgbyte/tianji/issues/64)) ([fc79c57](https://github.com/msgbyte/tianji/commit/fc79c5758817d95b1058e4c0093c88b262973d52))
* clear email input when submit success in price page ([3d16d8e](https://github.com/msgbyte/tianji/commit/3d16d8edd8c89fd3d553ad461a5ae383862b80fb))
* reduce homepage image size to improve user network ([6865a7c](https://github.com/msgbyte/tianji/commit/6865a7ca0bae30f017837a5e54a171be8ce6d85a))
* update custom script document ([7d370b4](https://github.com/msgbyte/tianji/commit/7d370b4fc51a52ee91b2d94afcc9d2367fcc5fbb))
* update qrcode ([ebb6c51](https://github.com/msgbyte/tianji/commit/ebb6c51f81d75b2736213403772f5418e778224e))
### Others
* add tooltip and time display on table ([9d3c034](https://github.com/msgbyte/tianji/commit/9d3c0344eee7b4309fad2e7ab827090136541d51))
* change weight of commit list search ([cc0bd73](https://github.com/msgbyte/tianji/commit/cc0bd73ed1c48c1fed0da95fcfc2890d1cd73012))
* change word: Countries -> Country or Region ([1ad9aa9](https://github.com/msgbyte/tianji/commit/1ad9aa95f9886342a6771cd6501d448f7801916e))
* common list add loading state ([9780aff](https://github.com/msgbyte/tianji/commit/9780affebca31f709d4f8f20e6c8f2593f1b198a))
* define survey model ([9143cc4](https://github.com/msgbyte/tianji/commit/9143cc468cd15d83b5419521adc9834576488b56))
* improve mobile display ([b2fb183](https://github.com/msgbyte/tianji/commit/b2fb1832e1a2573c7e5ad8abc38a937e4e63d1a1))
* improve mobile layout navbar display if have much features ([fd9108e](https://github.com/msgbyte/tianji/commit/fd9108e77fd8c0f4db5840512f1f0e9a73bce4d3))
* remove unused code ([342c076](https://github.com/msgbyte/tianji/commit/342c076966dcd2ede5341067a069743c09b967d1))
* update jwt secret generator more safe for user ([6e8c280](https://github.com/msgbyte/tianji/commit/6e8c28026e890774ce3edb49febde1f606aa0aee))
## [1.9.4](https://github.com/msgbyte/tianji/compare/v1.9.3...v1.9.4) (2024-05-06) ## [1.9.4](https://github.com/msgbyte/tianji/compare/v1.9.3...v1.9.4) (2024-05-06)

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@8.3.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

@ -32,21 +32,33 @@ It's good to specialize in one thing, if we are experts in related abilities we
- [x] telemetry - [x] telemetry
- [x] openapi - [x] openapi
- [x] website - [x] website
- [ ] team collaboration - [x] team collaboration
- [ ] utm track - [ ] utm track
- [ ] waitlist - [x] waitlist
- [ ] survey - [x] survey
- [ ] lighthouse report - [ ] survey page
- [ ] hooks - [x] lighthouse report
- [ ] links - [x] hooks
- [x] helm install support
- [x] allow install from public
- [ ] improve monitor reporter usage
- [x] uninstall guide
- [ ] download from server
- [ ] custom params guide
## Preview ## Preview
![](./website/static/img/preview1.png) ![](./website/static/img/preview/1.png)
![](./website/static/img/preview2.png) ![](./website/static/img/preview/2.png)
![](./website/static/img/preview3.png) ![](./website/static/img/preview/3.png)
![](./website/static/img/preview/4.png)
![](./website/static/img/preview/5.png)
![](./website/static/img/preview/6.png)
## Translation ## Translation

View File

@ -1,36 +0,0 @@
# 天机 Tianji
<img src="./website/static/img/logo.svg" width="128" />
**All-in-One 的数据洞察中心**
`网站分析器` + `状态监控器` + `服务状态上报` = `Tianji`
所有一切都在一起!
## Motivation
在我们对网站进行观察时。我们往往需要多个应用一起来组合使用。比如我们需要ga/umami等分析工具来查看pvuv以及各个页面的访问量我们需要uptime监控器来检查服务器的网络质量与连通性我们需要通关prometheus获取服务端上报的状态来检查服务器的质量。另外如果开发的是一个允许被开源部署的应用我们往往还需要一个遥测系统来帮助我们对其他人的部署情况做一个最简单的信息收集。
我认为这些工具应当是为同一个目的而服务的,那么有没有一款应用能够轻量级的将这些常见的需求整合为一体呢?毕竟在大部分时候我们并不需要非常专业与深入的功能。但是我为了实现全面的监控却需要安装如此多的服务。
专精于一项这很好如果我们是相关能力的专家我们需要这样的专业工具。但是对于大部分只有轻量级需求的用户而言一个all in one的应用会更加方便与易于使用
## Preview
![](./website/static/img/preview1.png)
![](./website/static/img/preview2.png)
![](./website/static/img/preview3.png)
## Open Source
`Tianji` is open source with `Apache 2.0` license.
And its inspired by `umami` license which under `MIT` and `uptime-kuma` which under `MIT` license too
### One-Click Deployment
[![Deploy on Sealos](https://cdn.jsdelivr.net/gh/labring-actions/templates@main/Deploy-on-Sealos.svg)](https://bja.sealos.run/?openapp=system-template%3FtemplateName%3Dtianji)

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:

1
k8s/helm/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
charts

23
k8s/helm/.helmignore Normal file
View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

6
k8s/helm/Chart.lock Normal file
View File

@ -0,0 +1,6 @@
dependencies:
- name: postgresql
repository: https://charts.bitnami.com/bitnami
version: 15.4.2
digest: sha256:57308af2821726255fe0c52374260c2ea98f9091c03d82cf82b9c6d499e6f214
generated: "2024-06-09T14:04:36.881437+08:00"

42
k8s/helm/Chart.yaml Normal file
View File

@ -0,0 +1,42 @@
apiVersion: v2
name: tianji
description: All-in-One Insight Hub
icon: https://tianji.msgbyte.com/img/logo.svg
maintainers:
- name: moonrailgun
email: moonrailgun@gmail.com
keywords:
- tianji
- uptime
- umami
- server status
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.17
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.11.2"
sources:
- https://github.com/msgbyte/tianji
dependencies:
- name: postgresql
version: 15.4.2
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled

View File

@ -0,0 +1,26 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "tianji.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "tianji.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "tianji.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "tianji.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
2. Tianji's default account:
Username: admin
Password: admin

View File

@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "tianji.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "tianji.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "tianji.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "tianji.labels" -}}
helm.sh/chart: {{ include "tianji.chart" . }}
{{ include "tianji.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "tianji.selectorLabels" -}}
app.kubernetes.io/name: {{ include "tianji.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "tianji.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "tianji.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "tianji.fullname" . }}
labels:
{{- include "tianji.labels" . | nindent 4 }}
data:
DATABASE_URL: postgresql://{{ .Values.postgresql.auth.username }}:{{ .Values.postgresql.auth.password }}@{{ .Release.Name }}-postgresql:5432/{{ .Values.postgresql.auth.database }}
{{/*
Rather than maintain a comprehensive ConfigMap, we map all sub-keys of the "env" value here.
This allows for more flexibility and less Chart churn as Drone evolves.
*/}}
{{- range $envKey, $envVal := .Values.env }}
{{ $envKey | upper }}: {{ $envVal | quote }}
{{- end }}

View File

@ -0,0 +1,71 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "tianji.fullname" . }}
labels:
{{- include "tianji.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "tianji.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "tianji.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "tianji.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "tianji.fullname" . }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,32 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "tianji.fullname" . }}
labels:
{{- include "tianji.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "tianji.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,61 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "tianji.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "tianji.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "tianji.fullname" . }}
labels:
{{- include "tianji.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "tianji.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "tianji.serviceAccountName" . }}
labels:
{{- include "tianji.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- end }}

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "tianji.fullname" . }}-test-connection"
labels:
{{- include "tianji.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "tianji.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

121
k8s/helm/values.yaml Normal file
View File

@ -0,0 +1,121 @@
# Default values for tianji.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: moonrailgun/tianji
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Automatically mount a ServiceAccount's API credentials?
automount: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podLabels: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
postgresql:
enabled: true
auth:
username: "tianji"
password: "tianji"
database: "tianji"
postgresPassword: "tianji"
service:
type: ClusterIP
port: 12345
env:
# DATABASE_URL: postgresql://tianji:tianji@postgresql:5432/tianji
JWT_SECRET: replace-me-with-a-random-string
ALLOW_REGISTER: "false"
ALLOW_OPENAPI: "true"
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
nodeSelector: {}
tolerations: []
affinity: {}

View File

@ -1,7 +1,8 @@
{ {
"name": "tianji", "name": "tianji",
"private": true, "private": true,
"version": "1.9.4", "version": "1.16.5",
"type": "module",
"scripts": { "scripts": {
"dev": "concurrently --kill-others npm:dev:server npm:dev:web", "dev": "concurrently --kill-others npm:dev:server npm:dev:web",
"dev:web": "cd src/client && pnpm dev", "dev:web": "cd src/client && pnpm dev",
@ -15,9 +16,9 @@
"build:app": "pnpm build:server && pnpm build:client", "build:app": "pnpm build:server && pnpm build:client",
"build:client": "cd src/client && pnpm build", "build:client": "cd src/client && pnpm build",
"build:server": "cd src/server && pnpm build", "build:server": "cd src/server && pnpm build",
"build:tracker": "ts-node scripts/build-tracker.ts", "build:tracker": "tsx scripts/build-tracker.ts",
"build:geo": "ts-node scripts/build-geo.ts", "build:geo": "tsx scripts/build-geo.ts",
"build:openapi": "ts-node --project ./tsconfig.base.json ./scripts/build-openapi-schema.ts && cd packages/client-sdk && pnpm generate:client", "build:openapi": "tsx --tsconfig ./tsconfig.base.json ./scripts/build-openapi-schema.ts && cd packages/client-sdk && pnpm generate:client",
"check:type": "pnpm -r check:type", "check:type": "pnpm -r check:type",
"release": "release-it", "release": "release-it",
"release:patch": "release-it -i patch", "release:patch": "release-it -i patch",
@ -30,12 +31,14 @@
"@types/tar": "^6.1.10", "@types/tar": "^6.1.10",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"prettier-plugin-tailwindcss": "^0.5.12", "prettier-plugin-tailwindcss": "^0.5.12",
"release-it": "^17.0.1", "release-it": "^17.0.1",
"tar": "^6.1.15", "tar": "^6.1.15",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.2.2", "tsx": "^4.16.2",
"typescript": "^5.3.3",
"vite": "^5.0.12" "vite": "^5.0.12"
}, },
"dependencies": { "dependencies": {
@ -43,9 +46,17 @@
"eventemitter-strict": "^1.0.1", "eventemitter-strict": "^1.0.1",
"zod": "^3.22.2" "zod": "^3.22.2"
}, },
"packageManager": "pnpm@9.7.1",
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"dayjs": "1.11.10" "@auth/core": "0.34.1",
"dayjs": "1.11.10",
"esbuild": "0.24.0",
"postman-code-generators": "1.8.0",
"typescript": "5.5.4"
},
"patchedDependencies": {
"zod-prisma@0.5.4": "patches/zod-prisma@0.5.4.patch"
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "tianji-client-sdk", "name": "tianji-client-sdk",
"version": "1.0.0", "version": "1.1.1",
"description": "", "description": "",
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {
@ -9,6 +9,10 @@
"generate:client": "openapi-ts -i ../../website/openapi.json -o src/open/client", "generate:client": "openapi-ts -i ../../website/openapi.json -o src/open/client",
"test": "vitest" "test": "vitest"
}, },
"files": [
"lib",
"src"
],
"keywords": [ "keywords": [
"tianji" "tianji"
], ],

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

@ -40,14 +40,14 @@ export type OpenAPIConfig = {
}; };
export const OpenAPI: OpenAPIConfig = { export const OpenAPI: OpenAPIConfig = {
BASE: '/open', BASE: 'http://localhost:12345/open',
CREDENTIALS: 'include', CREDENTIALS: 'include',
ENCODE_PATH: undefined, ENCODE_PATH: undefined,
HEADERS: undefined, HEADERS: undefined,
PASSWORD: undefined, PASSWORD: undefined,
TOKEN: undefined, TOKEN: undefined,
USERNAME: undefined, USERNAME: undefined,
VERSION: '1.9.3', VERSION: '1.16.1',
WITH_CREDENTIALS: false, WITH_CREDENTIALS: false,
interceptors: { interceptors: {
request: new Interceptors(), request: new Interceptors(),

View File

@ -69,16 +69,152 @@ export class UserService {
} }
export class WorkspaceService { export class WorkspaceService {
/**
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static workspaceCreate(data: $OpenApiTs['/workspace//create']['post']['req']): CancelablePromise<$OpenApiTs['/workspace//create']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/workspace//create',
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static workspaceSwitch(data: $OpenApiTs['/workspace//switch']['post']['req']): CancelablePromise<$OpenApiTs['/workspace//switch']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/workspace//switch',
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static workspaceRename(data: $OpenApiTs['/workspace//rename']['patch']['req']): CancelablePromise<$OpenApiTs['/workspace//rename']['patch']['res'][200]> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/workspace//rename',
body: data.requestBody,
mediaType: 'application/json'
});
}
/** /**
* @param data The data for the request. * @param data 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 workspaceGetServiceCount(data: $OpenApiTs['/workspace/{workspaceId}/getServiceCount']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/getServiceCount']['get']['res'][200]> { public static workspaceDelete(data: $OpenApiTs['/workspace//{workspaceId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/del']['delete']['res'][200]> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/workspace//{workspaceId}/del',
path: {
workspaceId: data.workspaceId
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @returns unknown Successful response
* @throws ApiError
*/
public static workspaceMembers(data: $OpenApiTs['/workspace//{workspaceId}/members']['get']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/members']['get']['res'][200]> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'GET', method: 'GET',
url: '/workspace/{workspaceId}/getServiceCount', url: '/workspace//{workspaceId}/members',
path: {
workspaceId: data.workspaceId
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static workspaceUpdateSettings(data: $OpenApiTs['/workspace//{workspaceId}/updateSettings']['post']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/updateSettings']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/workspace//{workspaceId}/updateSettings',
path: {
workspaceId: data.workspaceId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static workspaceInvite(data: $OpenApiTs['/workspace//{workspaceId}/invite']['post']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/invite']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/workspace//{workspaceId}/invite',
path: {
workspaceId: data.workspaceId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* Administrator kicks a user out of a workspace.
* @param data The data for the request.
* @param data.workspaceId
* @param data.targetUserId
* @returns unknown Successful response
* @throws ApiError
*/
public static workspaceTick(data: $OpenApiTs['/workspace//{workspaceId}/tick']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/tick']['delete']['res'][200]> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/workspace//{workspaceId}/tick',
path: {
workspaceId: data.workspaceId
},
query: {
targetUserId: data.targetUserId
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @returns unknown Successful response
* @throws ApiError
*/
public static workspaceGetServiceCount(data: $OpenApiTs['/workspace//{workspaceId}/getServiceCount']['get']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/getServiceCount']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace//{workspaceId}/getServiceCount',
path: { path: {
workspaceId: data.workspaceId workspaceId: data.workspaceId
} }
@ -123,6 +259,23 @@ export class WebsiteService {
}); });
} }
/**
* @param data The data for the request.
* @param data.workspaceId
* @returns number Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static websiteAllOverview(data: $OpenApiTs['/workspace/{workspaceId}/website/allOverview']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/allOverview']['get']['res'][200] | $OpenApiTs['/workspace/{workspaceId}/website/allOverview']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace/{workspaceId}/website/allOverview',
path: {
workspaceId: data.workspaceId
}
});
}
/** /**
* @param data The data for the request. * @param data The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -324,6 +477,24 @@ export class WebsiteService {
}); });
} }
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.websiteId
* @returns unknown Successful response
* @throws ApiError
*/
public static websiteDelete(data: $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/{websiteId}']['delete']['res'][200]> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/workspace/{workspaceId}/website/{websiteId}',
path: {
workspaceId: data.workspaceId,
websiteId: data.websiteId
}
});
}
/** /**
* @param data The data for the request. * @param data The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -345,6 +516,68 @@ export class WebsiteService {
}); });
} }
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.websiteId
* @param data.requestBody
* @returns string Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static websiteGenerateLighthouseReport(data: $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport']['post']['res'][200] | $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport',
path: {
workspaceId: data.workspaceId,
websiteId: data.websiteId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.websiteId
* @param data.limit
* @param data.cursor
* @returns unknown Successful response
* @throws ApiError
*/
public static websiteGetLighthouseReport(data: $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/getLighthouseReport']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/getLighthouseReport']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace/{workspaceId}/website/{websiteId}/getLighthouseReport',
path: {
workspaceId: data.workspaceId,
websiteId: data.websiteId
},
query: {
limit: data.limit,
cursor: data.cursor
}
});
}
/**
* @param data The data for the request.
* @param data.lighthouseId
* @returns unknown Successful response
* @throws ApiError
*/
public static websiteGetLighthouseJson(data: $OpenApiTs['/lighthouse/{lighthouseId}']['get']['req']): CancelablePromise<$OpenApiTs['/lighthouse/{lighthouseId}']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/lighthouse/{lighthouseId}',
path: {
lighthouseId: data.lighthouseId
}
});
}
} }
export class MonitorService { export class MonitorService {
@ -371,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
@ -434,6 +649,24 @@ export class MonitorService {
}); });
} }
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.monitorId
* @returns unknown Successful response
* @throws ApiError
*/
public static monitorDelete(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/del']['delete']['res'][200]> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/workspace/{workspaceId}/monitor/{monitorId}/del',
path: {
workspaceId: data.workspaceId,
monitorId: data.monitorId
}
});
}
/** /**
* @param data The data for the request. * @param data The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -501,6 +734,42 @@ export class MonitorService {
}); });
} }
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.monitorId
* @returns unknown Successful response
* @throws ApiError
*/
public static monitorPublicSummary(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicSummary']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicSummary']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace/{workspaceId}/monitor/{monitorId}/publicSummary',
path: {
workspaceId: data.workspaceId,
monitorId: data.monitorId
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.monitorId
* @returns unknown Successful response
* @throws ApiError
*/
public static monitorPublicData(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicData']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicData']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace/{workspaceId}/monitor/{monitorId}/publicData',
path: {
workspaceId: data.workspaceId,
monitorId: data.monitorId
}
});
}
/** /**
* @param data The data for the request. * @param data The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -944,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
@ -996,10 +1265,11 @@ export class SurveyService {
* @param data.workspaceId * @param data.workspaceId
* @param data.surveyId * @param data.surveyId
* @param data.requestBody * @param data.requestBody
* @returns unknown Successful response * @returns string Successful response
* @returns unknown Error response
* @throws ApiError * @throws ApiError
*/ */
public static surveySubmit(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/submit']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/submit']['post']['res'][200]> { public static surveySubmit(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/submit']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/submit']['post']['res'][200] | $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/submit']['post']['res'][200]> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: 'POST',
url: '/workspace/{workspaceId}/survey/{surveyId}/submit', url: '/workspace/{workspaceId}/survey/{surveyId}/submit',
@ -1076,6 +1346,8 @@ export class SurveyService {
* @param data.surveyId * @param data.surveyId
* @param data.limit * @param data.limit
* @param data.cursor * @param data.cursor
* @param data.startAt
* @param data.endAt
* @returns unknown Successful response * @returns unknown Successful response
* @throws ApiError * @throws ApiError
*/ */
@ -1089,7 +1361,9 @@ export class SurveyService {
}, },
query: { query: {
limit: data.limit, limit: data.limit,
cursor: data.cursor cursor: data.cursor,
startAt: data.startAt,
endAt: data.endAt
} }
}); });
} }
@ -1142,4 +1416,279 @@ export class BillingService {
}); });
} }
}
export class FeedService {
/**
* @param data The data for the request.
* @param data.workspaceId
* @returns unknown Successful response
* @throws ApiError
*/
public static feedChannels(data: $OpenApiTs['/workspace/{workspaceId}/feed/channels']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/channels']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace/{workspaceId}/feed/channels',
path: {
workspaceId: data.workspaceId
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.channelId
* @returns unknown Successful response
* @throws ApiError
*/
public static feedChannelInfo(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/info']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/info']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace/{workspaceId}/feed/{channelId}/info',
path: {
workspaceId: data.workspaceId,
channelId: data.channelId
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.channelId
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static feedUpdateChannelInfo(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/update']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/update']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/workspace/{workspaceId}/feed/{channelId}/update',
path: {
workspaceId: data.workspaceId,
channelId: data.channelId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* Fetch workspace feed channel events
* @param data The data for the request.
* @param data.workspaceId
* @param data.channelId
* @param data.limit
* @param data.cursor
* @param data.archived
* @returns unknown Successful response
* @throws ApiError
*/
public static feedFetchEventsByCursor(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/fetchEventsByCursor']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/fetchEventsByCursor']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace/{workspaceId}/feed/{channelId}/fetchEventsByCursor',
path: {
workspaceId: data.workspaceId,
channelId: data.channelId
},
query: {
limit: data.limit,
cursor: data.cursor,
archived: data.archived
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static feedCreateChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/createChannel']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/createChannel']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/workspace/{workspaceId}/feed/createChannel',
path: {
workspaceId: data.workspaceId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.channelId
* @returns unknown Successful response
* @throws ApiError
*/
public static feedDeleteChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/del']['delete']['res'][200]> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/workspace/{workspaceId}/feed/{channelId}/del',
path: {
workspaceId: data.workspaceId,
channelId: data.channelId
}
});
}
/**
* @param data The data for the request.
* @param data.channelId
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static feedSendEvent(data: $OpenApiTs['/feed/{channelId}/send']['post']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/send']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/feed/{channelId}/send',
path: {
channelId: data.channelId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.channelId
* @param data.eventId
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static feedArchiveEvent(data: $OpenApiTs['/feed/{channelId}/{eventId}/archive']['patch']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/{eventId}/archive']['patch']['res'][200]> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/feed/{channelId}/{eventId}/archive',
path: {
channelId: data.channelId,
eventId: data.eventId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.channelId
* @param data.eventId
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static feedUnarchiveEvent(data: $OpenApiTs['/feed/{channelId}/{eventId}/unarchive']['patch']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/{eventId}/unarchive']['patch']['res'][200]> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/feed/{channelId}/{eventId}/unarchive',
path: {
channelId: data.channelId,
eventId: data.eventId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.channelId
* @param data.requestBody
* @returns number Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static feedClearAllArchivedEvents(data: $OpenApiTs['/feed/{channelId}/clearAllArchivedEvents']['patch']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/clearAllArchivedEvents']['patch']['res'][200] | $OpenApiTs['/feed/{channelId}/clearAllArchivedEvents']['patch']['res'][200]> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/feed/{channelId}/clearAllArchivedEvents',
path: {
channelId: data.channelId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* webhook playground
* @param data The data for the request.
* @param data.workspaceId
* @returns string Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static feedIntegrationPlayground(data: $OpenApiTs['/feed/playground/{workspaceId}']['post']['req']): CancelablePromise<$OpenApiTs['/feed/playground/{workspaceId}']['post']['res'][200] | $OpenApiTs['/feed/playground/{workspaceId}']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/feed/playground/{workspaceId}',
path: {
workspaceId: data.workspaceId
}
});
}
/**
* integrate with github webhook
* @param data The data for the request.
* @param data.channelId
* @returns string Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static feedIntegrationGithub(data: $OpenApiTs['/feed/{channelId}/github']['post']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/github']['post']['res'][200] | $OpenApiTs['/feed/{channelId}/github']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/feed/{channelId}/github',
path: {
channelId: data.channelId
}
});
}
/**
* integrate with tencent-cloud webhook
* @param data The data for the request.
* @param data.channelId
* @returns string Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static feedIntegrationTencentCloudAlarm(data: $OpenApiTs['/feed/{channelId}/tencent-cloud/alarm']['post']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/tencent-cloud/alarm']['post']['res'][200] | $OpenApiTs['/feed/{channelId}/tencent-cloud/alarm']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/feed/{channelId}/tencent-cloud/alarm',
path: {
channelId: data.channelId
}
});
}
/**
* integrate with sentry webhook
* @param data The data for the request.
* @param data.channelId
* @returns string Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static feedIntegrationSentry(data: $OpenApiTs['/feed/{channelId}/sentry']['post']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/sentry']['post']['res'][200] | $OpenApiTs['/feed/{channelId}/sentry']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/feed/{channelId}/sentry',
path: {
channelId: data.channelId
}
});
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"module": "ESNext", "module": "CommonJS",
"outDir": "./lib", "outDir": "./lib",
"baseUrl": ".", "baseUrl": ".",
"declaration": true, "declaration": true,

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 one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,42 @@ module tianji-reporter
go 1.21.1 go 1.21.1
require ( require (
github.com/docker/docker v26.1.2+incompatible
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/shirou/gopsutil/v3 v3.20.10 github.com/shirou/gopsutil/v3 v3.20.10
github.com/stretchr/testify v1.9.0
) )
require ( require (
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.4 // indirect github.com/go-ole/go-ole v1.2.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/stretchr/testify v1.8.2 // indirect github.com/morikuni/aec v1.0.0 // indirect
golang.org/x/sys v0.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 // indirect
go.opentelemetry.io/otel/metric v1.26.0 // indirect
go.opentelemetry.io/otel/sdk v1.26.0 // indirect
go.opentelemetry.io/otel/trace v1.26.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/time v0.5.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect
) )

View File

@ -1,34 +1,142 @@
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v26.1.2+incompatible h1:UVX5ZOrrfTGZZYEP+ZDq3Xn9PdHNXaSYMFPDumMqG2k=
github.com/docker/docker v26.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shirou/gopsutil/v3 v3.20.10 h1:7zomV9HJv6UGk225YtvEa5+camNLpbua3MAz/GqiVJY= github.com/shirou/gopsutil/v3 v3.20.10 h1:7zomV9HJv6UGk225YtvEa5+camNLpbua3MAz/GqiVJY=
github.com/shirou/gopsutil/v3 v3.20.10/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4= github.com/shirou/gopsutil/v3 v3.20.10/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8=
go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0=
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

View File

@ -59,29 +59,32 @@ 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)
log.Println("Version:", version) log.Println("Version:", version)
for { for {
log.Println("Send report data to:", parsedURL.String()) log.Println("Sending report data to:", parsedURL.String())
payload := ReportData{ payload := ReportData{
WorkspaceId: *WorkspaceId, WorkspaceId: *WorkspaceId,
Name: name, Name: name,
Hostname: hostname, Hostname: hostname,
Timeout: interval * 2, Timeout: interval * 5,
Payload: utils.GetReportDataPaylod(interval, *IsVnstat), Payload: utils.GetReportDataPaylod(interval, *IsVnstat),
} }
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

@ -1,7 +1,12 @@
package utils package utils
import ( import (
"bytes"
"context"
"fmt" "fmt"
dockerTypes "github.com/docker/docker/api/types"
dockerContainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk" "github.com/shirou/gopsutil/v3/disk"
@ -21,19 +26,40 @@ import (
) )
type ReportDataPayload struct { type ReportDataPayload struct {
Uptime uint64 `json:"uptime"` Uptime uint64 `json:"uptime"`
Load jsoniter.Number `json:"load"` Load jsoniter.Number `json:"load"`
MemoryTotal uint64 `json:"memory_total"` MemoryTotal uint64 `json:"memory_total"`
MemoryUsed uint64 `json:"memory_used"` MemoryUsed uint64 `json:"memory_used"`
SwapTotal uint64 `json:"swap_total"` SwapTotal uint64 `json:"swap_total"`
SwapUsed uint64 `json:"swap_used"` SwapUsed uint64 `json:"swap_used"`
HddTotal uint64 `json:"hdd_total"` HddTotal uint64 `json:"hdd_total"`
HddUsed uint64 `json:"hdd_used"` HddUsed uint64 `json:"hdd_used"`
CPU jsoniter.Number `json:"cpu"` CPU jsoniter.Number `json:"cpu"`
NetworkTx uint64 `json:"network_tx"` NetworkTx uint64 `json:"network_tx"`
NetworkRx uint64 `json:"network_rx"` NetworkRx uint64 `json:"network_rx"`
NetworkIn uint64 `json:"network_in"` NetworkIn uint64 `json:"network_in"`
NetworkOut uint64 `json:"network_out"` NetworkOut uint64 `json:"network_out"`
Docker []DockerDataPayload `json:"docker,omitempty"`
}
type DockerDataPayload struct {
ID string `json:"id"`
Image string `json:"image"`
ImageID string `json:"imageId"`
Ports []dockerTypes.Port `json:"ports"`
CreatedAt int64 `json:"createdAt"`
State string `json:"state"`
Status string `json:"status"`
CpuPercent float64 `json:"cpuPercent"`
Memory float64 `json:"memory"`
MemLimit uint64 `json:"memLimit"`
MemPercent float64 `json:"memPercent"`
StorageWriteSize uint64 `json:"storageWriteSize"`
StorageReadSize uint64 `json:"storageReadSize"`
NetworkRx float64 `json:"networkRx"`
NetworkTx float64 `json:"networkTx"`
IORead uint64 `json:"ioRead"`
IOWrite uint64 `json:"ioWrite"`
} }
var checkIP int var checkIP int
@ -53,6 +79,9 @@ func GetReportDataPaylod(interval int, isVnstat bool) ReportDataPayload {
} }
} }
var dockerStat []DockerDataPayload
dockerStat, _ = GetDockerStat()
memoryTotal, memoryUsed, swapTotal, swapUsed := getMemory() memoryTotal, memoryUsed, swapTotal, swapUsed := getMemory()
hddTotal, hddUsed := getDisk(interval) hddTotal, hddUsed := getDisk(interval)
payload.CPU = jsoniter.Number(fmt.Sprintf("%.1f", getCpu(interval))) payload.CPU = jsoniter.Number(fmt.Sprintf("%.1f", getCpu(interval)))
@ -68,6 +97,7 @@ func GetReportDataPaylod(interval int, isVnstat bool) ReportDataPayload {
payload.NetworkTx = netTx payload.NetworkTx = netTx
payload.NetworkIn = netIn payload.NetworkIn = netIn
payload.NetworkOut = netOut payload.NetworkOut = netOut
payload.Docker = dockerStat
return payload return payload
} }
@ -219,3 +249,143 @@ func checkValidFs(name string) bool {
func BytesToString(b []byte) string { func BytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b)) return *(*string)(unsafe.Pointer(&b))
} }
func GetDockerStat() ([]DockerDataPayload, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, err
}
defer cli.Close()
ctx := context.Background()
containers, err := cli.ContainerList(context.Background(), dockerContainer.ListOptions{All: true})
if err != nil {
return nil, err
}
var dockerPayloads []DockerDataPayload
for _, container := range containers {
containerStats, err := cli.ContainerStats(ctx, container.ID, false)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
buf.ReadFrom(containerStats.Body)
newStr := buf.String()
v := dockerTypes.StatsJSON{}
jsoniter.Unmarshal([]byte(newStr), &v)
var cpuPercent float64
var blkRead, blkWrite uint64
var mem float64
var memPercent float64
if containerStats.OSType != "windows" {
if v.MemoryStats.Limit != 0 {
memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0
}
cpuPercent = calculateCPUPercentUnix(&v)
blkRead, blkWrite = calculateBlockIO(v.BlkioStats)
mem = float64(v.MemoryStats.Usage)
} else {
cpuPercent = calculateCPUPercentWindows(&v)
blkRead = v.StorageStats.ReadSizeBytes
blkWrite = v.StorageStats.WriteSizeBytes
mem = float64(v.MemoryStats.PrivateWorkingSet)
}
netRx, netTx := calculateNetwork(v.Networks)
dockerPayloads = append(dockerPayloads, DockerDataPayload{
ID: container.ID[:10],
Image: container.Image,
ImageID: container.ImageID,
Ports: container.Ports,
CreatedAt: container.Created,
State: container.State,
Status: container.Status,
CpuPercent: cpuPercent,
Memory: mem,
MemLimit: v.MemoryStats.Limit,
MemPercent: memPercent,
StorageWriteSize: v.StorageStats.WriteSizeBytes,
StorageReadSize: v.StorageStats.ReadSizeBytes,
NetworkRx: netRx,
NetworkTx: netTx,
IORead: blkRead,
IOWrite: blkWrite,
})
}
return dockerPayloads, nil
}
/**
* Reference: https://github.com/moby/moby/blob/eb131c5383db8cac633919f82abad86c99bffbe5/cli/command/container/stats_helpers.go#L175
*/
func calculateCPUPercentUnix(v *dockerTypes.StatsJSON) float64 {
previousCPU := v.PreCPUStats.CPUUsage.TotalUsage
previousSystem := v.PreCPUStats.SystemUsage
cpuPercent := 0.0
// calculate the change for the cpu usage of the container in between readings
cpuDelta := float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU)
// calculate the change for the entire system between readings
systemDelta := float64(v.CPUStats.SystemUsage) - float64(previousSystem)
if systemDelta > 0.0 && cpuDelta > 0.0 {
cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0
}
return cpuPercent
}
/**
* Reference: https://github.com/moby/moby/blob/eb131c5383db8cac633919f82abad86c99bffbe5/cli/command/container/stats_helpers.go#L190
*/
func calculateCPUPercentWindows(v *dockerTypes.StatsJSON) float64 {
// Max number of 100ns intervals between the previous time read and now
possIntervals := uint64(v.Read.Sub(v.PreRead).Nanoseconds()) // Start with number of ns intervals
possIntervals /= 100 // Convert to number of 100ns intervals
possIntervals *= uint64(v.NumProcs) // Multiple by the number of processors
// Intervals used
intervalsUsed := v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage
// Percentage avoiding divide-by-zero
if possIntervals > 0 {
return float64(intervalsUsed) / float64(possIntervals) * 100.0
}
return 0.00
}
/**
* Reference: https://github.com/moby/moby/blob/eb131c5383db8cac633919f82abad86c99bffbe5/cli/command/container/stats_helpers.go#L206
*/
func calculateBlockIO(blkio dockerTypes.BlkioStats) (blkRead uint64, blkWrite uint64) {
for _, bioEntry := range blkio.IoServiceBytesRecursive {
switch strings.ToLower(bioEntry.Op) {
case "read":
blkRead = blkRead + bioEntry.Value
case "write":
blkWrite = blkWrite + bioEntry.Value
}
}
return
}
/**
* Reference: https://github.com/moby/moby/blob/eb131c5383db8cac633919f82abad86c99bffbe5/cli/command/container/stats_helpers.go#L218
*/
func calculateNetwork(network map[string]dockerTypes.NetworkStats) (float64, float64) {
var rx, tx float64
for _, v := range network {
rx += float64(v.RxBytes)
tx += float64(v.TxBytes)
}
return rx, tx
}

View File

@ -0,0 +1,20 @@
package utils
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
)
func TestGetPayload(t *testing.T) {
payload := GetReportDataPaylod(5, false)
fmt.Println("{}", payload)
}
func TestGetDockerStat(t *testing.T) {
dockerPayloads, err := GetDockerStat()
assert.NoError(t, err, "Should can get docker stat")
fmt.Println("{}", dockerPayloads)
}

View File

@ -1,4 +1,4 @@
require('dotenv').config(); import 'dotenv/config';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import https from 'https'; import https from 'https';
@ -20,7 +20,7 @@ if (process.env.MAXMIND_LICENSE_KEY) {
`?edition_id=${db}&license_key=${process.env.MAXMIND_LICENSE_KEY}&suffix=tar.gz`; `?edition_id=${db}&license_key=${process.env.MAXMIND_LICENSE_KEY}&suffix=tar.gz`;
} }
const dest = path.resolve(__dirname, '../geo'); const dest = path.resolve(process.cwd(), './geo');
if (!fs.existsSync(dest)) { if (!fs.existsSync(dest)) {
fs.mkdirSync(dest); fs.mkdirSync(dest);

View File

@ -1,6 +1,10 @@
import { trpcOpenapiDocument } from '../src/server/trpc'; import { trpcOpenapiDocument } from '../src/server/trpc';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const target = path.resolve(__dirname, '../website/openapi.json'); const target = path.resolve(__dirname, '../website/openapi.json');
fs.writeJSON(target, trpcOpenapiDocument) fs.writeJSON(target, trpcOpenapiDocument)

View File

@ -1,5 +1,5 @@
import { resolve } from 'path'; import { resolve } from 'path';
import vite from 'vite'; import * as vite from 'vite';
console.log('Start Build Tracker'); console.log('Start Build Tracker');
@ -7,13 +7,13 @@ vite
.build({ .build({
build: { build: {
lib: { lib: {
entry: resolve(__dirname, '../src/tracker/index.js'), entry: resolve(process.cwd(), './src/tracker/index.js'),
name: 'tianji', name: 'tianji',
fileName: () => 'tracker.js', fileName: () => 'tracker.js',
formats: ['iife'], formats: ['iife'],
}, },
emptyOutDir: false, emptyOutDir: false,
outDir: resolve(__dirname, '../src/client/public'), outDir: resolve(process.cwd(), './src/client/public'),
}, },
}) })
.then((res) => { .then((res) => {

View File

@ -43,7 +43,7 @@ const AppRouter: React.FC = React.memo(() => {
<RouterProvider router={router} context={{ userInfo }} /> <RouterProvider router={router} context={{ userInfo }} />
</TooltipProvider> </TooltipProvider>
<Toaster /> <Toaster position="top-center" />
</BrowserRouter> </BrowserRouter>
); );
}); });

View File

@ -1,15 +1,44 @@
import axios from 'axios';
/**
* @deprecated
*/
const TOKEN_STORAGE_KEY = 'jsonwebtoken'; const TOKEN_STORAGE_KEY = 'jsonwebtoken';
/**
* @deprecated
*/
export function getJWT(): string | null { export function getJWT(): string | null {
const token = window.localStorage.getItem(TOKEN_STORAGE_KEY); const token = window.localStorage.getItem(TOKEN_STORAGE_KEY);
return token ?? null; return token ?? null;
} }
/**
* @deprecated
*/
export function setJWT(jwt: string) { export function setJWT(jwt: string) {
window.localStorage.setItem(TOKEN_STORAGE_KEY, jwt); window.localStorage.setItem(TOKEN_STORAGE_KEY, jwt);
} }
/**
* @deprecated
*/
export function clearJWT() { export function clearJWT() {
window.localStorage.removeItem(TOKEN_STORAGE_KEY); window.localStorage.removeItem(TOKEN_STORAGE_KEY);
} }
export async function getSession(): Promise<{
user: {
email?: string;
};
expires: string;
} | null> {
const { data } = await axios.get('/api/auth/session');
if (!data) {
return null;
}
return data;
}

View File

@ -0,0 +1,186 @@
/**
* This file is fork from next-auth/react
*/
import type {
ClientSafeProvider,
LiteralUnion,
SignInAuthorizationParams,
SignInOptions,
SignInResponse,
SignOutParams,
SignOutResponse,
} from './types';
import type {
BuiltInProviderType,
RedirectableProviderType,
} from '@auth/core/providers';
import { Session } from '@auth/core/types';
import axios from 'axios';
export * from './types';
type UpdateSession = (data?: any) => Promise<Session | null>;
export type SessionContextValue<R extends boolean = false> = R extends true
?
| { update: UpdateSession; data: Session; status: 'authenticated' }
| { update: UpdateSession; data: null; status: 'loading' }
:
| { update: UpdateSession; data: Session; status: 'authenticated' }
| {
update: UpdateSession;
data: null;
status: 'unauthenticated' | 'loading';
};
/**
* Returns the current Cross Site Request Forgery Token (CSRF Token)
* required to make POST requests (e.g. for signing in and signing out).
* You likely only need to use this if you are not using the built-in
* `signIn()` and `signOut()` methods.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getcsrftoken)
*/
export async function getCsrfToken() {
const { data } = await axios.get<{ csrfToken: string }>('/api/auth/csrf');
return data.csrfToken;
}
/**
* It calls `/api/auth/providers` and returns
* a list of the currently configured authentication providers.
* It can be useful if you are creating a dynamic custom sign in page.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getproviders)
*/
export async function getProviders() {
const { data } = await axios.get<
Record<LiteralUnion<BuiltInProviderType>, ClientSafeProvider>
>('/api/auth/providers');
return data;
}
/**
* Client-side method to initiate a signin flow
* or send the user to the signin page listing all possible providers.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signin)
*/
export async function signIn<
P extends RedirectableProviderType | undefined = undefined,
>(
provider?: LiteralUnion<
P extends RedirectableProviderType
? P | BuiltInProviderType
: BuiltInProviderType
>,
options?: SignInOptions,
authorizationParams?: SignInAuthorizationParams
): Promise<
P extends RedirectableProviderType ? SignInResponse | undefined : undefined
> {
const { callbackUrl = window.location.href, redirect = true } = options ?? {};
const baseUrl = '/api/auth';
const providers = await getProviders();
if (!providers) {
window.location.href = `${baseUrl}/error`;
return;
}
if (!provider || !(provider in providers)) {
window.location.href = `${baseUrl}/signin?${new URLSearchParams({
callbackUrl,
})}`;
return;
}
const isCredentials = providers[provider].type === 'credentials';
const isEmail = providers[provider].type === 'email';
const isSupportingReturn = isCredentials || isEmail;
const signInUrl = `${baseUrl}/${
isCredentials ? 'callback' : 'signin'
}/${provider}`;
const _signInUrl = `${signInUrl}${authorizationParams ? `?${new URLSearchParams(authorizationParams)}` : ''}`;
const res = await fetch(_signInUrl, {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Auth-Return-Redirect': '1',
},
// @ts-expect-error
body: new URLSearchParams({
...options,
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
});
const data = await res.json();
// TODO: Do not redirect for Credentials and Email providers by default in next major
if (redirect || !isSupportingReturn) {
const url = data.url ?? callbackUrl;
window.location.href = url;
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload();
return;
}
const error = new URL(data.url).searchParams.get('error');
return {
error,
status: res.status,
ok: res.ok,
url: error ? null : data.url,
} as any;
}
/**
* Signs the user out, by removing the session cookie.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signout)
*/
export async function signOut<R extends boolean = true>(
options?: SignOutParams<R>
): Promise<R extends true ? undefined : SignOutResponse> {
const { callbackUrl = window.location.href } = options ?? {};
const baseUrl = '/api/auth';
const fetchOptions = {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Auth-Return-Redirect': '1',
},
// @ts-expect-error
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
};
const res = await fetch(`${baseUrl}/signout`, fetchOptions);
const data = await res.json();
if (options?.redirect ?? true) {
const url = data.url ?? callbackUrl;
window.location.href = url;
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload();
// @ts-expect-error
return;
}
return data;
}

View File

@ -0,0 +1,55 @@
import type { Session } from '@auth/core/types';
import type { BuiltInProviderType, ProviderType } from '@auth/core/providers';
/**
* Util type that matches some strings literally, but allows any other string as well.
* @source https://github.com/microsoft/TypeScript/issues/29729#issuecomment-832522611
*/
export type LiteralUnion<T extends U, U = string> =
| T
| (U & Record<never, never>);
export interface ClientSafeProvider {
id: LiteralUnion<BuiltInProviderType>;
name: string;
type: ProviderType;
signinUrl: string;
callbackUrl: string;
}
export interface SignInOptions extends Record<string, unknown> {
/**
* Specify to which URL the user will be redirected after signing in. Defaults to the page URL the sign-in is initiated from.
*
* [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl)
*/
callbackUrl?: string;
/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option) */
redirect?: boolean;
}
export interface SignInResponse {
error: string | null;
status: number;
ok: boolean;
url: string | null;
}
/** Match `inputType` of `new URLSearchParams(inputType)` */
export type SignInAuthorizationParams =
| string
| string[][]
| Record<string, string>
| URLSearchParams;
/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1) */
export interface SignOutResponse {
url: string;
}
export interface SignOutParams<R extends boolean = true> {
/** [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl-1) */
callbackUrl?: string;
/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1 */
redirect?: R;
}

View File

@ -0,0 +1,84 @@
import { useEvent } from '@/hooks/useEvent';
import { signIn, SignInResponse, signOut } from './lib';
import { useUserStore } from '@/store/user';
import { toast } from 'sonner';
import { trpc } from '../trpc';
import { useTranslation } from '@i18next-toolkit/react';
import { BuiltInProviderType } from '@auth/core/providers';
export function useAuth() {
const trpcUtils = trpc.useUtils();
const { t } = useTranslation();
const loginWithPassword = useEvent(
async (username: string, password: string) => {
let res: SignInResponse | undefined;
try {
res = await signIn('account', {
username,
password,
redirect: false,
});
} catch (err) {
toast.error(t('Login failed'));
throw err;
}
if (res?.error) {
toast.error(t('Login failed, please check your username and password'));
throw new Error('Login failed');
}
const userInfo = await trpcUtils.user.info.fetch();
if (!userInfo) {
toast.error(t('Can not get current user info'));
throw new Error('Login failed, ');
}
return userInfo;
}
);
const loginWithOAuth = useEvent(
async (provider: BuiltInProviderType | 'custom') => {
let res: SignInResponse | undefined;
try {
res = await signIn(provider, {
redirect: false,
});
console.log('res', res);
} catch (err) {
toast.error(t('Login failed'));
throw err;
}
if (res?.error) {
toast.error(t('Login failed'));
throw new Error('Login failed');
}
const userInfo = await trpcUtils.user.info.fetch();
if (!userInfo) {
toast.error(t('Can not get current user info'));
throw new Error('Login failed, ');
}
return userInfo;
}
);
const logout = useEvent(async () => {
await signOut({
redirect: false,
});
useUserStore.setState({ info: null });
window.location.href = '/login'; // not good, need to invest to find better way.
});
return {
loginWithPassword,
loginWithOAuth,
logout,
};
}

View File

@ -1,7 +1,4 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useUserStore } from '../../store/user';
import { useEvent } from '../../hooks/useEvent';
import { clearJWT } from '../auth';
/** /**
* Mock * Mock
@ -10,14 +7,3 @@ import { clearJWT } from '../auth';
export function getUserTimezone(): string { export function getUserTimezone(): string {
return dayjs.tz.guess() ?? 'utc'; return dayjs.tz.guess() ?? 'utc';
} }
export function useLogout() {
const logout = useEvent(() => {
window.location.href = '/login'; // not good, need to invest to find better way.
useUserStore.setState({ info: null });
clearJWT();
});
return logout;
}

View File

@ -4,15 +4,6 @@ import { AppRouterOutput } from '../trpc';
export type WebsiteInfo = NonNullable<AppRouterOutput['website']['info']>; export type WebsiteInfo = NonNullable<AppRouterOutput['website']['info']>;
export async function deleteWorkspaceWebsite(
workspaceId: string,
websiteId: string
) {
await request.delete(`/api/workspace/${workspaceId}/website/${websiteId}`);
queryClient.resetQueries(['websites', workspaceId]);
}
export function refreshWorkspaceWebsites(workspaceId: string) { export function refreshWorkspaceWebsites(workspaceId: string) {
queryClient.refetchQueries(['websites', workspaceId]); queryClient.refetchQueries(['websites', workspaceId]);
} }

View File

@ -1,7 +1,6 @@
import { message } from 'antd'; import { message } from 'antd';
import axios from 'axios'; import axios from 'axios';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { getJWT } from './auth';
class RequestError extends Error {} class RequestError extends Error {}
@ -9,10 +8,6 @@ function createRequest() {
const ins = axios.create(); const ins = axios.create();
ins.interceptors.request.use(async (val) => { ins.interceptors.request.use(async (val) => {
if (!val.headers.Authorization) {
val.headers.Authorization = `Bearer ${getJWT()}`;
}
return val; return val;
}); });

View File

@ -1,5 +1,4 @@
import { io, Socket } from 'socket.io-client'; import { io, Socket } from 'socket.io-client';
import { getJWT } from './auth';
import type { SubscribeEventMap, SocketEventMap } from '../../server/ws/shared'; import type { SubscribeEventMap, SocketEventMap } from '../../server/ws/shared';
import { create } from 'zustand'; import { create } from 'zustand';
import { useEvent } from '../hooks/useEvent'; import { useEvent } from '../hooks/useEvent';
@ -8,21 +7,42 @@ import { useIsLogined } from '../store/user';
const useSocketStore = create<{ const useSocketStore = create<{
socket: Socket | null; socket: Socket | null;
connected: boolean;
}>(() => ({ }>(() => ({
socket: null, socket: null,
connected: false,
})); }));
export function createSocketIOClient(workspaceId: string) { export function createSocketIOClient(workspaceId: string) {
const token = getJWT(); const prev = useSocketStore.getState().socket;
if (prev) {
prev.disconnect();
}
const socket = io(`/${workspaceId}`, { const socket = io(`/${workspaceId}`, {
transports: ['websocket'], transports: ['websocket'],
reconnectionDelayMax: 10000, reconnectionDelayMax: 10000,
auth: {
token,
},
forceNew: true, forceNew: true,
}); });
socket.on('connect', () => {
useSocketStore.setState({
connected: true,
});
});
socket.on('disconnect', () => {
useSocketStore.setState({
connected: false,
});
});
socket.on('connect_error', () => {
useSocketStore.setState({
connected: false,
});
});
useSocketStore.setState({ useSocketStore.setState({
socket, socket,
}); });
@ -89,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 {
@ -111,6 +148,10 @@ export function useSocketSubscribe<T>(
return data; return data;
} }
export function useSocketConnected() {
return useSocketStore((state) => state.connected);
}
interface UseSocketSubscribeListOptions<K, T> { interface UseSocketSubscribeListOptions<K, T> {
filter?: (data: T) => boolean; filter?: (data: T) => boolean;
} }

View File

@ -8,7 +8,6 @@ import {
splitLink, splitLink,
TRPCClientErrorLike, TRPCClientErrorLike,
} from '@trpc/client'; } from '@trpc/client';
import { getJWT } from './auth';
import { message } from 'antd'; import { message } from 'antd';
import { isDev } from '../utils/env'; import { isDev } from '../utils/env';
@ -22,9 +21,7 @@ export type AppRouterOutput = inferRouterOutputs<AppRouter>;
const url = '/trpc'; const url = '/trpc';
function headers() { function headers() {
return { return {};
Authorization: `Bearer ${getJWT()}`,
};
} }
export const trpcClient = trpc.createClient({ export const trpcClient = trpc.createClient({

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -40,11 +40,13 @@ export const AlertConfirm: React.FC<AlertConfirmProps> = React.memo((props) => {
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel onClick={props.onCancel}> <AlertDialogCancel onClick={props.onCancel}>
{t('Cancel')} {props.onConfirm ? t('Cancel') : t('Continue')}
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction onClick={props.onConfirm}> {props.onConfirm && (
{t('Confirm')} <AlertDialogAction onClick={props.onConfirm}>
</AlertDialogAction> {t('Confirm')}
</AlertDialogAction>
)}
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>

View File

@ -2,7 +2,7 @@ import React from 'react';
export const Code: React.FC<React.PropsWithChildren> = React.memo((props) => { export const Code: React.FC<React.PropsWithChildren> = React.memo((props) => {
return ( return (
<span className="rounded-sm border border-zinc-800 bg-zinc-900 px-1 py-0.5"> <span className="rounded-sm border border-zinc-200 bg-zinc-100 px-1 py-0.5 dark:border-zinc-800 dark:bg-zinc-900">
{props.children} {props.children}
</span> </span>
); );

View File

@ -0,0 +1,43 @@
import { useEvent } from '@/hooks/useEvent';
import React, { useState } from 'react';
import { Button } from './ui/button';
import { LuCopy, LuCopyCheck } from 'react-icons/lu';
import { toast } from 'sonner';
import { useTranslation } from '@i18next-toolkit/react';
export const CodeBlock: React.FC<{
code: string;
height?: number;
}> = React.memo((props) => {
const [copied, setCopied] = useState(false);
const { t } = useTranslation();
const handleCopy = useEvent(() => {
window.navigator.clipboard.writeText(props.code);
toast(t('Copied into clipboard!'));
setCopied(true);
});
return (
<div className="group relative w-full overflow-auto">
<pre
className="rounded-sm border border-zinc-200 bg-zinc-100 p-3 pr-12 text-sm dark:border-zinc-800 dark:bg-zinc-900"
style={{
maxHeight: props.height || 384,
}}
>
<code>{props.code}</code>
</pre>
<Button
className="absolute right-1 top-1 bg-opacity-50 opacity-0 transition-opacity group-hover:opacity-100 dark:bg-opacity-50"
variant="outline"
size="icon"
Icon={copied ? LuCopyCheck : LuCopy}
onClick={handleCopy}
/>
</div>
);
});
CodeBlock.displayName = 'CodeBlock';

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

@ -10,16 +10,20 @@ import {
CommandSeparator, CommandSeparator,
} from '@/components/ui/command'; } from '@/components/ui/command';
import { import {
LuActivitySquare,
LuAreaChart, LuAreaChart,
LuBellDot, LuBellDot,
LuFilePieChart, LuFilePieChart,
LuKanbanSquare,
LuKeyRound,
LuMonitorDot, LuMonitorDot,
LuSearch, LuSearch,
LuServer, LuServer,
LuTableProperties,
LuUserCircle2, LuUserCircle2,
LuWifi, LuWifi,
} from 'react-icons/lu'; } from 'react-icons/lu';
import { RiSurveyLine } from 'react-icons/ri';
import { NavigateOptions, useNavigate } from '@tanstack/react-router'; import { NavigateOptions, useNavigate } from '@tanstack/react-router';
import { useEvent } from '@/hooks/useEvent'; import { useEvent } from '@/hooks/useEvent';
import { useCommandState } from 'cmdk'; import { useCommandState } from 'cmdk';
@ -139,9 +143,17 @@ export const CommandPanel: React.FC<CommandPanelProps> = React.memo((props) => {
to: '/survey', to: '/survey',
})} })}
> >
<LuTableProperties className="mr-2 h-4 w-4" /> <RiSurveyLine className="mr-2 h-4 w-4" />
{t('Survey')} {t('Survey')}
</CommandItem> </CommandItem>
<CommandItem
onSelect={handleJump({
to: '/feed',
})}
>
<LuActivitySquare className="mr-2 h-4 w-4" />
{t('Feed')}
</CommandItem>
</CommandGroup> </CommandGroup>
<CommandSeparator /> <CommandSeparator />
<CommandGroup heading={t('Settings')}> <CommandGroup heading={t('Settings')}>
@ -161,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>
@ -194,6 +222,9 @@ export const CommandPanelSearchGroup: React.FC<CommandPanelSearchGroupProps> =
const { data: surveys = [] } = trpc.survey.all.useQuery({ const { data: surveys = [] } = trpc.survey.all.useQuery({
workspaceId, workspaceId,
}); });
const { data: feedChannels = [] } = trpc.feed.channels.useQuery({
workspaceId,
});
if (!search) { if (!search) {
return null; return null;
@ -277,10 +308,26 @@ export const CommandPanelSearchGroup: React.FC<CommandPanelSearchGroupProps> =
}, },
})} })}
> >
<LuTableProperties className="mr-2 h-4 w-4" /> <RiSurveyLine className="mr-2 h-4 w-4" />
{s.name} {s.name}
</CommandItem> </CommandItem>
))} ))}
{feedChannels.map((channel) => (
<CommandItem
key={channel.id}
value={channel.id}
keywords={[channel.name, channel.id]}
onSelect={handleJump({
to: '/feed/$channelId',
params: {
channelId: channel.id,
},
})}
>
<LuActivitySquare className="mr-2 h-4 w-4" />
{channel.name}
</CommandItem>
))}
</CommandGroup> </CommandGroup>
); );
}); });

View File

@ -10,8 +10,10 @@ interface CommonHeaderProps {
export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => { export const CommonHeader: React.FC<CommonHeaderProps> = React.memo((props) => {
return ( return (
<div className="flex w-full items-center"> <div className="flex w-full items-center">
<div className="flex flex-1 items-center"> <div className="flex flex-1 flex-shrink items-center overflow-hidden">
<h1 className="text-xl font-bold">{props.title}</h1> <h1 className="overflow-hidden text-ellipsis whitespace-nowrap text-xl font-bold">
{props.title}
</h1>
{props.desc && ( {props.desc && (
<span className="text-muted-foreground ml-2 self-end text-sm"> <span className="text-muted-foreground ml-2 self-end text-sm">

View File

@ -7,6 +7,9 @@ import { LuSearch } from 'react-icons/lu';
import { Input } from './ui/input'; import { Input } from './ui/input';
import { useFuseSearch } from '@/hooks/useFuseSearch'; import { useFuseSearch } from '@/hooks/useFuseSearch';
import { Empty } from 'antd'; import { Empty } from 'antd';
import { globalEventBus } from '@/utils/event';
import { Spinner } from './ui/spinner';
import { formatNumber } from '@/utils/common';
export interface CommonListItem { export interface CommonListItem {
id: string; id: string;
@ -18,8 +21,10 @@ export interface CommonListItem {
} }
interface CommonListProps { interface CommonListProps {
isLoading?: boolean;
hasSearch?: boolean; hasSearch?: boolean;
items: CommonListItem[]; items: CommonListItem[];
emptyDescription?: React.ReactNode;
} }
export const CommonList: React.FC<CommonListProps> = React.memo((props) => { export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
const { location } = useRouterState(); const { location } = useRouterState();
@ -30,16 +35,16 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
{ {
keys: [ keys: [
{ {
name: 'id', name: 'title',
weight: 1, weight: 1,
}, },
{ {
name: 'title', name: 'id',
weight: 0.7, weight: 0.6,
}, },
{ {
name: 'tags', name: 'tags',
weight: 0.3, weight: 0.4,
}, },
], ],
} }
@ -67,7 +72,15 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">
<div className="flex flex-col gap-2 p-4"> <div className="flex flex-col gap-2 p-4">
{finalList.length === 0 && <Empty />} {props.isLoading && (
<div className="flex justify-center py-8">
<Spinner size={24} />
</div>
)}
{finalList.length === 0 && !props.isLoading && (
<Empty description={props.emptyDescription} />
)}
{finalList.map((item) => { {finalList.map((item) => {
const isSelected = item.href === location.pathname; const isSelected = item.href === location.pathname;
@ -76,20 +89,26 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
<button <button
key={item.id} key={item.id}
className={cn( className={cn(
'hover:bg-accent flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all', 'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all',
isSelected && 'bg-muted' 'hover:bg-gray-50 dark:hover:bg-gray-900',
isSelected && 'bg-gray-50 dark:bg-gray-900'
)} )}
onClick={() => onClick={() => {
globalEventBus.emit('commonListSelected');
navigate({ navigate({
to: item.href, to: item.href,
}) });
} }}
> >
<div className="flex w-full items-center justify-between gap-1"> <div className="flex w-full items-center justify-between gap-1">
<div className="font-semibold">{item.title}</div> <div className="overflow-hidden text-ellipsis font-semibold">
{item.title}
</div>
{item.number && item.number > 0 && ( {item.number && item.number > 0 && (
<span className="opacity-60">{item.number}</span> <span className="opacity-60" title={String(item.number)}>
{formatNumber(item.number)}
</span>
)} )}
</div> </div>

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

@ -4,6 +4,8 @@ import {
getCoreRowModel, getCoreRowModel,
useReactTable, useReactTable,
createColumnHelper, createColumnHelper,
getExpandedRowModel,
ExpandedState,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { import {
@ -15,6 +17,10 @@ import {
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { Empty } from 'antd'; import { Empty } from 'antd';
import React from 'react';
import { Button } from './ui/button';
import { LuChevronRight } from 'react-icons/lu';
import { cn } from '@/utils/style';
export type { ColumnDef }; export type { ColumnDef };
export { createColumnHelper }; export { createColumnHelper };
@ -22,24 +28,54 @@ export { createColumnHelper };
interface DataTableProps<TData> { interface DataTableProps<TData> {
columns: ColumnDef<TData, any>[]; columns: ColumnDef<TData, any>[];
data: TData[]; data: TData[];
ExpandComponent?: React.ComponentType<{ row: TData }>;
} }
export function DataTable<TData>({ columns, data }: DataTableProps<TData>) { export function DataTable<TData>({
columns,
data,
ExpandComponent,
}: DataTableProps<TData>) {
const [expanded, setExpanded] = React.useState<ExpandedState>({});
const canExpand = Boolean(ExpandComponent);
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns,
state: {
expanded,
},
onExpandedChange: setExpanded,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getRowCanExpand: () => canExpand,
}); });
const columnLen = canExpand ? columns.length + 1 : columns.length;
return ( return (
<div className="rounded-md border"> <div className="rounded-md border">
<Table> <Table>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{canExpand && (
<TableHead className="w-9">
<Button
variant="ghost"
size="icon"
className={cn(
'mr-1 h-5 w-5 transition-transform',
table.getIsAllRowsExpanded() && 'rotate-90'
)}
Icon={LuChevronRight}
onClick={table.getToggleAllRowsExpandedHandler()}
/>
</TableHead>
)}
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead key={header.id}> <TableHead key={header.id} className="text-nowrap">
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
@ -53,22 +89,56 @@ export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
))} ))}
</TableHeader> </TableHeader>
<TableBody className="overflow-auto"> <TableBody className="overflow-auto">
{table.getRowModel().rows?.length ? ( {table.getRowModel().rows?.length > 0 ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => {
<TableRow const renderedRow = (
key={row.id} <TableRow
data-state={row.getIsSelected() && 'selected'} key={row.id}
> data-state={row.getIsSelected() && 'selected'}
{row.getVisibleCells().map((cell) => ( >
<TableCell key={cell.id}> {row.getCanExpand() && (
{flexRender(cell.column.columnDef.cell, cell.getContext())} <TableCell className="w-9">
</TableCell> <Button
))} variant="ghost"
</TableRow> size="icon"
)) className={cn(
'mr-1 h-5 w-5 transition-transform',
row.getIsExpanded() && 'rotate-90'
)}
Icon={LuChevronRight}
onClick={row.getToggleExpandedHandler()}
/>
</TableCell>
)}
{row.getVisibleCells().map((cell, i) => (
<TableCell key={cell.id} className="text-nowrap">
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
);
return (
<>
{renderedRow}
{row.getIsExpanded() && ExpandComponent && (
<TableRow key={row.id + 'expand'}>
<TableCell colSpan={columnLen}>
<ExpandComponent row={row.original} />
</TableCell>
</TableRow>
)}
</>
);
})
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center"> <TableCell colSpan={columnLen} className="h-24 text-center">
<Empty /> <Empty />
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -78,3 +148,4 @@ export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
</div> </div>
); );
} }
DataTable.displayName = 'DataTable';

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

@ -9,7 +9,7 @@ export const DefaultError: React.FC = React.memo(() => {
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<Card className="min-w-[320px] bg-zinc-900"> <Card className="min-w-[320px] bg-zinc-50 dark:bg-zinc-900">
<CardHeader> <CardHeader>
<div className="text-center"> <div className="text-center">
<img className="m-auto h-24 w-24" src="/icon.svg" /> <img className="m-auto h-24 w-24" src="/icon.svg" />

View File

@ -13,7 +13,7 @@ export const DefaultNotFound: React.FC = React.memo(() => {
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<Card className="min-w-[320px] bg-zinc-900"> <Card className="min-w-[320px] bg-zinc-50 dark:bg-zinc-900">
<CardHeader> <CardHeader>
<div className="text-center"> <div className="text-center">
<img className="m-auto h-24 w-24" src="/icon.svg" /> <img className="m-auto h-24 w-24" src="/icon.svg" />

View File

@ -0,0 +1,34 @@
import React from 'react';
import { Badge } from './ui/badge';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { useTranslation } from '@i18next-toolkit/react';
interface DeprecatedBadgeProps {
tip?: string;
}
export const DeprecatedBadge: React.FC<DeprecatedBadgeProps> = React.memo(
(props) => {
const { t } = useTranslation();
const el = (
<Badge className="mx-1 px-1 py-0.5 text-xs" variant="secondary">
{t('Deprecated')}
</Badge>
);
if (!props.tip) {
return el;
} else {
return (
<Tooltip>
<TooltipTrigger>{el}</TooltipTrigger>
<TooltipContent>
<p>{props.tip}</p>
</TooltipContent>
</Tooltip>
);
}
}
);
DeprecatedBadge.displayName = 'DeprecatedBadge';

View File

@ -0,0 +1,11 @@
import { isDev } from '@/utils/env';
import React, { PropsWithChildren } from 'react';
export const DevContainer: React.FC<PropsWithChildren> = React.memo((props) => {
if (isDev) {
return <>{props.children}</>;
}
return null;
});
DevContainer.displayName = 'DevContainer';

View File

@ -0,0 +1,31 @@
import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from './ui/dialog';
interface DialogProps extends React.PropsWithChildren {
title?: string;
description?: string;
content?: React.ReactNode;
}
export const DialogWrapper: React.FC<DialogProps> = React.memo((props) => {
return (
<Dialog>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{props.title}</DialogTitle>
<DialogDescription>{props.description}</DialogDescription>
</DialogHeader>
{props.content}
</DialogContent>
</Dialog>
);
});
DialogWrapper.displayName = 'DialogWrapper';

View File

@ -0,0 +1,106 @@
import { useWatch } from '@/hooks/useWatch';
import { useTranslation } from '@i18next-toolkit/react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Empty } from 'antd';
import { last } from 'lodash-es';
import React, { useRef } from 'react';
interface VirtualListProps<T = any> {
allData: T[];
hasNextPage: boolean | undefined;
isFetchingNextPage: boolean;
onFetchNextPage: () => void;
estimateSize: number;
renderItem: (item: T) => React.ReactElement;
getItemKey?: (index: number) => string | number;
renderEmpty?: () => React.ReactElement;
}
export const DynamicVirtualList: React.FC<VirtualListProps> = React.memo(
(props) => {
const {
allData,
hasNextPage,
isFetchingNextPage,
onFetchNextPage,
estimateSize,
getItemKey,
renderItem,
renderEmpty,
} = props;
const parentRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const rowVirtualizer = useVirtualizer({
count: hasNextPage ? allData.length + 1 : allData.length,
getScrollElement: () => parentRef.current,
estimateSize: () => estimateSize,
overscan: 5,
getItemKey,
});
const virtualItems = rowVirtualizer.getVirtualItems();
useWatch([virtualItems], () => {
const lastItem = last(virtualItems);
if (!lastItem) {
return;
}
if (
lastItem.index >= allData.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
onFetchNextPage();
}
});
return (
<div
ref={parentRef}
className="h-full w-full overflow-y-auto"
style={{
contain: 'strict',
}}
>
<div
className="relative w-full"
style={{
height: rowVirtualizer.getTotalSize(),
}}
>
{virtualItems.length === 0 &&
(renderEmpty ? renderEmpty() : <Empty />)}
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,
}}
>
{virtualItems.map((virtualRow) => {
const isLoaderRow = virtualRow.index > allData.length - 1;
const data = allData[virtualRow.index];
return (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
>
{isLoaderRow
? hasNextPage
? t('Loading more...')
: t('Nothing more to load')
: renderItem(data)}
</div>
);
})}
</div>
</div>
</div>
);
}
);
DynamicVirtualList.displayName = 'DynamicVirtualList';

View File

@ -1,38 +1,46 @@
import { Input } from 'antd';
import clsx from 'clsx';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useWatch } from '../hooks/useWatch'; import { useWatch } from '../hooks/useWatch';
import { Input } from './ui/input';
import { useEvent } from '@/hooks/useEvent';
import { cn } from '@/utils/style';
interface EditableTextProps { interface EditableTextProps {
className?: string; className?: string;
enable?: boolean;
defaultValue: string; defaultValue: string;
onSave: (text: string) => void; onSave: (text: string) => void;
} }
export const EditableText: React.FC<EditableTextProps> = React.memo((props) => { export const EditableText: React.FC<EditableTextProps> = React.memo((props) => {
const [text, setText] = useState(props.defaultValue); const [text, setText] = useState(props.defaultValue);
const enable = props.enable ?? true; const [editing, setEditing] = useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
useWatch([props.defaultValue], () => { useWatch([props.defaultValue], () => {
setText(props.defaultValue); setText(props.defaultValue);
}); });
const handleClick = useEvent(() => {
setEditing(true);
});
return ( return (
<> <div className={cn('cursor-text', props.className)}>
{enable ? ( {editing ? (
<Input <Input
className={clsx( ref={inputRef}
props.className, autoFocus={true}
'rounded-none border-0 p-0 !shadow-none outline-0' type="text"
)} className="h-[1.5em] border-none p-0 text-base shadow-none focus-visible:ring-0"
value={text} value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
onBlur={(e) => props.onSave(e.target.value)} onBlur={() => {
setEditing(false);
props.onSave(text);
}}
/> />
) : ( ) : (
<span className={props.className}>{text}</span> <span onClick={handleClick}>{text}</span>
)} )}
</> </div>
); );
}); });
EditableText.displayName = 'EditableText'; EditableText.displayName = 'EditableText';

View File

@ -1,46 +1,60 @@
import { useResizeObserver } from '@/hooks/useResizeObserver';
import { getStatusBgColorClassName, HealthStatus } from '@/utils/health';
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;
} }
export interface HealthBarProps { export interface HealthBarProps {
className?: string;
size?: 'small' | 'large'; size?: 'small' | 'large';
beats: HealthBarBeat[]; beats: HealthBarBeat[];
} }
export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => { export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
const size = props.size ?? 'small'; const size = props.size ?? 'small';
const [containerRef, containerRect] = useResizeObserver();
const cellCount = props.beats.length;
const cellNeedWidth = size === 'small' ? 8 : 12; // include gap
return ( return (
<div <div
className={clsx('flex', { ref={containerRef}
'gap-[3px]': size === 'small', className={cn(
'gap-1': size === 'large', 'flex',
})} {
'gap-[3px] px-0.5 py-1.5': size === 'small',
'gap-1 px-0.5 py-2': size === 'large',
},
props.className
)}
> >
{props.beats.map((beat, i) => ( {props.beats
<div .slice(
key={i} Math.floor(
title={beat.title} Math.max(cellNeedWidth * cellCount - containerRect.width, 0) /
className={clsx( cellNeedWidth
'rounded-full transition-transform hover:scale-150', ),
{ cellCount
'h-4 w-[5px]': size === 'small', )
'h-8 w-2': size === 'large', .map((beat, i) => (
}, <div
{ key={i}
'bg-green-500': beat.status === 'health', title={beat.title}
'bg-red-600': beat.status === 'error', className={clsx(
'bg-yellow-400': beat.status === 'warning', 'rounded-full transition-transform hover:scale-150',
'bg-gray-400': beat.status === 'none', {
} 'h-4 w-[5px]': size === 'small',
)} 'h-8 w-2': size === 'large',
/> },
))} getStatusBgColorClassName(beat.status)
)}
/>
))}
</div> </div>
); );
}); });

View File

@ -1,8 +1,6 @@
import React from 'react'; import React from 'react';
import { Editor, EditorProps } from '@bytemd/react'; import { Editor, EditorProps } from '@bytemd/react';
import { plugins } from './plugins'; import { plugins } from './plugins';
import 'bytemd/dist/index.css';
import './style.less';
import { useLocale } from './useLocale'; import { useLocale } from './useLocale';
interface MarkdownEditorProps extends EditorProps {} interface MarkdownEditorProps extends EditorProps {}

View File

@ -1,4 +1,6 @@
import loadable from '@loadable/component'; import loadable from '@loadable/component';
import 'bytemd/dist/index.css';
import './style.less';
export const MarkdownEditor = loadable(() => export const MarkdownEditor = loadable(() =>
import('./editor').then((module) => module.MarkdownEditor) import('./editor').then((module) => module.MarkdownEditor)

View File

@ -1,6 +1,13 @@
import gfm from '@bytemd/plugin-gfm'; import gfm from '@bytemd/plugin-gfm';
import { BytemdPlugin } from 'bytemd';
import externalLinks from 'rehype-external-links';
export const plugins = [ export const plugins: BytemdPlugin[] = [
gfm(), gfm(),
// Add more plugins here {
rehype: (p) =>
p.use(externalLinks, {
target: '_blank',
}),
},
]; ];

View File

@ -7,12 +7,20 @@
z-index: 99; z-index: 99;
} }
.markdown {
.markdown-body {
a {
text-decoration: underline dotted;
}
}
}
.dark { .dark {
.bytemd { .bytemd {
@apply bg-zinc-900 border-zinc-800 text-foreground; @apply text-foreground border-zinc-800 bg-zinc-900;
.bytemd-toolbar { .bytemd-toolbar {
@apply bg-zinc-900 border-zinc-800; @apply border-zinc-800 bg-zinc-900;
.bytemd-toolbar-icon:hover { .bytemd-toolbar-icon:hover {
@apply bg-muted; @apply bg-muted;
@ -24,7 +32,7 @@
} }
.CodeMirror { .CodeMirror {
@apply bg-zinc-900 text-foreground; @apply text-foreground bg-zinc-900;
.CodeMirror-cursor { .CodeMirror-cursor {
@apply border-foreground; @apply border-foreground;

View File

@ -1,15 +1,17 @@
import React from 'react'; import React from 'react';
import { Viewer } from '@bytemd/react'; import { Viewer } from '@bytemd/react';
import { plugins } from './plugins'; import { plugins } from './plugins';
import 'bytemd/dist/index.css';
import './style.less';
interface MarkdownViewerProps { interface MarkdownViewerProps {
value: string; value: string;
} }
export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo( export const MarkdownViewer: React.FC<MarkdownViewerProps> = React.memo(
(props) => { (props) => {
return <Viewer plugins={plugins} value={props.value ?? ''} />; return (
<div className="markdown">
<Viewer plugins={plugins} value={props.value ?? ''} />
</div>
);
} }
); );
MarkdownViewer.displayName = 'MarkdownViewer'; MarkdownViewer.displayName = 'MarkdownViewer';

View File

@ -0,0 +1,92 @@
import { useWatch } from '@/hooks/useWatch';
import { useTranslation } from '@i18next-toolkit/react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Empty } from 'antd';
import { last } from 'lodash-es';
import React, { useRef } from 'react';
interface VirtualListProps<T = any> {
allData: T[];
hasNextPage: boolean | undefined;
isFetchingNextPage: boolean;
onFetchNextPage: () => void;
estimateSize: number;
renderItem: (item: T) => React.ReactElement;
renderEmpty?: () => React.ReactElement;
}
export const SimpleVirtualList: React.FC<VirtualListProps> = React.memo(
(props) => {
const {
allData,
hasNextPage,
isFetchingNextPage,
onFetchNextPage,
estimateSize,
renderItem,
renderEmpty,
} = props;
const parentRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const rowVirtualizer = useVirtualizer({
count: hasNextPage ? allData.length + 1 : allData.length,
getScrollElement: () => parentRef.current,
estimateSize: () => estimateSize,
overscan: 5,
});
const virtualItems = rowVirtualizer.getVirtualItems();
useWatch([virtualItems], () => {
const lastItem = last(virtualItems);
if (!lastItem) {
return;
}
if (
lastItem.index >= allData.length - 1 &&
hasNextPage &&
!isFetchingNextPage
) {
onFetchNextPage();
}
});
return (
<div ref={parentRef} className="h-full w-full overflow-auto">
{virtualItems.length === 0 && (renderEmpty ? renderEmpty() : <Empty />)}
<div
className="relative w-full"
style={{
height: rowVirtualizer.getTotalSize(),
}}
>
{virtualItems.map((virtualItem) => {
const isLoaderRow = virtualItem.index > allData.length - 1;
const data = allData[virtualItem.index];
return (
<div
key={virtualItem.index}
className="absolute left-0 top-0 w-full"
style={{
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{isLoaderRow
? hasNextPage
? t('Loading more...')
: t('Nothing more to load')
: renderItem(data)}
</div>
);
})}
</div>
</div>
);
}
);
SimpleVirtualList.displayName = 'SimpleVirtualList';

View File

@ -0,0 +1,94 @@
import React from 'react';
import { DragDropContext, DropResult } from 'react-beautiful-dnd';
import { useEvent } from '@/hooks/useEvent';
import { reorder } from '@/utils/reorder';
import { SortableItem } from './types';
interface SortableContextProps<T extends SortableItem> {
list: T[];
onChange: (list: T[]) => void;
children: React.ReactNode;
}
export const SortableContext = <T extends SortableItem>(
props: SortableContextProps<T>
) => {
const { list, onChange, children } = props;
const handleDragEnd = useEvent((result: DropResult) => {
// dropped outside the list
if (!result.destination) {
return;
}
if (result.type === 'root') {
const final = reorder(
list,
result.source.index,
result.destination.index
);
onChange(final);
return;
}
if (result.type === 'group') {
// move data from source to destination
// NOTICE: now only support 1 level
const final = [...list];
const sourceGroupIndex = final.findIndex(
(group) => group.key === result.source.droppableId
);
if (sourceGroupIndex === -1) {
return;
}
const destinationGroupIndex = final.findIndex(
(group) => group.key === result.destination?.droppableId
);
if (destinationGroupIndex === -1) {
return;
}
if (sourceGroupIndex === destinationGroupIndex) {
if (!('children' in final[sourceGroupIndex])) {
return;
}
// same group
final[sourceGroupIndex].children = reorder(
final[sourceGroupIndex].children!,
result.source.index,
result.destination.index
);
} else {
// cross group
if (
!('children' in final[sourceGroupIndex]) ||
!('children' in final[destinationGroupIndex])
) {
return;
}
const sourceGroupItems = Array.from(
final[sourceGroupIndex].children ?? []
);
const [removed] = sourceGroupItems.splice(result.source.index, 1);
const destinationGroupItems = Array.from(
final[destinationGroupIndex].children ?? []
);
destinationGroupItems.splice(result.destination.index, 0, removed);
final[sourceGroupIndex].children = sourceGroupItems;
final[destinationGroupIndex].children = destinationGroupItems;
}
onChange(final);
}
});
return (
<DragDropContext onDragEnd={handleDragEnd}>{children}</DragDropContext>
);
};
SortableContext.displayName = 'SortableGroup';

View File

@ -0,0 +1,111 @@
import React from 'react';
import { SortableContext } from './SortableContext';
import { StrictModeDroppable } from './StrictModeDroppable';
import { useEvent } from '@/hooks/useEvent';
import { Draggable } from 'react-beautiful-dnd';
import { SortableGroupItem, SortableItem, SortableLeafItem } from './types';
interface SortableGroupProps<GroupProps, ItemProps> {
list: SortableItem<GroupProps, ItemProps>[];
onChange: (list: SortableItem<GroupProps, ItemProps>[]) => void;
renderGroup: (
group: SortableGroupItem<GroupProps, ItemProps>,
children: React.ReactNode,
level: number
) => React.ReactNode;
renderItem: (
item: SortableLeafItem<ItemProps>,
index: number,
group: SortableGroupItem<GroupProps, ItemProps>
) => React.ReactNode;
}
export const SortableGroup = <GroupProps, ItemProps>(
props: SortableGroupProps<GroupProps, ItemProps>
) => {
const { list, onChange, renderGroup, renderItem } = props;
const renderItemEl = useEvent(
(
item: SortableLeafItem<ItemProps>,
index: number,
group: SortableGroupItem<GroupProps, ItemProps>
) => {
return (
<Draggable key={item.key} draggableId={item.key} index={index}>
{(dragProvided) => (
<div
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
>
{renderItem(item, index, group)}
</div>
)}
</Draggable>
);
}
);
const renderGroupEl = useEvent(
(group: SortableGroupItem<GroupProps, ItemProps>, level = 0) => {
return (
<StrictModeDroppable
droppableId={group.key}
type={level === 0 ? 'root' : 'group'}
key={group.key}
>
{(dropProvided) => (
<div ref={dropProvided.innerRef} {...dropProvided.droppableProps}>
{renderGroup(
group,
<>
{group.children.map((item, index) =>
!('children' in item) ? (
renderItemEl(item, index, group)
) : (
<Draggable
draggableId={item.key}
key={item.key}
index={index}
>
{(dragProvided) => (
<div
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
>
{renderGroupEl(item, level + 1)}
</div>
)}
</Draggable>
)
)}
</>,
level
)}
{dropProvided.placeholder}
</div>
)}
</StrictModeDroppable>
);
}
);
return (
<SortableContext<SortableItem<GroupProps, ItemProps>>
list={list}
onChange={onChange}
>
{renderGroupEl(
{
key: 'root',
children: list,
} as SortableGroupItem<GroupProps, ItemProps>,
0
)}
</SortableContext>
);
};
SortableGroup.displayName = 'SortableGroup';

View File

@ -0,0 +1,27 @@
import React, { useEffect, useState } from 'react';
import { Droppable, DroppableProps } from 'react-beautiful-dnd';
/**
* https://github.com/atlassian/react-beautiful-dnd/issues/2350
*/
export const StrictModeDroppable: React.FC<DroppableProps> = React.memo(
(props) => {
const [enabled, setEnabled] = useState(false);
useEffect(() => {
const animation = requestAnimationFrame(() => setEnabled(true));
return () => {
cancelAnimationFrame(animation);
setEnabled(false);
};
}, []);
if (!enabled) {
return null;
}
return <Droppable {...props}>{props.children}</Droppable>;
}
);
StrictModeDroppable.displayName = 'StrictModeDroppable';

View File

@ -0,0 +1,14 @@
import { Id } from 'react-beautiful-dnd';
export type SortableItem<GroupProps = unknown, ItemProps = unknown> =
| SortableLeafItem<ItemProps>
| SortableGroupItem<GroupProps, ItemProps>;
export type SortableGroupItem<GroupProps = unknown, ItemProps = unknown> = {
key: Id;
children: SortableItem<GroupProps, ItemProps>[];
} & GroupProps;
export type SortableLeafItem<ItemProps = unknown> = {
key: Id;
} & ItemProps;

View File

@ -1,46 +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 {
Area,
AreaChart,
CartesianGrid,
Customized,
XAxis,
YAxis,
} from 'recharts';
import {
ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from './ui/chart';
import { useStrokeDasharray } from '@/hooks/useStrokeDasharray';
const chartConfig = {
pv: {
label: 'PV',
},
uv: {
label: 'UV',
},
} satisfies ChartConfig;
export const TimeEventChart: React.FC<{ export const TimeEventChart: React.FC<{
data: { x: string; y: number; type: string }[]; labelMapping?: Record<string, 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 [calcStrokeDasharray, strokes] = useStrokeDasharray({});
const [strokeDasharray, setStrokeDasharray] = React.useState([...strokes]);
const handleAnimationEnd = () => setStrokeDasharray([...strokes]);
const getStrokeDasharray = (name: string) => {
const lineDasharray = strokeDasharray.find((s) => s.name === name);
return lineDasharray ? lineDasharray.strokeDasharray : undefined;
};
const [selectedItem, setSelectedItem] = useState<string[]>(['pv', 'uv']);
const config = useMemo( 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"
xAxis: { tickFormatter={(text) => formatDateWithUnit(text, props.unit)}
label: { />
autoHide: true, <YAxis mirror />
autoRotate: false, <ChartLegend
formatter: (text) => formatDateWithUnit(text, props.unit), content={
}, <ChartLegendContent
}, selectedItem={selectedItem}
}) satisfies ColumnConfig, onItemClick={(item) => {
[props.data, props.unit] setSelectedItem((selected) => {
if (selected.includes(item.value)) {
return selected.filter((s) => s !== item.value);
} else {
return [...selected, item.value];
}
});
}}
/>
}
/>
<CartesianGrid vertical={false} />
<ChartTooltip content={<ChartTooltipContent />} />
<Area
hide={!selectedItem.includes('pv')}
type="monotone"
dataKey="pv"
stroke={colors.chart.pv}
fillOpacity={1}
fill="url(#colorUv)"
strokeWidth={2}
strokeDasharray={getStrokeDasharray('pv')}
onAnimationEnd={handleAnimationEnd}
/>
<Area
hide={!selectedItem.includes('uv')}
type="monotone"
dataKey="uv"
stroke={colors.chart.uv}
fillOpacity={1}
fill="url(#colorPv)"
strokeWidth={2}
strokeDasharray={getStrokeDasharray('uv')}
onAnimationEnd={handleAnimationEnd}
/>
</AreaChart>
</ChartContainer>
); );
return <Column {...config} />;
}); });
TimeEventChart.displayName = 'TimeEventChart'; TimeEventChart.displayName = 'TimeEventChart';

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { getJWT, setJWT } from '../api/auth';
import { Loading } from './Loading'; import { Loading } from './Loading';
import { trpc } from '../api/trpc'; import { trpc } from '../api/trpc';
import { setUserInfo } from '../store/user'; import { setUserInfo } from '../store/user';
@ -7,26 +6,19 @@ import { setUserInfo } from '../store/user';
export const TokenLoginContainer: React.FC<React.PropsWithChildren> = export const TokenLoginContainer: React.FC<React.PropsWithChildren> =
React.memo((props) => { React.memo((props) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const mutation = trpc.user.loginWithToken.useMutation(); const trpcUtils = trpc.useUtils();
useEffect(() => { useEffect(() => {
const token = getJWT(); trpcUtils.user.info
if (token) { .fetch()
mutation .then((userInfo) => {
.mutateAsync({ if (userInfo) {
token, setUserInfo(userInfo);
}) }
.then((res) => { })
setJWT(res.token); .finally(() => {
setUserInfo(res.info); setLoading(false);
}) });
.catch((err) => {})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
}, []); }, []);
if (loading) { if (loading) {

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,227 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
//3 TanStack Libraries!!!
import {
ColumnDef,
flexRender,
getCoreRowModel,
Row,
useReactTable,
} from '@tanstack/react-table';
import { InfiniteData } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';
import {
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from './ui/table';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
interface VirtualizedInfiniteDataTableProps<TData> {
columns: ColumnDef<TData, any>[];
data: InfiniteData<{ items: TData[] }> | undefined;
onFetchNextPage: () => void;
isFetching: boolean;
isLoading: boolean;
hasNextPage: boolean | undefined;
}
export function VirtualizedInfiniteDataTable<TData>(
props: VirtualizedInfiniteDataTableProps<TData>
) {
const { columns, data, onFetchNextPage, isFetching, isLoading, hasNextPage } =
props;
//we need a reference to the scrolling element for logic down below
const tableContainerRef = useRef<HTMLDivElement>(null);
//flatten the array of arrays from the useInfiniteQuery hook
const flatData = useMemo(
() => data?.pages?.flatMap((page) => page.items) ?? [],
[data]
);
//called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
const fetchMoreOnBottomReached = useCallback(
(containerRefElement?: HTMLDivElement | null) => {
if (containerRefElement) {
const { scrollHeight, scrollTop, clientHeight } = containerRefElement;
//once the user has scrolled within 500px of the bottom of the table, fetch more data if we can
if (
scrollHeight - scrollTop - clientHeight < 500 &&
!isFetching &&
hasNextPage
) {
onFetchNextPage();
}
}
},
[onFetchNextPage, isFetching, hasNextPage]
);
// a check on mount and after a fetch to see if the table is already scrolled to the bottom and immediately needs to fetch more data
useEffect(() => {
fetchMoreOnBottomReached(tableContainerRef.current);
}, [fetchMoreOnBottomReached]);
const table = useReactTable({
data: flatData,
columns,
columnResizeMode: 'onChange',
getCoreRowModel: getCoreRowModel(),
});
const { rows } = table.getRowModel();
const rowVirtualizer = useVirtualizer({
count: rows.length,
estimateSize: () => 40, //estimate row height for accurate scrollbar dragging
getScrollElement: () => tableContainerRef.current,
//measure dynamic row height, except in firefox because it measures table border height incorrectly
measureElement:
typeof window !== 'undefined' &&
navigator.userAgent.indexOf('Firefox') === -1
? (element) => element?.getBoundingClientRect().height
: undefined,
overscan: 5,
});
/**
* Instead of calling `column.getSize()` on every render for every header
* and especially every data cell (very expensive),
* we will calculate all column sizes at once at the root table level in a useMemo
* and pass the column sizes down as CSS variables to the <table> element.
*/
const columnSizeVars = useMemo(() => {
const headers = table.getFlatHeaders();
const colSizes: { [key: string]: number } = {};
for (let i = 0; i < headers.length; i++) {
const header = headers[i]!;
colSizes[`--header-${header.id}-size`] = header.getSize();
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
}
return colSizes;
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div
className="virtualized-infinite-data-table relative h-full overflow-auto"
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}
ref={tableContainerRef}
>
{/* Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights */}
<table style={{ display: 'grid' }}>
<TableHeader
className="sticky top-0 z-10 grid"
style={{
...columnSizeVars,
}}
>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow
key={headerGroup.id}
className="bg-background hover:bg-background flex w-full"
>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
className="relative overflow-hidden text-ellipsis text-nowrap pt-2.5"
style={{
width: `calc(var(--header-${header?.id}-size) * 1px)`,
}}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
<div
{...{
onDoubleClick: () => header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
className: `resizer ${
header.column.getIsResizing() ? 'isResizing' : ''
}`,
}}
/>
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody
className="relative grid"
style={{
height: `${rowVirtualizer.getTotalSize()}px`, //tells scrollbar how big the table is
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index] as Row<TData>;
return (
<TableRow
data-index={virtualRow.index} //needed for dynamic row height measurement
ref={(node) => rowVirtualizer.measureElement(node)} //measure dynamic row height
key={row.id}
className="absolute flex w-full"
style={{
transform: `translateY(${virtualRow.start}px)`, //this should always be a `style` as it changes on scroll
}}
>
{row.getVisibleCells().map((cell) => {
const content = flexRender(
cell.column.columnDef.cell,
cell.getContext()
);
const value = cell.getValue();
const useSystemTooltip =
typeof value === 'string' || typeof value === 'number';
return (
<TableCell
key={cell.id}
className="flex"
style={{
width: cell.column.getSize(),
}}
>
{useSystemTooltip ? (
<div
className="w-full cursor-default overflow-hidden text-ellipsis whitespace-nowrap text-left"
title={String(value)}
>
{content}
</div>
) : (
<Tooltip>
<TooltipTrigger asChild={true}>
<div className="w-full cursor-default overflow-hidden text-ellipsis whitespace-nowrap text-left">
{content}
</div>
</TooltipTrigger>
<TooltipContent>{content}</TooltipContent>
</Tooltip>
)}
</TableCell>
);
})}
</TableRow>
);
})}
</TableBody>
</table>
{isFetching && <div>Fetching More...</div>}
</div>
);
}
VirtualizedInfiniteDataTable.displayName = 'VirtualizedInfiniteDataTable';

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

@ -1,14 +1,42 @@
import React from 'react'; import React, { useState } from 'react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { useUserInfo } from '@/store/user'; import {
import { RiRocket2Fill } from 'react-icons/ri'; changeUserCurrentWorkspace,
setUserInfo,
useCurrentWorkspace,
useCurrentWorkspaceSafe,
useUserInfo,
} from '@/store/user';
import { LuPlusCircle } from 'react-icons/lu';
import { useTranslation } from '@i18next-toolkit/react';
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
import { Button } from './ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
CommandSeparator,
} from './ui/command';
import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from './ui/dialog';
import { Label } from './ui/label';
import { Input } from './ui/input';
import { useEvent, useEventWithLoading } from '@/hooks/useEvent';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
import { trpc } from '@/api/trpc';
import { showErrorToast } from '@/utils/error';
import { first, upperCase } from 'lodash-es';
import { Empty } from 'antd';
interface WorkspaceSwitcherProps { interface WorkspaceSwitcherProps {
isCollapsed: boolean; isCollapsed: boolean;
@ -16,40 +44,220 @@ interface WorkspaceSwitcherProps {
export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo( export const WorkspaceSwitcher: React.FC<WorkspaceSwitcherProps> = React.memo(
(props) => { (props) => {
const userInfo = useUserInfo(); const userInfo = useUserInfo();
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
const [showNewWorkspaceDialog, setShowNewWorkspaceDialog] = useState(false);
const [newWorkspaceName, setNewWorkspaceName] = useState('');
const currentWorkspace = useCurrentWorkspaceSafe();
const createWorkspaceMutation = trpc.workspace.create.useMutation({
onSuccess: (userInfo) => {
setUserInfo(userInfo);
},
});
const switchWorkspaceMutation = trpc.workspace.switch.useMutation({
onSuccess: (userInfo) => {
setUserInfo(userInfo);
},
});
const handleSwitchWorkspace = useEvent(
async (workspace: { id: string; name: string }) => {
setOpen(false);
if (userInfo?.currentWorkspaceId === workspace.id) {
return;
}
try {
await switchWorkspaceMutation.mutateAsync({
workspaceId: workspace.id,
});
changeUserCurrentWorkspace(workspace.id);
} catch (err) {
showErrorToast(err);
}
}
);
const [handleCreateNewWorkspace, isCreateLoading] = useEventWithLoading(
async () => {
try {
await createWorkspaceMutation.mutateAsync({
name: newWorkspaceName,
});
setShowNewWorkspaceDialog(false);
} catch (err) {
showErrorToast(err);
}
}
);
if (!userInfo) { if (!userInfo) {
return null; return null;
} }
return ( return (
<Select value={userInfo.currentWorkspace.id}> <Dialog
<SelectTrigger open={showNewWorkspaceDialog}
className={cn( onOpenChange={setShowNewWorkspaceDialog}
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0', >
props.isCollapsed && <Popover open={open} onOpenChange={setOpen}>
'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden' <PopoverTrigger asChild>
)} <Button
aria-label="Select workspace" variant="outline"
> role="combobox"
<SelectValue placeholder="Select workspace"> aria-expanded={open}
<RiRocket2Fill /> className={cn(
'flex w-full justify-between',
props.isCollapsed && 'h-9 w-9 items-center justify-center p-0'
)}
>
{currentWorkspace ? (
<>
<Avatar
className={cn('h-5 w-5', props.isCollapsed ? '' : 'mr-2')}
>
<AvatarImage
src={`https://avatar.vercel.sh/${currentWorkspace.name}.png`}
alt={currentWorkspace.name}
className="grayscale"
/>
<AvatarFallback>
{upperCase(first(currentWorkspace.name))}
</AvatarFallback>
</Avatar>
<span className={cn('ml-2', props.isCollapsed && 'hidden')}> <span
{userInfo.currentWorkspace.name} className={cn(
</span> 'flex-1 overflow-hidden text-ellipsis text-left',
</SelectValue> props.isCollapsed && 'hidden'
</SelectTrigger> )}
<SelectContent> >
{userInfo.workspaces.map((w) => ( {currentWorkspace.name}
<SelectItem key={w.workspace.id} value={w.workspace.id}> </span>
<div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground"> </>
<RiRocket2Fill /> ) : (
{w.workspace.name} <span>{t('Select Workspace')}</span>
)}
<CaretSortIcon
className={cn(
'ml-auto h-4 w-4 shrink-0 opacity-50',
props.isCollapsed && 'hidden'
)}
/>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandList>
<CommandEmpty>{t('No workspace found.')}</CommandEmpty>
<CommandGroup key="workspace" heading={t('Workspace')}>
{userInfo.workspaces.length === 0 && (
<Empty
imageStyle={{ width: 80, height: 80, margin: 'auto' }}
description={t(
'Not any workspace has been found, please create first'
)}
/>
)}
{userInfo.workspaces.map(({ workspace }) => (
<CommandItem
key={workspace.id}
onSelect={() => {
handleSwitchWorkspace(workspace);
}}
className="text-sm"
>
<Avatar className="mr-2 h-5 w-5">
<AvatarImage
src={`https://avatar.vercel.sh/${workspace.name}.png`}
alt={workspace.name}
className="grayscale"
/>
<AvatarFallback>
{upperCase(first(workspace.name))}
</AvatarFallback>
</Avatar>
<span
className="overflow-hidden text-ellipsis"
title={workspace.name}
>
{workspace.name}
</span>
<CheckIcon
className={cn(
'ml-auto h-4 w-4',
currentWorkspace?.id === workspace.id
? 'opacity-100'
: 'opacity-0'
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
<CommandSeparator />
<CommandList>
<CommandGroup key="create">
<DialogTrigger asChild>
<CommandItem
aria-selected="false"
onSelect={() => {
setOpen(false);
setShowNewWorkspaceDialog(true);
}}
>
<LuPlusCircle className="mr-2" size={20} />
{t('Create Workspace')}
</CommandItem>
</DialogTrigger>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Create Workspace')}</DialogTitle>
<DialogDescription>
{t('Create a new workspace to cooperate with team members.')}
</DialogDescription>
</DialogHeader>
<div>
<div className="space-y-4 py-2 pb-4">
<div className="space-y-2">
<Label>{t('Workspace Name')}</Label>
<Input
value={newWorkspaceName}
onChange={(e) => setNewWorkspaceName(e.target.value)}
/>
</div> </div>
</SelectItem> </div>
))} </div>
</SelectContent> <DialogFooter>
</Select> <Button
variant="outline"
onClick={() => setShowNewWorkspaceDialog(false)}
>
{t('Cancel')}
</Button>
<Button
loading={isCreateLoading}
onClick={handleCreateNewWorkspace}
>
{t('Create')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
); );
} }
); );

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,167 +0,0 @@
import { Button, Dropdown, MenuProps, Space } from 'antd';
import React, { useState } from 'react';
import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user';
import { useDashboardStore } from '../../store/dashboard';
import { DownOutlined } from '@ant-design/icons';
import clsx from 'clsx';
import { useTranslation } from '@i18next-toolkit/react';
export const DashboardItemAddButton: React.FC = React.memo(() => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { data: websites = [], isLoading: isWebsiteLoading } =
trpc.website.all.useQuery({
workspaceId,
});
const { data: monitors = [], isLoading: isMonitorLoading } =
trpc.monitor.all.useQuery({
workspaceId,
});
const { addItem } = useDashboardStore();
const [open, setOpen] = useState(false);
const isLoading = isWebsiteLoading || isMonitorLoading;
const menu: MenuProps = {
items: [
{
key: 'website',
label: t('Website'),
children:
websites.length > 0
? websites.map((website) => ({
key: `website#${website.id}`,
label: website.name,
children: [
{
key: `website#${website.id}#overview`,
label: t('Overview'),
onClick: () => {
addItem(
'websiteOverview',
website.id,
`${website.name}'s Overview`
);
},
},
{
key: `website#${website.id}#events`,
label: t('Events'),
onClick: () => {
addItem(
'websiteEvents',
website.id,
`${website.name}'s Events`
);
},
},
],
}))
: [
{
key: `website#none`,
label: t('(None)'),
disabled: true,
},
],
},
{
key: 'monitor',
label: t('Monitor'),
children:
monitors.length > 0
? monitors.map((monitor) => ({
key: `monitor#${monitor.id}`,
label: monitor.name,
children: [
{
key: `monitor#${monitor.id}#healthBar`,
label: t('Health Bar'),
onClick: () => {
addItem(
'monitorHealthBar',
monitor.id,
t("{{monitorName}}'s Health", {
monitorName: monitor.name,
})
);
},
},
{
key: `monitor#${monitor.id}#metrics`,
label: t('Metrics'),
onClick: () => {
addItem(
'monitorMetrics',
monitor.id,
t("{{monitorName}}'s Metrics", {
monitorName: monitor.name,
})
);
},
},
{
key: `monitor#${monitor.id}#chart`,
label: t('Chart'),
onClick: () => {
addItem(
'monitorChart',
monitor.id,
t("{{monitorName}}'s Chart", {
monitorName: monitor.name,
})
);
},
},
{
key: `monitor#${monitor.id}#events`,
label: t('Events'),
onClick: () => {
addItem(
'monitorEvents',
monitor.id,
t("{{monitorName}}'s Events", {
monitorName: monitor.name,
})
);
},
},
],
}))
: [
{
key: `monitor#none`,
label: t('(None)'),
disabled: true,
},
],
},
],
};
return (
<div>
<Dropdown
trigger={['click']}
disabled={isLoading}
menu={menu}
open={open}
onOpenChange={setOpen}
>
<Button type="primary" size="large" className="w-32">
<Space>
<span>{t('Add')}</span>
<DownOutlined
className={clsx(
'scale-y-75 transition-transform',
open && 'rotate-180'
)}
/>
</Space>
</Button>
</Dropdown>
</div>
);
});
DashboardItemAddButton.displayName = 'DashboardItemAddButton';

View File

@ -1,102 +0,0 @@
import React, { useEffect } from 'react';
import { DashboardGrid } from './Grid';
import { DashboardItemAddButton } from './AddButton';
import { defaultBlankLayouts, useDashboardStore } from '../../store/dashboard';
import { useEvent } from '../../hooks/useEvent';
import { Layouts } from 'react-grid-layout';
import { Button, Empty, message } from 'antd';
import { DateFilter } from '../DateFilter';
import { trpc } from '../../api/trpc';
import { useCurrentWorkspace, useCurrentWorkspaceId } from '../../store/user';
import clsx from 'clsx';
import { useTranslation } from '@i18next-toolkit/react';
export const Dashboard: React.FC = React.memo(() => {
const { t } = useTranslation();
const { isEditMode, switchEditMode, layouts, items } = useDashboardStore();
const mutation = trpc.workspace.saveDashboardLayout.useMutation();
const workspaceId = useCurrentWorkspaceId();
const workspace = useCurrentWorkspace();
useEffect(() => {
// Init on mount
const { items = [], layouts = defaultBlankLayouts } =
workspace.dashboardLayout ?? {};
useDashboardStore.setState({
items,
layouts,
});
}, []);
const handleChangeLayouts = useEvent((layouts: Layouts) => {
useDashboardStore.setState({
layouts,
});
});
const handleSaveDashboardLayout = useEvent(async () => {
await mutation.mutateAsync({
workspaceId,
dashboardLayout: {
layouts,
items,
},
});
switchEditMode();
message.success(t('Layout saved success'));
});
return (
<div className="py-4">
<div
className={clsx(
'flex justify-end gap-2 bg-white py-2 dark:bg-gray-900',
isEditMode && 'sticky top-0 z-10'
)}
>
{isEditMode ? (
<>
<DashboardItemAddButton />
<Button
className="w-32"
size="large"
loading={mutation.isLoading}
disabled={mutation.isLoading}
onClick={handleSaveDashboardLayout}
>
{t('Done')}
</Button>
</>
) : (
<>
<DateFilter />
<Button
className="w-32"
type="primary"
size="large"
onClick={switchEditMode}
>
{t('Edit')}
</Button>
</>
)}
</div>
<DashboardGrid
layouts={layouts}
onChangeLayouts={handleChangeLayouts}
items={items}
isEditMode={isEditMode}
/>
{items.length === 0 && (
<Empty
description={t(
'You have not dashboard item yet, please enter edit mode and add you item.'
)}
/>
)}
</div>
);
});
Dashboard.displayName = 'Dashboard';

View File

@ -1,44 +0,0 @@
import React from 'react';
import { Layouts, Responsive, WidthProvider } from 'react-grid-layout';
import clsx from 'clsx';
import { DashboardGridItem } from './items';
import { DashboardItem } from '../../store/dashboard';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
const ResponsiveGridLayout = WidthProvider(Responsive);
interface DashboardGridProps {
isEditMode: boolean;
items: DashboardItem[];
layouts: Layouts;
onChangeLayouts: (layouts: Layouts) => void;
}
export const DashboardGrid: React.FC<DashboardGridProps> = React.memo(
(props) => {
const { layouts, onChangeLayouts, items, isEditMode } = props;
return (
<ResponsiveGridLayout
className={clsx('layout', isEditMode && 'select-none')}
layouts={layouts}
rowHeight={50}
draggableCancel=".non-draggable"
isDraggable={isEditMode}
isResizable={isEditMode}
breakpoints={{ lg: 1200, md: 768, sm: 0 }}
cols={{ lg: 4, md: 3, sm: 2 }}
onLayoutChange={(currentLayout, allLayouts) => {
onChangeLayouts(allLayouts);
}}
>
{items.map((item) => (
<div key={item.key}>
<DashboardGridItem item={item} />
</div>
))}
</ResponsiveGridLayout>
);
}
);
DashboardGrid.displayName = 'DashboardGrid';

View File

@ -1,9 +0,0 @@
import React from 'react';
import { MonitorDataChart } from '../../monitor/MonitorDataChart';
export const MonitorChartItem: React.FC<{
monitorId: string;
}> = React.memo((props) => {
return <MonitorDataChart monitorId={props.monitorId} />;
});
MonitorChartItem.displayName = 'MonitorChartItem';

View File

@ -1,9 +0,0 @@
import React from 'react';
import { MonitorEventList } from '../../monitor/MonitorEventList';
export const MonitorEventsItem: React.FC<{
monitorId: string;
}> = React.memo((props) => {
return <MonitorEventList monitorId={props.monitorId} />;
});
MonitorEventsItem.displayName = 'MonitorEventsItem';

View File

@ -1,20 +0,0 @@
import React from 'react';
import { MonitorHealthBar } from '../../monitor/MonitorHealthBar';
import { useCurrentWorkspaceId } from '../../../store/user';
export const MonitorHealthBarItem: React.FC<{
monitorId: string;
}> = React.memo((props) => {
const workspaceId = useCurrentWorkspaceId();
return (
<MonitorHealthBar
workspaceId={workspaceId}
monitorId={props.monitorId}
count={40}
size="large"
showCurrentStatus={true}
/>
);
});
MonitorHealthBarItem.displayName = 'MonitorHealthBarItem';

View File

@ -1,33 +0,0 @@
import React from 'react';
import { trpc } from '../../../api/trpc';
import { NotFoundTip } from '../../NotFoundTip';
import { useCurrentWorkspaceId } from '../../../store/user';
import { Loading } from '../../Loading';
import { MonitorDataMetrics } from '../../monitor/MonitorDataMetrics';
export const MonitorMetricsItem: React.FC<{
monitorId: string;
}> = React.memo((props) => {
const workspaceId = useCurrentWorkspaceId();
const { data: monitorInfo, isLoading } = trpc.monitor.get.useQuery({
workspaceId,
monitorId: props.monitorId,
});
if (isLoading) {
return <Loading />;
}
if (!monitorInfo) {
return <NotFoundTip />;
}
return (
<MonitorDataMetrics
monitorId={monitorInfo.id}
monitorType={monitorInfo.type}
/>
);
});
MonitorMetricsItem.displayName = 'MonitorMetricsItem';

View File

@ -1,24 +0,0 @@
import React from 'react';
import { WebsiteMetricsTable } from '../../website/WebsiteMetricsTable';
import { useGlobalRangeDate } from '../../../hooks/useGlobalRangeDate';
import { useTranslation } from '@i18next-toolkit/react';
export const WebsiteEventItem: React.FC<{
websiteId: string;
}> = React.memo((props) => {
const { t } = useTranslation();
const { startDate, endDate } = useGlobalRangeDate();
const startAt = startDate.valueOf();
const endAt = endDate.valueOf();
return (
<WebsiteMetricsTable
websiteId={props.websiteId}
type="event"
title={[t('Events'), t('Actions')]}
startAt={startAt}
endAt={endAt}
/>
);
});
WebsiteEventItem.displayName = 'WebsiteEventItem';

View File

@ -1,46 +0,0 @@
import React from 'react';
import { trpc } from '../../../api/trpc';
import { useCurrentWorkspaceId } from '../../../store/user';
import { Loading } from '../../Loading';
import { NotFoundTip } from '../../NotFoundTip';
import { WebsiteOverview } from '../../website/WebsiteOverview';
import { Button } from 'antd';
import { ArrowRightOutlined } from '@ant-design/icons';
import { useTranslation } from '@i18next-toolkit/react';
import { Link } from '@tanstack/react-router';
export const WebsiteOverviewItem: React.FC<{
websiteId: string;
}> = React.memo((props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const { data: websiteInfo, isLoading } = trpc.website.info.useQuery({
workspaceId,
websiteId: props.websiteId,
});
if (isLoading) {
return <Loading />;
}
if (!websiteInfo) {
return <NotFoundTip />;
}
return (
<WebsiteOverview
website={websiteInfo}
actions={
<>
<Link to="/website/$websiteId" params={{ websiteId: websiteInfo.id }}>
<Button type="primary" size="large">
{t('View Details')} <ArrowRightOutlined />
</Button>
</Link>
</>
}
/>
);
});
WebsiteOverviewItem.displayName = 'WebsiteOverviewItem';

View File

@ -1,77 +0,0 @@
import { useMemo } from 'react';
import { DashboardItem, useDashboardStore } from '../../../store/dashboard';
import { WebsiteOverviewItem } from './WebsiteOverviewItem';
import { NotFoundTip } from '../../NotFoundTip';
import { Button, Card, Input, Typography } from 'antd';
import React from 'react';
import { DeleteOutlined } from '@ant-design/icons';
import { useEvent } from '../../../hooks/useEvent';
import { WebsiteEventItem } from './WebsiteEventItem';
import { MonitorHealthBarItem } from './MonitorHealthBarItem';
import { MonitorMetricsItem } from './MonitorMetricsItem';
import { MonitorChartItem } from './MonitorChartItem';
import { MonitorEventsItem } from './MonitorEventsItem';
import { EditableText } from '../../EditableText';
interface DashboardGridItemProps {
item: DashboardItem;
}
export const DashboardGridItem: React.FC<DashboardGridItemProps> = React.memo(
(props) => {
const { isEditMode, removeItem, changeItemTitle } = useDashboardStore();
const { key, id, title, type } = props.item;
const inner = useMemo(() => {
if (type === 'websiteOverview') {
return <WebsiteOverviewItem websiteId={id} />;
} else if (type === 'websiteEvents') {
return <WebsiteEventItem websiteId={id} />;
} else if (type === 'monitorHealthBar') {
return <MonitorHealthBarItem monitorId={id} />;
} else if (type === 'monitorMetrics') {
return <MonitorMetricsItem monitorId={id} />;
} else if (type === 'monitorChart') {
return <MonitorChartItem monitorId={id} />;
} else if (type === 'monitorEvents') {
return <MonitorEventsItem monitorId={id} />;
} else {
return <NotFoundTip />;
}
}, [id, type]);
const handleDelete = useEvent(
(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.stopPropagation();
removeItem(key);
}
);
return (
<Card
className="h-full w-full overflow-auto"
title={
<EditableText
className="non-draggable"
enable={isEditMode}
defaultValue={title}
onSave={(text) => changeItemTitle(key, text)}
/>
}
headStyle={{ padding: 10, minHeight: 40 }}
bodyStyle={{ padding: 10 }}
extra={
isEditMode && (
<Button
shape="circle"
icon={<DeleteOutlined />}
onClick={handleDelete}
/>
)
}
>
{inner}
</Card>
);
}
);
DashboardGridItem.displayName = 'DashboardGridItem';

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