Compare commits

..

68 Commits

Author SHA1 Message Date
moonrailgun
162954606a feat: add auto language detect for browser 2024-11-11 01:33:06 +08:00
moonrailgun
3bf86b3e6e feat: add audit log clear feature 2024-11-10 06:08:22 +08:00
moonrailgun
843a581d42 feat: add subscription selection page 2024-11-09 20:28:27 +08:00
moonrailgun
fffc989336 chore: remove unused script 2024-11-09 20:27:35 +08:00
moonrailgun
ea75ed7f88 chore: add alert 2024-11-09 20:27:17 +08:00
moonrailgun
34f9fe6957 refactor: add usage limit and update card style 2024-11-08 01:57:35 +08:00
moonrailgun
71f75c27dd feat: add api key and usage to command panel 2024-11-08 01:56:07 +08:00
moonrailgun
a12fa3e6fe feat: add <UsageCard /> component which can render usage data and progress 2024-11-08 01:47:49 +08:00
moonrailgun
ae5f5a97d9 chore: remove passport package 2024-11-08 00:28:04 +08:00
moonrailgun
31ad64cd95 feat: add cronjob to check workspace limit which will pause workspace 2024-11-07 00:06:04 +08:00
tommy141x
1096e9ca9a Fix number casting issue 2024-11-06 12:11:08 +08:00
moonrailgun
b71bf6542e feat: add more usage stats 2024-11-06 01:19:57 +08:00
moonrailgun
e4b98b1c36 feat: add workspace subscription 2024-11-06 01:11:03 +08:00
moonrailgun
fa1ff3b5f6 refactor: move billing mode inside folder 2024-11-06 01:11:03 +08:00
moonrailgun
f0ddf6c5dd refactor: add apikey check before setup 2024-11-06 01:11:03 +08:00
moonrailgun
74d391afc1 feat: add lemonsqueezy subscription 2024-11-06 01:11:03 +08:00
moonrailgun
c70e69879f fix: fix isUser middleware will call twice problem 2024-11-04 00:54:57 +08:00
moonrailgun
6a4bdd324c feat: add api key fe and usage counter 2024-11-03 19:39:59 +08:00
moonrailgun
f7b1d33c5d feat: add user api key backend support 2024-11-03 17:56:47 +08:00
moonrailgun
7aec9e7237 chore: release v1.16.5 2024-11-03 00:02:15 +08:00
moonrailgun
f637ade70f refactor: refactor status header and add typescript and translation support 2024-11-01 07:22:52 +08:00
Tommy Johnston
5207338ac1 change: percent > 95 to percent >= 95 2024-11-01 06:58:22 +08:00
Tommy Johnston
cb476f7361 fix styling hiccup 2024-11-01 06:58:22 +08:00
tommy
f77acf9eac consider 95% or higher to be green 2024-11-01 06:58:22 +08:00
tommy
325ab38fbb use client local time for date display 2024-11-01 06:58:22 +08:00
tommy
9949b973bd fix: add key to Fragment in map for monitor items 2024-11-01 06:58:22 +08:00
tommy
6312ec6eed Add Status Header & Modify Styles 2024-11-01 06:58:22 +08:00
moonrailgun
59b874644f chore: fix ci problem 2024-11-01 00:45:19 +08:00
moonrailgun
266b08f2da refactor: update webhooks signature api guide 2024-10-31 22:20:01 +08:00
tommy
a8a47ed94d fix: retrieve date as string
Apparently prisma can automatically cast dates to UTC, formatting the date to a string in the query can prevent this.
2024-10-31 16:07:07 +08:00
moonrailgun
272505669e refactor: update amount in stripe 2024-10-30 02:57:24 +08:00
moonrailgun
6b3631eae1 feat: add webhookSignature in feed channel 2024-10-29 03:41:45 +08:00
moonrailgun
f592466d62 chore: release v1.16.4 2024-10-27 17:29:50 +08:00
moonrailgun
98298c4367 refactor: update currency symbols in feed 2024-10-27 03:16:06 +08:00
moonrailgun
09d0f02d84 feat: add stripe feed integration 2024-10-26 00:55:51 +08:00
moonrailgun
59d32e0119 chore: release v1.16.3 2024-10-25 00:22:32 +08:00
moonrailgun
1c5737e588 chore: fix ci problem and upgrade version 2024-10-24 02:46:57 +08:00
moonrailgun
ba580dd70b chore: release v1.16.2 2024-10-24 02:36:36 +08:00
moonrailgun
e402ee1688 chore: update openapi document 2024-10-23 23:53:32 +08:00
moonrailgun
1df32dc257 docs: update README 2024-10-22 23:39:56 +08:00
moonrailgun
79667a9644 fix: fix a bug which will match incorrect path #115 2024-10-22 02:26:20 +08:00
moonrailgun
554f902584 chore: fix ci problem 2024-10-22 01:16:23 +08:00
moonrailgun
fcb8f22116 feat: add prometheus report support 2024-10-22 00:37:46 +08:00
moonrailgun
f080830407 chore: release v1.16.1 2024-10-21 01:26:02 +08:00
moonrailgun
c7e20df516 feat: add timezone support #114 2024-10-20 22:47:22 +08:00
moonrailgun
83850f2981 refactor: update cronjob clear time 2024-10-20 22:29:07 +08:00
moonrailgun
3dca8fc27c feat: add workspace settings manage 2024-10-20 22:27:31 +08:00
moonrailgun
4e3fd9db64 feat: add test notify 2024-10-20 16:58:23 +08:00
moonrailgun
b4ab20ad32 chore: release v1.16.0 2024-10-20 01:12:56 +08:00
moonrailgun
7f70557c77 fix: fix reporter memory leak problem #103 2024-10-19 01:37:57 +08:00
moonrailgun
2a503ca250 chore: rename old tsconfig paths 2024-10-16 01:16:21 +08:00
moonrailgun
527f734bc4 refactor: ignore unknown sentry log 2024-10-16 01:10:47 +08:00
moonrailgun
c9f2458775 test: update md5 unit test 2024-10-16 01:09:04 +08:00
moonrailgun
f553f157dd refactor: add border radius in smtp template 2024-10-16 01:02:11 +08:00
moonrailgun
61980b37d3 refactor: let version text more prominent 2024-10-15 01:10:29 +08:00
moonrailgun
820b25baed chore: fix ci problem 2024-10-15 01:08:04 +08:00
moonrailgun
279e616bee feat: add click event for status page item which allow hide/show chart 2024-10-14 00:29:06 +08:00
moonrailgun
dcff57fe69 feat: add daily monitor data display for public 2024-10-14 00:29:06 +08:00
moonrailgun
316b95467d feat: add MonitorLatestResponse and up status summary 2024-10-14 00:29:06 +08:00
moonrailgun
e5e77dbdee refactor: change public summary display logic
old is recent data, now is monthly data
2024-10-14 00:29:06 +08:00
moonrailgun
bbb8d88116 feat: add monitor summary function 2024-10-14 00:29:06 +08:00
moonrailgun
f1513fe3f7 chore: release v1.15.8 2024-10-13 19:54:18 +08:00
moonrailgun
8c5c417a19 docs: change code command line style 2024-10-12 20:07:37 +08:00
moonrailgun
763810e8b7 docs: add shacdn to website 2024-10-12 20:07:11 +08:00
moonrailgun
c0e2ef0fe8 refactor: migrate monitor data chart to recharts and remove @ant-design/charts 2024-10-12 01:23:00 +08:00
moonrailgun
a218c22397 chore: update survey edit form 2024-10-11 01:06:25 +08:00
moonrailgun
f00163b2f1 feat: survey add webhook url field which can send webhook when receive any survey 2024-10-11 01:06:25 +08:00
moonrailgun
de572426eb feat: add survey webhook 2024-10-11 01:06:25 +08:00
137 changed files with 6862 additions and 2113 deletions

View File

@ -1,5 +1,151 @@
## [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) ## [1.15.7](https://github.com/msgbyte/tianji/compare/v1.15.6...v1.15.7) (2024-10-03)

View File

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

View File

