Compare commits

..

No commits in common. "master" and "feat/server-docker-support" have entirely different histories.

568 changed files with 28676 additions and 43609 deletions

View File

@ -1 +0,0 @@
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=false ALLOW_REGISTER=
ALLOW_OPENAPI=true ALLOW_OPENAPI=
# For analyze tianji self # For analyze tianji self
WEBSITE_ID= WEBSITE_ID=

View File

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

2
.npmrc
View File

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

View File

@ -3,9 +3,7 @@
"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,704 +1,5 @@
## [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) ## [1.10.0](https://github.com/msgbyte/tianji/compare/v1.9.4...v1.10.0) (2024-05-15)

View File

@ -7,28 +7,12 @@ COPY ./reporter/ ./reporter/
RUN apt update RUN apt update
RUN cd reporter && go build . RUN cd reporter && go build .
# Base ------------------------------ # # Base ------------------------------
# The current Chromium version in Alpine 3.20 is causing timeout issues with Puppeteer. Downgrading to Alpine 3.19 fixes the issue. See #11640, #12637, #12189 FROM node:20-alpine AS base
FROM node:20-alpine3.19 AS base
RUN npm install -g pnpm@9.7.1 RUN npm install -g pnpm@8.3.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
@ -38,10 +22,9 @@ ARG VERSION
COPY . . COPY . .
RUN pnpm install --filter @tianji/client... --config.dedupe-peer-dependents=false --frozen-lockfile RUN pnpm install --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
@ -49,11 +32,6 @@ 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,33 +32,21 @@ 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
- [x] team collaboration - [ ] team collaboration
- [ ] utm track - [ ] utm track
- [x] waitlist - [ ] waitlist
- [x] survey - [ ] survey
- [ ] survey page - [ ] lighthouse report
- [x] lighthouse report - [ ] hooks
- [x] hooks - [ ] links
- [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/preview/1.png) ![](./website/static/img/preview1.png)
![](./website/static/img/preview/2.png) ![](./website/static/img/preview2.png)
![](./website/static/img/preview/3.png) ![](./website/static/img/preview3.png)
![](./website/static/img/preview/4.png)
![](./website/static/img/preview/5.png)
![](./website/static/img/preview/6.png)
## Translation ## Translation

36
README.zh.md Normal file
View File

@ -0,0 +1,36 @@
# 天机 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,9 +2,6 @@ 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
View File

@ -1 +0,0 @@
charts

View File

@ -1,23 +0,0 @@
# 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/

View File

@ -1,6 +0,0 @@
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"

View File

@ -1,42 +0,0 @@
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

@ -1,26 +0,0 @@
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