@ -1,7 +1,7 @@
{ {
"name": "tianji", "name": "tianji",
"private": true, "private": true,
"version": "1.15.7", "version": "1.16.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently --kill-others npm:dev:server npm:dev:web", "dev": "concurrently --kill-others npm:dev:server npm:dev:web",

View File

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

View File

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

View File

@ -120,10 +120,10 @@ export class WorkspaceService {
* @returns unknown Successful response * @returns unknown Successful response
* @throws ApiError * @throws ApiError
*/ */
public static workspaceDelete(data: $OpenApiTs['/workspace//{workspaceId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}']['delete']['res'][200]> { public static workspaceDelete(data: $OpenApiTs['/workspace//{workspaceId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/del']['delete']['res'][200]> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'DELETE', method: 'DELETE',
url: '/workspace//{workspaceId}', url: '/workspace//{workspaceId}/del',
path: { path: {
workspaceId: data.workspaceId workspaceId: data.workspaceId
} }
@ -146,6 +146,25 @@ export class WorkspaceService {
}); });
} }
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static workspaceUpdateSettings(data: $OpenApiTs['/workspace//{workspaceId}/updateSettings']['post']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/updateSettings']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/workspace//{workspaceId}/updateSettings',
path: {
workspaceId: data.workspaceId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/** /**
* @param data The data for the request. * @param data The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -585,28 +604,10 @@ export class MonitorService {
* @returns unknown Successful response * @returns unknown Successful response
* @throws ApiError * @throws ApiError
*/ */
public static monitorGet(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['get']['res'][200]> { public static monitorGet(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/get']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/get']['get']['res'][200]> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'GET', method: 'GET',
url: '/workspace/{workspaceId}/monitor/{monitorId}', url: '/workspace/{workspaceId}/monitor/{monitorId}/get',
path: {
workspaceId: data.workspaceId,
monitorId: data.monitorId
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.monitorId
* @returns unknown Successful response
* @throws ApiError
*/
public static monitorDelete(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['delete']['res'][200]> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/workspace/{workspaceId}/monitor/{monitorId}',
path: { path: {
workspaceId: data.workspaceId, workspaceId: data.workspaceId,
monitorId: data.monitorId monitorId: data.monitorId
@ -648,6 +649,24 @@ export class MonitorService {
}); });
} }
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.monitorId
* @returns unknown Successful response
* @throws ApiError
*/
public static monitorDelete(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/del']['delete']['res'][200]> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/workspace/{workspaceId}/monitor/{monitorId}/del',
path: {
workspaceId: data.workspaceId,
monitorId: data.monitorId
}
});
}
/** /**
* @param data The data for the request. * @param data The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -715,6 +734,42 @@ export class MonitorService {
}); });
} }
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.monitorId
* @returns unknown Successful response
* @throws ApiError
*/
public static monitorPublicSummary(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicSummary']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicSummary']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace/{workspaceId}/monitor/{monitorId}/publicSummary',
path: {
workspaceId: data.workspaceId,
monitorId: data.monitorId
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.monitorId
* @returns unknown Successful response
* @throws ApiError
*/
public static monitorPublicData(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicData']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicData']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace/{workspaceId}/monitor/{monitorId}/publicData',
path: {
workspaceId: data.workspaceId,
monitorId: data.monitorId
}
});
}
/** /**
* @param data The data for the request. * @param data The data for the request.
* @param data.workspaceId * @param data.workspaceId
@ -1158,10 +1213,10 @@ export class SurveyService {
* @returns unknown Successful response * @returns unknown Successful response
* @throws ApiError * @throws ApiError
*/ */
public static surveyGet(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['res'][200]> { public static surveyGet(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/get']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/get']['get']['res'][200]> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'GET', method: 'GET',
url: '/workspace/{workspaceId}/survey/{surveyId}', url: '/workspace/{workspaceId}/survey/{surveyId}/get',
path: { path: {
workspaceId: data.workspaceId, workspaceId: data.workspaceId,
surveyId: data.surveyId surveyId: data.surveyId
@ -1472,10 +1527,10 @@ export class FeedService {
* @returns unknown Successful response * @returns unknown Successful response
* @throws ApiError * @throws ApiError
*/ */
public static feedDeleteChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}']['delete']['res'][200]> { public static feedDeleteChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/del']['delete']['res'][200]> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'DELETE', method: 'DELETE',
url: '/workspace/{workspaceId}/feed/{channelId}', url: '/workspace/{workspaceId}/feed/{channelId}/del',
path: { path: {
workspaceId: data.workspaceId, workspaceId: data.workspaceId,
channelId: data.channelId channelId: data.channelId

View File

@ -49,6 +49,9 @@ export type $OpenApiTs = {
workspace: { workspace: {
id: string; id: string;
name: string; name: string;
settings: {
[key: string]: unknown;
};
}; };
}>; }>;
}; };
@ -85,6 +88,9 @@ export type $OpenApiTs = {
workspace: { workspace: {
id: string; id: string;
name: string; name: string;
settings: {
[key: string]: unknown;
};
}; };
}>; }>;
}; };
@ -122,6 +128,9 @@ export type $OpenApiTs = {
workspace: { workspace: {
id: string; id: string;
name: string; name: string;
settings: {
[key: string]: unknown;
};
}; };
}>; }>;
}; };
@ -157,6 +166,9 @@ export type $OpenApiTs = {
workspace: { workspace: {
id: string; id: string;
name: string; name: string;
settings: {
[key: string]: unknown;
};
}; };
}>; }>;
}; };
@ -190,6 +202,9 @@ export type $OpenApiTs = {
workspace: { workspace: {
id: string; id: string;
name: string; name: string;
settings: {
[key: string]: unknown;
};
}; };
}>; }>;
}; };
@ -211,11 +226,14 @@ export type $OpenApiTs = {
200: { 200: {
id: string; id: string;
name: string; name: string;
settings: {
[key: string]: unknown;
}; };
}; };
}; };
}; };
'/workspace//{workspaceId}': { };
'/workspace//{workspaceId}/del': {
delete: { delete: {
req: { req: {
workspaceId: string; workspaceId: string;
@ -253,6 +271,30 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/workspace//{workspaceId}/updateSettings': {
post: {
req: {
requestBody: {
settings: {
[key: string]: unknown;
};
};
workspaceId: string;
};
res: {
/**
* Successful response
*/
200: {
id: string;
name: string;
settings: {
[key: string]: unknown;
};
};
};
};
};
'/workspace//{workspaceId}/invite': { '/workspace//{workspaceId}/invite': {
post: { post: {
req: { req: {
@ -692,7 +734,7 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/workspace/{workspaceId}/monitor/{monitorId}': { '/workspace/{workspaceId}/monitor/{monitorId}/get': {
get: { get: {
req: { req: {
monitorId: string; monitorId: string;
@ -722,9 +764,43 @@ export type $OpenApiTs = {
} | null; } | null;
}; };
}; };
delete: { };
'/monitor/getPublicInfo': {
post: {
req: { req: {
monitorId: string; requestBody: {
monitorIds: Array<(string)>;
};
};
res: {
/**
* Successful response
*/
200: Array<{
id: string;
name: string;
type: string;
trendingMode: boolean;
}>;
};
};
};
'/workspace/{workspaceId}/monitor/upsert': {
post: {
req: {
requestBody: {
id?: string;
name: string;
type: string;
active?: boolean;
interval?: number;
maxRetries?: number;
trendingMode?: boolean;
notificationIds?: Array<(string)>;
payload: {
[key: string]: unknown;
};
};
workspaceId: string; workspaceId: string;
}; };
res: { res: {
@ -749,41 +825,10 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/monitor/getPublicInfo': { '/workspace/{workspaceId}/monitor/{monitorId}/del': {
post: { delete: {
req: { req: {
requestBody: { monitorId: string;
monitorIds: Array<(string)>;
};
};
res: {
/**
* Successful response
*/
200: Array<{
id: string;
name: string;
type: string;
}>;
};
};
};
'/workspace/{workspaceId}/monitor/upsert': {
post: {
req: {
requestBody: {
id?: string;
name: string;
type: string;
active?: boolean;
interval?: number;
maxRetries?: number;
trendingMode?: boolean;
notificationIds?: Array<(string)>;
payload: {
[key: string]: unknown;
};
};
workspaceId: string; workspaceId: string;
}; };
res: { res: {
@ -876,6 +921,42 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/workspace/{workspaceId}/monitor/{monitorId}/publicSummary': {
get: {
req: {
monitorId: string;
workspaceId: string;
};
res: {
/**
* Successful response
*/
200: Array<{
day: string;
totalCount: number;
upCount: number;
upRate: number;
}>;
};
};
};
'/workspace/{workspaceId}/monitor/{monitorId}/publicData': {
get: {
req: {
monitorId: string;
workspaceId: string;
};
res: {
/**
* Successful response
*/
200: Array<{
value: number;
createdAt: string;
}>;
};
};
};
'/workspace/{workspaceId}/monitor/{monitorId}/dataMetrics': { '/workspace/{workspaceId}/monitor/{monitorId}/dataMetrics': {
get: { get: {
req: { req: {
@ -1384,13 +1465,14 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}>; }>;
}; };
}; };
}; };
'/workspace/{workspaceId}/survey/{surveyId}': { '/workspace/{workspaceId}/survey/{surveyId}/get': {
get: { get: {
req: { req: {
surveyId: string; surveyId: string;
@ -1414,6 +1496,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} | null; } | null;
@ -1499,6 +1582,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
}; };
workspaceId: string; workspaceId: string;
}; };
@ -1520,6 +1604,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
@ -1541,6 +1626,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds?: Array<(string)>; feedChannelIds?: Array<(string)>;
feedTemplate?: string; feedTemplate?: string;
webhookUrl?: string;
}; };
surveyId: string; surveyId: string;
workspaceId: string; workspaceId: string;
@ -1563,6 +1649,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
@ -1593,6 +1680,7 @@ export type $OpenApiTs = {
}; };
feedChannelIds: Array<(string)>; feedChannelIds: Array<(string)>;
feedTemplate: string; feedTemplate: string;
webhookUrl: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
@ -1820,7 +1908,7 @@ export type $OpenApiTs = {
}; };
}; };
}; };
'/workspace/{workspaceId}/feed/{channelId}': { '/workspace/{workspaceId}/feed/{channelId}/del': {
delete: { delete: {
req: { req: {
channelId: string; channelId: string;

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -59,7 +59,10 @@ func main() {
interval := *Interval interval := *Interval
ticker := time.Tick(time.Duration(interval) * time.Second) ticker := time.NewTicker(time.Duration(interval) * time.Second)
defer ticker.Stop()
httpClient := &http.Client{}
log.Println("Start reporting...") log.Println("Start reporting...")
log.Println("Mode:", *Mode) log.Println("Mode:", *Mode)
@ -78,10 +81,10 @@ func main() {
if *Mode == "udp" { if *Mode == "udp" {
sendUDPPack(*parsedURL, payload) sendUDPPack(*parsedURL, payload)
} else { } else {
sendHTTPRequest(*parsedURL, payload) sendHTTPRequest(*parsedURL, payload, httpClient)
} }
<-ticker <-ticker.C
} }
} }
@ -125,7 +128,7 @@ func sendUDPPack(url url.URL, payload ReportData) {
/** /**
* Send HTTP Request to report server data * Send HTTP Request to report server data
*/ */
func sendHTTPRequest(_url url.URL, payload ReportData) { func sendHTTPRequest(_url url.URL, payload ReportData, client *http.Client) {
jsonData, err := jsoniter.Marshal(payload) jsonData, err := jsoniter.Marshal(payload)
if err != nil { if err != nil {
log.Println("Error encoding JSON:", err) log.Println("Error encoding JSON:", err)
@ -148,7 +151,6 @@ func sendHTTPRequest(_url url.URL, payload ReportData) {
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-tianji-report-version", version) req.Header.Set("x-tianji-report-version", version)
client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
log.Println("Send request error:", err) log.Println("Send request error:", err)

View File

@ -14,6 +14,8 @@ import {
LuAreaChart, LuAreaChart,
LuBellDot, LuBellDot,
LuFilePieChart, LuFilePieChart,
LuKanbanSquare,
LuKeyRound,
LuMonitorDot, LuMonitorDot,
LuSearch, LuSearch,
LuServer, LuServer,
@ -171,6 +173,22 @@ export const CommandPanel: React.FC<CommandPanelProps> = React.memo((props) => {
<LuBellDot className="mr-2 h-4 w-4" /> <LuBellDot className="mr-2 h-4 w-4" />
<span>{t('Notifications')}</span> <span>{t('Notifications')}</span>
</CommandItem> </CommandItem>
<CommandItem
onSelect={handleJump({
to: '/settings/apiKey',
})}
>
<LuKeyRound className="mr-2 h-4 w-4" />
<span>{t('Api Key')}</span>
</CommandItem>
<CommandItem
onSelect={handleJump({
to: '/settings/usage',
})}
>
<LuKanbanSquare className="mr-2 h-4 w-4" />
<span>{t('Usage')}</span>
</CommandItem>
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command>

View File

@ -0,0 +1,32 @@
import { cn } from '@/utils/style';
import React, { PropsWithChildren } from 'react';
import copy from 'copy-to-clipboard';
import { useEvent } from '@/hooks/useEvent';
import { toast } from 'sonner';
import { useTranslation } from '@i18next-toolkit/react';
interface CopyableTextProps extends PropsWithChildren {
className?: string;
text: string;
}
export const CopyableText: React.FC<CopyableTextProps> = React.memo((props) => {
const { t } = useTranslation();
const handleClick = useEvent(() => {
copy(props.text);
toast.success(t('Copied'));
});
return (
<span
className={cn(
'cursor-pointer select-none rounded bg-white bg-opacity-10 px-2',
'hover:bg-white hover:bg-opacity-20',
props.className
)}
onClick={handleClick}
>
{props.children ?? props.text}
</span>
);
});
CopyableText.displayName = 'CopyableText';

View File

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

View File

@ -0,0 +1,61 @@
import React from 'react';
import { Card, CardContent, CardHeader } from './ui/card';
import { formatNumber } from '@/utils/common';
import { LuAlertCircle } from 'react-icons/lu';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { useTranslation } from '@i18next-toolkit/react';
import colors from 'tailwindcss/colors';
interface UsageCardProps {
title: string;
current: number;
limit?: number;
}
export const UsageCard: React.FC<UsageCardProps> = React.memo((props) => {
const { title, current, limit } = props;
const { t } = useTranslation();
return (
<Card className="relative h-full w-full overflow-hidden">
{limit && (
<div
className="absolute h-full bg-black bg-opacity-5 dark:bg-white dark:bg-opacity-10"
style={{ width: `${(current / limit) * 100}%` }}
/>
)}
{limit && current > limit && (
<div className="absolute right-2 top-2">
<Tooltip>
<TooltipTrigger>
<LuAlertCircle stroke={colors.red['500']} />
</TooltipTrigger>
<TooltipContent>
<div>
{t(
'Exceeded the limit, please upgrade your plan or your workspace will be paused soon.'
)}
</div>
</TooltipContent>
</Tooltip>
</div>
)}
<CardHeader className="text-muted-foreground">{title}</CardHeader>
<CardContent>
{limit && limit >= 0 ? (
<div>
<span className="text-2xl font-bold">{formatNumber(current)}</span>{' '}
/ <span>{formatNumber(limit)}</span>
</div>
) : (
<div>
<span className="text-2xl font-bold">{formatNumber(current)}</span>{' '}
/ <span></span>
</div>
)}
</CardContent>
</Card>
);
});
UsageCard.displayName = 'UsageCard';

View File

@ -0,0 +1,166 @@
import { Check } from 'lucide-react';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import React from 'react';
import { useTranslation } from '@i18next-toolkit/react';
import { useEvent } from '@/hooks/useEvent';
import { defaultErrorHandler, trpc } from '@/api/trpc';
import { useCurrentWorkspaceId } from '@/store/user';
import { cn } from '@/utils/style';
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
import { LuInfo } from 'react-icons/lu';
interface SubscriptionSelectionProps {
tier: 'FREE' | 'PRO' | 'TEAM' | 'UNLIMITED' | undefined;
}
export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> =
React.memo((props) => {
const { tier } = props;
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const checkoutMutation = trpc.billing.checkout.useMutation({
onError: defaultErrorHandler,
});
const handleCheckoutSubscribe = useEvent(
async (tier: 'free' | 'pro' | 'team') => {
const { url } = await checkoutMutation.mutateAsync({
workspaceId,
tier,
redirectUrl: location.href,
});
location.href = url;
}
);
const plans = [
{
id: 'FREE',
name: t('Free'),
price: 0,
features: [
t('Basic trial'),
t('Basic Usage'),
t('Up to 3 websites'),
t('Up to 3 surveys'),
t('Up to 3 feed channels'),
t('100K website events per month'),
t('100K monitor execution per month'),
t('10K feed event per month'),
t('Discord Community Support'),
],
onClick: () => handleCheckoutSubscribe('free'),
},
{
id: 'PRO',
name: 'Pro',
price: 19.99,
features: [
t('Sufficient for most situations'),
t('Priority access to advanced features'),
t('Up to 10 websites'),
t('Up to 20 surveys'),
t('Up to 20 feed channels'),
t('1M website events per month'),
t('1M monitor execution per month'),
t('100K feed events per month'),
t('Discord Community Support'),
],
onClick: () => handleCheckoutSubscribe('pro'),
},
{
id: 'TEAM',
name: 'Team',
price: 99.99,
features: [
t('Fully sufficient'),
t('Priority access to advanced features'),
t('Unlimited websites'),
t('Unlimited surveys'),
t('Unlimited feed channels'),
t('20M website events per month'),
t('20M monitor execution per month'),
t('1M feed events per month'),
t('Priority email support'),
],
onClick: () => handleCheckoutSubscribe('team'),
},
];
return (
<div className="container mx-auto px-4 py-8">
<h1 className="mb-8 text-center text-3xl font-bold">
{t('Subscription Plan')}
</h1>
<Alert className="mb-4">
<LuInfo className="h-4 w-4" />
<AlertTitle>{t('Current Plan')}</AlertTitle>
<AlertDescription>
{t('Your Current Plan is:')}{' '}
<span className="font-bold">{tier}</span>
</AlertDescription>
</Alert>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
{plans.map((plan) => {
const isCurrent = plan.id === tier;
return (
<Card
key={plan.name}
className={cn('flex flex-col', isCurrent && 'border-primary')}
>
<CardHeader>
<CardTitle>{plan.name}</CardTitle>
<CardDescription>${plan.price} per month</CardDescription>
</CardHeader>
<CardContent className="flex-grow">
<ul className="space-y-2">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center">
<Check className="mr-2 h-4 w-4 text-green-500" />
{feature}
</li>
))}
</ul>
</CardContent>
<CardFooter>
{isCurrent ? (
<Button className="w-full" disabled variant="outline">
{t('Current')}
</Button>
) : (
<Button
className="w-full"
disabled={checkoutMutation.isLoading}
onClick={plan.onClick}
>
{t('{{action}} to {{plan}}', {
action:
plans.indexOf(plan) <
plans.findIndex((p) => p.id === tier)
? t('Downgrade')
: t('Upgrade'),
plan: plan.name,
})}
</Button>
)}
</CardFooter>
</Card>
);
})}
</div>
</div>
);
});
SubscriptionSelection.displayName = 'SubscriptionSelection';

View File

@ -3,8 +3,11 @@ import React from 'react';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { CodeExample } from '../CodeExample'; import { CodeExample } from '../CodeExample';
export const FeedApiGuide: React.FC<{ channelId: string }> = React.memo( interface FeedApiGuideProps {
(props) => { channelId: string;
webhookSignature?: string;
}
export const FeedApiGuide: React.FC<FeedApiGuideProps> = React.memo((props) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -17,11 +20,11 @@ export const FeedApiGuide: React.FC<{ channelId: string }> = React.memo(
example={{ example={{
curl: { curl: {
label: 'curl', label: 'curl',
code: generateCurlCode(props.channelId), code: generateCurlCode(props.channelId, props.webhookSignature),
}, },
fetch: { fetch: {
label: 'fetch', label: 'fetch',
code: generateFetchCode(props.channelId), code: generateFetchCode(props.channelId, props.webhookSignature),
}, },
}} }}
/> />
@ -32,12 +35,24 @@ export const FeedApiGuide: React.FC<{ channelId: string }> = React.memo(
</CardContent> </CardContent>
</Card> </Card>
); );
} });
);
FeedApiGuide.displayName = 'FeedApiGuide'; FeedApiGuide.displayName = 'FeedApiGuide';
function generateCurlCode(channelId: string) { function generateCurlCode(channelId: string, webhookSignature?: string) {
const code = `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\ if (webhookSignature) {
return `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\
-H "Content-Type: application/json" \\
-H "X-Webhook-Signature: ${webhookSignature}" \\
-d '{
"eventName": "test name",
"eventContent": "test content",
"tags": ["test"],
"source": "custom",
"important": false
}'`;
}
return `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\
-H "Content-Type: application/json" \\ -H "Content-Type: application/json" \\
-d '{ -d '{
"eventName": "test name", "eventName": "test name",
@ -46,12 +61,27 @@ function generateCurlCode(channelId: string) {
"source": "custom", "source": "custom",
"important": false "important": false
}'`; }'`;
return code;
} }
function generateFetchCode(channelId: string) { function generateFetchCode(channelId: string, webhookSignature?: string) {
const code = `fetch('${window.location.origin}/open/feed/${channelId}/send', { if (webhookSignature) {
return `fetch('${window.location.origin}/open/feed/${channelId}/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': '${webhookSignature}'
},
body: JSON.stringify({
eventName: 'test name',
eventContent: 'test content',
tags: ['test'],
source: 'custom',
important: false,
})
})`;
}
return `fetch('${window.location.origin}/open/feed/${channelId}/send', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -64,6 +94,4 @@ function generateFetchCode(channelId: string) {
important: false, important: false,
}) })
})`; })`;
return code;
} }

View File

@ -24,9 +24,13 @@ import {
SelectValue, SelectValue,
} from '../ui/select'; } from '../ui/select';
import { NotificationPicker } from '../notification/NotificationPicker'; import { NotificationPicker } from '../notification/NotificationPicker';
import { LuRefreshCcw } from 'react-icons/lu';
import md5 from 'md5';
import dayjs from 'dayjs';
const addFormSchema = z.object({ const addFormSchema = z.object({
name: z.string(), name: z.string(),
webhookSignature: z.string().default(''),
notificationIds: z.array(z.string()).default([]), notificationIds: z.array(z.string()).default([]),
notifyFrequency: z.enum(['none', 'event', 'day', 'week', 'month']), notifyFrequency: z.enum(['none', 'event', 'day', 'week', 'month']),
}); });
@ -45,6 +49,7 @@ export const FeedChannelEditForm: React.FC<FeedChannelEditFormProps> =
resolver: zodResolver(addFormSchema), resolver: zodResolver(addFormSchema),
defaultValues: props.defaultValues ?? { defaultValues: props.defaultValues ?? {
name: 'New Channel', name: 'New Channel',
webhookSignature: '',
notificationIds: [], notificationIds: [],
notifyFrequency: 'none', notifyFrequency: 'none',
}, },
@ -79,6 +84,38 @@ export const FeedChannelEditForm: React.FC<FeedChannelEditFormProps> =
)} )}
/> />
<FormField
control={form.control}
name="webhookSignature"
render={({ field }) => (
<FormItem>
<FormLabel optional={true}>
{t('Webhook Signature')}
</FormLabel>
<FormControl>
<div className="flex">
<Input className="rounded-r-none" {...field} />
<Button
className="rounded-l-none"
type="button"
Icon={LuRefreshCcw}
onClick={() => {
form.setValue(
'webhookSignature',
md5(dayjs().valueOf().toString())
);
}}
/>
</div>
</FormControl>
<FormDescription>
{t('Optional, Webhook Signature for Incoming Webhook')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="notificationIds" name="notificationIds"

View File

@ -4,9 +4,11 @@ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { CodeBlock } from '../CodeBlock'; import { CodeBlock } from '../CodeBlock';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { SiSentry } from 'react-icons/si'; import { SiSentry } from 'react-icons/si';
import { FaStripe } from 'react-icons/fa6';
export const FeedIntegration: React.FC<{ export const FeedIntegration: React.FC<{
feedId: string; feedId: string;
webhookSignature: string;
}> = React.memo((props) => { }> = React.memo((props) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -57,6 +59,22 @@ export const FeedIntegration: React.FC<{
} }
/> />
<FeedIntegrationItem
icon={<FaStripe size={32} />}
label="Stripe"
content={
<div>
<div className="text-lg font-bold">{t('Receive Webhooks')}</div>
<div>{t('Add sentry webhook with url')}:</div>
<CodeBlock
code={`${window.location.origin}/open/feed/${props.feedId}/stripe`}
/>
</div>
}
/>
<div onClick={() => window.open('/feed/playground', '_blank')}> <div onClick={() => window.open('/feed/playground', '_blank')}>
<FeedIntegrationItemTrigger <FeedIntegrationItemTrigger
icon={<LuTestTube2 size={32} />} icon={<LuTestTube2 size={32} />}
@ -75,7 +93,7 @@ export const FeedIntegration: React.FC<{
<CodeBlock <CodeBlock
code={`POST ${window.location.origin}/open/feed/${props.feedId}/send code={`POST ${window.location.origin}/open/feed/${props.feedId}/send
${props.webhookSignature ? `\nHeader:\nX-Webhook-Signature: ${props.webhookSignature}\n` : ''}
Body Body
{ {
eventName: "", eventName: "",

View File

@ -23,7 +23,7 @@ import {
useUserInfo, useUserInfo,
useUserStore, useUserStore,
} from '@/store/user'; } from '@/store/user';
import { languages } from '@/utils/constants'; import { languages } from '@/utils/i18n';
import { useTranslation, setLanguage } from '@i18next-toolkit/react'; import { useTranslation, setLanguage } from '@i18next-toolkit/react';
import { useNavigate } from '@tanstack/react-router'; import { useNavigate } from '@tanstack/react-router';
import { version } from '@/utils/env'; import { version } from '@/utils/env';
@ -234,7 +234,7 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
</DropdownMenuPortal> </DropdownMenuPortal>
</DropdownMenuSub> </DropdownMenuSub>
<DropdownMenuLabel className="text-muted-foreground dark:text-muted"> <DropdownMenuLabel className="text-gray-500">
v{version} v{version}
</DropdownMenuLabel> </DropdownMenuLabel>
</DropdownMenuContent> </DropdownMenuContent>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@ -17,14 +17,13 @@
"keywords": [], "keywords": [],
"author": "moonrailgun <moonrailgun@gmail.com>", "author": "moonrailgun <moonrailgun@gmail.com>",
"dependencies": { "dependencies": {
"@ant-design/charts": "^1.4.2",
"@ant-design/icons": "^5.3.6", "@ant-design/icons": "^5.3.6",
"@antv/l7": "^2.20.14", "@antv/l7": "^2.20.14",
"@antv/larkmap": "^1.4.13", "@antv/larkmap": "^1.4.13",
"@bytemd/plugin-gfm": "^1.21.0", "@bytemd/plugin-gfm": "^1.21.0",
"@bytemd/react": "^1.21.0", "@bytemd/react": "^1.21.0",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@i18next-toolkit/react": "^1.1.0", "@i18next-toolkit/react": "2.0.0-rc.5",
"@loadable/component": "^5.16.3", "@loadable/component": "^5.16.3",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
@ -71,6 +70,7 @@
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide-react": "^0.358.0", "lucide-react": "^0.358.0",
"md5": "^2.3.0",
"millify": "^6.1.0", "millify": "^6.1.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"pretty-ms": "^9.0.0", "pretty-ms": "^9.0.0",
@ -107,6 +107,7 @@
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/loadable__component": "^5.13.8", "@types/loadable__component": "^5.13.8",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/md5": "^2.3.5",
"@types/react": "^18.2.22", "@types/react": "^18.2.22",
"@types/react-beautiful-dnd": "^13.1.8", "@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",

View File

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

View File

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

View File

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 450 B

View File

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

View File

Before

Width:  |  Height:  |  Size: 259 B

After

Width:  |  Height:  |  Size: 259 B

View File

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

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

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

View File

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 268 B

View File

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

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

@ -34,7 +34,9 @@ import { Route as SettingsWorkspaceImport } from './routes/settings/workspace'
import { Route as SettingsUsageImport } from './routes/settings/usage' import { Route as SettingsUsageImport } from './routes/settings/usage'
import { Route as SettingsProfileImport } from './routes/settings/profile' import { Route as SettingsProfileImport } from './routes/settings/profile'
import { Route as SettingsNotificationsImport } from './routes/settings/notifications' import { Route as SettingsNotificationsImport } from './routes/settings/notifications'
import { Route as SettingsBillingImport } from './routes/settings/billing'
import { Route as SettingsAuditLogImport } from './routes/settings/auditLog' import { Route as SettingsAuditLogImport } from './routes/settings/auditLog'
import { Route as SettingsApiKeyImport } from './routes/settings/apiKey'
import { Route as PageAddImport } from './routes/page/add' import { Route as PageAddImport } from './routes/page/add'
import { Route as PageSlugImport } from './routes/page/$slug' import { Route as PageSlugImport } from './routes/page/$slug'
import { Route as MonitorAddImport } from './routes/monitor/add' import { Route as MonitorAddImport } from './routes/monitor/add'
@ -166,11 +168,21 @@ const SettingsNotificationsRoute = SettingsNotificationsImport.update({
getParentRoute: () => SettingsRoute, getParentRoute: () => SettingsRoute,
} as any) } as any)
const SettingsBillingRoute = SettingsBillingImport.update({
path: '/billing',
getParentRoute: () => SettingsRoute,
} as any)
const SettingsAuditLogRoute = SettingsAuditLogImport.update({ const SettingsAuditLogRoute = SettingsAuditLogImport.update({
path: '/auditLog', path: '/auditLog',
getParentRoute: () => SettingsRoute, getParentRoute: () => SettingsRoute,
} as any) } as any)
const SettingsApiKeyRoute = SettingsApiKeyImport.update({
path: '/apiKey',
getParentRoute: () => SettingsRoute,
} as any)
const PageAddRoute = PageAddImport.update({ const PageAddRoute = PageAddImport.update({
path: '/add', path: '/add',
getParentRoute: () => PageRoute, getParentRoute: () => PageRoute,
@ -312,10 +324,18 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PageAddImport preLoaderRoute: typeof PageAddImport
parentRoute: typeof PageImport parentRoute: typeof PageImport
} }
'/settings/apiKey': {
preLoaderRoute: typeof SettingsApiKeyImport
parentRoute: typeof SettingsImport
}
'/settings/auditLog': { '/settings/auditLog': {
preLoaderRoute: typeof SettingsAuditLogImport preLoaderRoute: typeof SettingsAuditLogImport
parentRoute: typeof SettingsImport parentRoute: typeof SettingsImport
} }
'/settings/billing': {
preLoaderRoute: typeof SettingsBillingImport
parentRoute: typeof SettingsImport
}
'/settings/notifications': { '/settings/notifications': {
preLoaderRoute: typeof SettingsNotificationsImport preLoaderRoute: typeof SettingsNotificationsImport
parentRoute: typeof SettingsImport parentRoute: typeof SettingsImport
@ -411,7 +431,9 @@ export const routeTree = rootRoute.addChildren([
RegisterRoute, RegisterRoute,
ServerRoute, ServerRoute,
SettingsRoute.addChildren([ SettingsRoute.addChildren([
SettingsApiKeyRoute,
SettingsAuditLogRoute, SettingsAuditLogRoute,
SettingsBillingRoute,
SettingsNotificationsRoute, SettingsNotificationsRoute,
SettingsProfileRoute, SettingsProfileRoute,
SettingsUsageRoute, SettingsUsageRoute,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import express from 'express';
import 'express-async-errors'; import 'express-async-errors';
import compression from 'compression'; import compression from 'compression';
import swaggerUI from 'swagger-ui-express'; import swaggerUI from 'swagger-ui-express';
import passport from 'passport';
import morgan from 'morgan'; import morgan from 'morgan';
import { websiteRouter } from './router/website.js'; import { websiteRouter } from './router/website.js';
import { telemetryRouter } from './router/telemetry.js'; import { telemetryRouter } from './router/telemetry.js';
@ -22,17 +21,22 @@ import path from 'path';
import { monitorPageManager } from './model/monitor/page/manager.js'; import { monitorPageManager } from './model/monitor/page/manager.js';
import { ExpressAuth } from '@auth/express'; import { ExpressAuth } from '@auth/express';
import { authConfig } from './model/auth.js'; import { authConfig } from './model/auth.js';
import { prometheusApiVersion } from './middleware/prometheus/index.js';
import { billingRouter } from './router/billing.js';
const app = express(); const app = express();
app.set('trust proxy', true); app.set('trust proxy', true);
app.use(prometheusApiVersion());
app.use(compression()); app.use(compression());
app.use( app.use(
express.json({ express.json({
limit: '10mb', limit: '10mb',
verify: (req, res, buf) => {
(req as any).rawBody = buf;
},
}) })
); );
app.use(passport.initialize());
app.use(morgan('tiny')); app.use(morgan('tiny'));
app.use(cors()); app.use(cors());
@ -49,6 +53,7 @@ app.use(
app.use('/health', healthRouter); app.use('/health', healthRouter);
app.use('/api/auth/*', ExpressAuth(authConfig)); app.use('/api/auth/*', ExpressAuth(authConfig));
app.use('/api/website', websiteRouter); app.use('/api/website', websiteRouter);
app.use('/api/billing', billingRouter);
app.use('/monitor', monitorRouter); app.use('/monitor', monitorRouter);
app.use('/telemetry', telemetryRouter); app.use('/telemetry', telemetryRouter);
app.use('/serverStatus', serverStatusRouter); app.use('/serverStatus', serverStatusRouter);

View File

@ -9,6 +9,7 @@ import { token } from '../model/notification/token/index.js';
import pMap from 'p-map'; import pMap from 'p-map';
import { sendFeedEventsNotify } from '../model/feed/event.js'; import { sendFeedEventsNotify } from '../model/feed/event.js';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { checkWorkspaceUsage } from '../model/billing/cronjob.js';
type WebsiteEventCountSqlReturn = { type WebsiteEventCountSqlReturn = {
workspace_id: string; workspace_id: string;
@ -29,6 +30,10 @@ export function initCronjob() {
checkFeedEventsNotify(FeedChannelNotifyFrequency.day), checkFeedEventsNotify(FeedChannelNotifyFrequency.day),
]); ]);
if (env.billing.enable) {
await checkWorkspaceUsage();
}
logger.info('Daily cronjob completed'); logger.info('Daily cronjob completed');
} catch (err) { } catch (err) {
logger.error('Daily cronjob error:', err); logger.error('Daily cronjob error:', err);
@ -242,14 +247,14 @@ async function statDailyUsage() {
} }
/** /**
* Clear over 2 week data * Clear over 1 month data
*/ */
async function clearMonitorDataDaily() { async function clearMonitorDataDaily() {
if (env.disableAutoClear) { if (env.disableAutoClear) {
return; return;
} }
const date = dayjs().subtract(2, 'weeks').toDate(); const date = dayjs().subtract(1, 'months').toDate();
logger.info( logger.info(
'[clearMonitorDataDaily] Start clear monitor data before:', '[clearMonitorDataDaily] Start clear monitor data before:',
date.toISOString() date.toISOString()
@ -269,14 +274,14 @@ async function clearMonitorDataDaily() {
} }
/** /**
* Clear over 2 week data * Clear over 1 month data
*/ */
async function clearMonitorEventDaily() { async function clearMonitorEventDaily() {
if (env.disableAutoClear) { if (env.disableAutoClear) {
return; return;
} }
const date = dayjs().subtract(2, 'weeks').toDate(); const date = dayjs().subtract(1, 'month').toDate();
logger.info( logger.info(
'[clearMonitorEventDaily] Start clear monitor data before:', '[clearMonitorEventDaily] Start clear monitor data before:',
date.toISOString() date.toISOString()
@ -386,6 +391,9 @@ async function dailyHTTPCertCheckNotify() {
); );
} }
/**
* Check feed events notify
*/
async function checkFeedEventsNotify( async function checkFeedEventsNotify(
notifyFrequency: FeedChannelNotifyFrequency notifyFrequency: FeedChannelNotifyFrequency
) { ) {

View File

@ -1,8 +1,14 @@
import { version } from '@tianji/shared'; import { version } from '@tianji/shared';
import axios from 'axios'; import axios from 'axios';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import timezone from 'dayjs/plugin/timezone.js';
axios.defaults.headers.common['User-Agent'] = `tianji/${version}`; axios.defaults.headers.common['User-Agent'] = `tianji/${version}`;
dayjs.extend(utc);
dayjs.extend(timezone);
(BigInt.prototype as any).toJSON = function () { (BigInt.prototype as any).toJSON = function () {
const int = Number.parseInt(this.toString()); const int = Number.parseInt(this.toString());
return int ?? this.toString(); return int ?? this.toString();

View File

@ -9,6 +9,7 @@ import { initCronjob } from './cronjob/index.js';
import { logger } from './utils/logger.js'; import { logger } from './utils/logger.js';
import { app } from './app.js'; import { app } from './app.js';
import { runMQWorker } from './mq/worker.js'; import { runMQWorker } from './mq/worker.js';
import { initCounter } from './utils/prometheus/index.js';
const port = env.port; const port = env.port;
@ -20,6 +21,8 @@ initSocketio(httpServer);
initCronjob(); initCronjob();
initCounter();
runMQWorker(); runMQWorker();
monitorManager.startAll(); monitorManager.startAll();

View File

@ -1,7 +1,3 @@
import { findUser } from '../model/user.js';
import passport from 'passport';
import { Handler } from 'express';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { jwtSecret } from '../utils/common.js'; import { jwtSecret } from '../utils/common.js';
@ -14,38 +10,6 @@ export interface JWTPayload {
role: string; role: string;
} }
passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtSecret,
issuer: jwtIssuer,
audience: jwtAudience,
},
function (jwt_payload, done) {
findUser(jwt_payload.id)
.then((user) => {
if (user) {
done(null, user);
} else {
done(null, false);
}
})
.catch((err) => {
done(err);
});
}
)
);
passport.serializeUser(function (user: any, cb) {
cb(null, { id: user.id, username: user.username });
});
passport.deserializeUser(function (user: any, cb) {
cb(null, user);
});
export function jwtSign(payload: JWTPayload): string { export function jwtSign(payload: JWTPayload): string {
const token = jwt.sign( const token = jwt.sign(
{ {
@ -72,9 +36,3 @@ export function jwtVerify(token: string): JWTPayload {
return payload as JWTPayload; return payload as JWTPayload;
} }
export function auth(): Handler {
return passport.authenticate('jwt', {
session: false,
});
}

View File

@ -0,0 +1 @@
This folder is fork from https://github.com/PayU/prometheus-api-metrics

View File

@ -0,0 +1,132 @@
import Prometheus from 'prom-client';
import * as utils from './utils.js';
import { SetupOptions, ApiMetricsOpts } from './types.js';
import { ExpressMiddleware } from './middleware.js';
export function prometheusApiVersion(options: ApiMetricsOpts = {}) {
const appVersion = '1.0.0';
const projectName = 'tianji';
const {
metricsPath,
defaultMetricsInterval = 10000,
durationBuckets,
requestSizeBuckets,
responseSizeBuckets,
useUniqueHistogramName,
metricsPrefix,
excludeRoutes,
includeQueryParams,
additionalLabels = [],
extractAdditionalLabelValuesFn,
} = options;
const setupOptions: SetupOptions = {};
setupOptions.metricsRoute = utils.validateInput({
input: metricsPath,
isValidInputFn: utils.isString,
defaultValue: '/_prom/metrics',
errorMessage: 'metricsPath should be an string',
});
setupOptions.excludeRoutes = utils.validateInput({
input: excludeRoutes,
isValidInputFn: utils.isArray,
defaultValue: [],
errorMessage: 'excludeRoutes should be an array',
});
setupOptions.includeQueryParams = includeQueryParams;
setupOptions.defaultMetricsInterval = defaultMetricsInterval;
setupOptions.additionalLabels = utils.validateInput({
input: additionalLabels,
isValidInputFn: utils.isArray,
defaultValue: [],
errorMessage: 'additionalLabels should be an array',
});
setupOptions.extractAdditionalLabelValuesFn = utils.validateInput({
input: extractAdditionalLabelValuesFn,
isValidInputFn: utils.isFunction,
defaultValue: () => ({}),
errorMessage: 'extractAdditionalLabelValuesFn should be a function',
});
const metricNames = utils.getMetricNames(
{
http_request_duration_seconds: 'http_request_duration_seconds',
app_version: 'app_version',
http_request_size_bytes: 'http_request_size_bytes',
http_response_size_bytes: 'http_response_size_bytes',
defaultMetricsPrefix: '',
},
useUniqueHistogramName ?? false,
metricsPrefix ?? '',
projectName
);
Prometheus.collectDefaultMetrics({
eventLoopMonitoringPrecision: defaultMetricsInterval,
prefix: `${metricNames.defaultMetricsPrefix}`,
});
PrometheusRegisterAppVersion(appVersion, metricNames.app_version);
const metricLabels = ['method', 'route', 'code', ...additionalLabels].filter(
Boolean
);
// Buckets for response time from 1ms to 500ms
const defaultDurationSecondsBuckets = [
0.001, 0.005, 0.015, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5,
];
// Buckets for request size from 5 bytes to 10000 bytes
const defaultSizeBytesBuckets = [
5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000,
];
setupOptions.responseTimeHistogram =
Prometheus.register.getSingleMetric(
metricNames.http_request_duration_seconds
) ||
new Prometheus.Histogram({
name: metricNames.http_request_duration_seconds,
help: 'Duration of HTTP requests in seconds',
labelNames: metricLabels,
buckets: durationBuckets || defaultDurationSecondsBuckets,
});
setupOptions.requestSizeHistogram =
Prometheus.register.getSingleMetric(metricNames.http_request_size_bytes) ||
new Prometheus.Histogram({
name: metricNames.http_request_size_bytes,
help: 'Size of HTTP requests in bytes',
labelNames: metricLabels,
buckets: requestSizeBuckets || defaultSizeBytesBuckets,
});
setupOptions.responseSizeHistogram =
Prometheus.register.getSingleMetric(metricNames.http_response_size_bytes) ||
new Prometheus.Histogram({
name: metricNames.http_response_size_bytes,
help: 'Size of HTTP response in bytes',
labelNames: metricLabels,
buckets: responseSizeBuckets || defaultSizeBytesBuckets,
});
const middleware = new ExpressMiddleware(setupOptions);
return middleware.middleware.bind(middleware);
}
function PrometheusRegisterAppVersion(appVersion: string, metricName: string) {
const version = new Prometheus.Gauge({
name: metricName,
help: 'The service version by package.json',
labelNames: ['version', 'major', 'minor', 'patch'],
});
const [major, minor, patch] = appVersion.split('.');
version.labels(appVersion, major, minor, patch).set(1);
}

View File

@ -0,0 +1,135 @@
import Prometheus from 'prom-client';
import { SetupOptions } from './types.js';
import { Request, Response, NextFunction } from 'express';
import * as utils from './utils.js';
export class ExpressMiddleware {
constructor(public setupOptions: SetupOptions) {}
_collectDefaultServerMetrics(timeout: number) {
const NUMBER_OF_CONNECTIONS_METRICS_NAME =
'expressjs_number_of_open_connections';
this.setupOptions.numberOfConnectionsGauge =
Prometheus.register.getSingleMetric(NUMBER_OF_CONNECTIONS_METRICS_NAME) ||
new Prometheus.Gauge({
name: NUMBER_OF_CONNECTIONS_METRICS_NAME,
help: 'Number of open connections to the Express.js server',
});
if (this.setupOptions.server) {
setInterval(this._getConnections.bind(this), timeout).unref();
}
}
_getConnections() {
if (this.setupOptions && this.setupOptions.server) {
this.setupOptions.server.getConnections((error: any, count: any) => {
if (error) {
// debug('Error while collection number of open connections', error);
} else {
this.setupOptions.numberOfConnectionsGauge.set(count);
}
});
}
}
_handleResponse(req: Request, res: Response) {
const responseLength = parseInt(res.get('Content-Length')!) || 0;
const route = this._getRoute(req);
if (
route &&
utils.shouldLogMetrics(this.setupOptions.excludeRoutes!, route)
) {
const labels = {
method: req.method,
route,
code: res.statusCode,
...this.setupOptions.extractAdditionalLabelValuesFn!(req, res),
};
this.setupOptions.requestSizeHistogram.observe(
labels,
(req as any).metrics.contentLength
);
(req as any).metrics.timer(labels);
this.setupOptions.responseSizeHistogram.observe(labels, responseLength);
}
}
_getRoute(req: Request) {
let route = req.baseUrl;
if (req.route) {
if (req.route.path !== '/') {
route = route ? route + req.route.path : req.route.path;
}
if (!route || route === '' || typeof route !== 'string') {
route = req.originalUrl.split('?')[0];
} else {
const splittedRoute = route.split('/');
const splittedUrl = req.originalUrl.split('?')[0].split('/');
const routeIndex = splittedUrl.length - splittedRoute.length + 1;
const baseUrl = splittedUrl.slice(0, routeIndex).join('/');
route = baseUrl + route;
}
if (
this.setupOptions.includeQueryParams === true &&
Object.keys(req.query).length > 0
) {
route = `${route}?${Object.keys(req.query)
.sort()
.map((queryParam) => `${queryParam}=<?>`)
.join('&')}`;
}
}
// nest.js - build request url pattern if exists
if (typeof req.params === 'object') {
Object.keys(req.params).forEach((paramName) => {
route = route.replace(req.params[paramName], ':' + paramName);
});
}
// this condition will evaluate to true only in
// express framework and no route was found for the request. if we log this metrics
// we'll risk in a memory leak since the route is not a pattern but a hardcoded string.
if (!route || route === '') {
// if (!req.route && res && res.statusCode === 404) {
route = 'N/A';
}
return route;
}
async middleware(req: Request, res: Response, next: NextFunction) {
if (!this.setupOptions.server && req.socket) {
this.setupOptions.server = (req.socket as any).server;
this._collectDefaultServerMetrics(
this.setupOptions.defaultMetricsInterval as any
);
}
const routeUrl = req.originalUrl || req.url;
if (routeUrl === this.setupOptions.metricsRoute) {
res.set('Content-Type', Prometheus.register.contentType);
return res.end(await Prometheus.register.metrics());
}
if (routeUrl === `${this.setupOptions.metricsRoute}.json`) {
return res.json(await Prometheus.register.getMetricsAsJSON());
}
(req as any).metrics = {
timer: (this.setupOptions as any).responseTimeHistogram.startTimer(),
contentLength: parseInt(req.get('content-length')!) || 0,
} as any;
res.once('finish', () => {
this._handleResponse(req, res);
});
return next();
}
}

View File

@ -0,0 +1,40 @@
import { Request, Response } from 'express';
import Prometheus from 'prom-client';
export interface ApiMetricsOpts {
metricsPath?: string;
defaultMetricsInterval?: number;
durationBuckets?: number[];
requestSizeBuckets?: number[];
responseSizeBuckets?: number[];
useUniqueHistogramName?: boolean;
metricsPrefix?: string;
excludeRoutes?: string[];
includeQueryParams?: boolean;
additionalLabels?: string[];
extractAdditionalLabelValuesFn?: (
req: Request,
res: Response
) => Record<string, unknown>;
}
export interface CollectorOpts {
durationBuckets?: number[];
countClientErrors?: boolean;
useUniqueHistogramName?: boolean;
prefix?: string;
}
export interface SetupOptions {
metricsRoute?: string;
excludeRoutes?: string[];
includeQueryParams?: boolean;
defaultMetricsInterval?: number;
additionalLabels?: string[];
extractAdditionalLabelValuesFn?: (
req: Request,
res: Response
) => Record<string, unknown>;
responseTimeHistogram?: Prometheus.Metric<string> | undefined;
[other: string]: any;
}

View File

@ -0,0 +1,63 @@
'use strict';
type MetricNames = { [key: string]: string };
const getMetricNames = (
metricNames: MetricNames,
useUniqueHistogramName: boolean,
metricsPrefix: string,
projectName: string
): MetricNames => {
const prefix = useUniqueHistogramName === true ? projectName : metricsPrefix;
if (prefix) {
Object.keys(metricNames).forEach((key) => {
metricNames[key] = `${prefix}_${metricNames[key]}`;
});
}
return metricNames;
};
const isArray = (input: unknown): input is any[] => Array.isArray(input);
const isFunction = (input: unknown): input is Function =>
typeof input === 'function';
const isString = (input: unknown): input is string => typeof input === 'string';
const shouldLogMetrics = (excludeRoutes: string[], route: string): boolean =>
excludeRoutes.every((path) => !route.includes(path));
interface ValidateInputParams<T> {
input: T | undefined;
isValidInputFn: (input: T) => boolean;
defaultValue: T;
errorMessage: string;
}
const validateInput = <T>({
input,
isValidInputFn,
defaultValue,
errorMessage,
}: ValidateInputParams<T>): T => {
if (typeof input !== 'undefined') {
if (isValidInputFn(input)) {
return input;
} else {
throw new Error(errorMessage);
}
}
return defaultValue;
};
export {
getMetricNames,
isArray,
isFunction,
isString,
shouldLogMetrics,
validateInput,
};

View File

@ -19,6 +19,7 @@ export const workspaceDashboardLayoutSchema = z.object({
export const workspaceSchema = z.object({ export const workspaceSchema = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
settings: z.record(z.string(), z.any()),
}); });
export const userInfoSchema = z.object({ export const userInfoSchema = z.object({

View File

@ -9,4 +9,5 @@ export const monitorPublicInfoSchema = MonitorModelSchema.pick({
id: true, id: true,
name: true, name: true,
type: true, type: true,
trendingMode: true,
}); });

View File

@ -0,0 +1,65 @@
import pMap from 'p-map';
import { prisma } from '../_client.js';
import { WorkspaceSubscriptionTier } from '@prisma/client';
import { logger } from '../../utils/logger.js';
import { getTierLimit } from './limit.js';
import { getWorkspaceUsage, pauseWorkspace } from './workspace.js';
import dayjs from 'dayjs';
import { getWorkspaceServiceCount } from '../workspace.js';
/**
* Check workspace usage
* if over limit, pause workspace
*/
export async function checkWorkspaceUsage() {
logger.info('[checkWorkspaceUsage] Start run checkWorkspaceUsage');
const workspaces = await prisma.workspace.findMany({
where: {
paused: false,
},
include: {
subscription: true,
},
});
await pMap(
workspaces,
async (workspace) => {
const tier =
workspace.subscription?.tier ?? WorkspaceSubscriptionTier.FREE;
if (tier === WorkspaceSubscriptionTier.UNLIMITED) {
return;
}
const [usage, serviceCount] = await Promise.all([
getWorkspaceUsage(
workspace.id,
dayjs().startOf('month').valueOf(),
dayjs().valueOf()
),
getWorkspaceServiceCount(workspace.id),
]);
const limit = getTierLimit(tier);
const overUsage =
serviceCount.website > limit.maxWebsiteCount ||
usage.websiteEventCount > limit.maxWebsiteEventCount ||
usage.monitorExecutionCount > limit.maxMonitorExecutionCount ||
usage.websiteEventCount > limit.maxWebsiteEventCount ||
usage.surveyCount > limit.maxSurveyCount ||
serviceCount.feed > limit.maxFeedChannelCount ||
usage.feedEventCount > limit.maxFeedEventCount;
if (overUsage) {
// pause workspace
await pauseWorkspace(workspace.id);
}
},
{
concurrency: 5,
}
);
}

View File

@ -0,0 +1,194 @@
import {
lemonSqueezySetup,
createCheckout,
updateSubscription,
cancelSubscription as lsCancelSubscription,
} from '@lemonsqueezy/lemonsqueezy.js';
import { env } from '../../utils/env.js';
import { prisma } from '../_client.js';
import { WorkspaceSubscriptionTier } from '@prisma/client';
export const billingAvailable = Boolean(env.billing.lemonSqueezy.apiKey);
if (billingAvailable) {
lemonSqueezySetup({
apiKey: env.billing.lemonSqueezy.apiKey,
onError: (error) => console.error('Error!', error),
});
}
export type SubscriptionTierType =
keyof typeof env.billing.lemonSqueezy.tierVariantId;
export function getTierNameByvariantId(variantId: string) {
const tierName = Object.keys(env.billing.lemonSqueezy.tierVariantId).find(
(key) =>
env.billing.lemonSqueezy.tierVariantId[key as SubscriptionTierType] ===
variantId
);
if (!tierName) {
throw new Error('Unknown Tier Name');
}
return tierName;
}
export function getTierEnumByVariantId(
variantId: string
): WorkspaceSubscriptionTier {
const name = getTierNameByvariantId(variantId);
if (name === 'free') {
return WorkspaceSubscriptionTier.FREE;
} else if (name === 'pro') {
return WorkspaceSubscriptionTier.PRO;
} else if (name === 'team') {
return WorkspaceSubscriptionTier.TEAM;
}
return WorkspaceSubscriptionTier.FREE; // not cool, fallback to free
}
export function checkIsValidProduct(storeId: string, variantId: string) {
if (String(storeId) !== env.billing.lemonSqueezy.storeId) {
return false;
}
if (
!Object.values(env.billing.lemonSqueezy.tierVariantId).includes(variantId)
) {
return false;
}
return true;
}
export async function createCheckoutBilling(
workspaceId: string,
userId: string,
subscriptionTier: SubscriptionTierType,
redirectUrl?: string
) {
const variantId = env.billing.lemonSqueezy.tierVariantId[subscriptionTier];
if (!variantId) {
throw new Error('Unknown subscription tier');
}
const userInfo = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (!userInfo) {
throw new Error('User not found');
}
const subscription = await prisma.lemonSqueezySubscription.findUnique({
where: {
workspaceId,
},
});
if (subscription) {
throw new Error('This workspace already has a subscription');
}
// not existed subscription
const checkout = await createCheckout(
env.billing.lemonSqueezy.storeId,
variantId,
{
checkoutData: {
name: userInfo.nickname ?? undefined,
email: userInfo.email ?? undefined,
custom: {
userId,
workspaceId,
},
},
productOptions: {
redirectUrl,
},
}
);
if (checkout.error) {
throw checkout.error;
}
const checkoutData = checkout.data.data;
return checkoutData;
}
export async function updateWorkspaceSubscription(
workspaceId: string,
subscriptionTier: WorkspaceSubscriptionTier
) {
const res = await prisma.workspaceSubscription.upsert({
where: {
workspaceId,
},
create: {
workspaceId,
tier: subscriptionTier,
},
update: {
tier: subscriptionTier,
},
});
return res;
}
export async function changeSubscription(
workspaceId: string,
subscriptionTier: SubscriptionTierType
) {
const variantId = env.billing.lemonSqueezy.tierVariantId[subscriptionTier];
if (!variantId) {
throw new Error('Unknown subscription tier');
}
const subscription = await prisma.lemonSqueezySubscription.findUnique({
where: {
workspaceId,
},
});
if (!subscription) {
throw new Error('Can not found existed subscription');
}
const res = await updateSubscription(subscription.subscriptionId, {
variantId: Number(variantId),
});
if (res.error) {
throw res.error;
}
return res.data.data;
}
export async function cancelSubscription(workspaceId: string) {
const subscription = await prisma.lemonSqueezySubscription.findUnique({
where: {
workspaceId,
},
});
if (!subscription) {
throw new Error('Can not found existed subscription');
}
const res = await lsCancelSubscription(subscription.subscriptionId);
if (res.error) {
throw res.error;
}
return res.data.data;
}

View File

@ -0,0 +1,61 @@
import { WorkspaceSubscriptionTier } from '@prisma/client';
import { z } from 'zod';
export const TierLimitSchema = z.object({
maxWebsiteCount: z.number(),
maxWebsiteEventCount: z.number(),
maxMonitorExecutionCount: z.number(),
maxSurveyCount: z.number(),
maxFeedChannelCount: z.number(),
maxFeedEventCount: z.number(),
});
type TierLimit = z.infer<typeof TierLimitSchema>;
/**
* Limit, Every month
*/
export function getTierLimit(tier: WorkspaceSubscriptionTier): TierLimit {
if (tier === WorkspaceSubscriptionTier.FREE) {
return {
maxWebsiteCount: 3,
maxWebsiteEventCount: 100_000,
maxMonitorExecutionCount: 100_000,
maxSurveyCount: 3,
maxFeedChannelCount: 3,
maxFeedEventCount: 10_000,
};
}
if (tier === WorkspaceSubscriptionTier.PRO) {
return {
maxWebsiteCount: 10,
maxWebsiteEventCount: 1_000_000,
maxMonitorExecutionCount: 1_000_000,
maxSurveyCount: 20,
maxFeedChannelCount: 20,
maxFeedEventCount: 100_000,
};
}
if (tier === WorkspaceSubscriptionTier.TEAM) {
return {
maxWebsiteCount: -1,
maxWebsiteEventCount: 20_000_000,
maxMonitorExecutionCount: 20_000_000,
maxSurveyCount: -1,
maxFeedChannelCount: -1,
maxFeedEventCount: 1_000_000,
};
}
// Unlimited
return {
maxWebsiteCount: -1,
maxWebsiteEventCount: -1,
maxMonitorExecutionCount: -1,
maxSurveyCount: -1,
maxFeedChannelCount: -1,
maxFeedEventCount: -1,
};
}

View File

@ -0,0 +1,56 @@
import { WorkspaceSubscriptionTier } from '@prisma/client';
import { prisma } from '../_client.js';
export async function getWorkspaceUsage(
workspaceId: string,
startAt: number,
endAt: number
) {
const res = await prisma.workspaceDailyUsage.aggregate({
where: {
workspaceId,
date: {
gte: new Date(startAt),
lte: new Date(endAt),
},
},
_sum: {
websiteAcceptedCount: true,
websiteEventCount: true,
monitorExecutionCount: true,
surveyCount: true,
feedEventCount: true,
},
});
return {
websiteAcceptedCount: res._sum.websiteAcceptedCount ?? 0,
websiteEventCount: res._sum.websiteEventCount ?? 0,
monitorExecutionCount: res._sum.monitorExecutionCount ?? 0,
surveyCount: res._sum.surveyCount ?? 0,
feedEventCount: res._sum.feedEventCount ?? 0,
};
}
export async function getWorkspaceSubscription(
workspaceId: string
): Promise<WorkspaceSubscriptionTier> {
const subscription = await prisma.workspaceSubscription.findFirst({
where: {
workspaceId,
},
});
return subscription?.tier ?? WorkspaceSubscriptionTier.FREE;
}
export async function pauseWorkspace(workspaceId: string) {
await prisma.workspace.update({
where: {
id: workspaceId,
},
data: {
paused: true,
},
});
}

View File

@ -1,3 +1,4 @@
import dayjs from 'dayjs';
import { prisma } from '../_client.js'; import { prisma } from '../_client.js';
import { monitorPublicInfoSchema } from '../_schema/monitor.js'; import { monitorPublicInfoSchema } from '../_schema/monitor.js';
import { MonitorManager } from './manager.js'; import { MonitorManager } from './manager.js';
@ -67,3 +68,57 @@ export function getMonitorRecentData(
}) })
.then((arr) => arr.reverse()); .then((arr) => arr.reverse());
} }
export async function getMonitorSummaryWithDay(
monitorId: string,
beforeDay: number = 30
) {
interface MonitorSummaryItem {
day: string;
total_count: number;
up_count: number;
up_rate: number;
}
const list = await prisma.$queryRaw<MonitorSummaryItem[]>`
SELECT
TO_CHAR(DATE("createdAt"), 'YYYY-MM-DD') AS day,
COUNT(1) AS total_count,
SUM(CASE WHEN "value" >= 0 THEN 1 ELSE 0 END) AS up_count,
(SUM(CASE WHEN "value" >= 0 THEN 1 ELSE 0 END) * 100.0 / COUNT(1)) AS up_rate
FROM
"MonitorData"
WHERE
"monitorId" = ${monitorId} AND
"createdAt" >= CURRENT_DATE - INTERVAL '1 day' * ${beforeDay}
GROUP BY
DATE("createdAt")
ORDER BY
day;`;
const map: Record<string, MonitorSummaryItem> = {};
for (const item of list) {
const date = dayjs(item.day).format('YYYY-MM-DD');
map[date] = item;
}
return Array.from({ length: beforeDay }).map((_, i) => {
const target = dayjs().subtract(i, 'days').format('YYYY-MM-DD');
if (map[target]) {
return {
day: target,
totalCount: Number(map[target].total_count),
upCount: Number(map[target].up_count),
upRate: Number(Number(map[target].up_rate).toFixed(1)),
};
} else {
return {
day: target,
totalCount: 0,
upCount: 0,
upRate: 0,
};
}
});
}

View File

@ -1,7 +1,9 @@
import { Monitor, Notification } from '@prisma/client'; import { Monitor } from '@prisma/client';
import { prisma } from '../_client.js'; import { prisma } from '../_client.js';
import { MonitorRunner } from './runner.js'; import { MonitorRunner } from './runner.js';
import { logger } from '../../utils/logger.js'; import { logger } from '../../utils/logger.js';
import { MonitorWithNotification } from './types.js';
import { promMonitorRunnerCounter } from '../../utils/prometheus/client.js';
export type MonitorUpsertData = Pick< export type MonitorUpsertData = Pick<
Monitor, Monitor,
@ -13,8 +15,6 @@ export type MonitorUpsertData = Pick<
payload: Record<string, any>; payload: Record<string, any>;
}; };
type MonitorWithNotification = Monitor & { notifications: Notification[] };
export class MonitorManager { export class MonitorManager {
private monitorRunner: Record<string, MonitorRunner> = {}; private monitorRunner: Record<string, MonitorRunner> = {};
private isStarted = false; private isStarted = false;
@ -64,9 +64,7 @@ export class MonitorManager {
delete this.monitorRunner[monitor.id]; delete this.monitorRunner[monitor.id];
} }
const runner = (this.monitorRunner[monitor.id] = new MonitorRunner( const runner = await this.createRunner(monitor);
monitor
));
runner.startMonitor(); runner.startMonitor();
return monitor; return monitor;
@ -112,7 +110,7 @@ export class MonitorManager {
Promise.all( Promise.all(
monitors.map(async (m) => { monitors.map(async (m) => {
try { try {
const runner = new MonitorRunner(m); const runner = await this.createRunner(m);
this.monitorRunner[m.id] = runner; this.monitorRunner[m.id] = runner;
await runner.startMonitor(); await runner.startMonitor();
} catch (err) { } catch (err) {
@ -128,11 +126,37 @@ export class MonitorManager {
return this.monitorRunner[monitorId]; return this.monitorRunner[monitorId];
} }
createRunner(monitor: MonitorWithNotification) { /**
* Restart all runner basic on workspace id
*/
restartWithWorkspaceId(workspaceId: string) {
Object.values(this.monitorRunner).map((runner) => {
if (runner.workspace.id === workspaceId) {
this.createRunner(runner.monitor);
}
});
}
/**
* create runner
*/
async createRunner(monitor: MonitorWithNotification) {
if (this.monitorRunner[monitor.id]) {
this.monitorRunner[monitor.id].stopMonitor();
}
const workspace = await prisma.workspace.findUniqueOrThrow({
where: {
id: monitor.workspaceId,
},
});
const runner = (this.monitorRunner[monitor.id] = new MonitorRunner( const runner = (this.monitorRunner[monitor.id] = new MonitorRunner(
workspace,
monitor monitor
)); ));
promMonitorRunnerCounter.set(Object.keys(this.monitorRunner).length);
return runner; return runner;
} }
} }

View File

@ -1,4 +1,4 @@
import { Monitor, Notification } from '@prisma/client'; import { Notification, Workspace } from '@prisma/client';
import { subscribeEventBus } from '../../ws/shared.js'; import { subscribeEventBus } from '../../ws/shared.js';
import { prisma } from '../_client.js'; import { prisma } from '../_client.js';
import { monitorProviders } from './provider/index.js'; import { monitorProviders } from './provider/index.js';
@ -8,6 +8,8 @@ import { logger } from '../../utils/logger.js';
import { token } from '../notification/token/index.js'; import { token } from '../notification/token/index.js';
import { ContentToken } from '../notification/token/type.js'; import { ContentToken } from '../notification/token/type.js';
import { createAuditLog } from '../auditLog.js'; import { createAuditLog } from '../auditLog.js';
import { MonitorWithNotification } from './types.js';
import { get } from 'lodash-es';
/** /**
* Class which actually run monitor data collect * Class which actually run monitor data collect
@ -17,7 +19,14 @@ export class MonitorRunner {
timer: NodeJS.Timeout | null = null; timer: NodeJS.Timeout | null = null;
retriedNum = 0; retriedNum = 0;
constructor(public monitor: Monitor & { notifications: Notification[] }) {} constructor(
public workspace: Workspace,
public monitor: MonitorWithNotification
) {}
getTimezone(): string {
return get(this.workspace, ['settings', 'timezone']) || 'utc';
}
/** /**
* Start single monitor * Start single monitor
@ -74,9 +83,9 @@ export class MonitorRunner {
); );
await this.notify(`[${monitor.name}] 🔴 Down`, [ await this.notify(`[${monitor.name}] 🔴 Down`, [
token.text( token.text(
`[${monitor.name}] 🔴 Down\nTime: ${dayjs().format( `[${monitor.name}] 🔴 Down\nTime: ${dayjs()
'YYYY-MM-DD HH:mm:ss (z)' .tz(this.getTimezone())
)}` .format('YYYY-MM-DD HH:mm:ss (z)')}`
), ),
]); ]);
currentStatus = 'DOWN'; currentStatus = 'DOWN';
@ -88,9 +97,9 @@ export class MonitorRunner {
); );
await this.notify(`[${monitor.name}] ✅ Up`, [ await this.notify(`[${monitor.name}] ✅ Up`, [
token.text( token.text(
`[${monitor.name}] ✅ Up\nTime: ${dayjs().format( `[${monitor.name}] ✅ Up\nTime: ${dayjs()
'YYYY-MM-DD HH:mm:ss (z)' .tz(this.getTimezone())
)}` .format('YYYY-MM-DD HH:mm:ss (z)')}`
), ),
]); ]);
currentStatus = 'UP'; currentStatus = 'UP';

View File

@ -0,0 +1,5 @@
import { Monitor, Notification } from '@prisma/client';
export type MonitorWithNotification = Monitor & {
notifications: Notification[];
};

View File

@ -1,4 +1,5 @@
import { ServerStatusInfo } from '../../types/index.js'; import { ServerStatusInfo } from '../../types/index.js';
import { promServerCounter } from '../utils/prometheus/client.js';
import { createSubscribeInitializer, subscribeEventBus } from '../ws/shared.js'; import { createSubscribeInitializer, subscribeEventBus } from '../ws/shared.js';
import { isServerOnline } from '@tianji/shared'; import { isServerOnline } from '@tianji/shared';
@ -42,6 +43,13 @@ export function recordServerStatus(info: ServerStatusInfo) {
payload, payload,
}; };
promServerCounter.set(
{
workspaceId,
},
Object.keys(serverMap[workspaceId]).length
);
subscribeEventBus.emit( subscribeEventBus.emit(
'onServerStatusUpdate', 'onServerStatusUpdate',
workspaceId, workspaceId,

View File

@ -5,8 +5,9 @@ import { jwtVerify } from '../middleware/auth.js';
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { AdapterUser } from '@auth/core/adapters'; import { AdapterUser } from '@auth/core/adapters';
import { md5 } from '../utils/common.js'; import { md5, sha256 } from '../utils/common.js';
import { logger } from '../utils/logger.js'; import { logger } from '../utils/logger.js';
import { promUserCounter } from '../utils/prometheus/client.js';
async function hashPassword(password: string) { async function hashPassword(password: string) {
return await bcryptjs.hash(password, 10); return await bcryptjs.hash(password, 10);
@ -41,6 +42,7 @@ export const createUserSelect = {
select: { select: {
id: true, id: true,
name: true, name: true,
settings: true,
}, },
}, },
}, },
@ -87,6 +89,8 @@ export async function createAdminUser(username: string, password: string) {
return user; return user;
}); });
promUserCounter.inc();
return user; return user;
} }
@ -129,6 +133,8 @@ export async function createUser(username: string, password: string) {
return user; return user;
}); });
promUserCounter.inc();
return user; return user;
} }
@ -335,3 +341,56 @@ export async function leaveWorkspace(userId: string, workspaceId: string) {
throw new Error('Leave Workspace Failed.'); throw new Error('Leave Workspace Failed.');
} }
} }
/**
* Generate User Api Key, for user to call api
*/
export async function generateUserApiKey(userId: string, expiredAt?: Date) {
const apiKey = `sk_${sha256(`${userId}.${Date.now()}`)}`;
const result = await prisma.userApiKey.create({
data: {
apiKey,
userId,
expiredAt,
},
});
return result.apiKey;
}
/**
* Verify User Api Key
*/
export async function verifyUserApiKey(apiKey: string) {
const result = await prisma.userApiKey.findUnique({
where: {
apiKey,
},
select: {
user: true,
expiredAt: true,
},
});
if (result?.expiredAt && result.expiredAt.valueOf() < Date.now()) {
throw new Error('Api Key has been expired.');
}
if (!result) {
throw new Error('Api Key not found');
}
prisma.userApiKey.update({
where: {
apiKey,
},
data: {
usage: {
increment: 1,
},
},
});
return result.user;
}

View File

@ -72,3 +72,47 @@ export async function getWorkspaceWebsiteDateRange(websiteId: string) {
min: res._min.createdAt, min: res._min.createdAt,
}; };
} }
export async function getWorkspaceServiceCount(workspaceId: string) {
const [website, monitor, telemetry, page, survey, feed] = await Promise.all([
prisma.website.count({
where: {
workspaceId,
},
}),
prisma.monitor.count({
where: {
workspaceId,
},
}),
prisma.telemetry.count({
where: {
workspaceId,
},
}),
prisma.monitorStatusPage.count({
where: {
workspaceId,
},
}),
prisma.survey.count({
where: {
workspaceId,
},
}),
prisma.feedChannel.count({
where: {
workspaceId,
},
}),
]);
return {
website,
monitor,
telemetry,
page,
survey,
feed,
};
}

View File

@ -8,6 +8,7 @@
}, },
"scripts": { "scripts": {
"dev": "tsx watch --env-file=.env ./main.ts", "dev": "tsx watch --env-file=.env ./main.ts",
"dev:debug": "tsx --inspect-brk --env-file=.env ./main.ts",
"build": "tsc", "build": "tsc",
"postinstall": "pnpm db:generate", "postinstall": "pnpm db:generate",
"check:type": "tsc --noEmit --skipLibCheck", "check:type": "tsc --noEmit --skipLibCheck",
@ -25,6 +26,7 @@
"dependencies": { "dependencies": {
"@auth/core": "^0.34.1", "@auth/core": "^0.34.1",
"@auth/express": "^0.5.5", "@auth/express": "^0.5.5",
"@lemonsqueezy/lemonsqueezy.js": "^3.3.1",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@prisma/client": "5.14.0", "@prisma/client": "5.14.0",
"@tianji/shared": "workspace:^", "@tianji/shared": "workspace:^",
@ -42,6 +44,7 @@
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"detect-browser": "^5.3.0", "detect-browser": "^5.3.0",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"easy-currency-symbol": "^1.0.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
@ -57,9 +60,9 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"nodemailer": "^6.9.8", "nodemailer": "^6.9.8",
"passport": "^0.7.0", "p-map": "4.0.0",
"passport-jwt": "^4.0.1",
"ping": "^0.4.4", "ping": "^0.4.4",
"prom-client": "^15.1.3",
"puppeteer": "23.4.1", "puppeteer": "23.4.1",
"request-ip": "^3.3.0", "request-ip": "^3.3.0",
"socket.io": "^4.7.4", "socket.io": "^4.7.4",
@ -88,8 +91,6 @@
"@types/morgan": "^1.9.5", "@types/morgan": "^1.9.5",
"@types/node": "^18.17.12", "@types/node": "^18.17.12",
"@types/nodemailer": "^6.4.11", "@types/nodemailer": "^6.4.11",
"@types/passport": "^1.0.12",
"@types/passport-jwt": "^3.0.9",
"@types/ping": "^0.4.2", "@types/ping": "^0.4.2",
"@types/request-ip": "^0.0.38", "@types/request-ip": "^0.0.38",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
@ -97,7 +98,6 @@
"@types/tcp-ping": "^0.1.5", "@types/tcp-ping": "^0.1.5",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"execa": "^5.1.1", "execa": "^5.1.1",
"p-map": "4.0.0",
"prisma": "5.14.0", "prisma": "5.14.0",
"prisma-json-types-generator": "3.0.3", "prisma-json-types-generator": "3.0.3",
"prisma-zod-generator": "0.8.13", "prisma-zod-generator": "0.8.13",

View File

@ -0,0 +1,35 @@
-- CreateTable
CREATE TABLE "LemonSqueezySubscription" (
"subscriptionId" TEXT NOT NULL,
"workspaceId" VARCHAR(30) NOT NULL,
"storeId" TEXT NOT NULL,
"productId" TEXT NOT NULL,
"variantId" TEXT NOT NULL,
"status" TEXT NOT NULL,
"cardBrand" TEXT NOT NULL,
"cardLastFour" TEXT NOT NULL,
"renewsAt" TIMESTAMPTZ(6) NOT NULL,
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
CONSTRAINT "LemonSqueezySubscription_pkey" PRIMARY KEY ("subscriptionId")
);
-- CreateTable
CREATE TABLE "LemonSqueezyWebhookEvent" (
"id" VARCHAR(30) NOT NULL,
"eventName" TEXT NOT NULL,
"payload" JSON NOT NULL,
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "LemonSqueezyWebhookEvent_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "LemonSqueezySubscription_subscriptionId_key" ON "LemonSqueezySubscription"("subscriptionId");
-- CreateIndex
CREATE UNIQUE INDEX "LemonSqueezySubscription_workspaceId_key" ON "LemonSqueezySubscription"("workspaceId");
-- CreateIndex
CREATE UNIQUE INDEX "LemonSqueezyWebhookEvent_id_key" ON "LemonSqueezyWebhookEvent"("id");

View File

@ -0,0 +1,23 @@
-- CreateEnum
CREATE TYPE "WorkspaceSubscriptionTier" AS ENUM ('FREE', 'PRO', 'TEAM', 'UNLIMITED');
-- CreateTable
CREATE TABLE "WorkspaceSubscription" (
"id" VARCHAR(30) NOT NULL,
"workspaceId" VARCHAR(30) NOT NULL,
"tier" "WorkspaceSubscriptionTier" NOT NULL DEFAULT 'FREE',
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
CONSTRAINT "WorkspaceSubscription_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "WorkspaceSubscription_workspaceId_key" ON "WorkspaceSubscription"("workspaceId");
-- AddForeignKey
ALTER TABLE "WorkspaceSubscription" ADD CONSTRAINT "WorkspaceSubscription_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Set admin workspace to UNLIMITED
INSERT INTO "WorkspaceSubscription" ("id", "workspaceId", "tier", "createdAt", "updatedAt") VALUES ('cm1yqv4xd002154qnfhzg9i5d', 'clnzoxcy10001vy2ohi4obbi0', 'UNLIMITED', '2024-10-07 08:22:45.169+00', '2024-10-07 08:22:45.169+00');

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "FeedChannel" ADD COLUMN "webhookSignature" VARCHAR(100) NOT NULL DEFAULT '';

View File

@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "UserApiKey" (
"apiKey" VARCHAR(128) NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
"expiredAt" TIMESTAMP(3),
CONSTRAINT "UserApiKey_pkey" PRIMARY KEY ("apiKey")
);
-- CreateIndex
CREATE UNIQUE INDEX "UserApiKey_apiKey_key" ON "UserApiKey"("apiKey");
-- AddForeignKey
ALTER TABLE "UserApiKey" ADD CONSTRAINT "UserApiKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "UserApiKey" ADD COLUMN "usage" INTEGER NOT NULL DEFAULT 0;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Workspace" ADD COLUMN "paused" BOOLEAN NOT NULL DEFAULT false;

View File

@ -34,6 +34,18 @@ model User {
accounts Account[] accounts Account[]
sessions Session[] sessions Session[]
workspaces WorkspacesOnUsers[] workspaces WorkspacesOnUsers[]
apiKeys UserApiKey[]
}
model UserApiKey {
apiKey String @id @unique @db.VarChar(128)
userId String
usage Int @default(0)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
expiredAt DateTime?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
} }
model Account { model Account {
@ -84,9 +96,12 @@ model Workspace {
/// [CommonPayload] /// [CommonPayload]
/// @zod.custom(imports.CommonPayloadSchema) /// @zod.custom(imports.CommonPayloadSchema)
settings Json @default("{}") settings Json @default("{}")
paused Boolean @default(false) // if workspace over billing, its will marked as pause and not receive and input.
createdAt DateTime @default(now()) @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6)
subscription WorkspaceSubscription?
users WorkspacesOnUsers[] users WorkspacesOnUsers[]
websites Website[] websites Website[]
notifications Notification[] notifications Notification[]
@ -116,6 +131,49 @@ model WorkspacesOnUsers {
@@index([workspaceId]) @@index([workspaceId])
} }
enum WorkspaceSubscriptionTier {
FREE
PRO
TEAM
UNLIMITED // This type should only use for special people or admin workspace
}
model WorkspaceSubscription {
id String @id() @default(cuid()) @db.VarChar(30)
workspaceId String @unique @db.VarChar(30)
tier WorkspaceSubscriptionTier @default(FREE) // free, pro, team
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
workspace Workspace @relation(fields: [workspaceId], references: [id], onUpdate: Cascade, onDelete: Cascade)
}
model LemonSqueezySubscription {
subscriptionId String @id @unique
workspaceId String @unique @db.VarChar(30)
storeId String
productId String
variantId String
status String
cardBrand String
cardLastFour String
renewsAt DateTime @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
}
model LemonSqueezyWebhookEvent {
id String @id @unique @default(cuid()) @db.VarChar(30)
eventName String
/// [CommonPayload]
/// @zod.custom(imports.CommonPayloadSchema)
payload Json @db.Json // Other payload info get from query params, should be a object
createdAt DateTime @default(now()) @db.Timestamptz(6)
}
model Website { model Website {
id String @id @unique @default(cuid()) @db.VarChar(30) id String @id @unique @default(cuid()) @db.VarChar(30)
workspaceId String @db.VarChar(30) workspaceId String @db.VarChar(30)
@ -543,6 +601,7 @@ model FeedChannel {
id String @id @default(cuid()) @db.VarChar(30) id String @id @default(cuid()) @db.VarChar(30)
workspaceId String @db.VarChar(30) workspaceId String @db.VarChar(30)
name String name String
webhookSignature String @default("") @db.VarChar(100)
notifyFrequency FeedChannelNotifyFrequency @default(day) notifyFrequency FeedChannelNotifyFrequency @default(day)
createdAt DateTime @default(now()) @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6)

View File

@ -7,6 +7,7 @@ export const FeedChannelModelSchema = z.object({
id: z.string(), id: z.string(),
workspaceId: z.string(), workspaceId: z.string(),
name: z.string(), name: z.string(),
webhookSignature: z.string(),
notifyFrequency: z.nativeEnum(FeedChannelNotifyFrequency), notifyFrequency: z.nativeEnum(FeedChannelNotifyFrequency),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),

View File

@ -1,9 +1,13 @@
export * from "./user.js" export * from "./user.js"
export * from "./userapikey.js"
export * from "./account.js" export * from "./account.js"
export * from "./session.js" export * from "./session.js"
export * from "./verificationtoken.js" export * from "./verificationtoken.js"
export * from "./workspace.js" export * from "./workspace.js"
export * from "./workspacesonusers.js" export * from "./workspacesonusers.js"
export * from "./workspacesubscription.js"
export * from "./lemonsqueezysubscription.js"
export * from "./lemonsqueezywebhookevent.js"
export * from "./website.js" export * from "./website.js"
export * from "./websitesession.js" export * from "./websitesession.js"
export * from "./websiteevent.js" export * from "./websiteevent.js"

View File

@ -0,0 +1,16 @@
import * as z from "zod"
import * as imports from "./schemas/index.js"
export const LemonSqueezySubscriptionModelSchema = z.object({
subscriptionId: z.string(),
workspaceId: z.string(),
storeId: z.string(),
productId: z.string(),
variantId: z.string(),
status: z.string(),
cardBrand: z.string(),
cardLastFour: z.string(),
renewsAt: z.date(),
createdAt: z.date(),
updatedAt: z.date(),
})

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