@ -1,62 +0,0 @@
{{/*
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

@ -1,15 +0,0 @@
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

@ -1,71 +0,0 @@
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

@ -1,32 +0,0 @@
{{- 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

@ -1,61 +0,0 @@
{{- 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

@ -1,15 +0,0 @@
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

@ -1,13 +0,0 @@
{{- 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

@ -1,15 +0,0 @@
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

View File

@ -1,121 +0,0 @@
# 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,8 +1,7 @@
{ {
"name": "tianji", "name": "tianji",
"private": true, "private": true,
"version": "1.16.5", "version": "1.10.0",
"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",
@ -16,9 +15,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": "tsx scripts/build-tracker.ts", "build:tracker": "ts-node scripts/build-tracker.ts",
"build:geo": "tsx scripts/build-geo.ts", "build:geo": "ts-node scripts/build-geo.ts",
"build:openapi": "tsx --tsconfig ./tsconfig.base.json ./scripts/build-openapi-schema.ts && cd packages/client-sdk && pnpm generate:client", "build:openapi": "ts-node --project ./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",
@ -31,14 +30,12 @@
"@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",
"tsx": "^4.16.2", "typescript": "^5.2.2",
"typescript": "^5.3.3",
"vite": "^5.0.12" "vite": "^5.0.12"
}, },
"dependencies": { "dependencies": {
@ -46,17 +43,9 @@
"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": {
"@auth/core": "0.34.1", "dayjs": "1.11.10"
"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.1.1", "version": "1.0.1",
"description": "", "description": "",
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {
@ -9,10 +9,6 @@
"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

@ -1,13 +0,0 @@
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,4 +2,3 @@ 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: 'http://localhost:12345/open', BASE: '/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.16.1', VERSION: '1.9.3',
WITH_CREDENTIALS: false, WITH_CREDENTIALS: false,
interceptors: { interceptors: {
request: new Interceptors(), request: new Interceptors(),

View File

@ -69,152 +69,16 @@ 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 workspaceDelete(data: $OpenApiTs['/workspace//{workspaceId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/del']['delete']['res'][200]> { public static workspaceGetServiceCount(data: $OpenApiTs['/workspace/{workspaceId}/getServiceCount']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/getServiceCount']['get']['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}/members', url: '/workspace/{workspaceId}/getServiceCount',
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
} }
@ -259,23 +123,6 @@ 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
@ -477,24 +324,6 @@ 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
@ -516,68 +345,6 @@ 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 {
@ -604,10 +371,28 @@ 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']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/get']['get']['res'][200]> { public static monitorGet(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['get']['res'][200]> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'GET', method: 'GET',
url: '/workspace/{workspaceId}/monitor/{monitorId}/get', url: '/workspace/{workspaceId}/monitor/{monitorId}',
path: {
workspaceId: data.workspaceId,
monitorId: data.monitorId
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.monitorId
* @returns unknown Successful response
* @throws ApiError
*/
public static monitorDelete(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['delete']['res'][200]> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/workspace/{workspaceId}/monitor/{monitorId}',
path: { path: {
workspaceId: data.workspaceId, workspaceId: data.workspaceId,
monitorId: data.monitorId monitorId: data.monitorId
@ -649,24 +434,6 @@ 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
@ -734,42 +501,6 @@ 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
@ -1213,10 +944,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']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/get']['get']['res'][200]> { public static surveyGet(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['res'][200]> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'GET', method: 'GET',
url: '/workspace/{workspaceId}/survey/{surveyId}/get', url: '/workspace/{workspaceId}/survey/{surveyId}',
path: { path: {
workspaceId: data.workspaceId, workspaceId: data.workspaceId,
surveyId: data.surveyId surveyId: data.surveyId
@ -1265,11 +996,10 @@ export class SurveyService {
* @param data.workspaceId * @param data.workspaceId
* @param data.surveyId * @param data.surveyId
* @param data.requestBody * @param data.requestBody
* @returns string Successful response * @returns unknown 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] | $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]> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: 'POST',
url: '/workspace/{workspaceId}/survey/{surveyId}/submit', url: '/workspace/{workspaceId}/survey/{surveyId}/submit',
@ -1346,8 +1076,6 @@ 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
*/ */
@ -1361,9 +1089,7 @@ export class SurveyService {
}, },
query: { query: {
limit: data.limit, limit: data.limit,
cursor: data.cursor, cursor: data.cursor
startAt: data.startAt,
endAt: data.endAt
} }
}); });
} }
@ -1417,278 +1143,3 @@ 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": "CommonJS", "module": "ESNext",
"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.1", "version": "1.0.0",
"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']['get']['res']['200']; openApiClient.$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['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

@ -59,10 +59,7 @@ func main() {
interval := *Interval interval := *Interval
ticker := time.NewTicker(time.Duration(interval) * time.Second) ticker := time.Tick(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)
@ -74,17 +71,17 @@ func main() {
WorkspaceId: *WorkspaceId, WorkspaceId: *WorkspaceId,
Name: name, Name: name,
Hostname: hostname, Hostname: hostname,
Timeout: interval * 5, Timeout: interval * 2,
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, httpClient) sendHTTPRequest(*parsedURL, payload)
} }
<-ticker.C <-ticker
} }
} }
@ -128,7 +125,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, client *http.Client) { func sendHTTPRequest(_url url.URL, payload ReportData) {
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)
@ -151,6 +148,7 @@ func sendHTTPRequest(_url url.URL, payload ReportData, client *http.Client) {
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,4 +1,4 @@
import 'dotenv/config'; require('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(process.cwd(), './geo'); const dest = path.resolve(__dirname, '../geo');
if (!fs.existsSync(dest)) { if (!fs.existsSync(dest)) {
fs.mkdirSync(dest); fs.mkdirSync(dest);

View File

@ -1,10 +1,6 @@
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 * as vite from 'vite'; import 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(process.cwd(), './src/tracker/index.js'), entry: resolve(__dirname, '../src/tracker/index.js'),
name: 'tianji', name: 'tianji',
fileName: () => 'tracker.js', fileName: () => 'tracker.js',
formats: ['iife'], formats: ['iife'],
}, },
emptyOutDir: false, emptyOutDir: false,
outDir: resolve(process.cwd(), './src/client/public'), outDir: resolve(__dirname, '../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 position="top-center" /> <Toaster />
</BrowserRouter> </BrowserRouter>
); );
}); });

View File

@ -1,44 +1,15 @@
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

@ -1,186 +0,0 @@
/**
* 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

@ -1,55 +0,0 @@
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

@ -1,84 +0,0 @@
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,4 +1,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useUserStore } from '../../store/user';
import { useEvent } from '../../hooks/useEvent';
import { clearJWT } from '../auth';
/** /**
* Mock * Mock
@ -7,3 +10,14 @@ import dayjs from 'dayjs';
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,6 +4,15 @@ 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,6 +1,7 @@
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 {}
@ -8,6 +9,10 @@ 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,4 +1,5 @@
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';
@ -7,42 +8,21 @@ 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 prev = useSocketStore.getState().socket; const token = getJWT();
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,
}); });
@ -109,24 +89,7 @@ export function useSocket() {
return { socket, emit, subscribe }; return { socket, emit, subscribe };
} }
export function useSocketSubscribe<K extends keyof SubscribeEventMap>( export function useSocketSubscribe<T>(
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 {
@ -148,10 +111,6 @@ export function useSocketSubscribeData<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,6 +8,7 @@ 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';
@ -21,7 +22,9 @@ 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

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -40,13 +40,11 @@ export const AlertConfirm: React.FC<AlertConfirmProps> = React.memo((props) => {
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel onClick={props.onCancel}> <AlertDialogCancel onClick={props.onCancel}>
{props.onConfirm ? t('Cancel') : t('Continue')} {t('Cancel')}
</AlertDialogCancel> </AlertDialogCancel>
{props.onConfirm && (
<AlertDialogAction onClick={props.onConfirm}> <AlertDialogAction onClick={props.onConfirm}>
{t('Confirm')} {t('Confirm')}
</AlertDialogAction> </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-200 bg-zinc-100 px-1 py-0.5 dark:border-zinc-800 dark:bg-zinc-900"> <span className="rounded-sm border border-zinc-800 bg-zinc-900 px-1 py-0.5">
{props.children} {props.children}
</span> </span>
); );

View File

@ -7,7 +7,6 @@ import { useTranslation } from '@i18next-toolkit/react';
export const CodeBlock: React.FC<{ export const CodeBlock: React.FC<{
code: string; code: string;
height?: number;
}> = React.memo((props) => { }> = React.memo((props) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@ -21,13 +20,8 @@ export const CodeBlock: React.FC<{
}); });
return ( return (
<div className="group relative w-full overflow-auto"> <div className="group relative overflow-auto">
<pre <pre className="rounded-sm border border-zinc-800 bg-zinc-900 p-3 pr-12 text-sm">
className="rounded-sm border border-zinc-200 bg-zinc-100 p-3 pr-12 text-sm dark:border-zinc-800 dark:bg-zinc-900"
style={{
maxHeight: props.height || 384,
}}
>
<code>{props.code}</code> <code>{props.code}</code>
</pre> </pre>
<Button <Button

View File

@ -1,37 +0,0 @@
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,12 +10,9 @@ 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,
@ -146,14 +143,6 @@ export const CommandPanel: React.FC<CommandPanelProps> = React.memo((props) => {
<RiSurveyLine 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')}>
@ -173,22 +162,6 @@ 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>
@ -222,9 +195,6 @@ 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;
@ -312,22 +282,6 @@ export const CommandPanelSearchGroup: React.FC<CommandPanelSearchGroupProps> =
{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,10 +10,8 @@ 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 flex-shrink items-center overflow-hidden"> <div className="flex flex-1 items-center">
<h1 className="overflow-hidden text-ellipsis whitespace-nowrap text-xl font-bold"> <h1 className="text-xl font-bold">{props.title}</h1>
{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

@ -9,7 +9,6 @@ import { useFuseSearch } from '@/hooks/useFuseSearch';
import { Empty } from 'antd'; import { Empty } from 'antd';
import { globalEventBus } from '@/utils/event'; import { globalEventBus } from '@/utils/event';
import { Spinner } from './ui/spinner'; import { Spinner } from './ui/spinner';
import { formatNumber } from '@/utils/common';
export interface CommonListItem { export interface CommonListItem {
id: string; id: string;
@ -24,7 +23,6 @@ interface CommonListProps {
isLoading?: boolean; 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();
@ -78,9 +76,7 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
</div> </div>
)} )}
{finalList.length === 0 && !props.isLoading && ( {finalList.length === 0 && !props.isLoading && <Empty />}
<Empty description={props.emptyDescription} />
)}
{finalList.map((item) => { {finalList.map((item) => {
const isSelected = item.href === location.pathname; const isSelected = item.href === location.pathname;
@ -89,9 +85,8 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
<button <button
key={item.id} key={item.id}
className={cn( className={cn(
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all', 'hover:bg-accent flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all',
'hover:bg-gray-50 dark:hover:bg-gray-900', isSelected && 'bg-muted'
isSelected && 'bg-gray-50 dark:bg-gray-900'
)} )}
onClick={() => { onClick={() => {
globalEventBus.emit('commonListSelected'); globalEventBus.emit('commonListSelected');
@ -101,14 +96,10 @@ export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
}} }}
> >
<div className="flex w-full items-center justify-between gap-1"> <div className="flex w-full items-center justify-between gap-1">
<div className="overflow-hidden text-ellipsis font-semibold"> <div className="font-semibold">{item.title}</div>
{item.title}
</div>
{item.number && item.number > 0 && ( {item.number && item.number > 0 && (
<span className="opacity-60" title={String(item.number)}> <span className="opacity-60">{item.number}</span>
{formatNumber(item.number)}
</span>
)} )}
</div> </div>

View File

@ -1,32 +0,0 @@
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

@ -75,7 +75,7 @@ export function DataTable<TData>({
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead key={header.id} className="text-nowrap"> <TableHead key={header.id}>
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
@ -112,7 +112,7 @@ export function DataTable<TData>({
)} )}
{row.getVisibleCells().map((cell, i) => ( {row.getVisibleCells().map((cell, i) => (
<TableCell key={cell.id} className="text-nowrap"> <TableCell key={cell.id}>
{flexRender( {flexRender(
cell.column.columnDef.cell, cell.column.columnDef.cell,
cell.getContext() cell.getContext()
@ -148,4 +148,3 @@ export function DataTable<TData>({
</div> </div>
); );
} }
DataTable.displayName = 'DataTable';

View File

@ -27,15 +27,6 @@ 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-50 dark:bg-zinc-900"> <Card className="min-w-[320px] 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-50 dark:bg-zinc-900"> <Card className="min-w-[320px] 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

@ -1,34 +0,0 @@
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

@ -1,11 +0,0 @@
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

@ -1,31 +0,0 @@
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

@ -1,106 +0,0 @@
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,46 +1,38 @@
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 [editing, setEditing] = useState(false); const enable = props.enable ?? true;
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)}> <>
{editing ? ( {enable ? (
<Input <Input
ref={inputRef} className={clsx(
autoFocus={true} props.className,
type="text" 'rounded-none border-0 p-0 !shadow-none outline-0'
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={() => { onBlur={(e) => props.onSave(e.target.value)}
setEditing(false);
props.onSave(text);
}}
/> />
) : ( ) : (
<span onClick={handleClick}>{text}</span> <span className={props.className}>{text}</span>
)} )}
</div> </>
); );
}); });
EditableText.displayName = 'EditableText'; EditableText.displayName = 'EditableText';

View File

@ -1,47 +1,28 @@
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
ref={containerRef} className={clsx('flex', {
className={cn( 'gap-[3px]': size === 'small',
'flex', 'gap-1': size === 'large',
{ })}
'gap-[3px] px-0.5 py-1.5': size === 'small',
'gap-1 px-0.5 py-2': size === 'large',
},
props.className
)}
> >
{props.beats {props.beats.map((beat, i) => (
.slice(
Math.floor(
Math.max(cellNeedWidth * cellCount - containerRect.width, 0) /
cellNeedWidth
),
cellCount
)
.map((beat, i) => (
<div <div
key={i} key={i}
title={beat.title} title={beat.title}
@ -51,7 +32,12 @@ export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
'h-4 w-[5px]': size === 'small', 'h-4 w-[5px]': size === 'small',
'h-8 w-2': size === 'large', 'h-8 w-2': size === 'large',
}, },
getStatusBgColorClassName(beat.status) {
'bg-green-500': beat.status === 'health',
'bg-red-600': beat.status === 'error',
'bg-yellow-400': beat.status === 'warning',
'bg-gray-400': beat.status === 'none',
}
)} )}
/> />
))} ))}

View File

@ -1,6 +1,8 @@
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,6 +1,4 @@
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,13 +1,6 @@
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: BytemdPlugin[] = [ export const plugins = [
gfm(), gfm(),
{ // Add more plugins here
rehype: (p) =>
p.use(externalLinks, {
target: '_blank',
}),
},
]; ];

View File

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

View File

@ -1,17 +1,15 @@
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 ( return <Viewer plugins={plugins} value={props.value ?? ''} />;
<div className="markdown">
<Viewer plugins={plugins} value={props.value ?? ''} />
</div>
);
} }
); );
MarkdownViewer.displayName = 'MarkdownViewer'; MarkdownViewer.displayName = 'MarkdownViewer';

View File

@ -1,92 +0,0 @@
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

@ -1,94 +0,0 @@
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

@ -1,111 +0,0 @@
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

@ -1,27 +0,0 @@
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

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

View File

@ -1,4 +1,5 @@
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';
@ -6,19 +7,26 @@ 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 trpcUtils = trpc.useUtils(); const mutation = trpc.user.loginWithToken.useMutation();
useEffect(() => { useEffect(() => {
trpcUtils.user.info const token = getJWT();
.fetch() if (token) {
.then((userInfo) => { mutation
if (userInfo) { .mutateAsync({
setUserInfo(userInfo); token,
}
}) })
.then((res) => {
setJWT(res.token);
setUserInfo(res.info);
})
.catch((err) => {})
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
}); });
} else {
setLoading(false);
}
}, []); }, []);
if (loading) { if (loading) {

View File

@ -1,61 +0,0 @@
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

@ -1,227 +0,0 @@
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

@ -1,230 +0,0 @@
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,42 +1,14 @@
import React, { useState } from 'react'; import React from 'react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { cn } from '@/utils/style'; import { cn } from '@/utils/style';
import { import { useUserInfo } from '@/store/user';
changeUserCurrentWorkspace, import { RiRocket2Fill } from 'react-icons/ri';
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;
@ -44,220 +16,40 @@ 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 (
<Dialog <Select value={userInfo.currentWorkspace.id}>
open={showNewWorkspaceDialog} <SelectTrigger
onOpenChange={setShowNewWorkspaceDialog}
>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn( className={cn(
'flex w-full justify-between', '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 && 'h-9 w-9 items-center justify-center p-0' props.isCollapsed &&
'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
)} )}
aria-label="Select workspace"
> >
{currentWorkspace ? ( <SelectValue placeholder="Select workspace">
<> <RiRocket2Fill />
<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 <span className={cn('ml-2', props.isCollapsed && 'hidden')}>
className={cn( {userInfo.currentWorkspace.name}
'flex-1 overflow-hidden text-ellipsis text-left',
props.isCollapsed && 'hidden'
)}
>
{currentWorkspace.name}
</span> </span>
</> </SelectValue>
) : ( </SelectTrigger>
<span>{t('Select Workspace')}</span> <SelectContent>
)} {userInfo.workspaces.map((w) => (
<SelectItem key={w.workspace.id} value={w.workspace.id}>
<CaretSortIcon <div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
className={cn( <RiRocket2Fill />
'ml-auto h-4 w-4 shrink-0 opacity-50', {w.workspace.name}
props.isCollapsed && 'hidden' </div>
)} </SelectItem>
/>
</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> </SelectContent>
</CommandList> </Select>
<CommandSeparator />
<CommandList>
<CommandGroup key="create">
<DialogTrigger asChild>
<CommandItem
aria-selected="false"
onSelect={() => {
setOpen(false);
setShowNewWorkspaceDialog(true);
}}
>
<LuPlusCircle className="mr-2" size={20} />
{t('Create Workspace')}
</CommandItem>
</DialogTrigger>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Create Workspace')}</DialogTitle>
<DialogDescription>
{t('Create a new workspace to cooperate with team members.')}
</DialogDescription>
</DialogHeader>
<div>
<div className="space-y-4 py-2 pb-4">
<div className="space-y-2">
<Label>{t('Workspace Name')}</Label>
<Input
value={newWorkspaceName}
onChange={(e) => setNewWorkspaceName(e.target.value)}
/>
</div>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowNewWorkspaceDialog(false)}
>
{t('Cancel')}
</Button>
<Button
loading={isCreateLoading}
onClick={handleCreateNewWorkspace}
>
{t('Create')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
); );
} }
); );

View File

@ -1,166 +0,0 @@
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

@ -0,0 +1,167 @@
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

@ -0,0 +1,102 @@
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

@ -0,0 +1,44 @@
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

@ -0,0 +1,9 @@
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

@ -0,0 +1,9 @@
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

@ -0,0 +1,20 @@
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

@ -0,0 +1,33 @@
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

@ -0,0 +1,24 @@
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

@ -0,0 +1,46 @@
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

@ -0,0 +1,77 @@
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';

View File

@ -1,97 +0,0 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import React from 'react';
import { useTranslation } from '@i18next-toolkit/react';
import { CodeExample } from '../CodeExample';
interface FeedApiGuideProps {
channelId: string;
webhookSignature?: string;
}
export const FeedApiGuide: React.FC<FeedApiGuideProps> = React.memo((props) => {
const { t } = useTranslation();
return (
<Card className="w-full overflow-hidden">
<CardHeader>
<div>{t('You can send a message to this channel with:')}</div>
</CardHeader>
<CardContent className="flex w-full flex-col gap-5 overflow-hidden">
<CodeExample
example={{
curl: {
label: 'curl',
code: generateCurlCode(props.channelId, props.webhookSignature),
},
fetch: {
label: 'fetch',
code: generateFetchCode(props.channelId, props.webhookSignature),
},
}}
/>
<div className="pl-2 font-bold">{t('OR')}</div>
<div>{t('Integrate with third party with webhook')}</div>
</CardContent>
</Card>
);
});
FeedApiGuide.displayName = 'FeedApiGuide';
function generateCurlCode(channelId: string, webhookSignature?: string) {
if (webhookSignature) {
return `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\
-H "Content-Type: application/json" \\
-H "X-Webhook-Signature: ${webhookSignature}" \\
-d '{
"eventName": "test name",
"eventContent": "test content",
"tags": ["test"],
"source": "custom",
"important": false
}'`;
}
return `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\
-H "Content-Type: application/json" \\
-d '{
"eventName": "test name",
"eventContent": "test content",
"tags": ["test"],
"source": "custom",
"important": false
}'`;
}
function generateFetchCode(channelId: string, webhookSignature?: string) {
if (webhookSignature) {
return `fetch('${window.location.origin}/open/feed/${channelId}/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': '${webhookSignature}'
},
body: JSON.stringify({
eventName: 'test name',
eventContent: 'test content',
tags: ['test'],
source: 'custom',
important: false,
})
})`;
}
return `fetch('${window.location.origin}/open/feed/${channelId}/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
eventName: 'test name',
eventContent: 'test content',
tags: ['test'],
source: 'custom',
important: false,
})
})`;
}

View File

@ -1,139 +0,0 @@
import React, { useMemo } from 'react';
import { Button } from '../ui/button';
import { LuArchive, LuArchiveRestore } from 'react-icons/lu';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { AppRouterOutput, trpc } from '@/api/trpc';
import { useCurrentWorkspaceId, useHasAdminPermission } from '@/store/user';
import { DynamicVirtualList } from '../DynamicVirtualList';
import { get } from 'lodash-es';
import { FeedEventItem } from './FeedEventItem';
import { useTranslation } from '@i18next-toolkit/react';
import { Separator } from '../ui/separator';
import { useEvent } from '@/hooks/useEvent';
import { toast } from 'sonner';
import { AlertConfirm } from '../AlertConfirm';
type FeedItem = AppRouterOutput['feed']['fetchEventsByCursor']['items'][number];
interface FeedArchivePageButtonProps {
channelId: string;
}
export const FeedArchivePageButton: React.FC<FeedArchivePageButtonProps> =
React.memo((props) => {
const workspaceId = useCurrentWorkspaceId();
const channelId = props.channelId;
const { t } = useTranslation();
const unarchiveEventMutation = trpc.feed.unarchiveEvent.useMutation();
const clearAllArchivedEventsMutation =
trpc.feed.clearAllArchivedEvents.useMutation();
const trpcUtils = trpc.useUtils();
const hasAdminPermission = useHasAdminPermission();
const {
data,
isInitialLoading,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = trpc.feed.fetchEventsByCursor.useInfiniteQuery(
{
workspaceId,
channelId,
archived: true,
},
{
refetchOnMount: false,
refetchOnWindowFocus: false,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
const handleUnArchive = useEvent(async (event: FeedItem) => {
await unarchiveEventMutation.mutateAsync({
workspaceId,
channelId,
eventId: event.id,
});
trpcUtils.feed.fetchEventsByCursor.refetch();
toast.success(t('Event unarchived'));
});
const handleClear = useEvent(async () => {
const count = await clearAllArchivedEventsMutation.mutateAsync({
workspaceId,
channelId,
});
trpcUtils.feed.fetchEventsByCursor.refetch();
toast.success(t('{{num}} events cleared', { num: count }));
});
const fullEvents = useMemo(
() => data?.pages.flatMap((p) => p.items) ?? [],
[data]
);
return (
<Popover>
<PopoverTrigger>
<Button size="icon" variant="outline">
<LuArchive />
</Button>
</PopoverTrigger>
<PopoverContent
className="flex h-[50vh] w-96 flex-col overflow-hidden"
side="bottom"
align="end"
>
<div className="flex items-center justify-between">
<h1 className="text-lg font-bold">{t('Archived Events')}</h1>
{hasAdminPermission && (
<AlertConfirm onConfirm={handleClear}>
<Button size="sm">{t('Clear')}</Button>
</AlertConfirm>
)}
</div>
<Separator className="my-2" />
<div className="flex-1">
<DynamicVirtualList
allData={fullEvents}
estimateSize={100}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onFetchNextPage={fetchNextPage}
getItemKey={(index) => get(fullEvents, [index, 'id'])}
renderItem={(item) => (
<FeedEventItem
className="animate-fade-in mb-2"
event={item}
actions={
<Button
size="icon"
variant="secondary"
className="h-6 w-6 overflow-hidden"
onClick={() => handleUnArchive(item)}
>
<LuArchiveRestore size={12} />
</Button>
}
/>
)}
renderEmpty={() => (
<div className="w-full overflow-hidden p-4">
<div className="text-muted text-center">
{isInitialLoading
? t('Loading...')
: t('No archived events')}
</div>
</div>
)}
/>
</div>
</PopoverContent>
</Popover>
);
});
FeedArchivePageButton.displayName = 'FeedArchivePageButton';

View File

@ -1,186 +0,0 @@
import { useTranslation } from '@i18next-toolkit/react';
import { Button } from '@/components/ui/button';
import { useEventWithLoading } from '@/hooks/useEvent';
import { Card, CardContent, CardFooter } from '@/components/ui/card';
import { z } from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import React from 'react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../ui/select';
import { NotificationPicker } from '../notification/NotificationPicker';
import { LuRefreshCcw } from 'react-icons/lu';
import md5 from 'md5';
import dayjs from 'dayjs';
const addFormSchema = z.object({
name: z.string(),
webhookSignature: z.string().default(''),
notificationIds: z.array(z.string()).default([]),
notifyFrequency: z.enum(['none', 'event', 'day', 'week', 'month']),
});
export type FeedChannelEditFormValues = z.infer<typeof addFormSchema>;
interface FeedChannelEditFormProps {
defaultValues?: FeedChannelEditFormValues;
onSubmit: (values: FeedChannelEditFormValues) => Promise<void>;
}
export const FeedChannelEditForm: React.FC<FeedChannelEditFormProps> =
React.memo((props) => {
const { t } = useTranslation();
const form = useForm<FeedChannelEditFormValues>({
resolver: zodResolver(addFormSchema),
defaultValues: props.defaultValues ?? {
name: 'New Channel',
webhookSignature: '',
notificationIds: [],
notifyFrequency: 'none',
},
});
const [handleSubmit, isLoading] = useEventWithLoading(
async (values: FeedChannelEditFormValues) => {
await props.onSubmit(values);
form.reset();
}
);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-8">
<Card>
<CardContent className="pt-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('Channel Name')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t('Channel Name to Display')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="webhookSignature"
render={({ field }) => (
<FormItem>
<FormLabel optional={true}>
{t('Webhook Signature')}
</FormLabel>
<FormControl>
<div className="flex">
<Input className="rounded-r-none" {...field} />
<Button
className="rounded-l-none"
type="button"
Icon={LuRefreshCcw}
onClick={() => {
form.setValue(
'webhookSignature',
md5(dayjs().valueOf().toString())
);
}}
/>
</div>
</FormControl>
<FormDescription>
{t('Optional, Webhook Signature for Incoming Webhook')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notificationIds"
render={({ field }) => (
<FormItem>
<FormLabel>{t('Notification')}</FormLabel>
<FormControl>
<NotificationPicker
className="w-full"
{...field}
allowClear={true}
mode="multiple"
/>
</FormControl>
<FormDescription>
{t('Select Notification for send')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="notifyFrequency"
render={({ field }) => (
<FormItem>
<FormLabel>{t('Notification Frequency')}</FormLabel>
<FormControl>
<Select
defaultValue={field.value}
onValueChange={field.onChange}
>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">{t('None')}</SelectItem>
<SelectItem value="event">
{t('Every Event')}
</SelectItem>
<SelectItem value="day">{t('Every Day')}</SelectItem>
<SelectItem value="week">
{t('Every Week')}
</SelectItem>
<SelectItem value="month">
{t('Every Month')}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
<CardFooter>
<Button type="submit" loading={isLoading}>
{props.defaultValues ? t('Update') : t('Create')}
</Button>
</CardFooter>
</Card>
</form>
</Form>
);
});
FeedChannelEditForm.displayName = 'FeedChannelEditForm';

View File

@ -1,56 +0,0 @@
import { Button, Empty, Select, SelectProps } from 'antd';
import React, { useMemo } from 'react';
import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user';
import { PlusOutlined } from '@ant-design/icons';
import { useTranslation } from '@i18next-toolkit/react';
import { useNavigate } from '@tanstack/react-router';
interface FeedChannelPickerProps extends SelectProps<string | string[]> {}
export const FeedChannelPicker: React.FC<FeedChannelPickerProps> = React.memo(
(props) => {
const { t } = useTranslation();
const workspaceId = useCurrentWorkspaceId();
const navigate = useNavigate();
const { data: allChannels = [] } = trpc.feed.channels.useQuery({
workspaceId,
});
const options = useMemo(
() =>
allChannels.map((m) => ({
label: m.name,
value: m.id,
})),
[allChannels]
);
return (
<Select
notFoundContent={
<Empty
description={
<div className="py-2">
<div className="mb-1">{t('Not found any feed channel')}</div>
<Button
icon={<PlusOutlined />}
onClick={() =>
navigate({
to: '/feed/add',
})
}
>
{t('Create Now')}
</Button>
</div>
}
/>
}
{...props}
options={options}
optionFilterProp="label"
/>
);
}
);
FeedChannelPicker.displayName = 'FeedChannelPicker';

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