Compare commits
3 Commits
master
...
feat/surve
Author | SHA1 | Date | |
---|---|---|---|
|
cbd6821def | ||
|
8a6a75f3f5 | ||
|
fd63f2a22e |
146
CHANGELOG.md
@ -1,151 +1,5 @@
|
||||
|
||||
|
||||
## [1.16.5](https://github.com/msgbyte/tianji/compare/v1.16.4...v1.16.5) (2024-11-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add webhookSignature in feed channel ([6b3631e](https://github.com/msgbyte/tianji/commit/6b3631eae186b9cacf64d0ddcfbb66378e041281))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add key to Fragment in map for monitor items ([9949b97](https://github.com/msgbyte/tianji/commit/9949b973bd63b4ad6b5e71f7b819442f505c09a6))
|
||||
* retrieve date as string ([a8a47ed](https://github.com/msgbyte/tianji/commit/a8a47ed94dda87c3fe4cdecc0acb9a31f53f00a5))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* fix ci problem ([59b8746](https://github.com/msgbyte/tianji/commit/59b874644fd3427bc86cd2a7e948054e827de080))
|
||||
* refactor status header and add typescript and translation support ([f637ade](https://github.com/msgbyte/tianji/commit/f637ade70f230fbf472bdee84105c9b284d6b8d4))
|
||||
* update amount in stripe ([2725056](https://github.com/msgbyte/tianji/commit/272505669e450d882930cbf594dac39a879b2072))
|
||||
* update webhooks signature api guide ([266b08f](https://github.com/msgbyte/tianji/commit/266b08f2da16d0457a5a44b4a7a251d28502abc9))
|
||||
|
||||
## [1.16.4](https://github.com/msgbyte/tianji/compare/v1.16.3...v1.16.4) (2024-10-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add stripe feed integration ([09d0f02](https://github.com/msgbyte/tianji/commit/09d0f02d844159565e97bb64f076e0bbe218ce98))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* update currency symbols in feed ([98298c4](https://github.com/msgbyte/tianji/commit/98298c43670326b4e2300a6bbdeee3daa53f0eb3))
|
||||
|
||||
## [1.16.3](https://github.com/msgbyte/tianji/compare/v1.16.2...v1.16.3) (2024-10-24)
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* fix ci problem and upgrade version ([1c5737e](https://github.com/msgbyte/tianji/commit/1c5737e588d19e0657be6437792cf4484b6fdddb))
|
||||
|
||||
## [1.16.2](https://github.com/msgbyte/tianji/compare/v1.16.1...v1.16.2) (2024-10-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add prometheus report support ([fcb8f22](https://github.com/msgbyte/tianji/commit/fcb8f221168281ab710d3d3f12064a99d17b39e7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix a bug which will match incorrect path [#115](https://github.com/msgbyte/tianji/issues/115) ([79667a9](https://github.com/msgbyte/tianji/commit/79667a9644b78451400acb6a6bbf07b6ca61e6e0))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* update README ([1df32dc](https://github.com/msgbyte/tianji/commit/1df32dc2579f32649afd6c008512c1190a45fd9e))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* fix ci problem ([554f902](https://github.com/msgbyte/tianji/commit/554f9025847defe0b05492cf07a5dc8acc6c3685))
|
||||
* update openapi document ([e402ee1](https://github.com/msgbyte/tianji/commit/e402ee1688bb77d83463ce70c5e730c97f68a695))
|
||||
|
||||
## [1.16.1](https://github.com/msgbyte/tianji/compare/v1.16.0...v1.16.1) (2024-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add test notify ([4e3fd9d](https://github.com/msgbyte/tianji/commit/4e3fd9db64629f7721e6092b86b06144c47f521d))
|
||||
* add timezone support [#114](https://github.com/msgbyte/tianji/issues/114) ([c7e20df](https://github.com/msgbyte/tianji/commit/c7e20df516bf3a991ce46c937223948bcdb6b8f0))
|
||||
* add workspace settings manage ([3dca8fc](https://github.com/msgbyte/tianji/commit/3dca8fc27c82bd96dbab423b111e4de57f3b4bd8))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* update cronjob clear time ([83850f2](https://github.com/msgbyte/tianji/commit/83850f2981ded0b6624556ee3430f684752b8ea3))
|
||||
|
||||
## [1.16.0](https://github.com/msgbyte/tianji/compare/v1.15.8...v1.16.0) (2024-10-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add click event for status page item which allow hide/show chart ([279e616](https://github.com/msgbyte/tianji/commit/279e616bee510ee5b0c5a3c9a3705a79efd5d3cb))
|
||||
* add daily monitor data display for public ([dcff57f](https://github.com/msgbyte/tianji/commit/dcff57fe69273c7f9b3dd9c28e8acc9cb6e430a9))
|
||||
* add monitor summary function ([bbb8d88](https://github.com/msgbyte/tianji/commit/bbb8d881168df695ccc70743f46320b39c1d7718))
|
||||
* add MonitorLatestResponse and up status summary ([316b954](https://github.com/msgbyte/tianji/commit/316b95467d49b3ebe93d03006d4b90f9ca482262))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix reporter memory leak problem [#103](https://github.com/msgbyte/tianji/issues/103) ([7f70557](https://github.com/msgbyte/tianji/commit/7f70557c776c35e4e01a5533d2c05cecc711e113))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add border radius in smtp template ([f553f15](https://github.com/msgbyte/tianji/commit/f553f157dd9708d553c9d6cfca4d119a62d849c3))
|
||||
* change public summary display logic ([e5e77db](https://github.com/msgbyte/tianji/commit/e5e77dbdeeeecb773237b84e3c671dd16e61d458))
|
||||
* fix ci problem ([820b25b](https://github.com/msgbyte/tianji/commit/820b25baedc6fec02010ca19b43e4da99bf4b820))
|
||||
* ignore unknown sentry log ([527f734](https://github.com/msgbyte/tianji/commit/527f734bc442458018d86df9a7e750a8e8de4495))
|
||||
* let version text more prominent ([61980b3](https://github.com/msgbyte/tianji/commit/61980b37d3cecce32fa87b2b9810f4c715990a71))
|
||||
* rename old tsconfig paths ([2a503ca](https://github.com/msgbyte/tianji/commit/2a503ca2501e705430c0c35cb7c8279927c1d4d5))
|
||||
|
||||
## [1.15.8](https://github.com/msgbyte/tianji/compare/v1.15.7...v1.15.8) (2024-10-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add payload for feed event integration and send function ([572d96b](https://github.com/msgbyte/tianji/commit/572d96babb348858911105659bfe304e869915e4))
|
||||
* add ping animation in website realtime visitor ([6da0e6f](https://github.com/msgbyte/tianji/commit/6da0e6f415e863448cd36246eb16e1f09dcd8a79))
|
||||
* add plausible tracking(for testing) ([6474cef](https://github.com/msgbyte/tianji/commit/6474cefd896b36b872460786b11005b8deaf4436))
|
||||
* add realtime datarange which can visit data more easy ([f3d8f55](https://github.com/msgbyte/tianji/commit/f3d8f5543d4e277fe34940ce98cd552dee45f2a8))
|
||||
* add survey curl example code ([5d54ca1](https://github.com/msgbyte/tianji/commit/5d54ca1cbc01b69b9bd03d167edd0db242df350e))
|
||||
* add survey webhook ([de57242](https://github.com/msgbyte/tianji/commit/de572426ebf99e99ff97ce49caf0b8ac13b68154))
|
||||
* sdk add send feed function export ([f5933ec](https://github.com/msgbyte/tianji/commit/f5933ec0548fb4ac327152b6d7afe7ca2978bade))
|
||||
* survey add webhook url field which can send webhook when receive any survey ([f00163b](https://github.com/msgbyte/tianji/commit/f00163b2f107bcf08d4ff398fa5dbd92ac36fda8))
|
||||
* time event chart legend add some interaction ([4b78771](https://github.com/msgbyte/tianji/commit/4b7877155fd54416fb9231a02aca0e868aec97d2))
|
||||
|
||||
|
||||
### Document
|
||||
|
||||
* add shacdn to website ([763810e](https://github.com/msgbyte/tianji/commit/763810e8b7e6cd41fdc0d83d28262dc7d747e4bf))
|
||||
* add sitemap to improve SEO ([384224c](https://github.com/msgbyte/tianji/commit/384224cb624030522057df22527312957665d8e9))
|
||||
* add website more language: de, fr, ja, zh-Hans ([7bda542](https://github.com/msgbyte/tianji/commit/7bda5420c5f22f6205d2dbd5086dd0bdbbc7f558))
|
||||
* change code command line style ([8c5c417](https://github.com/msgbyte/tianji/commit/8c5c417a197531c187452fc4937d034d3bdc05a7))
|
||||
* remove used blog directory ([3d9d032](https://github.com/msgbyte/tianji/commit/3d9d03296e9fb26eb363b2dba2f830f2eb9d58f3))
|
||||
* resolve build problem with update source document content ([9e6e031](https://github.com/msgbyte/tianji/commit/9e6e03117cc3bd37058563e31817034876d35450))
|
||||
* update depenpendency to resolve issue of docusaurus build ([de38363](https://github.com/msgbyte/tianji/commit/de38363315275ecbc7384c935c313940fca1d4fc))
|
||||
* upgrade openapi ([1e57905](https://github.com/msgbyte/tianji/commit/1e57905f3239cc4fb3a949b469161dc9c8d2b40c))
|
||||
|
||||
|
||||
### Others
|
||||
|
||||
* add CodeExample component ([29f184c](https://github.com/msgbyte/tianji/commit/29f184c15d36e42af750261f81ad67b49fe58c0b))
|
||||
* comment sitemap to make sure its can build safe ([9b9799e](https://github.com/msgbyte/tianji/commit/9b9799ec6f846eb40762f5ca8cc0aa6c27fb7e02))
|
||||
* fix ci problem ([a32f3d9](https://github.com/msgbyte/tianji/commit/a32f3d9824a09a2d8c9e97ba211ee5d44c8e2763))
|
||||
* fix isolated-vm version ([43b4c9f](https://github.com/msgbyte/tianji/commit/43b4c9fe3763673dc54b61b01e50bc5b3d24a371))
|
||||
* fix version of postman-code-generators ([eaffe3a](https://github.com/msgbyte/tianji/commit/eaffe3ab21022215a76c3441ef0b9b2c37386227))
|
||||
* improve display of visitor map if data is too much ([9bc8c63](https://github.com/msgbyte/tianji/commit/9bc8c63fe2ea2ab080e7db3cf7d0c7636fabf8d1))
|
||||
* migrate monitor data chart to recharts and remove @ant-design/charts ([c0e2ef0](https://github.com/msgbyte/tianji/commit/c0e2ef0fe8f5520a7b935eeeb44f6be9224e56a4))
|
||||
* update ci run trigger path ([7322ad7](https://github.com/msgbyte/tianji/commit/7322ad741dcfc5c033b5057e1862e91d27244f7f))
|
||||
* update pnpm lock file to resolve some magic problem ([064dbe9](https://github.com/msgbyte/tianji/commit/064dbe9985767b32492de4264d08c26206318cd4))
|
||||
* update survey edit form ([a218c22](https://github.com/msgbyte/tianji/commit/a218c2239725deb5bcdee2e8d2de377e04dec941))
|
||||
* upgrade @radix-ui/react-scroll-area version ([9d559b9](https://github.com/msgbyte/tianji/commit/9d559b93d16c130cf58649e0f12edf9e795ba8a5))
|
||||
* upgrade @tianji/website docusaurus version ([e46f970](https://github.com/msgbyte/tianji/commit/e46f97097a593fe4bd5a8946237fd2f46fea69f6))
|
||||
* use prebuilt rather than deploy build ([e51a880](https://github.com/msgbyte/tianji/commit/e51a88044fcef7727b2e7f17c5dd9eff08329cdc))
|
||||
|
||||
## [1.15.7](https://github.com/msgbyte/tianji/compare/v1.15.6...v1.15.7) (2024-10-03)
|
||||
|
||||
|
||||
|
@ -37,8 +37,9 @@ It's good to specialize in one thing, if we are experts in related abilities we
|
||||
- [x] waitlist
|
||||
- [x] survey
|
||||
- [ ] survey page
|
||||
- [x] lighthouse report
|
||||
- [ ] lighthouse report
|
||||
- [x] hooks
|
||||
- [ ] links
|
||||
- [x] helm install support
|
||||
- [x] allow install from public
|
||||
- [ ] improve monitor reporter usage
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tianji",
|
||||
"private": true,
|
||||
"version": "1.16.5",
|
||||
"version": "1.15.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently --kill-others npm:dev:server npm:dev:web",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tianji-client-sdk",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.0",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
|
@ -47,7 +47,7 @@ export const OpenAPI: OpenAPIConfig = {
|
||||
PASSWORD: undefined,
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
VERSION: '1.16.1',
|
||||
VERSION: '1.15.7',
|
||||
WITH_CREDENTIALS: false,
|
||||
interceptors: {
|
||||
request: new Interceptors(),
|
||||
|
@ -120,10 +120,10 @@ export class WorkspaceService {
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static workspaceDelete(data: $OpenApiTs['/workspace//{workspaceId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}/del']['delete']['res'][200]> {
|
||||
public static workspaceDelete(data: $OpenApiTs['/workspace//{workspaceId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace//{workspaceId}']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace//{workspaceId}/del',
|
||||
url: '/workspace//{workspaceId}',
|
||||
path: {
|
||||
workspaceId: data.workspaceId
|
||||
}
|
||||
@ -146,25 +146,6 @@ 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.workspaceId
|
||||
@ -604,10 +585,28 @@ export class MonitorService {
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static monitorGet(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/get']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/get']['get']['res'][200]> {
|
||||
public static monitorGet(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}/get',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
monitorId: data.monitorId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.monitorId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static monitorDelete(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
monitorId: data.monitorId
|
||||
@ -649,24 +648,6 @@ export class MonitorService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.monitorId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static monitorDelete(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/del']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}/del',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
monitorId: data.monitorId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
@ -734,42 +715,6 @@ export class MonitorService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.monitorId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static monitorPublicSummary(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicSummary']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicSummary']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}/publicSummary',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
monitorId: data.monitorId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
* @param data.monitorId
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static monitorPublicData(data: $OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicData']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/monitor/{monitorId}/publicData']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/monitor/{monitorId}/publicData',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
monitorId: data.monitorId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data The data for the request.
|
||||
* @param data.workspaceId
|
||||
@ -1213,10 +1158,10 @@ export class SurveyService {
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static surveyGet(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/get']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/get']['get']['res'][200]> {
|
||||
public static surveyGet(data: $OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/workspace/{workspaceId}/survey/{surveyId}/get',
|
||||
url: '/workspace/{workspaceId}/survey/{surveyId}',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
surveyId: data.surveyId
|
||||
@ -1527,10 +1472,10 @@ export class FeedService {
|
||||
* @returns unknown Successful response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static feedDeleteChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/del']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}/del']['delete']['res'][200]> {
|
||||
public static feedDeleteChannel(data: $OpenApiTs['/workspace/{workspaceId}/feed/{channelId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/feed/{channelId}']['delete']['res'][200]> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/workspace/{workspaceId}/feed/{channelId}/del',
|
||||
url: '/workspace/{workspaceId}/feed/{channelId}',
|
||||
path: {
|
||||
workspaceId: data.workspaceId,
|
||||
channelId: data.channelId
|
||||
|
@ -49,9 +49,6 @@ export type $OpenApiTs = {
|
||||
workspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
settings: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
@ -88,9 +85,6 @@ export type $OpenApiTs = {
|
||||
workspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
settings: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
@ -128,9 +122,6 @@ export type $OpenApiTs = {
|
||||
workspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
settings: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
@ -166,9 +157,6 @@ export type $OpenApiTs = {
|
||||
workspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
settings: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
@ -202,9 +190,6 @@ export type $OpenApiTs = {
|
||||
workspace: {
|
||||
id: string;
|
||||
name: string;
|
||||
settings: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
@ -226,14 +211,11 @@ export type $OpenApiTs = {
|
||||
200: {
|
||||
id: string;
|
||||
name: string;
|
||||
settings: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
'/workspace//{workspaceId}/del': {
|
||||
'/workspace//{workspaceId}': {
|
||||
delete: {
|
||||
req: {
|
||||
workspaceId: string;
|
||||
@ -271,30 +253,6 @@ 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': {
|
||||
post: {
|
||||
req: {
|
||||
@ -734,7 +692,7 @@ export type $OpenApiTs = {
|
||||
};
|
||||
};
|
||||
};
|
||||
'/workspace/{workspaceId}/monitor/{monitorId}/get': {
|
||||
'/workspace/{workspaceId}/monitor/{monitorId}': {
|
||||
get: {
|
||||
req: {
|
||||
monitorId: string;
|
||||
@ -764,43 +722,9 @@ export type $OpenApiTs = {
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
'/monitor/getPublicInfo': {
|
||||
post: {
|
||||
delete: {
|
||||
req: {
|
||||
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;
|
||||
};
|
||||
};
|
||||
monitorId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
res: {
|
||||
@ -825,10 +749,41 @@ export type $OpenApiTs = {
|
||||
};
|
||||
};
|
||||
};
|
||||
'/workspace/{workspaceId}/monitor/{monitorId}/del': {
|
||||
delete: {
|
||||
'/monitor/getPublicInfo': {
|
||||
post: {
|
||||
req: {
|
||||
monitorId: string;
|
||||
requestBody: {
|
||||
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;
|
||||
};
|
||||
res: {
|
||||
@ -921,42 +876,6 @@ 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': {
|
||||
get: {
|
||||
req: {
|
||||
@ -1465,14 +1384,13 @@ export type $OpenApiTs = {
|
||||
};
|
||||
feedChannelIds: Array<(string)>;
|
||||
feedTemplate: string;
|
||||
webhookUrl: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
'/workspace/{workspaceId}/survey/{surveyId}/get': {
|
||||
'/workspace/{workspaceId}/survey/{surveyId}': {
|
||||
get: {
|
||||
req: {
|
||||
surveyId: string;
|
||||
@ -1496,7 +1414,6 @@ export type $OpenApiTs = {
|
||||
};
|
||||
feedChannelIds: Array<(string)>;
|
||||
feedTemplate: string;
|
||||
webhookUrl: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
@ -1582,7 +1499,6 @@ export type $OpenApiTs = {
|
||||
};
|
||||
feedChannelIds: Array<(string)>;
|
||||
feedTemplate: string;
|
||||
webhookUrl: string;
|
||||
};
|
||||
workspaceId: string;
|
||||
};
|
||||
@ -1604,7 +1520,6 @@ export type $OpenApiTs = {
|
||||
};
|
||||
feedChannelIds: Array<(string)>;
|
||||
feedTemplate: string;
|
||||
webhookUrl: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@ -1626,7 +1541,6 @@ export type $OpenApiTs = {
|
||||
};
|
||||
feedChannelIds?: Array<(string)>;
|
||||
feedTemplate?: string;
|
||||
webhookUrl?: string;
|
||||
};
|
||||
surveyId: string;
|
||||
workspaceId: string;
|
||||
@ -1649,7 +1563,6 @@ export type $OpenApiTs = {
|
||||
};
|
||||
feedChannelIds: Array<(string)>;
|
||||
feedTemplate: string;
|
||||
webhookUrl: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@ -1680,7 +1593,6 @@ export type $OpenApiTs = {
|
||||
};
|
||||
feedChannelIds: Array<(string)>;
|
||||
feedTemplate: string;
|
||||
webhookUrl: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@ -1908,7 +1820,7 @@ export type $OpenApiTs = {
|
||||
};
|
||||
};
|
||||
};
|
||||
'/workspace/{workspaceId}/feed/{channelId}/del': {
|
||||
'/workspace/{workspaceId}/feed/{channelId}': {
|
||||
delete: {
|
||||
req: {
|
||||
channelId: string;
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tianji-client-react",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
} from 'tianji-client-sdk';
|
||||
|
||||
type SurveyInfo =
|
||||
openApiClient.$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}/get']['get']['res']['200'];
|
||||
openApiClient.$OpenApiTs['/workspace/{workspaceId}/survey/{surveyId}']['get']['res']['200'];
|
||||
|
||||
interface UseTianjiSurveyOptions {
|
||||
baseUrl?: string;
|
||||
|
1737
pnpm-lock.yaml
@ -59,10 +59,7 @@ func main() {
|
||||
|
||||
interval := *Interval
|
||||
|
||||
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
httpClient := &http.Client{}
|
||||
ticker := time.Tick(time.Duration(interval) * time.Second)
|
||||
|
||||
log.Println("Start reporting...")
|
||||
log.Println("Mode:", *Mode)
|
||||
@ -81,10 +78,10 @@ func main() {
|
||||
if *Mode == "udp" {
|
||||
sendUDPPack(*parsedURL, payload)
|
||||
} else {
|
||||
sendHTTPRequest(*parsedURL, payload, httpClient)
|
||||
sendHTTPRequest(*parsedURL, payload)
|
||||
}
|
||||
|
||||
<-ticker.C
|
||||
<-ticker
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,7 +125,7 @@ func sendUDPPack(url url.URL, payload ReportData) {
|
||||
/**
|
||||
* Send HTTP Request to report server data
|
||||
*/
|
||||
func sendHTTPRequest(_url url.URL, payload ReportData, client *http.Client) {
|
||||
func sendHTTPRequest(_url url.URL, payload ReportData) {
|
||||
jsonData, err := jsoniter.Marshal(payload)
|
||||
if err != nil {
|
||||
log.Println("Error encoding JSON:", err)
|
||||
@ -151,6 +148,7 @@ func sendHTTPRequest(_url url.URL, payload ReportData, client *http.Client) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-tianji-report-version", version)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Println("Send request error:", err)
|
||||
|
@ -14,8 +14,6 @@ import {
|
||||
LuAreaChart,
|
||||
LuBellDot,
|
||||
LuFilePieChart,
|
||||
LuKanbanSquare,
|
||||
LuKeyRound,
|
||||
LuMonitorDot,
|
||||
LuSearch,
|
||||
LuServer,
|
||||
@ -173,22 +171,6 @@ export const CommandPanel: React.FC<CommandPanelProps> = React.memo((props) => {
|
||||
<LuBellDot className="mr-2 h-4 w-4" />
|
||||
<span>{t('Notifications')}</span>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={handleJump({
|
||||
to: '/settings/apiKey',
|
||||
})}
|
||||
>
|
||||
<LuKeyRound className="mr-2 h-4 w-4" />
|
||||
<span>{t('Api Key')}</span>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
onSelect={handleJump({
|
||||
to: '/settings/usage',
|
||||
})}
|
||||
>
|
||||
<LuKanbanSquare className="mr-2 h-4 w-4" />
|
||||
<span>{t('Usage')}</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
@ -1,32 +0,0 @@
|
||||
import { cn } from '@/utils/style';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
|
||||
interface CopyableTextProps extends PropsWithChildren {
|
||||
className?: string;
|
||||
text: string;
|
||||
}
|
||||
export const CopyableText: React.FC<CopyableTextProps> = React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
const handleClick = useEvent(() => {
|
||||
copy(props.text);
|
||||
toast.success(t('Copied'));
|
||||
});
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'cursor-pointer select-none rounded bg-white bg-opacity-10 px-2',
|
||||
'hover:bg-white hover:bg-opacity-20',
|
||||
props.className
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{props.children ?? props.text}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
CopyableText.displayName = 'CopyableText';
|
@ -1,9 +1,10 @@
|
||||
import { useResizeObserver } from '@/hooks/useResizeObserver';
|
||||
import { getStatusBgColorClassName, HealthStatus } from '@/utils/health';
|
||||
import { cn } from '@/utils/style';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
type HealthStatus = 'health' | 'error' | 'warning' | 'none';
|
||||
|
||||
export interface HealthBarBeat {
|
||||
title?: string;
|
||||
status: HealthStatus;
|
||||
@ -51,7 +52,12 @@ export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
|
||||
'h-4 w-[5px]': size === 'small',
|
||||
'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',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardHeader } from './ui/card';
|
||||
import { formatNumber } from '@/utils/common';
|
||||
import { LuAlertCircle } from 'react-icons/lu';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
interface UsageCardProps {
|
||||
title: string;
|
||||
current: number;
|
||||
limit?: number;
|
||||
}
|
||||
export const UsageCard: React.FC<UsageCardProps> = React.memo((props) => {
|
||||
const { title, current, limit } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card className="relative h-full w-full overflow-hidden">
|
||||
{limit && (
|
||||
<div
|
||||
className="absolute h-full bg-black bg-opacity-5 dark:bg-white dark:bg-opacity-10"
|
||||
style={{ width: `${(current / limit) * 100}%` }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{limit && current > limit && (
|
||||
<div className="absolute right-2 top-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LuAlertCircle stroke={colors.red['500']} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div>
|
||||
{t(
|
||||
'Exceeded the limit, please upgrade your plan or your workspace will be paused soon.'
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-muted-foreground">{title}</CardHeader>
|
||||
<CardContent>
|
||||
{limit && limit >= 0 ? (
|
||||
<div>
|
||||
<span className="text-2xl font-bold">{formatNumber(current)}</span>{' '}
|
||||
/ <span>{formatNumber(limit)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<span className="text-2xl font-bold">{formatNumber(current)}</span>{' '}
|
||||
/ <span>∞</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
UsageCard.displayName = 'UsageCard';
|
@ -1,166 +0,0 @@
|
||||
import { Check } from 'lucide-react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { defaultErrorHandler, trpc } from '@/api/trpc';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { cn } from '@/utils/style';
|
||||
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
||||
import { LuInfo } from 'react-icons/lu';
|
||||
|
||||
interface SubscriptionSelectionProps {
|
||||
tier: 'FREE' | 'PRO' | 'TEAM' | 'UNLIMITED' | undefined;
|
||||
}
|
||||
export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> =
|
||||
React.memo((props) => {
|
||||
const { tier } = props;
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const checkoutMutation = trpc.billing.checkout.useMutation({
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const handleCheckoutSubscribe = useEvent(
|
||||
async (tier: 'free' | 'pro' | 'team') => {
|
||||
const { url } = await checkoutMutation.mutateAsync({
|
||||
workspaceId,
|
||||
tier,
|
||||
redirectUrl: location.href,
|
||||
});
|
||||
|
||||
location.href = url;
|
||||
}
|
||||
);
|
||||
|
||||
const plans = [
|
||||
{
|
||||
id: 'FREE',
|
||||
name: t('Free'),
|
||||
price: 0,
|
||||
features: [
|
||||
t('Basic trial'),
|
||||
t('Basic Usage'),
|
||||
t('Up to 3 websites'),
|
||||
t('Up to 3 surveys'),
|
||||
t('Up to 3 feed channels'),
|
||||
t('100K website events per month'),
|
||||
t('100K monitor execution per month'),
|
||||
t('10K feed event per month'),
|
||||
t('Discord Community Support'),
|
||||
],
|
||||
onClick: () => handleCheckoutSubscribe('free'),
|
||||
},
|
||||
{
|
||||
id: 'PRO',
|
||||
name: 'Pro',
|
||||
price: 19.99,
|
||||
features: [
|
||||
t('Sufficient for most situations'),
|
||||
t('Priority access to advanced features'),
|
||||
t('Up to 10 websites'),
|
||||
t('Up to 20 surveys'),
|
||||
t('Up to 20 feed channels'),
|
||||
t('1M website events per month'),
|
||||
t('1M monitor execution per month'),
|
||||
t('100K feed events per month'),
|
||||
t('Discord Community Support'),
|
||||
],
|
||||
onClick: () => handleCheckoutSubscribe('pro'),
|
||||
},
|
||||
{
|
||||
id: 'TEAM',
|
||||
name: 'Team',
|
||||
price: 99.99,
|
||||
features: [
|
||||
t('Fully sufficient'),
|
||||
t('Priority access to advanced features'),
|
||||
t('Unlimited websites'),
|
||||
t('Unlimited surveys'),
|
||||
t('Unlimited feed channels'),
|
||||
t('20M website events per month'),
|
||||
t('20M monitor execution per month'),
|
||||
t('1M feed events per month'),
|
||||
t('Priority email support'),
|
||||
],
|
||||
onClick: () => handleCheckoutSubscribe('team'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="mb-8 text-center text-3xl font-bold">
|
||||
{t('Subscription Plan')}
|
||||
</h1>
|
||||
|
||||
<Alert className="mb-4">
|
||||
<LuInfo className="h-4 w-4" />
|
||||
<AlertTitle>{t('Current Plan')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('Your Current Plan is:')}{' '}
|
||||
<span className="font-bold">{tier}</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{plans.map((plan) => {
|
||||
const isCurrent = plan.id === tier;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={plan.name}
|
||||
className={cn('flex flex-col', isCurrent && 'border-primary')}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription>${plan.price} per month</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow">
|
||||
<ul className="space-y-2">
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center">
|
||||
<Check className="mr-2 h-4 w-4 text-green-500" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{isCurrent ? (
|
||||
<Button className="w-full" disabled variant="outline">
|
||||
{t('Current')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={checkoutMutation.isLoading}
|
||||
onClick={plan.onClick}
|
||||
>
|
||||
{t('{{action}} to {{plan}}', {
|
||||
action:
|
||||
plans.indexOf(plan) <
|
||||
plans.findIndex((p) => p.id === tier)
|
||||
? t('Downgrade')
|
||||
: t('Upgrade'),
|
||||
plan: plan.name,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SubscriptionSelection.displayName = 'SubscriptionSelection';
|
@ -3,11 +3,8 @@ import React from 'react';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { CodeExample } from '../CodeExample';
|
||||
|
||||
interface FeedApiGuideProps {
|
||||
channelId: string;
|
||||
webhookSignature?: string;
|
||||
}
|
||||
export const FeedApiGuide: React.FC<FeedApiGuideProps> = React.memo((props) => {
|
||||
export const FeedApiGuide: React.FC<{ channelId: string }> = React.memo(
|
||||
(props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -20,11 +17,11 @@ export const FeedApiGuide: React.FC<FeedApiGuideProps> = React.memo((props) => {
|
||||
example={{
|
||||
curl: {
|
||||
label: 'curl',
|
||||
code: generateCurlCode(props.channelId, props.webhookSignature),
|
||||
code: generateCurlCode(props.channelId),
|
||||
},
|
||||
fetch: {
|
||||
label: 'fetch',
|
||||
code: generateFetchCode(props.channelId, props.webhookSignature),
|
||||
code: generateFetchCode(props.channelId),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@ -35,24 +32,12 @@ export const FeedApiGuide: React.FC<FeedApiGuideProps> = React.memo((props) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
FeedApiGuide.displayName = 'FeedApiGuide';
|
||||
|
||||
function generateCurlCode(channelId: string, webhookSignature?: string) {
|
||||
if (webhookSignature) {
|
||||
return `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-Webhook-Signature: ${webhookSignature}" \\
|
||||
-d '{
|
||||
"eventName": "test name",
|
||||
"eventContent": "test content",
|
||||
"tags": ["test"],
|
||||
"source": "custom",
|
||||
"important": false
|
||||
}'`;
|
||||
}
|
||||
|
||||
return `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\
|
||||
function generateCurlCode(channelId: string) {
|
||||
const code = `curl -X POST ${window.location.origin}/open/feed/${channelId}/send \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"eventName": "test name",
|
||||
@ -61,27 +46,12 @@ function generateCurlCode(channelId: string, webhookSignature?: string) {
|
||||
"source": "custom",
|
||||
"important": false
|
||||
}'`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
function generateFetchCode(channelId: string, webhookSignature?: string) {
|
||||
if (webhookSignature) {
|
||||
return `fetch('${window.location.origin}/open/feed/${channelId}/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Webhook-Signature': '${webhookSignature}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
eventName: 'test name',
|
||||
eventContent: 'test content',
|
||||
tags: ['test'],
|
||||
source: 'custom',
|
||||
important: false,
|
||||
})
|
||||
})`;
|
||||
}
|
||||
|
||||
return `fetch('${window.location.origin}/open/feed/${channelId}/send', {
|
||||
function generateFetchCode(channelId: string) {
|
||||
const code = `fetch('${window.location.origin}/open/feed/${channelId}/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@ -94,4 +64,6 @@ function generateFetchCode(channelId: string, webhookSignature?: string) {
|
||||
important: false,
|
||||
})
|
||||
})`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
@ -24,13 +24,9 @@ import {
|
||||
SelectValue,
|
||||
} from '../ui/select';
|
||||
import { NotificationPicker } from '../notification/NotificationPicker';
|
||||
import { LuRefreshCcw } from 'react-icons/lu';
|
||||
import md5 from 'md5';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const addFormSchema = z.object({
|
||||
name: z.string(),
|
||||
webhookSignature: z.string().default(''),
|
||||
notificationIds: z.array(z.string()).default([]),
|
||||
notifyFrequency: z.enum(['none', 'event', 'day', 'week', 'month']),
|
||||
});
|
||||
@ -49,7 +45,6 @@ export const FeedChannelEditForm: React.FC<FeedChannelEditFormProps> =
|
||||
resolver: zodResolver(addFormSchema),
|
||||
defaultValues: props.defaultValues ?? {
|
||||
name: 'New Channel',
|
||||
webhookSignature: '',
|
||||
notificationIds: [],
|
||||
notifyFrequency: 'none',
|
||||
},
|
||||
@ -84,38 +79,6 @@ 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
|
||||
control={form.control}
|
||||
name="notificationIds"
|
||||
|
@ -4,11 +4,9 @@ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
import { CodeBlock } from '../CodeBlock';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { SiSentry } from 'react-icons/si';
|
||||
import { FaStripe } from 'react-icons/fa6';
|
||||
|
||||
export const FeedIntegration: React.FC<{
|
||||
feedId: string;
|
||||
webhookSignature: string;
|
||||
}> = React.memo((props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -59,22 +57,6 @@ export const FeedIntegration: React.FC<{
|
||||
}
|
||||
/>
|
||||
|
||||
<FeedIntegrationItem
|
||||
icon={<FaStripe size={32} />}
|
||||
label="Stripe"
|
||||
content={
|
||||
<div>
|
||||
<div className="text-lg font-bold">{t('Receive Webhooks')}</div>
|
||||
|
||||
<div>{t('Add sentry webhook with url')}:</div>
|
||||
|
||||
<CodeBlock
|
||||
code={`${window.location.origin}/open/feed/${props.feedId}/stripe`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div onClick={() => window.open('/feed/playground', '_blank')}>
|
||||
<FeedIntegrationItemTrigger
|
||||
icon={<LuTestTube2 size={32} />}
|
||||
@ -93,7 +75,7 @@ export const FeedIntegration: React.FC<{
|
||||
|
||||
<CodeBlock
|
||||
code={`POST ${window.location.origin}/open/feed/${props.feedId}/send
|
||||
${props.webhookSignature ? `\nHeader:\nX-Webhook-Signature: ${props.webhookSignature}\n` : ''}
|
||||
|
||||
Body
|
||||
{
|
||||
eventName: "",
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
useUserInfo,
|
||||
useUserStore,
|
||||
} from '@/store/user';
|
||||
import { languages } from '@/utils/i18n';
|
||||
import { languages } from '@/utils/constants';
|
||||
import { useTranslation, setLanguage } from '@i18next-toolkit/react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { version } from '@/utils/env';
|
||||
@ -234,7 +234,7 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuLabel className="text-gray-500">
|
||||
<DropdownMenuLabel className="text-muted-foreground dark:text-muted">
|
||||
v{version}
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuContent>
|
||||
|
@ -1,49 +0,0 @@
|
||||
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';
|
@ -1,35 +1,13 @@
|
||||
import { AreaConfig, Area } from '@ant-design/charts';
|
||||
import { Select } from 'antd';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { get, takeRight, uniqBy } from 'lodash-es';
|
||||
import { max, min, uniqBy } from 'lodash-es';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useSocketSubscribeList } from '../../api/socketio';
|
||||
import { trpc } from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import { getMonitorProvider, getProviderDisplay } from './provider';
|
||||
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(
|
||||
(props) => {
|
||||
@ -37,7 +15,6 @@ export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { monitorId } = props;
|
||||
const [rangeType, setRangeType] = useState('recent');
|
||||
const { colors } = useTheme();
|
||||
const subscribedDataList = useSocketSubscribeList(
|
||||
'onMonitorReceiveNewData',
|
||||
{
|
||||
@ -84,26 +61,100 @@ export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
|
||||
|
||||
const providerInfo = getMonitorProvider(monitorInfo?.type ?? '');
|
||||
|
||||
const { data } = useMemo(() => {
|
||||
const { data, annotations } = useMemo(() => {
|
||||
const annotations: AreaConfig['annotations'] = [];
|
||||
let start: number | null = null;
|
||||
let fetchedData = rangeType === 'recent' ? _recentData : _data;
|
||||
const data = takeRight(
|
||||
uniqBy([...fetchedData, ...subscribedDataList], 'createdAt'),
|
||||
fetchedData.length
|
||||
const data = uniqBy(
|
||||
[...fetchedData, ...subscribedDataList],
|
||||
'createdAt'
|
||||
).map((d, i, arr) => {
|
||||
const value = d.value > 0 ? d.value : null;
|
||||
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 {
|
||||
value,
|
||||
time,
|
||||
};
|
||||
});
|
||||
|
||||
return { data };
|
||||
return { data, annotations };
|
||||
}, [_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 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 (
|
||||
<div>
|
||||
<div className="mb-4 text-right">
|
||||
@ -121,76 +172,7 @@ export const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo(
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<Area {...config} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -61,10 +61,6 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
const testNotifyScriptMutation = trpc.monitor.testNotifyScript.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const trpcUtils = trpc.useContext();
|
||||
|
||||
@ -233,15 +229,6 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
|
||||
label: t('Show Badge'),
|
||||
onClick: () => setShowBadge(true),
|
||||
},
|
||||
{
|
||||
key: 'testNotify',
|
||||
label: t('Test Notify'),
|
||||
onClick: () =>
|
||||
testNotifyScriptMutation.mutateAsync({
|
||||
workspaceId,
|
||||
monitorId,
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
|
@ -1,146 +0,0 @@
|
||||
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';
|
@ -1,22 +1,9 @@
|
||||
import { AppRouterOutput, trpc } from '@/api/trpc';
|
||||
import React, { useMemo, useReducer } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { bodySchema } from './schema';
|
||||
import { Empty } from 'antd';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
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';
|
||||
import { MonitorListItem } from '../MonitorListItem';
|
||||
|
||||
interface StatusPageBodyProps {
|
||||
workspaceId: string;
|
||||
@ -37,13 +24,12 @@ export const StatusPageBody: React.FC<StatusPageBodyProps> = React.memo(
|
||||
}, [info.body]);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200/80 dark:border-gray-700/25">
|
||||
<div>
|
||||
{body.groups.map((group) => (
|
||||
<div key={group.key} className="m-4 rounded-lg bg-neutral-500/15">
|
||||
<div className="ml-4 pl-2.5 pt-2.5 text-lg font-semibold">
|
||||
{group.title}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 rounded-md p-2.5">
|
||||
<div key={group.key} className="mb-6">
|
||||
<div className="mb-2 text-lg font-semibold">{group.title}</div>
|
||||
|
||||
<div className="flex flex-col gap-4 rounded-md border border-gray-200 p-2.5 dark:border-gray-700">
|
||||
{group.children.length === 0 && (
|
||||
<Empty description={t('No any monitor has been set')} />
|
||||
)}
|
||||
@ -51,14 +37,12 @@ export const StatusPageBody: React.FC<StatusPageBodyProps> = React.memo(
|
||||
{group.children.map((item) => {
|
||||
if (item.type === 'monitor') {
|
||||
return (
|
||||
<React.Fragment key={item.key}>
|
||||
<Separator />
|
||||
<StatusItemMonitor
|
||||
key={item.key}
|
||||
workspaceId={props.workspaceId}
|
||||
monitorId={item.id}
|
||||
id={item.id}
|
||||
showCurrent={item.showCurrent ?? false}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@ -74,145 +58,33 @@ export const StatusPageBody: React.FC<StatusPageBodyProps> = React.memo(
|
||||
StatusPageBody.displayName = 'StatusPageBody';
|
||||
|
||||
export const StatusItemMonitor: React.FC<{
|
||||
monitorId: string;
|
||||
id: string;
|
||||
showCurrent: boolean;
|
||||
workspaceId: string;
|
||||
}> = React.memo((props) => {
|
||||
const { data: info } = trpc.monitor.getPublicInfo.useQuery(
|
||||
{
|
||||
monitorIds: [props.monitorId],
|
||||
},
|
||||
{
|
||||
select: (data) => data[0],
|
||||
}
|
||||
);
|
||||
|
||||
const { data: list = [], isLoading } = trpc.monitor.publicSummary.useQuery({
|
||||
workspaceId: props.workspaceId,
|
||||
monitorId: props.monitorId,
|
||||
const { data: list = [], isLoading } = trpc.monitor.getPublicInfo.useQuery({
|
||||
monitorIds: [props.id],
|
||||
});
|
||||
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = list[0];
|
||||
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<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
|
||||
<MonitorListItem
|
||||
key={item.id}
|
||||
workspaceId={props.workspaceId}
|
||||
monitorId={info.id}
|
||||
monitorType={info.type}
|
||||
monitorId={item.id}
|
||||
monitorName={item.name}
|
||||
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';
|
||||
|
||||
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';
|
||||
|
@ -1,199 +0,0 @@
|
||||
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;
|
@ -9,7 +9,6 @@ import clsx from 'clsx';
|
||||
import { useRequest } from '../../../hooks/useRequest';
|
||||
import { ColorSchemeSwitcher } from '../../ColorSchemeSwitcher';
|
||||
import { StatusPageServices } from './Services';
|
||||
import { StatusPageHeader } from './StatusHeader';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
import { Helmet } from 'react-helmet';
|
||||
@ -147,14 +146,8 @@ export const MonitorStatusPage: React.FC<MonitorStatusPageProps> = React.memo(
|
||||
</div>
|
||||
)}
|
||||
|
||||
{info && (
|
||||
<div className="my-6">
|
||||
<StatusPageHeader info={info} workspaceId={info.workspaceId} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desc */}
|
||||
<div className="mb-6 text-center">
|
||||
<div className="mb-4">
|
||||
<MarkdownViewer value={info?.description ?? ''} />
|
||||
</div>
|
||||
|
||||
|
@ -1,59 +0,0 @@
|
||||
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 }
|
@ -22,9 +22,6 @@ export function useGlobalConfig(): AppRouterOutput['global']['config'] {
|
||||
{
|
||||
staleTime: 1000 * 60 * 60 * 1, // 1 hour
|
||||
onSuccess(data) {
|
||||
/**
|
||||
* Call anonymous telemetry if not disabled
|
||||
*/
|
||||
if (data.disableAnonymousTelemetry !== true) {
|
||||
callAnonymousTelemetry();
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { useColorSchema } from '@/store/settings';
|
||||
import { theme, ThemeConfig } from 'antd';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { colord } from 'colord';
|
||||
import twColors from 'tailwindcss/colors';
|
||||
|
||||
const THEME_CONFIG = 'tianji.theme';
|
||||
|
||||
@ -20,9 +19,6 @@ const THEME_COLORS = {
|
||||
gray700: '#6e6e6e',
|
||||
gray800: '#4b4b4b',
|
||||
gray900: '#2c2c2c',
|
||||
green400: twColors.green['400'],
|
||||
green500: twColors.green['500'],
|
||||
green600: twColors.green['600'],
|
||||
},
|
||||
dark: {
|
||||
primary: '#2680eb',
|
||||
@ -37,9 +33,6 @@ const THEME_COLORS = {
|
||||
gray700: '#b9b9b9',
|
||||
gray800: '#e3e3e3',
|
||||
gray900: '#ffffff',
|
||||
green400: twColors.green['600'],
|
||||
green500: twColors.green['500'],
|
||||
green600: twColors.green['400'],
|
||||
},
|
||||
};
|
||||
|
||||
@ -62,14 +55,7 @@ export function useTheme() {
|
||||
const customTheme = window.localStorage.getItem(THEME_CONFIG);
|
||||
const theme = isValidTheme(customTheme) ? customTheme : defaultTheme;
|
||||
|
||||
const primaryColor = useMemo(
|
||||
() => colord(THEME_COLORS[theme].primary),
|
||||
[theme]
|
||||
);
|
||||
const healthColor = useMemo(
|
||||
() => colord(THEME_COLORS[theme].green400),
|
||||
[theme]
|
||||
);
|
||||
const primaryColor = useMemo(() => colord(THEME_COLORS[theme].primary), []);
|
||||
|
||||
const colors = useMemo(
|
||||
() => ({
|
||||
@ -77,12 +63,10 @@ export function useTheme() {
|
||||
...THEME_COLORS[theme],
|
||||
},
|
||||
chart: {
|
||||
error: twColors.red[500],
|
||||
text: THEME_COLORS[theme].gray700,
|
||||
line: THEME_COLORS[theme].gray200,
|
||||
pv: primaryColor.alpha(0.4).toRgbString(),
|
||||
uv: primaryColor.alpha(0.6).toRgbString(),
|
||||
monitor: healthColor.alpha(0.8).toRgbString(),
|
||||
},
|
||||
map: {
|
||||
baseColor: THEME_COLORS[theme].primary,
|
||||
|
@ -1,6 +1,6 @@
|
||||
/** @type {import('@i18next-toolkit/cli').I18nextToolkitConfig} */
|
||||
const config = {
|
||||
locales: ['en', 'zh-CN', 'ja-JP', 'fr-FR', 'de-DE', 'pl-PL', 'pt-PT', 'ru-RU'],
|
||||
locales: ['en', 'zh', 'jp', 'fr', 'de', 'pl', 'pt', 'ru'],
|
||||
verbose: true,
|
||||
namespaces: ['translation'],
|
||||
translator: {
|
||||
|
@ -1,3 +0,0 @@
|
||||
import { initI18N } from './utils/i18n';
|
||||
|
||||
initI18N();
|
@ -1,6 +1,5 @@
|
||||
import './index.css';
|
||||
import './styles/global.less';
|
||||
import './init';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
@ -17,13 +17,14 @@
|
||||
"keywords": [],
|
||||
"author": "moonrailgun <moonrailgun@gmail.com>",
|
||||
"dependencies": {
|
||||
"@ant-design/charts": "^1.4.2",
|
||||
"@ant-design/icons": "^5.3.6",
|
||||
"@antv/l7": "^2.20.14",
|
||||
"@antv/larkmap": "^1.4.13",
|
||||
"@bytemd/plugin-gfm": "^1.21.0",
|
||||
"@bytemd/react": "^1.21.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@i18next-toolkit/react": "2.0.0-rc.5",
|
||||
"@i18next-toolkit/react": "^1.1.0",
|
||||
"@loadable/component": "^5.16.3",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
@ -70,7 +71,6 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.358.0",
|
||||
"md5": "^2.3.0",
|
||||
"millify": "^6.1.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"pretty-ms": "^9.0.0",
|
||||
@ -107,7 +107,6 @@
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/loadable__component": "^5.13.8",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/react": "^18.2.22",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
|
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 269 B |
@ -19,14 +19,11 @@
|
||||
"k17058821": "Website Lighthouse Berichte",
|
||||
"k172a09c3": "Vorschläge",
|
||||
"k1777bbf2": "Manuell",
|
||||
"k1940fd6": "Allgemein",
|
||||
"k1964b988": "Stopp",
|
||||
"k1bd89236": "Reporter mit ausführen",
|
||||
"k1c33c293": "Einstellungen",
|
||||
"k1d8f92b4": "Tablet",
|
||||
"k1da4ecc2": "Sie können eine Nachricht an diesen Kanal senden mit:",
|
||||
"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",
|
||||
"k2099f2e0": "Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort",
|
||||
"k20edf271": "24 Stunden",
|
||||
@ -56,15 +53,12 @@
|
||||
"k2c84fe32": "Feed-Ereigniszähler",
|
||||
"k2cecf817": "Typ",
|
||||
"k2dad13e3": "Sprache",
|
||||
"k2db2c0c5": "Testbenachrichtigung",
|
||||
"k2e6dbf02": "An E-Mail",
|
||||
"k2ea8a019": "Überwachen",
|
||||
"k30b5f01b": "Arbeitsbereiche",
|
||||
"k30d33d71": "Webhook-Signatur",
|
||||
"k310fee": "Letzte 30 Tage",
|
||||
"k32344f64": "Daten löschen",
|
||||
"k3260f019": "Abmelden",
|
||||
"k3404b72f": "Neuer Arbeitsbereichsname",
|
||||
"k340547f0": "Entschuldigung, aber etwas ist schief gelaufen",
|
||||
"k3471e956": "Neues Passwort wiederholen",
|
||||
"k34981fea": "Docker treibt auf See und findet seinen Weg nicht. Bitte starten Sie Docker, um wieder auf Kurs zu kommen.",
|
||||
@ -91,7 +85,6 @@
|
||||
"k3e8b13f8": "Discord beitreten",
|
||||
"k3eaab921": "ÜberwachungsListe",
|
||||
"k3f36e17e": "Twitter folgen",
|
||||
"k406089a4": "Aktion",
|
||||
"k406e9ad8": "Bestätigen",
|
||||
"k41d3ce6c": "Ereignis wiederhergestellt",
|
||||
"k42347b91": "Website-Ereigniszählung",
|
||||
@ -100,8 +93,7 @@
|
||||
"k44186b66": "Zählung",
|
||||
"k44cad477": "(Aktuell)",
|
||||
"k45f80a27": "Erweitert",
|
||||
"k4727e4db": "Ablaufdatum",
|
||||
"k477b7ee4": "Teilweise Systemausfälle",
|
||||
"k4738284": "Sie können jede Nachricht in diesen Kanal mit folgendem senden:",
|
||||
"k47fe1f95": "Fügen Sie diesen Beispielcode zu Ihrem Projekt hinzu",
|
||||
"k48186ce": "Zurück zur Startseite",
|
||||
"k4905ed7b": "KEINE",
|
||||
@ -115,7 +107,6 @@
|
||||
"k4de48e75": "Maximale Wiederholungen",
|
||||
"k4e08cf58": "Detailnummer anzeigen",
|
||||
"k4eea9393": "Profil",
|
||||
"k4f182a7c": "Wichtige Systemausfälle",
|
||||
"k4fc2b5b": "Bild",
|
||||
"k4fe1b4de": "Telemetrie",
|
||||
"k505c2733": "Bericht erstellen",
|
||||
@ -132,12 +123,9 @@
|
||||
"k58267a45": "Quelle",
|
||||
"k58f90514": "Bot-Token",
|
||||
"k593cf342": "Sind Sie sicher, diesen Monitor zu löschen?",
|
||||
"k5a782f4b": "Website-Anzahl",
|
||||
"k5a839f71": "Betriebszeit",
|
||||
"k5b5be0d4": "Aktuelle Rolle",
|
||||
"k5c18db28": "Statusseiteninformationen ändern",
|
||||
"k5d00536d": "Kopiert",
|
||||
"k5d49d751": "Neuer API-Schlüssel wurde in Ihre Zwischenablage kopiert!",
|
||||
"k5eb87a8b": "Start",
|
||||
"k5ec0de4": "Für die HTTPS-Überwachung werden bei Zuweisung einer Benachrichtigungsmethode Benachrichtigungen 1, 3, 7 und 14 Tage vor Ablauf gesendet.",
|
||||
"k5ecf04b0": "Ansicht",
|
||||
@ -147,7 +135,6 @@
|
||||
"k62e19375": "Letzte Aktualisierung: {{date}}",
|
||||
"k6488f302": "Optional",
|
||||
"k659b065": "Zum Beispiel: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
|
||||
"k678e2f90": "Anforderungsinhalt",
|
||||
"k67c5a895": "Gestern",
|
||||
"k683be220": "Ausführen",
|
||||
"k691b7170": "Gestoppt",
|
||||
@ -160,11 +147,9 @@
|
||||
"k6e96fc3": "Formularinfo",
|
||||
"k6ea11aff": "Holen!",
|
||||
"k6f15bcc3": "Host",
|
||||
"k71067412": "Optional, Webhook-Signatur für eingehenden Webhook",
|
||||
"k721589c1": "Heute",
|
||||
"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.",
|
||||
"k736f3e4c": "Kopieren als",
|
||||
"k75581e13": "Kreditkarte",
|
||||
"k75bfaaa6": "Fügen Sie diesen Code in das Kopf-Skript Ihrer Website ein",
|
||||
"k763816ac": "Vorschau",
|
||||
@ -172,17 +157,16 @@
|
||||
"k78b1ef6a": "Eingeben",
|
||||
"k7927b824": "Sind Sie sicher, alle Offline-Knoten zu löschen?",
|
||||
"k7a132ce8": "Entschuldigung, aber diese Seite wurde nicht gefunden",
|
||||
"k7a15497a": "Echtzeit",
|
||||
"k7ac44a6e": "Sitzungsschlüssel",
|
||||
"k7b74a43f": "Besucher",
|
||||
"k7b75e24c": "Integration",
|
||||
"k7b9aa48c": "Inhalt",
|
||||
"k7cac602a": "Status",
|
||||
"k7d8cd81c": "URL kopieren",
|
||||
"k7e0360fd": "Es wurde keine Gruppe erstellt, klicken Sie auf die Schaltfläche, um eine zu erstellen",
|
||||
"k7e61b1af": "Arbeitsbereich auswählen",
|
||||
"k7f01b47c": "Prüfprotokoll",
|
||||
"k7f03a704": "Denken Sie daran, keine Daten mit application/json zu senden",
|
||||
"k7f29bae5": "Seitenaufrufe",
|
||||
"k8037cc6b": "Server",
|
||||
"k816ce026": "Herunterladen",
|
||||
"k819633bc": "Zur Speicherung verwenden",
|
||||
@ -192,7 +176,6 @@
|
||||
"k84ce1618": "(24 Stunden)",
|
||||
"k84e82947": "{{num}} Ereignisse gelöscht",
|
||||
"k85344b23": "Laden",
|
||||
"k85a116ee": "Webhook-URL",
|
||||
"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.",
|
||||
"k873c90e6": "Anzeigelabel",
|
||||
@ -205,7 +188,6 @@
|
||||
"k88d2647b": "Webseite",
|
||||
"k89056082": "(30 Tage)",
|
||||
"k892f84b6": "Aktuelle Benutzerinformationen können nicht abgerufen werden",
|
||||
"k895cafe1": "Optional, Webhook-URL zum Senden der Umfrage-Payload",
|
||||
"k899fd0cd": "Ports",
|
||||
"k89d54f7a": "Überwachung der Ausführungszählung",
|
||||
"k8a1deb63": "Mitglieder",
|
||||
@ -225,10 +207,7 @@
|
||||
"k90b603b8": "Duplizieren",
|
||||
"k90b668e5": "Letzte 24 Stunden",
|
||||
"k93374bc9": "Website löschen",
|
||||
"k93458b98": "Spielplatz",
|
||||
"k951a939a": "Akzeptierte Zählung der Website",
|
||||
"k95f932a": "Warten derzeit auf eine neue Anfrage vom Remote-Server",
|
||||
"k97b02874": "Seitenanzahl",
|
||||
"k98f433ee": "Reporter herunterladen von",
|
||||
"k9991c290": "Gemeinschaft",
|
||||
"k9a272ecf": "Sind das Ihre Server?",
|
||||
@ -254,7 +233,6 @@
|
||||
"ka6ee7455": "Website-ID",
|
||||
"ka71c12e1": "Die beiden Passwörter stimmen nicht überein",
|
||||
"ka765ad32": "Benachrichtigung",
|
||||
"ka7d8617e": "Feed-Kanalanzahl",
|
||||
"ka7fe5937": "Festplattenlesen/-schreiben",
|
||||
"ka8e41156": "Suche und schneller Sprung",
|
||||
"ka90bc019": "Deinstallieren",
|
||||
@ -276,7 +254,6 @@
|
||||
"kb0e351e0": "Aktualisiert",
|
||||
"kb114a2e8": "Veraltet",
|
||||
"kb15a6374": "Sie können Ihre Statusseite in Ihrer eigenen Domain konfigurieren, zum Beispiel: status.beispiel.com",
|
||||
"kb2dded49": "Schlüssel",
|
||||
"kb320aac4": "Überwacht seit {{dayNum}} Tagen",
|
||||
"kb35cde91": "Suche",
|
||||
"kb35d71ed": "ODER",
|
||||
@ -284,7 +261,6 @@
|
||||
"kb5673707": "Letzte 7 Tage",
|
||||
"kb659c1bc": "Zert. Ablauf",
|
||||
"kb6d350b6": "Feed-Kanäle",
|
||||
"kb7bf8869": "API-Schlüssel",
|
||||
"kb7fa344a": "Wählen Sie einen Feed-Kanal zum Senden aus",
|
||||
"kb8de8c50": "BCC",
|
||||
"kbb31d3db": "Statistikdatum",
|
||||
@ -320,16 +296,13 @@
|
||||
"kcc9c1bff": "Jede Woche",
|
||||
"kccaa732a": "Keine aufeinanderfolgenden Bindestriche",
|
||||
"kccb42483": "Passwort",
|
||||
"kcd56f27b": "Zuletzt aktualisiert",
|
||||
"kcd643ef3": "Lade...",
|
||||
"kce77d0c1": "Zeitzone",
|
||||
"kcff78587": "Zuletzt verwendet am",
|
||||
"kd005f7a8": "Alle Feeds werden entfernt",
|
||||
"kd031b383": "Ansichten",
|
||||
"kd044d5d4": "Session",
|
||||
"kd092de58": "Aktueller Arbeitsbereich:",
|
||||
"kd1f7e695": "Abmelden bestätigen",
|
||||
"kd211e2d4": "Versionsseite",
|
||||
"kd25f123a": "Status unbekannt",
|
||||
"kd2a7ad83": "Feed-Vorlage",
|
||||
"kd3262a4a": "Konfig",
|
||||
"kd3396544": "Allgemein werden wir ein ein Pixel großes leeres Bild verwenden, sodass es die normale Nutzung des Benutzers nicht beeinträchtigt.",
|
||||
@ -338,18 +311,14 @@
|
||||
"kd7279fa6": "Code",
|
||||
"kd7985726": "{{num}} Benutzer",
|
||||
"kd92fa3e7": "Host-Name",
|
||||
"kdaa6ae2b": "Überwachungsanzahl",
|
||||
"kdaff25a6": "Zeige den neuesten Wert",
|
||||
"kdb61adbb": "Offline verbergen",
|
||||
"kdbadcf43": "Alle Systeme betriebsbereit",
|
||||
"kdbe222b": "API-Schlüssel",
|
||||
"kdc10ee1a": "Erstellen Sie einen neuen Arbeitsbereich, um mit Teammitgliedern zusammenzuarbeiten.",
|
||||
"kdc15c5d": "Daten",
|
||||
"kdc1bf80e": "Url ist erforderlich",
|
||||
"kdc51b5db": "Webseiten",
|
||||
"kdd44ac01": "Anzuzeigender Telemetrie-Name",
|
||||
"kdd55936a": "Resolver-Port",
|
||||
"kde315178": "Umbenennen",
|
||||
"kde37bc27": "Zurück zum Admin",
|
||||
"kdeba7706": "Geräte",
|
||||
"kdeecbfea": "Resolver-Server",
|
||||
@ -390,7 +359,6 @@
|
||||
"kf246dd2e": "Es wurde kein Arbeitsbereich gefunden, bitte zuerst erstellen",
|
||||
"kf3b749ef": "Unterstützt Direktchat / Gruppe / Kanal-Chat-ID",
|
||||
"kf55495e0": "Speichern",
|
||||
"kf5c3b616": "Anforderungsheader",
|
||||
"kf5c9520e": "Noch keine Statusseite vorhanden, Sie können eine neue erstellen, um den Status Ihres Dienstes der Öffentlichkeit anzuzeigen.",
|
||||
"kf6339d4f": "Verifiziert",
|
||||
"kf6582ba": "Arbeitsbereich",
|
||||
@ -406,7 +374,6 @@
|
||||
"kf97b6f71": "Führen Sie diesen Befehl auf Ihrer Linux-Maschine aus",
|
||||
"kf9877f28": "Details anzeigen",
|
||||
"kf9965c19": "Alle Inhalte in diesem Arbeitsbereich werden zerstört und können nicht wiederhergestellt werden.",
|
||||
"kf9a498c7": "Lighthouse-Bericht abgeschlossen!",
|
||||
"kfc98929b": "{{num}} Tage",
|
||||
"kfd33c459": "Kopieren erfolgreich!",
|
||||
"kfdaf0bb3": "Zuletzt online: {{time}}",
|
@ -19,14 +19,11 @@
|
||||
"k17058821": "Website Lighthouse Reports",
|
||||
"k172a09c3": "Suggestions",
|
||||
"k1777bbf2": "Manual",
|
||||
"k1940fd6": "General",
|
||||
"k1964b988": "Stop",
|
||||
"k1bd89236": "run reporter with",
|
||||
"k1c33c293": "Settings",
|
||||
"k1d8f92b4": "Tablet",
|
||||
"k1da4ecc2": "You can send a message to this channel with:",
|
||||
"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",
|
||||
"k2099f2e0": "Login failed, please check your username and password",
|
||||
"k20edf271": "24h",
|
||||
@ -56,15 +53,12 @@
|
||||
"k2c84fe32": "Feed Event Count",
|
||||
"k2cecf817": "Type",
|
||||
"k2dad13e3": "Language",
|
||||
"k2db2c0c5": "Test Notify",
|
||||
"k2e6dbf02": "To Email",
|
||||
"k2ea8a019": "Monitor",
|
||||
"k30b5f01b": "Workspaces",
|
||||
"k30d33d71": "Webhook Signature",
|
||||
"k310fee": "Last 30 days",
|
||||
"k32344f64": "Clear Data",
|
||||
"k3260f019": "Logout",
|
||||
"k3404b72f": "New Workspace Name",
|
||||
"k340547f0": "Sorry, but something went wrong",
|
||||
"k3471e956": "Repaet New Password",
|
||||
"k34981fea": "Docker is adrift at sea, unable to find its way. Please start Docker to get back on course.",
|
||||
@ -91,7 +85,6 @@
|
||||
"k3e8b13f8": "Join Discord",
|
||||
"k3eaab921": "Monitor List",
|
||||
"k3f36e17e": "Follow Twitter",
|
||||
"k406089a4": "Action",
|
||||
"k406e9ad8": "Confirm",
|
||||
"k41d3ce6c": "Event unarchived",
|
||||
"k42347b91": "Website Event Count",
|
||||
@ -100,8 +93,7 @@
|
||||
"k44186b66": "Count",
|
||||
"k44cad477": "(Current)",
|
||||
"k45f80a27": "Advanced",
|
||||
"k4727e4db": "Expired At",
|
||||
"k477b7ee4": "Partial System Outage",
|
||||
"k4738284": "You can send a message to this channel with:",
|
||||
"k47fe1f95": "Add this example code into your project",
|
||||
"k48186ce": "Back to Homepage",
|
||||
"k4905ed7b": "NONE",
|
||||
@ -115,7 +107,6 @@
|
||||
"k4de48e75": "Max Retries",
|
||||
"k4e08cf58": "Show Detail Number",
|
||||
"k4eea9393": "Profile",
|
||||
"k4f182a7c": "Major System Outage",
|
||||
"k4fc2b5b": "Image",
|
||||
"k4fe1b4de": "Telemetry",
|
||||
"k505c2733": "Create Report",
|
||||
@ -132,12 +123,9 @@
|
||||
"k58267a45": "Source",
|
||||
"k58f90514": "Bot Token",
|
||||
"k593cf342": "Are you sure you want to delete this monitor?",
|
||||
"k5a782f4b": "Website Count",
|
||||
"k5a839f71": "Uptime",
|
||||
"k5b5be0d4": "Current Role",
|
||||
"k5c18db28": "Modify Status Page Info",
|
||||
"k5d00536d": "Copied",
|
||||
"k5d49d751": "New api key has been copied into your clipboard!",
|
||||
"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.",
|
||||
"k5ecf04b0": "View",
|
||||
@ -147,7 +135,6 @@
|
||||
"k62e19375": "Last updated at: {{date}}",
|
||||
"k6488f302": "Optional",
|
||||
"k659b065": "For example: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
|
||||
"k678e2f90": "Request Body",
|
||||
"k67c5a895": "Yesterday",
|
||||
"k683be220": "Run",
|
||||
"k691b7170": "Stopped",
|
||||
@ -160,11 +147,9 @@
|
||||
"k6e96fc3": "Form Info",
|
||||
"k6ea11aff": "Get!",
|
||||
"k6f15bcc3": "Host",
|
||||
"k71067412": "Optional, Webhook Signature for Incoming Webhook",
|
||||
"k721589c1": "Today",
|
||||
"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.",
|
||||
"k736f3e4c": "Copy as",
|
||||
"k75581e13": "CC",
|
||||
"k75bfaaa6": "Add this code into your website head script",
|
||||
"k763816ac": "Preview",
|
||||
@ -172,17 +157,16 @@
|
||||
"k78b1ef6a": "Enter",
|
||||
"k7927b824": "Are you sure to clear all offline nodes?",
|
||||
"k7a132ce8": "Sorry, but this page is not found",
|
||||
"k7a15497a": "Realtime",
|
||||
"k7ac44a6e": "Session Key",
|
||||
"k7b74a43f": "visitors",
|
||||
"k7b75e24c": "Integration",
|
||||
"k7b9aa48c": "Body",
|
||||
"k7cac602a": "Status",
|
||||
"k7d8cd81c": "Copy URL",
|
||||
"k7e0360fd": "No group has been created yet, click button to create one",
|
||||
"k7e61b1af": "Select Workspace",
|
||||
"k7f01b47c": "Audit Log",
|
||||
"k7f03a704": "Dont remember send data with application/json",
|
||||
"k7f29bae5": "pageview",
|
||||
"k8037cc6b": "Servers",
|
||||
"k816ce026": "Download",
|
||||
"k819633bc": "Use for storage",
|
||||
@ -192,7 +176,6 @@
|
||||
"k84ce1618": "(24 hour)",
|
||||
"k84e82947": "{{num}} events cleared",
|
||||
"k85344b23": "Load",
|
||||
"k85a116ee": "Webhook Url",
|
||||
"k85c5fd4c": "No monitor has been set",
|
||||
"k85db19da": "No feed channel yet. Use feed feature to receive any event from your network or service.",
|
||||
"k873c90e6": "Display Label",
|
||||
@ -205,7 +188,6 @@
|
||||
"k88d2647b": "Website",
|
||||
"k89056082": "(30 days)",
|
||||
"k892f84b6": "Can not get current user info",
|
||||
"k895cafe1": "Optional, webhook url to send survey payload",
|
||||
"k899fd0cd": "ports",
|
||||
"k89d54f7a": "Monitor Execution Count",
|
||||
"k8a1deb63": "Members",
|
||||
@ -225,10 +207,7 @@
|
||||
"k90b603b8": "Duplicate",
|
||||
"k90b668e5": "Last 24 Hours",
|
||||
"k93374bc9": "Delete Website",
|
||||
"k93458b98": "Playground",
|
||||
"k951a939a": "Website Accepted Count",
|
||||
"k95f932a": "Currently waiting for a new request from the remote server",
|
||||
"k97b02874": "Page Count",
|
||||
"k98f433ee": "Download reporter from",
|
||||
"k9991c290": "Community",
|
||||
"k9a272ecf": "Is this your servers?",
|
||||
@ -254,7 +233,6 @@
|
||||
"ka6ee7455": "Website ID",
|
||||
"ka71c12e1": "The two passwords are not consistent",
|
||||
"ka765ad32": "Notification",
|
||||
"ka7d8617e": "Feed Channel Count",
|
||||
"ka7fe5937": "Disk read/write",
|
||||
"ka8e41156": "Search and quick jump",
|
||||
"ka90bc019": "Uninstall",
|
||||
@ -276,7 +254,6 @@
|
||||
"kb0e351e0": "Refreshed",
|
||||
"kb114a2e8": "Deprecated",
|
||||
"kb15a6374": "You can config your status page in your own domain, for example: status.example.com",
|
||||
"kb2dded49": "Key",
|
||||
"kb320aac4": "Monitored for {{dayNum}} days",
|
||||
"kb35cde91": "Search",
|
||||
"kb35d71ed": "OR",
|
||||
@ -284,7 +261,6 @@
|
||||
"kb5673707": "Last 7 days",
|
||||
"kb659c1bc": "Cert Exp.",
|
||||
"kb6d350b6": "Feed Channels",
|
||||
"kb7bf8869": "Api Keys",
|
||||
"kb7fa344a": "Select Feed Channel for send",
|
||||
"kb8de8c50": "BCC",
|
||||
"kbb31d3db": "Statistic Date",
|
||||
@ -320,16 +296,13 @@
|
||||
"kcc9c1bff": "Every Week",
|
||||
"kccaa732a": "No consecutive dashes",
|
||||
"kccb42483": "Password",
|
||||
"kcd56f27b": "Last updated",
|
||||
"kcd643ef3": "Loading...",
|
||||
"kce77d0c1": "Timezone",
|
||||
"kcff78587": "Last Use At",
|
||||
"kd005f7a8": "All feed will be remove",
|
||||
"kd031b383": "Views",
|
||||
"kd044d5d4": "session",
|
||||
"kd092de58": "Current Workspace:",
|
||||
"kd1f7e695": "Confirm to logout",
|
||||
"kd211e2d4": "Releases Page",
|
||||
"kd25f123a": "Status Unknown",
|
||||
"kd2a7ad83": "Feed Template",
|
||||
"kd3262a4a": "Config",
|
||||
"kd3396544": "Generally, we will use a one-pixel blank image so that it will not affect the user's normal use.",
|
||||
@ -338,18 +311,14 @@
|
||||
"kd7279fa6": "Code",
|
||||
"kd7985726": "{{num}} users",
|
||||
"kd92fa3e7": "Host Name",
|
||||
"kdaa6ae2b": "Monitor Count",
|
||||
"kdaff25a6": "Show Latest Value",
|
||||
"kdb61adbb": "Hide Offline",
|
||||
"kdbadcf43": "All Systems Operational",
|
||||
"kdbe222b": "Api Key",
|
||||
"kdc10ee1a": "Create a new workspace to cooperate with team members.",
|
||||
"kdc15c5d": "Data",
|
||||
"kdc1bf80e": "Url is required",
|
||||
"kdc51b5db": "Websites",
|
||||
"kdd44ac01": "Telemetry Name to Display",
|
||||
"kdd55936a": "Resolver Port",
|
||||
"kde315178": "Rename",
|
||||
"kde37bc27": "Back to Admin",
|
||||
"kdeba7706": "Devices",
|
||||
"kdeecbfea": "Resolver Server",
|
||||
@ -390,7 +359,6 @@
|
||||
"kf246dd2e": "Not any workspace has been found, please create first",
|
||||
"kf3b749ef": "Support Direct Chat / Group / Channel's Chat ID",
|
||||
"kf55495e0": "Save",
|
||||
"kf5c3b616": "Request Header",
|
||||
"kf5c9520e": "No any status page yet, you can create a new one to show your service status to public.",
|
||||
"kf6339d4f": "Verified",
|
||||
"kf6582ba": "Workspace",
|
||||
@ -406,7 +374,6 @@
|
||||
"kf97b6f71": "Run this command in your linux machine",
|
||||
"kf9877f28": "View Details",
|
||||
"kf9965c19": "All content in this workspace will be destory and can not recover.",
|
||||
"kf9a498c7": "Lighthouse report completed!",
|
||||
"kfc98929b": "{{num}} days",
|
||||
"kfd33c459": "Copy success!",
|
||||
"kfdaf0bb3": "Last online: {{time}}",
|
||||
|
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 267 B |
@ -19,14 +19,11 @@
|
||||
"k17058821": "Rapports Lighthouse du site Web",
|
||||
"k172a09c3": "Suggestions",
|
||||
"k1777bbf2": "Manuel",
|
||||
"k1940fd6": "Général",
|
||||
"k1964b988": "Arrêter",
|
||||
"k1bd89236": "exécuter le rapporteur avec",
|
||||
"k1c33c293": "Paramètres",
|
||||
"k1d8f92b4": "Tablette",
|
||||
"k1da4ecc2": "Vous pouvez envoyer un message à ce canal avec :",
|
||||
"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",
|
||||
"k2099f2e0": "Échec de la connexion, veuillez vérifier votre nom d'utilisateur et votre mot de passe",
|
||||
"k20edf271": "24h",
|
||||
@ -56,15 +53,12 @@
|
||||
"k2c84fe32": "Nombre d'événements de flux",
|
||||
"k2cecf817": "Type",
|
||||
"k2dad13e3": "Langue",
|
||||
"k2db2c0c5": "Test Notify",
|
||||
"k2e6dbf02": "À l'email",
|
||||
"k2ea8a019": "Moniteur",
|
||||
"k30b5f01b": "Espaces de travail",
|
||||
"k30d33d71": "Signature du Webhook",
|
||||
"k310fee": "30 derniers jours",
|
||||
"k32344f64": "Effacer les données",
|
||||
"k3260f019": "Déconnexion",
|
||||
"k3404b72f": "Nouveau nom d'espace de travail",
|
||||
"k340547f0": "Désolé, mais quelque chose s'est mal passé",
|
||||
"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.",
|
||||
@ -91,7 +85,6 @@
|
||||
"k3e8b13f8": "Rejoindre Discord",
|
||||
"k3eaab921": "Liste de surveillance",
|
||||
"k3f36e17e": "Suivre Twitter",
|
||||
"k406089a4": "Action",
|
||||
"k406e9ad8": "Confirmer",
|
||||
"k41d3ce6c": "Événement désarchivé",
|
||||
"k42347b91": "Nombre d'événements sur le site Web",
|
||||
@ -100,8 +93,7 @@
|
||||
"k44186b66": "Compte",
|
||||
"k44cad477": "(Actuel)",
|
||||
"k45f80a27": "Avancé",
|
||||
"k4727e4db": "Expiré À",
|
||||
"k477b7ee4": "Panne partielle du système",
|
||||
"k4738284": "Vous pouvez envoyer n'importe quel message dans ce canal avec :",
|
||||
"k47fe1f95": "Ajoutez ce code d'exemple à votre projet",
|
||||
"k48186ce": "Retour à la page d'accueil",
|
||||
"k4905ed7b": "AUCUN",
|
||||
@ -115,7 +107,6 @@
|
||||
"k4de48e75": "Nombre maximum de tentatives",
|
||||
"k4e08cf58": "Afficher le numéro de détail",
|
||||
"k4eea9393": "Profil",
|
||||
"k4f182a7c": "Panne majeure du système",
|
||||
"k4fc2b5b": "Image",
|
||||
"k4fe1b4de": "Télémétrie",
|
||||
"k505c2733": "Créer un rapport",
|
||||
@ -132,12 +123,9 @@
|
||||
"k58267a45": "Source",
|
||||
"k58f90514": "Jeton de bot",
|
||||
"k593cf342": "Êtes-vous sûr de vouloir supprimer ce moniteur ?",
|
||||
"k5a782f4b": "Nombre de Sites Web",
|
||||
"k5a839f71": "Disponibilité",
|
||||
"k5b5be0d4": "Rôle actuel",
|
||||
"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",
|
||||
"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",
|
||||
@ -147,7 +135,6 @@
|
||||
"k62e19375": "Dernière mise à jour : {{date}}",
|
||||
"k6488f302": "Optionnel",
|
||||
"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",
|
||||
"k683be220": "Exécuter",
|
||||
"k691b7170": "Arrêté",
|
||||
@ -160,11 +147,9 @@
|
||||
"k6e96fc3": "Informations sur le formulaire",
|
||||
"k6ea11aff": "Obtenir !",
|
||||
"k6f15bcc3": "Hôte",
|
||||
"k71067412": "Optionnel, signature du webhook pour le webhook entrant",
|
||||
"k721589c1": "Aujourd'hui",
|
||||
"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.",
|
||||
"k736f3e4c": "Copier en tant que",
|
||||
"k75581e13": "Sous-titres",
|
||||
"k75bfaaa6": "Ajoutez ce code dans le script de tête de votre site web",
|
||||
"k763816ac": "Aperçu",
|
||||
@ -172,17 +157,16 @@
|
||||
"k78b1ef6a": "Entrer",
|
||||
"k7927b824": "Êtes-vous sûr de vouloir effacer tous les nœuds hors ligne ?",
|
||||
"k7a132ce8": "Désolé, mais cette page est introuvable",
|
||||
"k7a15497a": "Temps réel",
|
||||
"k7ac44a6e": "Clé de session",
|
||||
"k7b74a43f": "visiteurs",
|
||||
"k7b75e24c": "Intégration",
|
||||
"k7b9aa48c": "Corps",
|
||||
"k7cac602a": "Statut",
|
||||
"k7d8cd81c": "Copier l'URL",
|
||||
"k7e0360fd": "Aucun groupe n'a été créé, cliquez sur le bouton pour en créer un",
|
||||
"k7e61b1af": "Sélectionner l'espace de travail",
|
||||
"k7f01b47c": "Journal d'audit",
|
||||
"k7f03a704": "N'oubliez pas de ne pas envoyer de données avec application/json",
|
||||
"k7f29bae5": "Vue de la page",
|
||||
"k8037cc6b": "Serveurs",
|
||||
"k816ce026": "Télécharger",
|
||||
"k819633bc": "Utiliser pour le stockage",
|
||||
@ -192,7 +176,6 @@
|
||||
"k84ce1618": "(24 heures)",
|
||||
"k84e82947": "{{num}} événements effacés",
|
||||
"k85344b23": "Charge",
|
||||
"k85a116ee": "URL du Webhook",
|
||||
"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.",
|
||||
"k873c90e6": "Étiquette d'affichage",
|
||||
@ -205,7 +188,6 @@
|
||||
"k88d2647b": "Site Web",
|
||||
"k89056082": "(30 jours)",
|
||||
"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",
|
||||
"k89d54f7a": "Compte des exécutions surveillées",
|
||||
"k8a1deb63": "Membres",
|
||||
@ -225,10 +207,7 @@
|
||||
"k90b603b8": "Dupliquer",
|
||||
"k90b668e5": "24 dernières heures",
|
||||
"k93374bc9": "Supprimer le site Web",
|
||||
"k93458b98": "Terrain de jeu",
|
||||
"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",
|
||||
"k9991c290": "Communauté",
|
||||
"k9a272ecf": "S'agit-il de vos serveurs ?",
|
||||
@ -254,7 +233,6 @@
|
||||
"ka6ee7455": "ID du site Web",
|
||||
"ka71c12e1": "Les deux mots de passe ne sont pas cohérents",
|
||||
"ka765ad32": "Notification",
|
||||
"ka7d8617e": "Nombre de Canaux de Flux",
|
||||
"ka7fe5937": "Lecture/écriture de disque",
|
||||
"ka8e41156": "Rechercher et sauter rapidement",
|
||||
"ka90bc019": "Désinstaller",
|
||||
@ -276,7 +254,6 @@
|
||||
"kb0e351e0": "Rafraîchi",
|
||||
"kb114a2e8": "Obsolète",
|
||||
"kb15a6374": "Vous pouvez configurer votre page de statut sur votre propre domaine, par exemple : status.example.com",
|
||||
"kb2dded49": "Clé",
|
||||
"kb320aac4": "Surveillé pendant {{dayNum}} jours",
|
||||
"kb35cde91": "Recherche",
|
||||
"kb35d71ed": "OU",
|
||||
@ -284,7 +261,6 @@
|
||||
"kb5673707": "7 derniers jours",
|
||||
"kb659c1bc": "Expiration du cert.",
|
||||
"kb6d350b6": "Canaux de flux",
|
||||
"kb7bf8869": "Clés API",
|
||||
"kb7fa344a": "Sélectionner le canal de flux à envoyer",
|
||||
"kb8de8c50": "CCI",
|
||||
"kbb31d3db": "Date de statistique",
|
||||
@ -320,16 +296,13 @@
|
||||
"kcc9c1bff": "Toutes les semaines",
|
||||
"kccaa732a": "Pas de tirets consécutifs",
|
||||
"kccb42483": "Mot de passe",
|
||||
"kcd56f27b": "Dernière mise à jour",
|
||||
"kcd643ef3": "Chargement...",
|
||||
"kce77d0c1": "Fuseau horaire",
|
||||
"kcff78587": "Dernière Utilisation À",
|
||||
"kd005f7a8": "Tous les flux seront supprimés",
|
||||
"kd031b383": "Vues",
|
||||
"kd044d5d4": "Session",
|
||||
"kd092de58": "Espace de travail actuel :",
|
||||
"kd1f7e695": "Confirmer la déconnexion",
|
||||
"kd211e2d4": "Page des versions",
|
||||
"kd25f123a": "Statut inconnu",
|
||||
"kd2a7ad83": "Modèle de flux",
|
||||
"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.",
|
||||
@ -338,18 +311,14 @@
|
||||
"kd7279fa6": "Code",
|
||||
"kd7985726": "{{num}} utilisateurs",
|
||||
"kd92fa3e7": "Nom de l'hôte",
|
||||
"kdaa6ae2b": "Nombre de Moniteurs",
|
||||
"kdaff25a6": "Afficher la dernière valeur",
|
||||
"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.",
|
||||
"kdc15c5d": "Données",
|
||||
"kdc1bf80e": "L'URL est requise",
|
||||
"kdc51b5db": "Sites Web",
|
||||
"kdd44ac01": "Nom de la télémétrie à afficher",
|
||||
"kdd55936a": "Port de résolveur",
|
||||
"kde315178": "Renommer",
|
||||
"kde37bc27": "Retour à l'administrateur",
|
||||
"kdeba7706": "Appareils",
|
||||
"kdeecbfea": "Serveur de résolveur",
|
||||
@ -390,7 +359,6 @@
|
||||
"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",
|
||||
"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.",
|
||||
"kf6339d4f": "Vérifié",
|
||||
"kf6582ba": "Espace de travail",
|
||||
@ -406,7 +374,6 @@
|
||||
"kf97b6f71": "Exécutez cette commande sur votre machine Linux",
|
||||
"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é.",
|
||||
"kf9a498c7": "Rapport Lighthouse terminé !",
|
||||
"kfc98929b": "{{num}} jours",
|
||||
"kfd33c459": "Copie réussie !",
|
||||
"kfdaf0bb3": "Dernière connexion : {{time}}",
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 450 B |
@ -19,14 +19,11 @@
|
||||
"k17058821": "ウェブサイト ライトハウス レポート",
|
||||
"k172a09c3": "提案",
|
||||
"k1777bbf2": "マニュアル",
|
||||
"k1940fd6": "一般",
|
||||
"k1964b988": "停止",
|
||||
"k1bd89236": "レポーターを実行する",
|
||||
"k1c33c293": "設定",
|
||||
"k1d8f92b4": "タブレット",
|
||||
"k1da4ecc2": "このチャンネルにメッセージを送信できます:",
|
||||
"k1eb5b3ed": "概要",
|
||||
"k1ee0c2ca": "Webhook URLを<1></1>に設定し、このウィンドウをアクティブに保ってください。完了すると、ここでWebhookリクエストを受信し始めます。",
|
||||
"k1f6dea0": "チャンネル名",
|
||||
"k2099f2e0": "ログインに失敗しました。ユーザー名とパスワードを確認してください",
|
||||
"k20edf271": "24時間",
|
||||
@ -56,15 +53,12 @@
|
||||
"k2c84fe32": "フィードイベント数",
|
||||
"k2cecf817": "タイプ",
|
||||
"k2dad13e3": "言語",
|
||||
"k2db2c0c5": "テスト通知",
|
||||
"k2e6dbf02": "メールアドレスへ",
|
||||
"k2ea8a019": "モニター",
|
||||
"k30b5f01b": "ワークスペース",
|
||||
"k30d33d71": "Webhook署名",
|
||||
"k310fee": "過去30日間",
|
||||
"k32344f64": "データクリア",
|
||||
"k3260f019": "ログアウト",
|
||||
"k3404b72f": "新しいワークスペース名",
|
||||
"k340547f0": "申し訳ありませんが、何か問題が発生しました",
|
||||
"k3471e956": "新しいパスワードの再入力",
|
||||
"k34981fea": "Dockerは海上で漂流しており、方向を見失っています。Dockerを起動して進路を修正してください。",
|
||||
@ -91,7 +85,6 @@
|
||||
"k3e8b13f8": "Discordに参加",
|
||||
"k3eaab921": "モニターリスト",
|
||||
"k3f36e17e": "Twitterをフォロー",
|
||||
"k406089a4": "アクション",
|
||||
"k406e9ad8": "確認",
|
||||
"k41d3ce6c": "イベントがアーカイブ解除されました",
|
||||
"k42347b91": "ウェブサイトイベント数",
|
||||
@ -100,8 +93,7 @@
|
||||
"k44186b66": "カウント",
|
||||
"k44cad477": "(現在)",
|
||||
"k45f80a27": "詳細",
|
||||
"k4727e4db": "期限切れ",
|
||||
"k477b7ee4": "部分的なシステム障害",
|
||||
"k4738284": "次の方法でこのチャンネルにメッセージを送信できます:",
|
||||
"k47fe1f95": "このサンプルコードをプロジェクトに追加してください",
|
||||
"k48186ce": "ホームページに戻る",
|
||||
"k4905ed7b": "なし",
|
||||
@ -115,7 +107,6 @@
|
||||
"k4de48e75": "最大リトライ回数",
|
||||
"k4e08cf58": "詳細番号を表示",
|
||||
"k4eea9393": "プロファイル",
|
||||
"k4f182a7c": "重大なシステム障害",
|
||||
"k4fc2b5b": "画像",
|
||||
"k4fe1b4de": "テレメトリー",
|
||||
"k505c2733": "レポートを作成",
|
||||
@ -132,12 +123,9 @@
|
||||
"k58267a45": "ソース",
|
||||
"k58f90514": "ボットトークン",
|
||||
"k593cf342": "このモニターを削除してもよろしいですか?",
|
||||
"k5a782f4b": "ウェブサイト数",
|
||||
"k5a839f71": "アップタイム",
|
||||
"k5b5be0d4": "現在の役割",
|
||||
"k5c18db28": "ステータスページ情報を変更",
|
||||
"k5d00536d": "コピー済み",
|
||||
"k5d49d751": "新しいAPIキーがクリップボードにコピーされました!",
|
||||
"k5eb87a8b": "開始",
|
||||
"k5ec0de4": "HTTPSモニタリングの場合、通知方法が割り当てられている場合、有効期限の1、3、7、14日前に通知が送信されます。",
|
||||
"k5ecf04b0": "ビュー",
|
||||
@ -147,7 +135,6 @@
|
||||
"k62e19375": "最終更新:{{date}}",
|
||||
"k6488f302": "オプション",
|
||||
"k659b065": "例:https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
|
||||
"k678e2f90": "リクエストボディ",
|
||||
"k67c5a895": "昨日",
|
||||
"k683be220": "実行",
|
||||
"k691b7170": "停止済み",
|
||||
@ -160,11 +147,9 @@
|
||||
"k6e96fc3": "フォーム情報",
|
||||
"k6ea11aff": "取得!",
|
||||
"k6f15bcc3": "ホスト",
|
||||
"k71067412": "オプション、受信WebhookのWebhook署名",
|
||||
"k721589c1": "今日",
|
||||
"k7247683c": "ワークスペースを削除",
|
||||
"k7350bd93": "同時に、CLIの使用頻度の収集、自己ホスト型アプリのインストールの収集など、クライアントサイドのアプリケーションシナリオでも使用することができます。",
|
||||
"k736f3e4c": "コピーとして",
|
||||
"k75581e13": "CC",
|
||||
"k75bfaaa6": "このコードをウェブサイトのヘッドスクリプトに追加してください",
|
||||
"k763816ac": "プレビュー",
|
||||
@ -172,17 +157,16 @@
|
||||
"k78b1ef6a": "入力",
|
||||
"k7927b824": "すべてのオフラインノードをクリアしてもよろしいですか?",
|
||||
"k7a132ce8": "申し訳ありませんが、このページは見つかりません",
|
||||
"k7a15497a": "リアルタイム",
|
||||
"k7ac44a6e": "セッションキー",
|
||||
"k7b74a43f": "訪問者",
|
||||
"k7b75e24c": "統合",
|
||||
"k7b9aa48c": "ボディ",
|
||||
"k7cac602a": "ステータス",
|
||||
"k7d8cd81c": "URLをコピー",
|
||||
"k7e0360fd": "グループが作成されていません。ボタンをクリックして作成してください",
|
||||
"k7e61b1af": "ワークスペースを選択",
|
||||
"k7f01b47c": "監査ログ",
|
||||
"k7f03a704": "application/json でデータを送信しないことを忘れないでください",
|
||||
"k7f29bae5": "ページビュー",
|
||||
"k8037cc6b": "サーバー",
|
||||
"k816ce026": "ダウンロード",
|
||||
"k819633bc": "ストレージ用",
|
||||
@ -192,7 +176,6 @@
|
||||
"k84ce1618": "(24時間)",
|
||||
"k84e82947": "{{num}} イベントがクリアされました",
|
||||
"k85344b23": "ロード",
|
||||
"k85a116ee": "Webhook URL",
|
||||
"k85c5fd4c": "まだモニターが設定されていません",
|
||||
"k85db19da": "まだフィードチャンネルがありません。ネットワークや自分のサービスからのすべてのイベントを受信するには、フィード機能を使用してください。",
|
||||
"k873c90e6": "表示ラベル",
|
||||
@ -205,7 +188,6 @@
|
||||
"k88d2647b": "ウェブサイト",
|
||||
"k89056082": "(30日間)",
|
||||
"k892f84b6": "現在のユーザー情報を取得できません",
|
||||
"k895cafe1": "オプション、調査ペイロードを送信するためのWebhook URL",
|
||||
"k899fd0cd": "ポート",
|
||||
"k89d54f7a": "実行カウントの監視",
|
||||
"k8a1deb63": "メンバー",
|
||||
@ -225,10 +207,7 @@
|
||||
"k90b603b8": "重複",
|
||||
"k90b668e5": "過去24時間",
|
||||
"k93374bc9": "ウェブサイトを削除",
|
||||
"k93458b98": "プレイグラウンド",
|
||||
"k951a939a": "ウェブサイト承認カウント",
|
||||
"k95f932a": "現在、リモートサーバーからの新しいリクエストを待機中です",
|
||||
"k97b02874": "ページ数",
|
||||
"k98f433ee": "からレポーターをダウンロード",
|
||||
"k9991c290": "コミュニティ",
|
||||
"k9a272ecf": "これはあなたのサーバーですか?",
|
||||
@ -254,7 +233,6 @@
|
||||
"ka6ee7455": "ウェブサイトID",
|
||||
"ka71c12e1": "2つのパスワードが一致しません",
|
||||
"ka765ad32": "通知",
|
||||
"ka7d8617e": "フィードチャンネル数",
|
||||
"ka7fe5937": "ディスク読み取り/書き込み",
|
||||
"ka8e41156": "検索して素早く移動",
|
||||
"ka90bc019": "アンインストール",
|
||||
@ -276,7 +254,6 @@
|
||||
"kb0e351e0": "更新されました",
|
||||
"kb114a2e8": "非推奨",
|
||||
"kb15a6374": "自分のドメインでステータスページを設定できます。たとえば、status.example.com",
|
||||
"kb2dded49": "キー",
|
||||
"kb320aac4": "{{dayNum}}日間監視",
|
||||
"kb35cde91": "検索",
|
||||
"kb35d71ed": "または",
|
||||
@ -284,7 +261,6 @@
|
||||
"kb5673707": "過去7日間",
|
||||
"kb659c1bc": "証明書の有効期限",
|
||||
"kb6d350b6": "フィードチャンネル",
|
||||
"kb7bf8869": "APIキー",
|
||||
"kb7fa344a": "送信するフィードチャンネルを選択",
|
||||
"kb8de8c50": "BCC",
|
||||
"kbb31d3db": "統計日",
|
||||
@ -320,16 +296,13 @@
|
||||
"kcc9c1bff": "毎週",
|
||||
"kccaa732a": "連続ダッシュなし",
|
||||
"kccb42483": "パスワード",
|
||||
"kcd56f27b": "最終更新",
|
||||
"kcd643ef3": "読み込み中...",
|
||||
"kce77d0c1": "タイムゾーン",
|
||||
"kcff78587": "最終使用日時",
|
||||
"kd005f7a8": "すべてのフィードが削除されます",
|
||||
"kd031b383": "ビュー",
|
||||
"kd044d5d4": "セッション",
|
||||
"kd092de58": "現在のワークスペース:",
|
||||
"kd1f7e695": "ログアウトを確認",
|
||||
"kd211e2d4": "リリースページ",
|
||||
"kd25f123a": "ステータス不明",
|
||||
"kd2a7ad83": "フィードテンプレート",
|
||||
"kd3262a4a": "設定",
|
||||
"kd3396544": "一般的に、ユーザーの通常の使用に影響を与えないように、1ピクセルの空白画像を使用します。",
|
||||
@ -338,18 +311,14 @@
|
||||
"kd7279fa6": "コード",
|
||||
"kd7985726": "{{num}}人のユーザー",
|
||||
"kd92fa3e7": "ホスト名",
|
||||
"kdaa6ae2b": "モニター数",
|
||||
"kdaff25a6": "最新値を表示",
|
||||
"kdb61adbb": "オフラインを隠す",
|
||||
"kdbadcf43": "すべてのシステムが稼働中",
|
||||
"kdbe222b": "APIキー",
|
||||
"kdc10ee1a": "チームメンバーと協力するために新しいワークスペースを作成します。",
|
||||
"kdc15c5d": "データ",
|
||||
"kdc1bf80e": "URLは必須です",
|
||||
"kdc51b5db": "ウェブサイト",
|
||||
"kdd44ac01": "表示するテレメトリー名",
|
||||
"kdd55936a": "リゾルバーポート",
|
||||
"kde315178": "名前を変更",
|
||||
"kde37bc27": "管理者に戻る",
|
||||
"kdeba7706": "デバイス",
|
||||
"kdeecbfea": "リゾルバーサーバー",
|
||||
@ -390,7 +359,6 @@
|
||||
"kf246dd2e": "ワークスペースが見つかりません。最初に作成してください。",
|
||||
"kf3b749ef": "ダイレクトチャット/グループ/チャネルのチャットIDをサポート",
|
||||
"kf55495e0": "保存",
|
||||
"kf5c3b616": "リクエストヘッダー",
|
||||
"kf5c9520e": "まだステータスページがありません。新しいステータスページを作成して、サービスのステータスを公開することができます。",
|
||||
"kf6339d4f": "確認済み",
|
||||
"kf6582ba": "ワークスペース",
|
||||
@ -406,7 +374,6 @@
|
||||
"kf97b6f71": "Linuxマシンでこのコマンドを実行してください",
|
||||
"kf9877f28": "詳細を見る",
|
||||
"kf9965c19": "このワークスペース内のすべてのコンテンツは破壊され、復元できません。",
|
||||
"kf9a498c7": "Lighthouseレポートが完了しました!",
|
||||
"kfc98929b": "{{num}}日",
|
||||
"kfd33c459": "コピーに成功しました!",
|
||||
"kfdaf0bb3": "最後のオンライン:{{time}}",
|
Before Width: | Height: | Size: 259 B After Width: | Height: | Size: 259 B |
@ -19,14 +19,11 @@
|
||||
"k17058821": "Raporty Lighthouse Strony",
|
||||
"k172a09c3": "Sugestie",
|
||||
"k1777bbf2": "Instrukcja obsługi",
|
||||
"k1940fd6": "Ogólne",
|
||||
"k1964b988": "Zatrzymaj",
|
||||
"k1bd89236": "uruchom raportera z",
|
||||
"k1c33c293": "Ustawienia",
|
||||
"k1d8f92b4": "Tablet",
|
||||
"k1da4ecc2": "Możesz wysłać wiadomość do tego kanału za pomocą:",
|
||||
"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",
|
||||
"k2099f2e0": "Logowanie nie powiodło się, sprawdź swoją nazwę użytkownika i hasło",
|
||||
"k20edf271": "24h",
|
||||
@ -56,15 +53,12 @@
|
||||
"k2c84fe32": "Liczba zdarzeń w kanale",
|
||||
"k2cecf817": "Typ",
|
||||
"k2dad13e3": "Język",
|
||||
"k2db2c0c5": "Test Powiadomienie",
|
||||
"k2e6dbf02": "Do e-maila",
|
||||
"k2ea8a019": "Monitorować",
|
||||
"k30b5f01b": "Obszary robocze",
|
||||
"k30d33d71": "Podpis Webhooka",
|
||||
"k310fee": "Ostatnie 30 dni",
|
||||
"k32344f64": "Wyczyść dane",
|
||||
"k3260f019": "Wyloguj",
|
||||
"k3404b72f": "Nowa nazwa przestrzeni roboczej",
|
||||
"k340547f0": "Przepraszamy, ale coś poszło nie tak",
|
||||
"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.",
|
||||
@ -91,7 +85,6 @@
|
||||
"k3e8b13f8": "Dołącz do Discorda",
|
||||
"k3eaab921": "Lista monitorów",
|
||||
"k3f36e17e": "Śledź na Twitterze",
|
||||
"k406089a4": "Akcja",
|
||||
"k406e9ad8": "Potwierdź",
|
||||
"k41d3ce6c": "Wydarzenie odarchiwizowane",
|
||||
"k42347b91": "Liczba zdarzeń na stronie internetowej",
|
||||
@ -100,8 +93,7 @@
|
||||
"k44186b66": "Liczba",
|
||||
"k44cad477": "(Obecny)",
|
||||
"k45f80a27": "Zaawansowane",
|
||||
"k4727e4db": "Wygasło",
|
||||
"k477b7ee4": "Częściowa awaria systemu",
|
||||
"k4738284": "Możesz wysłać dowolną wiadomość do tego kanału za pomocą:",
|
||||
"k47fe1f95": "Dodaj ten przykładowy kod do swojego projektu",
|
||||
"k48186ce": "Powrót do strony głównej",
|
||||
"k4905ed7b": "BRAK",
|
||||
@ -115,7 +107,6 @@
|
||||
"k4de48e75": "Maksymalna liczba prób",
|
||||
"k4e08cf58": "Pokaż liczbę szczegółów",
|
||||
"k4eea9393": "Profil",
|
||||
"k4f182a7c": "Poważna awaria systemu",
|
||||
"k4fc2b5b": "Obraz",
|
||||
"k4fe1b4de": "Telemetria",
|
||||
"k505c2733": "Utwórz Raport",
|
||||
@ -132,12 +123,9 @@
|
||||
"k58267a45": "Źródło",
|
||||
"k58f90514": "Token Bota",
|
||||
"k593cf342": "Czy na pewno chcesz usunąć ten monitor?",
|
||||
"k5a782f4b": "Liczba stron internetowych",
|
||||
"k5a839f71": "Czas działania",
|
||||
"k5b5be0d4": "Aktualna Rola",
|
||||
"k5c18db28": "Zmień informacje na stronie statusu",
|
||||
"k5d00536d": "Skopiowane",
|
||||
"k5d49d751": "Nowy klucz API został skopiowany do schowka!",
|
||||
"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.",
|
||||
"k5ecf04b0": "Widok",
|
||||
@ -147,7 +135,6 @@
|
||||
"k62e19375": "Ostatnia aktualizacja: {{date}}",
|
||||
"k6488f302": "Opcjonalne",
|
||||
"k659b065": "Na przykład: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
|
||||
"k678e2f90": "Treść żądania",
|
||||
"k67c5a895": "Wczoraj",
|
||||
"k683be220": "Uruchom",
|
||||
"k691b7170": "Zatrzymany",
|
||||
@ -160,11 +147,9 @@
|
||||
"k6e96fc3": "Informacje o formularzu",
|
||||
"k6ea11aff": "OK!",
|
||||
"k6f15bcc3": "Host",
|
||||
"k71067412": "Opcjonalne, podpis webhooka dla przychodzącego webhooka",
|
||||
"k721589c1": "Dziś",
|
||||
"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.",
|
||||
"k736f3e4c": "Kopiuj jako",
|
||||
"k75581e13": "DW",
|
||||
"k75bfaaa6": "Dodaj ten kod do sekcji head na swojej stronie internetowej",
|
||||
"k763816ac": "Podgląd",
|
||||
@ -172,17 +157,16 @@
|
||||
"k78b1ef6a": "Wprowadź",
|
||||
"k7927b824": "Czy na pewno chcesz wyczyścić wszystkie wyłączone węzły?",
|
||||
"k7a132ce8": "Przepraszamy, ale ta strona nie została znaleziona",
|
||||
"k7a15497a": "Na żywo",
|
||||
"k7ac44a6e": "Klucz sesji",
|
||||
"k7b74a43f": "odwiedzający",
|
||||
"k7b75e24c": "Integracja",
|
||||
"k7b9aa48c": "Treść",
|
||||
"k7cac602a": "Status",
|
||||
"k7d8cd81c": "Kopiuj URL",
|
||||
"k7e0360fd": "Nie utworzono żadnej grupy, kliknij przycisk, aby utworzyć jedną",
|
||||
"k7e61b1af": "Wybierz przestrzeń roboczą",
|
||||
"k7f01b47c": "Dziennik audytu",
|
||||
"k7f03a704": "Pamiętaj, aby nie wysyłać danych za pomocą application/json",
|
||||
"k7f29bae5": "wyświetlenia strony",
|
||||
"k8037cc6b": "Serwery",
|
||||
"k816ce026": "Pobierz",
|
||||
"k819633bc": "Użyj do przechowywania",
|
||||
@ -192,7 +176,6 @@
|
||||
"k84ce1618": "(24 godziny)",
|
||||
"k84e82947": "{{num}} zdarzeń usuniętych",
|
||||
"k85344b23": "Obciążenie",
|
||||
"k85a116ee": "Adres URL Webhooka",
|
||||
"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.",
|
||||
"k873c90e6": "Etykieta wyświetlania",
|
||||
@ -205,7 +188,6 @@
|
||||
"k88d2647b": "Strona internetowa",
|
||||
"k89056082": "(30 dni)",
|
||||
"k892f84b6": "Nie można uzyskać informacji o bieżącym użytkowniku",
|
||||
"k895cafe1": "Opcjonalne, adres url webhooka do wysyłania ładunku ankiety",
|
||||
"k899fd0cd": "Porty",
|
||||
"k89d54f7a": "Liczba wykonań monitora",
|
||||
"k8a1deb63": "Członkowie",
|
||||
@ -225,10 +207,7 @@
|
||||
"k90b603b8": "Duplikat",
|
||||
"k90b668e5": "Ostatnie 24 godziny",
|
||||
"k93374bc9": "Usuń stronę internetową",
|
||||
"k93458b98": "Plac zabaw",
|
||||
"k951a939a": "Liczba zaakceptowanych stron internetowych",
|
||||
"k95f932a": "Obecnie czekam na nowe żądanie z zdalnego serwera",
|
||||
"k97b02874": "Liczba stron",
|
||||
"k98f433ee": "Pobierz reporter z",
|
||||
"k9991c290": "Społeczność",
|
||||
"k9a272ecf": "Czy to twoje serwery?",
|
||||
@ -254,7 +233,6 @@
|
||||
"ka6ee7455": "ID strony internetowej",
|
||||
"ka71c12e1": "Dwa hasła nie są zgodne",
|
||||
"ka765ad32": "Powiadomienie",
|
||||
"ka7d8617e": "Liczba kanałów feed",
|
||||
"ka7fe5937": "Odczyt/zapis dysku",
|
||||
"ka8e41156": "Wyszukiwanie i szybkie przeskakiwanie",
|
||||
"ka90bc019": "Odinstaluj",
|
||||
@ -276,7 +254,6 @@
|
||||
"kb0e351e0": "Odświeżone",
|
||||
"kb114a2e8": "Przestarzałe",
|
||||
"kb15a6374": "Możesz skonfigurować swoją stronę statusu pod własną domeną, na przykład: status.example.com",
|
||||
"kb2dded49": "Klucz",
|
||||
"kb320aac4": "Monitorowane przez {{dayNum}} dni",
|
||||
"kb35cde91": "Szukaj",
|
||||
"kb35d71ed": "LUB",
|
||||
@ -284,7 +261,6 @@
|
||||
"kb5673707": "Ostatnie 7 dni",
|
||||
"kb659c1bc": "Wygaśnięcie certyfikatu",
|
||||
"kb6d350b6": "Kanały feedu",
|
||||
"kb7bf8869": "Klucze API",
|
||||
"kb7fa344a": "Wybierz kanał feedu do wysłania",
|
||||
"kb8de8c50": "DWU",
|
||||
"kbb31d3db": "Data statystyk",
|
||||
@ -308,7 +284,7 @@
|
||||
"kc5f82d53": "Na przykład: pushdeer://pushKey",
|
||||
"kc6888ac4": "Automatyczny",
|
||||
"kc6cac621": "(Brak)",
|
||||
"kc6dc3c38": "Komputer stacjonarny",
|
||||
"kc6dc3c38": "Desktop",
|
||||
"kc70d69ad": "Odpowiedź",
|
||||
"kc9b446d1": "Zakończono uruchamianie",
|
||||
"kcacbfde1": "Utwórz teraz",
|
||||
@ -320,16 +296,13 @@
|
||||
"kcc9c1bff": "Każdy tydzień",
|
||||
"kccaa732a": "Brak kolejnych myślników",
|
||||
"kccb42483": "Hasło",
|
||||
"kcd56f27b": "Ostatnia aktualizacja",
|
||||
"kcd643ef3": "Ładowanie...",
|
||||
"kce77d0c1": "Strefa czasowa",
|
||||
"kcff78587": "Ostatnie użycie",
|
||||
"kd005f7a8": "Wszystkie kanały informacyjne zostaną usunięte",
|
||||
"kd031b383": "Odsłony",
|
||||
"kd044d5d4": "sesja",
|
||||
"kd092de58": "Aktualna przestrzeń robocza:",
|
||||
"kd1f7e695": "Potwierdź wylogowanie",
|
||||
"kd211e2d4": "Strona wydań",
|
||||
"kd25f123a": "Status nieznany",
|
||||
"kd2a7ad83": "Szablon feedu",
|
||||
"kd3262a4a": "Konfiguracja",
|
||||
"kd3396544": "Zazwyczaj użyjemy pustego obrazu o rozmiarze jednego piksela, aby nie wpływał na normalne użytkowanie użytkownika.",
|
||||
@ -338,18 +311,14 @@
|
||||
"kd7279fa6": "Kod",
|
||||
"kd7985726": "{{num}} użytkowników",
|
||||
"kd92fa3e7": "Nazwa hosta",
|
||||
"kdaa6ae2b": "Liczba monitorów",
|
||||
"kdaff25a6": "Pokaż najnowszą wartość",
|
||||
"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.",
|
||||
"kdc15c5d": "Dane",
|
||||
"kdc1bf80e": "Url jest wymagany",
|
||||
"kdc51b5db": "Strony internetowe",
|
||||
"kdd44ac01": "Nazwa telemetrii do wyświetlenia",
|
||||
"kdd55936a": "Port resolvera",
|
||||
"kde315178": "Zmień nazwę",
|
||||
"kde37bc27": "Powrót do panelu administratora",
|
||||
"kdeba7706": "Urządzenia",
|
||||
"kdeecbfea": "Serwer resolvera",
|
||||
@ -390,7 +359,6 @@
|
||||
"kf246dd2e": "Nie znaleziono żadnej przestrzeni roboczej, proszę najpierw utworzyć",
|
||||
"kf3b749ef": "Wsparcie dla czatu bezpośredniego / grupy / czatu kanału",
|
||||
"kf55495e0": "Zapisz",
|
||||
"kf5c3b616": "Nagłówek żądania",
|
||||
"kf5c9520e": "Brak stron statusu, możesz utworzyć nową, aby pokazać stan swojej usługi publicznie.",
|
||||
"kf6339d4f": "Zweryfikowane",
|
||||
"kf6582ba": "Przestrzeń robocza",
|
||||
@ -406,7 +374,6 @@
|
||||
"kf97b6f71": "Uruchom to polecenie na swojej maszynie z systemem Linux",
|
||||
"kf9877f28": "Pokaż szczegóły",
|
||||
"kf9965c19": "Cała zawartość w tej przestrzeni roboczej zostanie zniszczona i nie można jej odzyskać.",
|
||||
"kf9a498c7": "Raport Lighthouse zakończony!",
|
||||
"kfc98929b": "{{num}} dni",
|
||||
"kfd33c459": "Kopiowanie powiodło się!",
|
||||
"kfdaf0bb3": "O na pewno chcesz usunąć wszystkie zdarzenia dla tego monitora?",
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
@ -19,14 +19,11 @@
|
||||
"k17058821": "Relatórios do Website Lighthouse",
|
||||
"k172a09c3": "Sugestões",
|
||||
"k1777bbf2": "Manual",
|
||||
"k1940fd6": "Geral",
|
||||
"k1964b988": "Parar",
|
||||
"k1bd89236": "correr repórter com",
|
||||
"k1c33c293": "Definições",
|
||||
"k1d8f92b4": "Tablet",
|
||||
"k1da4ecc2": "Você pode enviar uma mensagem para este canal com:",
|
||||
"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",
|
||||
"k2099f2e0": "Falha no login, verifique seu nome de usuário e senha",
|
||||
"k20edf271": "24 horas",
|
||||
@ -56,15 +53,12 @@
|
||||
"k2c84fe32": "Contagem de eventos de feed",
|
||||
"k2cecf817": "Tipo",
|
||||
"k2dad13e3": "Idioma",
|
||||
"k2db2c0c5": "Notificação de Teste",
|
||||
"k2e6dbf02": "Para o e-mail",
|
||||
"k2ea8a019": "Monitorar",
|
||||
"k30b5f01b": "Áreas de trabalho",
|
||||
"k30d33d71": "Assinatura do Webhook",
|
||||
"k310fee": "Últimos 30 dias",
|
||||
"k32344f64": "Limpar dados",
|
||||
"k3260f019": "Terminar sessão",
|
||||
"k3404b72f": "Novo Nome do Espaço de Trabalho",
|
||||
"k340547f0": "Desculpe, mas algo correu mal",
|
||||
"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.",
|
||||
@ -91,7 +85,6 @@
|
||||
"k3e8b13f8": "Aderir ao Discord",
|
||||
"k3eaab921": "Lista de Monitoramento",
|
||||
"k3f36e17e": "Seguir o Twitter",
|
||||
"k406089a4": "Ação",
|
||||
"k406e9ad8": "Confirmar",
|
||||
"k41d3ce6c": "Evento desarquivado",
|
||||
"k42347b91": "Contagem de eventos do sítio Web",
|
||||
@ -100,8 +93,7 @@
|
||||
"k44186b66": "Contar",
|
||||
"k44cad477": "(Atual)",
|
||||
"k45f80a27": "Avançado",
|
||||
"k4727e4db": "Expirado Em",
|
||||
"k477b7ee4": "Interrupção Parcial do Sistema",
|
||||
"k4738284": "Você pode enviar qualquer mensagem para este canal com:",
|
||||
"k47fe1f95": "Adicione este código de exemplo ao seu projeto",
|
||||
"k48186ce": "Voltar à página inicial",
|
||||
"k4905ed7b": "NENHUM",
|
||||
@ -115,7 +107,6 @@
|
||||
"k4de48e75": "Máximo de tentativas",
|
||||
"k4e08cf58": "Mostrar número de pormenor",
|
||||
"k4eea9393": "Perfil",
|
||||
"k4f182a7c": "Interrupção Maior do Sistema",
|
||||
"k4fc2b5b": "Imagem",
|
||||
"k4fe1b4de": "Telemetria",
|
||||
"k505c2733": "Criar Relatório",
|
||||
@ -132,12 +123,9 @@
|
||||
"k58267a45": "Tipo de Letra",
|
||||
"k58f90514": "Token de Bot",
|
||||
"k593cf342": "De certeza que eliminou este monitor?",
|
||||
"k5a782f4b": "Contagem de Sites",
|
||||
"k5a839f71": "Tempo de atividade",
|
||||
"k5b5be0d4": "Função Atual",
|
||||
"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",
|
||||
"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",
|
||||
@ -147,7 +135,6 @@
|
||||
"k62e19375": "Última atualização em: {{date}}",
|
||||
"k6488f302": "Opcional",
|
||||
"k659b065": "Por exemplo: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
|
||||
"k678e2f90": "Corpo da Solicitação",
|
||||
"k67c5a895": "Ontem",
|
||||
"k683be220": "Correr",
|
||||
"k691b7170": "Parado",
|
||||
@ -160,11 +147,9 @@
|
||||
"k6e96fc3": "Informações do formulário",
|
||||
"k6ea11aff": "Obter!",
|
||||
"k6f15bcc3": "Anfitrião",
|
||||
"k71067412": "Opcional, Assinatura do Webhook para Webhook de Entrada",
|
||||
"k721589c1": "Hoje",
|
||||
"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.",
|
||||
"k736f3e4c": "Copiar como",
|
||||
"k75581e13": "CC",
|
||||
"k75bfaaa6": "Adicionar este código ao script principal do seu sítio Web",
|
||||
"k763816ac": "Pré-visualização",
|
||||
@ -172,17 +157,16 @@
|
||||
"k78b1ef6a": "Entrar",
|
||||
"k7927b824": "Tem a certeza de que pretende limpar todos os nós offline?",
|
||||
"k7a132ce8": "Desculpe, mas esta página não foi encontrada",
|
||||
"k7a15497a": "Em Tempo Real",
|
||||
"k7ac44a6e": "Chave de sessão",
|
||||
"k7b74a43f": "visitantes",
|
||||
"k7b75e24c": "Integração",
|
||||
"k7b9aa48c": "Corpo",
|
||||
"k7cac602a": "Estado",
|
||||
"k7d8cd81c": "Copiar URL",
|
||||
"k7e0360fd": "Nenhum grupo foi criado, clique no botão para criar um",
|
||||
"k7e61b1af": "Selecionar Espaço de Trabalho",
|
||||
"k7f01b47c": "Registo de auditoria",
|
||||
"k7f03a704": "Não se esqueça de não enviar dados com application/json",
|
||||
"k7f29bae5": "visualização de página",
|
||||
"k8037cc6b": "Servidores",
|
||||
"k816ce026": "Baixar",
|
||||
"k819633bc": "Usar para armazenamento",
|
||||
@ -192,7 +176,6 @@
|
||||
"k84ce1618": "(24 horas)",
|
||||
"k84e82947": "{{num}} eventos limpos",
|
||||
"k85344b23": "Carregar",
|
||||
"k85a116ee": "URL do Webhook",
|
||||
"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.",
|
||||
"k873c90e6": "Etiqueta de Exibição",
|
||||
@ -205,7 +188,6 @@
|
||||
"k88d2647b": "Sítio Web",
|
||||
"k89056082": "(30 dias)",
|
||||
"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",
|
||||
"k89d54f7a": "Contagem de Execução do Monitor",
|
||||
"k8a1deb63": "Membros",
|
||||
@ -225,10 +207,7 @@
|
||||
"k90b603b8": "Duplicar",
|
||||
"k90b668e5": "Últimas 24 horas",
|
||||
"k93374bc9": "Eliminar sítio Web",
|
||||
"k93458b98": "Playground",
|
||||
"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",
|
||||
"k9991c290": "Comunidade",
|
||||
"k9a272ecf": "Estes são os vossos servidores?",
|
||||
@ -254,7 +233,6 @@
|
||||
"ka6ee7455": "ID do sítio Web",
|
||||
"ka71c12e1": "As duas palavras-passe não são consistentes",
|
||||
"ka765ad32": "Notificação",
|
||||
"ka7d8617e": "Contagem de Canais de Feed",
|
||||
"ka7fe5937": "Leitura/escrita de disco",
|
||||
"ka8e41156": "Pesquisa e salto rápido",
|
||||
"ka90bc019": "Desinstalar",
|
||||
@ -276,7 +254,6 @@
|
||||
"kb0e351e0": "Atualizado",
|
||||
"kb114a2e8": "Obsoleto",
|
||||
"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",
|
||||
"kb35cde91": "Pesquisar",
|
||||
"kb35d71ed": "OU",
|
||||
@ -284,7 +261,6 @@
|
||||
"kb5673707": "Últimos 7 dias",
|
||||
"kb659c1bc": "Exp. do certificado",
|
||||
"kb6d350b6": "Canais de Feed",
|
||||
"kb7bf8869": "Chaves de API",
|
||||
"kb7fa344a": "Selecione o Canal de Feed para enviar",
|
||||
"kb8de8c50": "CCO",
|
||||
"kbb31d3db": "Data da estatística",
|
||||
@ -320,16 +296,13 @@
|
||||
"kcc9c1bff": "Toda semana",
|
||||
"kccaa732a": "Sem traços consecutivos",
|
||||
"kccb42483": "Palavra-passe",
|
||||
"kcd56f27b": "Última atualização",
|
||||
"kcd643ef3": "Carregando...",
|
||||
"kce77d0c1": "Fuso Horário",
|
||||
"kcff78587": "Último Uso Em",
|
||||
"kd005f7a8": "Todos os feeds serão removidos",
|
||||
"kd031b383": "Vistas",
|
||||
"kd044d5d4": "sessão",
|
||||
"kd092de58": "Espaço de Trabalho Atual:",
|
||||
"kd1f7e695": "Confirmar para terminar a sessão",
|
||||
"kd211e2d4": "Página de lançamentos",
|
||||
"kd25f123a": "Status Desconhecido",
|
||||
"kd2a7ad83": "Modelo de Feed",
|
||||
"kd3262a4a": "Configuração",
|
||||
"kd3396544": "Geralmente, utilizamos uma imagem em branco de um pixel para que não afecte a utilização normal do utilizador.",
|
||||
@ -338,18 +311,14 @@
|
||||
"kd7279fa6": "Código",
|
||||
"kd7985726": "{{num}} utilizadores",
|
||||
"kd92fa3e7": "Nome do anfitrião",
|
||||
"kdaa6ae2b": "Contagem de Monitores",
|
||||
"kdaff25a6": "Mostrar valor mais recente",
|
||||
"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.",
|
||||
"kdc15c5d": "Dados",
|
||||
"kdc1bf80e": "Url é obrigatório",
|
||||
"kdc51b5db": "Sites",
|
||||
"kdd44ac01": "Nome de telemetria a apresentar",
|
||||
"kdd55936a": "Porta do resolvedor",
|
||||
"kde315178": "Renomear",
|
||||
"kde37bc27": "Voltar ao Administrador",
|
||||
"kdeba7706": "Dispositivos",
|
||||
"kdeecbfea": "Servidor de resolução",
|
||||
@ -390,7 +359,6 @@
|
||||
"kf246dd2e": "Nenhum espaço de trabalho foi encontrado, por favor crie primeiro",
|
||||
"kf3b749ef": "ID de Chat Direto do Suporte / Grupo / Canal",
|
||||
"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.",
|
||||
"kf6339d4f": "Verificado",
|
||||
"kf6582ba": "Espaço de Trabalho",
|
||||
@ -406,7 +374,6 @@
|
||||
"kf97b6f71": "Executar este comando na sua máquina linux",
|
||||
"kf9877f28": "Ver detalhes",
|
||||
"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",
|
||||
"kfd33c459": "Cópia bem sucedida!",
|
||||
"kfdaf0bb3": "Última vez online: {{tempo}}",
|
Before Width: | Height: | Size: 268 B After Width: | Height: | Size: 268 B |
@ -19,14 +19,11 @@
|
||||
"k17058821": "Отчеты Lighthouse для веб-сайтов",
|
||||
"k172a09c3": "Предложения",
|
||||
"k1777bbf2": "Вручную",
|
||||
"k1940fd6": "Общее",
|
||||
"k1964b988": "Остановить",
|
||||
"k1bd89236": "запустить репортер с",
|
||||
"k1c33c293": "Настройки",
|
||||
"k1d8f92b4": "Планшет",
|
||||
"k1da4ecc2": "Вы можете отправить сообщение в этот канал с помощью:",
|
||||
"k1eb5b3ed": "Обзор",
|
||||
"k1ee0c2ca": "Установите URL вебхука на <1></1> и оставьте это окно активным. После завершения вы начнете получать запросы вебхука здесь.",
|
||||
"k1f6dea0": "Название канала",
|
||||
"k2099f2e0": "Ошибка входа, проверьте имя пользователя и пароль",
|
||||
"k20edf271": "24ч",
|
||||
@ -56,15 +53,12 @@
|
||||
"k2c84fe32": "Количество событий ленты",
|
||||
"k2cecf817": "Тип",
|
||||
"k2dad13e3": "Язык",
|
||||
"k2db2c0c5": "Тестовое уведомление",
|
||||
"k2e6dbf02": "На Email",
|
||||
"k2ea8a019": "Монитор",
|
||||
"k30b5f01b": "Рабочие области",
|
||||
"k30d33d71": "Подпись вебхука",
|
||||
"k310fee": "Последние 30 дней",
|
||||
"k32344f64": "Очистить данные",
|
||||
"k3260f019": "Выйти",
|
||||
"k3404b72f": "Новое имя рабочего пространства",
|
||||
"k340547f0": "Извините, но что-то пошло не так",
|
||||
"k3471e956": "Повтор нового пароля",
|
||||
"k34981fea": "Docker дрейфует в море, не может найти свой путь. Пожалуйста, запустите Docker, чтобы вернуться на правильный курс.",
|
||||
@ -91,7 +85,6 @@
|
||||
"k3e8b13f8": "Присоединяйтесь к Discord",
|
||||
"k3eaab921": "Список мониторинга",
|
||||
"k3f36e17e": "Подписаться на Twitter",
|
||||
"k406089a4": "Действие",
|
||||
"k406e9ad8": "Подтвердить",
|
||||
"k41d3ce6c": "Событие восстановлено",
|
||||
"k42347b91": "Количество событий на сайте",
|
||||
@ -100,8 +93,7 @@
|
||||
"k44186b66": "Количество",
|
||||
"k44cad477": "(Текущий)",
|
||||
"k45f80a27": "Расширенный",
|
||||
"k4727e4db": "Истекает",
|
||||
"k477b7ee4": "Частичный сбой системы",
|
||||
"k4738284": "Вы можете отправить любое сообщение в этот канал с помощью:",
|
||||
"k47fe1f95": "Добавьте этот пример кода в ваш проект",
|
||||
"k48186ce": "Вернуться на главную страницу",
|
||||
"k4905ed7b": "НИКАКОЙ",
|
||||
@ -115,7 +107,6 @@
|
||||
"k4de48e75": "Макс. попыток",
|
||||
"k4e08cf58": "Показать подробное количество",
|
||||
"k4eea9393": "Профиль",
|
||||
"k4f182a7c": "Крупный сбой системы",
|
||||
"k4fc2b5b": "Изображение",
|
||||
"k4fe1b4de": "Телеметрия",
|
||||
"k505c2733": "Создать отчет",
|
||||
@ -132,12 +123,9 @@
|
||||
"k58267a45": "Источник",
|
||||
"k58f90514": "Токен бота",
|
||||
"k593cf342": "Вы уверены, что хотите удалить этот монитор?",
|
||||
"k5a782f4b": "Количество сайтов",
|
||||
"k5a839f71": "Время работы",
|
||||
"k5b5be0d4": "Текущая роль",
|
||||
"k5c18db28": "Изменить информацию на странице статуса",
|
||||
"k5d00536d": "Скопировано",
|
||||
"k5d49d751": "Новый API-ключ скопирован в буфер обмена!",
|
||||
"k5eb87a8b": "Старт",
|
||||
"k5ec0de4": "Для мониторинга HTTPS, если назначен любой метод уведомления, уведомления будут отправлены за 1, 3, 7 и 14 дней до истечения срока действия.",
|
||||
"k5ecf04b0": "Просмотр",
|
||||
@ -147,7 +135,6 @@
|
||||
"k62e19375": "Последнее обновление: {{date}}",
|
||||
"k6488f302": "Необязательно",
|
||||
"k659b065": "Например: https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
|
||||
"k678e2f90": "Тело запроса",
|
||||
"k67c5a895": "Вчера",
|
||||
"k683be220": "Запустить",
|
||||
"k691b7170": "Остановлено",
|
||||
@ -160,11 +147,9 @@
|
||||
"k6e96fc3": "Информация формы",
|
||||
"k6ea11aff": "Получить!",
|
||||
"k6f15bcc3": "Хост",
|
||||
"k71067412": "Необязательно, подпись вебхука для входящего вебхука",
|
||||
"k721589c1": "Сегодня",
|
||||
"k7247683c": "Удалить рабочее пространство",
|
||||
"k7350bd93": "В то же время, мы также можем использовать это в некоторых сценариях клиентского приложения, таких как сбор частоты использования cli, сбор установок самостоятельно размещенных приложений и так далее.",
|
||||
"k736f3e4c": "Копировать как",
|
||||
"k75581e13": "Копия",
|
||||
"k75bfaaa6": "Добавьте этот код в скрипт заголовка вашего веб-сайта",
|
||||
"k763816ac": "Предварительный просмотр",
|
||||
@ -172,17 +157,16 @@
|
||||
"k78b1ef6a": "Ввод",
|
||||
"k7927b824": "Вы уверены, что хотите очистить все офлайн узлы?",
|
||||
"k7a132ce8": "Извините, но эта страница не найдена",
|
||||
"k7a15497a": "В реальном времени",
|
||||
"k7ac44a6e": "Ключ сессии",
|
||||
"k7b74a43f": "посетители",
|
||||
"k7b75e24c": "Интеграция",
|
||||
"k7b9aa48c": "Тело",
|
||||
"k7cac602a": "Статус",
|
||||
"k7d8cd81c": "Копировать URL",
|
||||
"k7e0360fd": "Не создано ни одной группы, нажмите кнопку, чтобы создать одну",
|
||||
"k7e61b1af": "Выбрать рабочее пространство",
|
||||
"k7f01b47c": "Журнал аудита",
|
||||
"k7f03a704": "Не забудьте не отправлять данные с application/json",
|
||||
"k7f29bae5": "Просмотр страницы",
|
||||
"k8037cc6b": "Серверы",
|
||||
"k816ce026": "Скачать",
|
||||
"k819633bc": "Использовать для хранения",
|
||||
@ -192,7 +176,6 @@
|
||||
"k84ce1618": "(24 часа)",
|
||||
"k84e82947": "{{num}} события очищены",
|
||||
"k85344b23": "Нагрузка",
|
||||
"k85a116ee": "URL вебхука",
|
||||
"k85c5fd4c": "Мониторы еще не настроены",
|
||||
"k85db19da": "Пока нет ни одного канала. Используйте функцию канала для получения всех событий из сети или вашей собственной службы.",
|
||||
"k873c90e6": "Метка отображения",
|
||||
@ -205,7 +188,6 @@
|
||||
"k88d2647b": "Веб-сайт",
|
||||
"k89056082": "(30 дней)",
|
||||
"k892f84b6": "Не удается получить информацию о текущем пользователе",
|
||||
"k895cafe1": "Необязательно, URL вебхука для отправки полезной нагрузки опроса",
|
||||
"k899fd0cd": "Порты",
|
||||
"k89d54f7a": "Количество выполнений мониторинга",
|
||||
"k8a1deb63": "Участники",
|
||||
@ -225,10 +207,7 @@
|
||||
"k90b603b8": "Дублировать",
|
||||
"k90b668e5": "Последние 24 часа",
|
||||
"k93374bc9": "Удалить веб-сайт",
|
||||
"k93458b98": "Площадка",
|
||||
"k951a939a": "Количество принятых сайтом",
|
||||
"k95f932a": "В настоящее время ожидает нового запроса от удаленного сервера",
|
||||
"k97b02874": "Количество страниц",
|
||||
"k98f433ee": "Скачать репортер с",
|
||||
"k9991c290": "Сообщество",
|
||||
"k9a272ecf": "Это ваши серверы?",
|
||||
@ -254,7 +233,6 @@
|
||||
"ka6ee7455": "ID веб-сайта",
|
||||
"ka71c12e1": "Два пароля не совпадают",
|
||||
"ka765ad32": "Уведомления",
|
||||
"ka7d8617e": "Количество каналов ленты",
|
||||
"ka7fe5937": "Чтение/запись на диск",
|
||||
"ka8e41156": "Поиск и быстрый переход",
|
||||
"ka90bc019": "Удалить",
|
||||
@ -276,7 +254,6 @@
|
||||
"kb0e351e0": "Обновлено",
|
||||
"kb114a2e8": "Устаревший",
|
||||
"kb15a6374": "Вы можете настроить свою страницу статуса на своем собственном домене, например: status.example.com",
|
||||
"kb2dded49": "Ключ",
|
||||
"kb320aac4": "Мониторинг в течение {{dayNum}} дней",
|
||||
"kb35cde91": "Поиск",
|
||||
"kb35d71ed": "ИЛИ",
|
||||
@ -284,7 +261,6 @@
|
||||
"kb5673707": "Последние 7 дней",
|
||||
"kb659c1bc": "Истечение серт.",
|
||||
"kb6d350b6": "Каналы обратной связи",
|
||||
"kb7bf8869": "API-ключи",
|
||||
"kb7fa344a": "Выберите канал обратной связи для отправки",
|
||||
"kb8de8c50": "Скрытая копия",
|
||||
"kbb31d3db": "Дата статистики",
|
||||
@ -320,16 +296,13 @@
|
||||
"kcc9c1bff": "Каждую неделю",
|
||||
"kccaa732a": "Без последовательных тире",
|
||||
"kccb42483": "Пароль",
|
||||
"kcd56f27b": "Последнее обновление",
|
||||
"kcd643ef3": "Загрузка...",
|
||||
"kce77d0c1": "Часовой пояс",
|
||||
"kcff78587": "Последнее использование",
|
||||
"kd005f7a8": "Все ленты будут удалены",
|
||||
"kd031b383": "Просмотры",
|
||||
"kd044d5d4": "Сессия",
|
||||
"kd092de58": "Текущее рабочее пространство:",
|
||||
"kd1f7e695": "Подтвердить выход",
|
||||
"kd211e2d4": "Страница релизов",
|
||||
"kd25f123a": "Статус неизвестен",
|
||||
"kd2a7ad83": "Шаблон обратной связи",
|
||||
"kd3262a4a": "Настройка",
|
||||
"kd3396544": "Обычно мы будем использовать однопиксельное пустое изображение, так что это не повлияет на нормальное использование пользователя.",
|
||||
@ -338,18 +311,14 @@
|
||||
"kd7279fa6": "Код",
|
||||
"kd7985726": "{{num}} пользователей",
|
||||
"kd92fa3e7": "Имя хоста",
|
||||
"kdaa6ae2b": "Количество мониторов",
|
||||
"kdaff25a6": "Показать последнее значение",
|
||||
"kdb61adbb": "Скрыть офлайн",
|
||||
"kdbadcf43": "Все системы работают",
|
||||
"kdbe222b": "API-ключ",
|
||||
"kdc10ee1a": "Создайте новое рабочее пространство для сотрудничества с членами команды.",
|
||||
"kdc15c5d": "Данные",
|
||||
"kdc1bf80e": "URL обязателен",
|
||||
"kdc51b5db": "Веб-сайты",
|
||||
"kdd44ac01": "Отображаемое имя телеметрии",
|
||||
"kdd55936a": "Порт разрешителя",
|
||||
"kde315178": "Переименовать",
|
||||
"kde37bc27": "Вернуться к администратору",
|
||||
"kdeba7706": "Устройства",
|
||||
"kdeecbfea": "Сервер разрешителя",
|
||||
@ -390,7 +359,6 @@
|
||||
"kf246dd2e": "Рабочее пространство не найдено, пожалуйста, создайте его сначала",
|
||||
"kf3b749ef": "Поддержка прямого чата / группы / ID чата канала",
|
||||
"kf55495e0": "Сохранить",
|
||||
"kf5c3b616": "Заголовок запроса",
|
||||
"kf5c9520e": "Пока нет страницы состояния, вы можете создать новую, чтобы показать статус вашего сервиса общественности.",
|
||||
"kf6339d4f": "Подтверждено",
|
||||
"kf6582ba": "Рабочее пространство",
|
||||
@ -406,7 +374,6 @@
|
||||
"kf97b6f71": "Запустите эту команду на вашем Linux-машине",
|
||||
"kf9877f28": "Посмотреть детали",
|
||||
"kf9965c19": "Всё содержимое в этом рабочем пространстве будет уничтожено и не может быть восстановлено.",
|
||||
"kf9a498c7": "Отчет Lighthouse завершен!",
|
||||
"kfc98929b": "{{num}} дней",
|
||||
"kfd33c459": "Копирование успешно!",
|
||||
"kfdaf0bb3": "Последний онлайн: {{time}}",
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@ -19,14 +19,11 @@
|
||||
"k17058821": "网站灯塔报告",
|
||||
"k172a09c3": "建议",
|
||||
"k1777bbf2": "手动",
|
||||
"k1940fd6": "常规",
|
||||
"k1964b988": "停止",
|
||||
"k1bd89236": "运行报告器",
|
||||
"k1c33c293": "设置",
|
||||
"k1d8f92b4": "平板电脑",
|
||||
"k1da4ecc2": "您可以通过以下方式向此频道发送消息:",
|
||||
"k1eb5b3ed": "概览",
|
||||
"k1ee0c2ca": "将 webhook URL 设置为 <1></1>,并保持此窗口处于活动状态。完成后,您将开始在此接收 webhook 请求。",
|
||||
"k1f6dea0": "频道名称",
|
||||
"k2099f2e0": "登录失败,请检查您的用户名和密码",
|
||||
"k20edf271": "24小时",
|
||||
@ -56,15 +53,12 @@
|
||||
"k2c84fe32": "事件计数",
|
||||
"k2cecf817": "类型",
|
||||
"k2dad13e3": "语言",
|
||||
"k2db2c0c5": "测试通知",
|
||||
"k2e6dbf02": "发邮件到",
|
||||
"k2ea8a019": "监控器",
|
||||
"k30b5f01b": "工作区",
|
||||
"k30d33d71": "Webhook 签名",
|
||||
"k310fee": "最近30天",
|
||||
"k32344f64": "清除数据",
|
||||
"k3260f019": "登出",
|
||||
"k3404b72f": "新工作区名称",
|
||||
"k340547f0": "抱歉,出了点问题",
|
||||
"k3471e956": "重复新密码",
|
||||
"k34981fea": "Docker在海上漂流,无法找到方向。请启动Docker以重新导航。",
|
||||
@ -91,7 +85,6 @@
|
||||
"k3e8b13f8": "加入 Discord",
|
||||
"k3eaab921": "监控列表",
|
||||
"k3f36e17e": "关注 Twitter",
|
||||
"k406089a4": "操作",
|
||||
"k406e9ad8": "确认",
|
||||
"k41d3ce6c": "事件已取消归档",
|
||||
"k42347b91": "网站事件计数",
|
||||
@ -100,8 +93,7 @@
|
||||
"k44186b66": "计数",
|
||||
"k44cad477": "(当前)",
|
||||
"k45f80a27": "高级",
|
||||
"k4727e4db": "到期时间",
|
||||
"k477b7ee4": "部分系统故障",
|
||||
"k4738284": "你可以通过以下方式向此频道发送任何消息:",
|
||||
"k47fe1f95": "将此示例代码添加到您的项目中",
|
||||
"k48186ce": "返回首页",
|
||||
"k4905ed7b": "无",
|
||||
@ -115,7 +107,6 @@
|
||||
"k4de48e75": "最大重试次数",
|
||||
"k4e08cf58": "显示详细数字",
|
||||
"k4eea9393": "个人资料",
|
||||
"k4f182a7c": "重大系统故障",
|
||||
"k4fc2b5b": "图片",
|
||||
"k4fe1b4de": "遥测",
|
||||
"k505c2733": "创建报告",
|
||||
@ -132,12 +123,9 @@
|
||||
"k58267a45": "源",
|
||||
"k58f90514": "机器人令牌",
|
||||
"k593cf342": "您确定要删除这个监控器吗?",
|
||||
"k5a782f4b": "网站数量",
|
||||
"k5a839f71": "正常运行时间",
|
||||
"k5b5be0d4": "当前角色",
|
||||
"k5c18db28": "修改状态页面信息",
|
||||
"k5d00536d": "已复制",
|
||||
"k5d49d751": "新的 API 密钥已复制到您的剪贴板!",
|
||||
"k5eb87a8b": "开始",
|
||||
"k5ec0de4": "对于 HTTPS 监控,如果分配了任何通知方法,则将在到期前 1、3、7 和 14 天发送通知。",
|
||||
"k5ecf04b0": "查看",
|
||||
@ -147,7 +135,6 @@
|
||||
"k62e19375": "最后更新时间:{{date}}",
|
||||
"k6488f302": "可选",
|
||||
"k659b065": "示例:https://open.feishu.cn/open-apis/bot/v2/hook/00000000-0000-0000-0000-000000000000",
|
||||
"k678e2f90": "请求体",
|
||||
"k67c5a895": "昨天",
|
||||
"k683be220": "运行",
|
||||
"k691b7170": "已停止",
|
||||
@ -160,11 +147,9 @@
|
||||
"k6e96fc3": "表单信息",
|
||||
"k6ea11aff": "获取!",
|
||||
"k6f15bcc3": "主机",
|
||||
"k71067412": "可选,传入 webhook 的 webhook 签名",
|
||||
"k721589c1": "今天",
|
||||
"k7247683c": "删除工作区",
|
||||
"k7350bd93": "同时,我们也可以在一些客户端应用场景中使用它,比如收集cli使用频率,比如收集自托管应用的安装情况等。",
|
||||
"k736f3e4c": "复制为",
|
||||
"k75581e13": "抄送",
|
||||
"k75bfaaa6": "将此代码添加到您的网站头部脚本中",
|
||||
"k763816ac": "预览",
|
||||
@ -172,17 +157,16 @@
|
||||
"k78b1ef6a": "输入",
|
||||
"k7927b824": "您确定要清除所有离线节点吗?",
|
||||
"k7a132ce8": "抱歉,找不到此页面",
|
||||
"k7a15497a": "实时",
|
||||
"k7ac44a6e": "会话密钥",
|
||||
"k7b74a43f": "访客",
|
||||
"k7b75e24c": "集成",
|
||||
"k7b9aa48c": "正文",
|
||||
"k7cac602a": "状态",
|
||||
"k7d8cd81c": "复制 URL",
|
||||
"k7e0360fd": "尚未创建任何组,点击按钮创建一个",
|
||||
"k7e61b1af": "选择工作区",
|
||||
"k7f01b47c": "审计日志",
|
||||
"k7f03a704": "记得不要使用 application/json 发送数据",
|
||||
"k7f29bae5": "页面查看",
|
||||
"k8037cc6b": "服务器",
|
||||
"k816ce026": "下载",
|
||||
"k819633bc": "用于存储",
|
||||
@ -192,7 +176,6 @@
|
||||
"k84ce1618": "(24小时)",
|
||||
"k84e82947": "{{num}} 事件已清除",
|
||||
"k85344b23": "负载",
|
||||
"k85a116ee": "Webhook Url",
|
||||
"k85c5fd4c": "还没有设置任何监控器",
|
||||
"k85db19da": "还没有任何订阅频道。使用订阅功能接收来自网络或您自己服务的所有事件。",
|
||||
"k873c90e6": "显示标签",
|
||||
@ -205,7 +188,6 @@
|
||||
"k88d2647b": "网站",
|
||||
"k89056082": "(30天)",
|
||||
"k892f84b6": "无法获取当前用户信息",
|
||||
"k895cafe1": "可选,发送调查有效负载的 webhook url",
|
||||
"k899fd0cd": "端口",
|
||||
"k89d54f7a": "监控执行计数",
|
||||
"k8a1deb63": "成员",
|
||||
@ -225,10 +207,7 @@
|
||||
"k90b603b8": "重复",
|
||||
"k90b668e5": "最近24小时",
|
||||
"k93374bc9": "删除网站",
|
||||
"k93458b98": "游乐场",
|
||||
"k951a939a": "网站接受计数",
|
||||
"k95f932a": "当前正在等待来自远程服务器的新请求",
|
||||
"k97b02874": "页面数量",
|
||||
"k98f433ee": "从这里下载报告器",
|
||||
"k9991c290": "社区",
|
||||
"k9a272ecf": "这是您的服务器吗?",
|
||||
@ -254,7 +233,6 @@
|
||||
"ka6ee7455": "网站ID",
|
||||
"ka71c12e1": "两次密码不一致",
|
||||
"ka765ad32": "通知",
|
||||
"ka7d8617e": "Feed 渠道数量",
|
||||
"ka7fe5937": "磁盘读/写",
|
||||
"ka8e41156": "搜索和快速跳转",
|
||||
"ka90bc019": "卸载",
|
||||
@ -276,7 +254,6 @@
|
||||
"kb0e351e0": "已刷新",
|
||||
"kb114a2e8": "已弃用",
|
||||
"kb15a6374": "您可以在自己的域名中配置您的状态页面,例如:status.example.com",
|
||||
"kb2dded49": "密钥",
|
||||
"kb320aac4": "已监控{{dayNum}}天",
|
||||
"kb35cde91": "搜索",
|
||||
"kb35d71ed": "或",
|
||||
@ -284,7 +261,6 @@
|
||||
"kb5673707": "最近7天",
|
||||
"kb659c1bc": "证书到期",
|
||||
"kb6d350b6": "馈送频道",
|
||||
"kb7bf8869": "API 密钥",
|
||||
"kb7fa344a": "选择要发送的馈送频道",
|
||||
"kb8de8c50": "密送",
|
||||
"kbb31d3db": "统计日期",
|
||||
@ -320,16 +296,13 @@
|
||||
"kcc9c1bff": "每周",
|
||||
"kccaa732a": "无连续破折号",
|
||||
"kccb42483": "密码",
|
||||
"kcd56f27b": "最后更新",
|
||||
"kcd643ef3": "加载中...",
|
||||
"kce77d0c1": "时区",
|
||||
"kcff78587": "最后使用时间",
|
||||
"kd005f7a8": "所有订阅将被删除",
|
||||
"kd031b383": "视图",
|
||||
"kd044d5d4": "会话",
|
||||
"kd092de58": "当前工作区:",
|
||||
"kd1f7e695": "确认注销",
|
||||
"kd211e2d4": "发布页面",
|
||||
"kd25f123a": "状态未知",
|
||||
"kd2a7ad83": "馈送模板",
|
||||
"kd3262a4a": "配置",
|
||||
"kd3396544": "通常,我们会使用一个 1x1 像素的空白图片,这样不会影响用户的正常使用。",
|
||||
@ -338,18 +311,14 @@
|
||||
"kd7279fa6": "代码",
|
||||
"kd7985726": "{{num}}个用户",
|
||||
"kd92fa3e7": "主机名",
|
||||
"kdaa6ae2b": "监控数量",
|
||||
"kdaff25a6": "显示最新值",
|
||||
"kdb61adbb": "隐藏离线",
|
||||
"kdbadcf43": "所有系统正常运行",
|
||||
"kdbe222b": "API 密钥",
|
||||
"kdc10ee1a": "创建一个新的工作区以与团队成员合作。",
|
||||
"kdc15c5d": "数据",
|
||||
"kdc1bf80e": "网址是必需的",
|
||||
"kdc51b5db": "网站",
|
||||
"kdd44ac01": "显示的遥测名称",
|
||||
"kdd55936a": "解析器端口",
|
||||
"kde315178": "重命名",
|
||||
"kde37bc27": "返回管理员",
|
||||
"kdeba7706": "设备",
|
||||
"kdeecbfea": "解析器服务器",
|
||||
@ -390,7 +359,6 @@
|
||||
"kf246dd2e": "未找到任何工作区,请先创建",
|
||||
"kf3b749ef": "支持直接聊天/群组/频道的聊天ID",
|
||||
"kf55495e0": "保存",
|
||||
"kf5c3b616": "请求头",
|
||||
"kf5c9520e": "还没有任何状态页面,您可以创建一个新的状态页面向公众展示您的服务状态。",
|
||||
"kf6339d4f": "已验证",
|
||||
"kf6582ba": "工作区",
|
||||
@ -406,7 +374,6 @@
|
||||
"kf97b6f71": "在您的Linux机器上运行此命令",
|
||||
"kf9877f28": "查看详情",
|
||||
"kf9965c19": "此工作区中的所有内容将被销毁,无法恢复。",
|
||||
"kf9a498c7": "灯塔报告已完成!",
|
||||
"kfc98929b": "{{num}}天",
|
||||
"kfd33c459": "复制成功!",
|
||||
"kfdaf0bb3": "最后在线时间:{{time}}",
|
@ -34,9 +34,7 @@ import { Route as SettingsWorkspaceImport } from './routes/settings/workspace'
|
||||
import { Route as SettingsUsageImport } from './routes/settings/usage'
|
||||
import { Route as SettingsProfileImport } from './routes/settings/profile'
|
||||
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 SettingsApiKeyImport } from './routes/settings/apiKey'
|
||||
import { Route as PageAddImport } from './routes/page/add'
|
||||
import { Route as PageSlugImport } from './routes/page/$slug'
|
||||
import { Route as MonitorAddImport } from './routes/monitor/add'
|
||||
@ -168,21 +166,11 @@ const SettingsNotificationsRoute = SettingsNotificationsImport.update({
|
||||
getParentRoute: () => SettingsRoute,
|
||||
} as any)
|
||||
|
||||
const SettingsBillingRoute = SettingsBillingImport.update({
|
||||
path: '/billing',
|
||||
getParentRoute: () => SettingsRoute,
|
||||
} as any)
|
||||
|
||||
const SettingsAuditLogRoute = SettingsAuditLogImport.update({
|
||||
path: '/auditLog',
|
||||
getParentRoute: () => SettingsRoute,
|
||||
} as any)
|
||||
|
||||
const SettingsApiKeyRoute = SettingsApiKeyImport.update({
|
||||
path: '/apiKey',
|
||||
getParentRoute: () => SettingsRoute,
|
||||
} as any)
|
||||
|
||||
const PageAddRoute = PageAddImport.update({
|
||||
path: '/add',
|
||||
getParentRoute: () => PageRoute,
|
||||
@ -324,18 +312,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof PageAddImport
|
||||
parentRoute: typeof PageImport
|
||||
}
|
||||
'/settings/apiKey': {
|
||||
preLoaderRoute: typeof SettingsApiKeyImport
|
||||
parentRoute: typeof SettingsImport
|
||||
}
|
||||
'/settings/auditLog': {
|
||||
preLoaderRoute: typeof SettingsAuditLogImport
|
||||
parentRoute: typeof SettingsImport
|
||||
}
|
||||
'/settings/billing': {
|
||||
preLoaderRoute: typeof SettingsBillingImport
|
||||
parentRoute: typeof SettingsImport
|
||||
}
|
||||
'/settings/notifications': {
|
||||
preLoaderRoute: typeof SettingsNotificationsImport
|
||||
parentRoute: typeof SettingsImport
|
||||
@ -431,9 +411,7 @@ export const routeTree = rootRoute.addChildren([
|
||||
RegisterRoute,
|
||||
ServerRoute,
|
||||
SettingsRoute.addChildren([
|
||||
SettingsApiKeyRoute,
|
||||
SettingsAuditLogRoute,
|
||||
SettingsBillingRoute,
|
||||
SettingsNotificationsRoute,
|
||||
SettingsProfileRoute,
|
||||
SettingsUsageRoute,
|
||||
|
@ -114,12 +114,7 @@ function PageComponent() {
|
||||
{info?.id && (
|
||||
<DialogWrapper
|
||||
title={t('Integration')}
|
||||
content={
|
||||
<FeedIntegration
|
||||
feedId={info.id}
|
||||
webhookSignature={info.webhookSignature}
|
||||
/>
|
||||
}
|
||||
content={<FeedIntegration feedId={info.id} />}
|
||||
>
|
||||
<Button variant="default" size="icon" Icon={LuWebhook} />
|
||||
</DialogWrapper>
|
||||
@ -199,12 +194,7 @@ function PageComponent() {
|
||||
)}
|
||||
renderEmpty={() => (
|
||||
<div className="w-full overflow-hidden p-4">
|
||||
{!isInitialLoading && (
|
||||
<FeedApiGuide
|
||||
channelId={channelId}
|
||||
webhookSignature={info?.webhookSignature}
|
||||
/>
|
||||
)}
|
||||
{!isInitialLoading && <FeedApiGuide channelId={channelId} />}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
@ -8,12 +8,6 @@ import { useState } from 'react';
|
||||
import { EditableText } from '@/components/EditableText';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { WebhookPlayground } from '@/components/WebhookPlayground';
|
||||
import React from 'react';
|
||||
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
|
||||
export const Route = createFileRoute('/playground')({
|
||||
beforeLoad: () => {
|
||||
@ -54,22 +48,18 @@ function PageComponent() {
|
||||
|
||||
return (
|
||||
<div className="h-full w-full p-4">
|
||||
<Tabs defaultValue="billing" className="flex h-full flex-col">
|
||||
<Tabs defaultValue="current" className="flex h-full flex-col">
|
||||
<div>
|
||||
<TabsList>
|
||||
<TabsTrigger value="billing">Billing</TabsTrigger>
|
||||
<TabsTrigger value="webhook">Webhook</TabsTrigger>
|
||||
<TabsTrigger value="misc">Misc</TabsTrigger>
|
||||
<TabsTrigger value="current">Current</TabsTrigger>
|
||||
<TabsTrigger value="history">History</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="billing">
|
||||
<BillingPlayground />
|
||||
</TabsContent>
|
||||
<TabsContent value="webhook" className="flex-1 overflow-hidden">
|
||||
<TabsContent value="current" className="flex-1 overflow-hidden">
|
||||
<WebhookPlayground />
|
||||
</TabsContent>
|
||||
<TabsContent value="misc">
|
||||
<TabsContent value="history">
|
||||
<div>
|
||||
<EditableText
|
||||
defaultValue="fooooooooo"
|
||||
@ -83,114 +73,3 @@ function PageComponent() {
|
||||
</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';
|
||||
|
@ -2,7 +2,6 @@ import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { CommonList } from '@/components/CommonList';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { Layout } from '@/components/layout';
|
||||
import { useGlobalConfig } from '@/hooks/useConfig';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import {
|
||||
@ -10,7 +9,6 @@ import {
|
||||
useNavigate,
|
||||
useRouterState,
|
||||
} from '@tanstack/react-router';
|
||||
import { compact } from 'lodash-es';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/settings')({
|
||||
@ -24,9 +22,8 @@ function PageComponent() {
|
||||
const pathname = useRouterState({
|
||||
select: (state) => state.location.pathname,
|
||||
});
|
||||
const { enableBilling } = useGlobalConfig();
|
||||
|
||||
const items = compact([
|
||||
const items = [
|
||||
{
|
||||
id: 'profile',
|
||||
title: t('Profile'),
|
||||
@ -42,11 +39,6 @@ function PageComponent() {
|
||||
title: t('Workspace'),
|
||||
href: '/settings/workspace',
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: t('Api Key'),
|
||||
href: '/settings/apiKey',
|
||||
},
|
||||
{
|
||||
id: 'auditLog',
|
||||
title: t('Audit Log'),
|
||||
@ -57,12 +49,7 @@ function PageComponent() {
|
||||
title: t('Usage'),
|
||||
href: '/settings/usage',
|
||||
},
|
||||
enableBilling && {
|
||||
id: 'billing',
|
||||
title: t('Billing'),
|
||||
href: '/settings/billing',
|
||||
},
|
||||
]);
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname === Route.fullPath) {
|
||||
|
@ -1,157 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -2,13 +2,11 @@ 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 { Empty, List } from 'antd';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import {
|
||||
defaultErrorHandler,
|
||||
defaultSuccessHandler,
|
||||
trpc,
|
||||
} from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId, useHasAdminPermission } from '../../store/user';
|
||||
import { trpc } from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { last } from 'lodash-es';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
@ -16,9 +14,6 @@ import { useWatch } from '@/hooks/useWatch';
|
||||
import dayjs from 'dayjs';
|
||||
import { ColorTag } from '@/components/ColorTag';
|
||||
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')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
@ -29,9 +24,8 @@ function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const hasAdminPermission = useHasAdminPermission();
|
||||
|
||||
const { data, hasNextPage, fetchNextPage, isFetchingNextPage, refetch } =
|
||||
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } =
|
||||
trpc.auditLog.fetchByCursor.useInfiniteQuery(
|
||||
{
|
||||
workspaceId,
|
||||
@ -41,11 +35,6 @@ function PageComponent() {
|
||||
}
|
||||
);
|
||||
|
||||
const clearMutation = trpc.auditLog.clear.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const allData = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
@ -80,27 +69,7 @@ function PageComponent() {
|
||||
});
|
||||
|
||||
return (
|
||||
<CommonWrapper
|
||||
header={
|
||||
<CommonHeader
|
||||
title={t('Audit Log')}
|
||||
actions={
|
||||
<>
|
||||
{hasAdminPermission && (
|
||||
<AlertConfirm
|
||||
onConfirm={() => {
|
||||
clearMutation.mutateAsync({ workspaceId });
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button variant="outline" size="icon" Icon={LuTrash2} />
|
||||
</AlertConfirm>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CommonWrapper header={<CommonHeader title={t('Audit Log')} />}>
|
||||
<div className="h-full overflow-hidden p-4">
|
||||
<SimpleVirtualList
|
||||
allData={allData}
|
||||
|
@ -1,124 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -10,7 +10,6 @@ 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';
|
||||
|
||||
export const Route = createFileRoute('/settings/usage')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
@ -25,20 +24,12 @@ function PageComponent() {
|
||||
[]
|
||||
);
|
||||
|
||||
const { data: serviceCountData } = trpc.workspace.getServiceCount.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const { data: billingUsageData } = trpc.billing.usage.useQuery({
|
||||
const { data } = trpc.billing.usage.useQuery({
|
||||
workspaceId,
|
||||
startAt: startDate.valueOf(),
|
||||
endAt: endDate.valueOf(),
|
||||
});
|
||||
|
||||
const { data: limit } = trpc.billing.limit.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return (
|
||||
<CommonWrapper header={<CommonHeader title={t('Usage')} />}>
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
@ -54,61 +45,50 @@ function PageComponent() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
<UsageCard
|
||||
title={t('Website Count')}
|
||||
current={serviceCountData?.website ?? 0}
|
||||
limit={limit?.maxWebsiteCount}
|
||||
/>
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Website Accepted Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.websiteAcceptedCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<UsageCard
|
||||
title={t('Monitor Count')}
|
||||
current={serviceCountData?.monitor ?? 0}
|
||||
/>
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Website Event Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.websiteEventCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<UsageCard
|
||||
title={t('Survey Count')}
|
||||
current={serviceCountData?.survey ?? 0}
|
||||
/>
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Monitor Execution Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.monitorExecutionCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<UsageCard
|
||||
title={t('Page Count')}
|
||||
current={serviceCountData?.page ?? 0}
|
||||
/>
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Survey Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.surveyCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<UsageCard
|
||||
title={t('Feed Channel Count')}
|
||||
current={serviceCountData?.feed ?? 0}
|
||||
limit={limit?.maxFeedChannelCount}
|
||||
/>
|
||||
|
||||
<UsageCard
|
||||
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}
|
||||
/>
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Feed Event Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.feedEventCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
@ -38,22 +38,13 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useEvent, useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { useEventWithLoading } from '@/hooks/useEvent';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { AlertConfirm } from '@/components/AlertConfirm';
|
||||
import { ROLES } from '@tianji/shared';
|
||||
import { cn } from '@/utils/style';
|
||||
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')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
@ -71,7 +62,7 @@ const columnHelper = createColumnHelper<MemberInfo>();
|
||||
|
||||
function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const { id: workspaceId, name, role, settings } = useCurrentWorkspace();
|
||||
const { id: workspaceId, name, role } = useCurrentWorkspace();
|
||||
const hasAdminPermission = useHasAdminPermission();
|
||||
const { data: members = [], refetch: refetchMembers } =
|
||||
trpc.workspace.members.useQuery({
|
||||
@ -80,9 +71,6 @@ function PageComponent() {
|
||||
const updateCurrentWorkspaceName = useUserStore(
|
||||
(state) => state.updateCurrentWorkspaceName
|
||||
);
|
||||
const updateCurrentWorkspaceSettings = useUserStore(
|
||||
(state) => state.updateCurrentWorkspaceSettings
|
||||
);
|
||||
const form = useForm<InviteFormValues>({
|
||||
resolver: zodResolver(inviteFormSchema),
|
||||
defaultValues: {
|
||||
@ -101,10 +89,6 @@ function PageComponent() {
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
const updateSettings = trpc.workspace.updateSettings.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const [renameWorkspaceName, setRenameWorkspaceName] = useState('');
|
||||
const [handleRename, isRenameLoading] = useEventWithLoading(async () => {
|
||||
@ -128,19 +112,6 @@ 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(() => {
|
||||
return [
|
||||
columnHelper.accessor(
|
||||
@ -196,36 +167,6 @@ function PageComponent() {
|
||||
</CardContent>
|
||||
</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
|
||||
onSubmit={form.handleSubmit(handleInvite)}
|
||||
|
@ -10,7 +10,6 @@ export type UserLoginInfo = NonNullable<AppRouterOutput['user']['info']>;
|
||||
interface UserState {
|
||||
info: UserLoginInfo | null;
|
||||
updateCurrentWorkspaceName: (name: string) => void;
|
||||
updateCurrentWorkspaceSettings: (settings: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export const useUserStore = createWithEqualityFn<UserState>()(
|
||||
@ -28,21 +27,6 @@ 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
|
||||
);
|
||||
@ -104,7 +88,6 @@ export function useCurrentWorkspaceSafe() {
|
||||
id: currentWorkspace.workspace.id,
|
||||
name: currentWorkspace.workspace.name,
|
||||
role: currentWorkspace.role,
|
||||
settings: currentWorkspace.workspace.settings,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -13,7 +13,6 @@ module.exports = {
|
||||
'./components/**/*.{js,jsx,ts,tsx}',
|
||||
'./pages/**/*.{js,jsx,ts,tsx}',
|
||||
'./routes/**/*.{js,jsx,ts,tsx}',
|
||||
'./utils/health.ts',
|
||||
],
|
||||
},
|
||||
theme: {
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { setupI18nInstance } from '@i18next-toolkit/react';
|
||||
|
||||
export const languages = [
|
||||
{
|
||||
label: 'English',
|
||||
@ -7,36 +5,31 @@ export const languages = [
|
||||
},
|
||||
{
|
||||
label: 'Deutsch',
|
||||
key: 'de-DE',
|
||||
key: 'de',
|
||||
},
|
||||
{
|
||||
label: 'Français',
|
||||
key: 'fr-FR',
|
||||
key: 'fr',
|
||||
},
|
||||
{
|
||||
label: '日本語',
|
||||
key: 'ja-JP',
|
||||
key: 'jp',
|
||||
},
|
||||
{
|
||||
label: 'Polski',
|
||||
key: 'pl-PL',
|
||||
key: 'pl',
|
||||
},
|
||||
{
|
||||
label: 'Português',
|
||||
key: 'pt-PT',
|
||||
key: 'pt',
|
||||
},
|
||||
{
|
||||
label: 'Русский',
|
||||
key: 'ru-RU',
|
||||
key: 'ru',
|
||||
},
|
||||
|
||||
{
|
||||
label: '简体中文',
|
||||
key: 'zh-CN',
|
||||
key: 'zh',
|
||||
},
|
||||
];
|
||||
|
||||
export function initI18N() {
|
||||
setupI18nInstance({
|
||||
supportedLngs: languages.map((l) => l.key),
|
||||
});
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
@ -68,25 +68,3 @@ export function formatDateWithUnit(val: dayjs.ConfigType, unit: DateUnit) {
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -1,45 +0,0 @@
|
||||
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('');
|
||||
});
|
||||
});
|
@ -1,36 +0,0 @@
|
||||
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 '';
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import express from 'express';
|
||||
import 'express-async-errors';
|
||||
import compression from 'compression';
|
||||
import swaggerUI from 'swagger-ui-express';
|
||||
import passport from 'passport';
|
||||
import morgan from 'morgan';
|
||||
import { websiteRouter } from './router/website.js';
|
||||
import { telemetryRouter } from './router/telemetry.js';
|
||||
@ -21,22 +22,17 @@ import path from 'path';
|
||||
import { monitorPageManager } from './model/monitor/page/manager.js';
|
||||
import { ExpressAuth } from '@auth/express';
|
||||
import { authConfig } from './model/auth.js';
|
||||
import { prometheusApiVersion } from './middleware/prometheus/index.js';
|
||||
import { billingRouter } from './router/billing.js';
|
||||
|
||||
const app = express();
|
||||
|
||||
app.set('trust proxy', true);
|
||||
app.use(prometheusApiVersion());
|
||||
app.use(compression());
|
||||
app.use(
|
||||
express.json({
|
||||
limit: '10mb',
|
||||
verify: (req, res, buf) => {
|
||||
(req as any).rawBody = buf;
|
||||
},
|
||||
})
|
||||
);
|
||||
app.use(passport.initialize());
|
||||
app.use(morgan('tiny'));
|
||||
app.use(cors());
|
||||
|
||||
@ -53,7 +49,6 @@ app.use(
|
||||
app.use('/health', healthRouter);
|
||||
app.use('/api/auth/*', ExpressAuth(authConfig));
|
||||
app.use('/api/website', websiteRouter);
|
||||
app.use('/api/billing', billingRouter);
|
||||
app.use('/monitor', monitorRouter);
|
||||
app.use('/telemetry', telemetryRouter);
|
||||
app.use('/serverStatus', serverStatusRouter);
|
||||
|
@ -9,7 +9,6 @@ import { token } from '../model/notification/token/index.js';
|
||||
import pMap from 'p-map';
|
||||
import { sendFeedEventsNotify } from '../model/feed/event.js';
|
||||
import { get } from 'lodash-es';
|
||||
import { checkWorkspaceUsage } from '../model/billing/cronjob.js';
|
||||
|
||||
type WebsiteEventCountSqlReturn = {
|
||||
workspace_id: string;
|
||||
@ -30,10 +29,6 @@ export function initCronjob() {
|
||||
checkFeedEventsNotify(FeedChannelNotifyFrequency.day),
|
||||
]);
|
||||
|
||||
if (env.billing.enable) {
|
||||
await checkWorkspaceUsage();
|
||||
}
|
||||
|
||||
logger.info('Daily cronjob completed');
|
||||
} catch (err) {
|
||||
logger.error('Daily cronjob error:', err);
|
||||
@ -247,14 +242,14 @@ async function statDailyUsage() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear over 1 month data
|
||||
* Clear over 2 week data
|
||||
*/
|
||||
async function clearMonitorDataDaily() {
|
||||
if (env.disableAutoClear) {
|
||||
return;
|
||||
}
|
||||
|
||||
const date = dayjs().subtract(1, 'months').toDate();
|
||||
const date = dayjs().subtract(2, 'weeks').toDate();
|
||||
logger.info(
|
||||
'[clearMonitorDataDaily] Start clear monitor data before:',
|
||||
date.toISOString()
|
||||
@ -274,14 +269,14 @@ async function clearMonitorDataDaily() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear over 1 month data
|
||||
* Clear over 2 week data
|
||||
*/
|
||||
async function clearMonitorEventDaily() {
|
||||
if (env.disableAutoClear) {
|
||||
return;
|
||||
}
|
||||
|
||||
const date = dayjs().subtract(1, 'month').toDate();
|
||||
const date = dayjs().subtract(2, 'weeks').toDate();
|
||||
logger.info(
|
||||
'[clearMonitorEventDaily] Start clear monitor data before:',
|
||||
date.toISOString()
|
||||
@ -391,9 +386,6 @@ async function dailyHTTPCertCheckNotify() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check feed events notify
|
||||
*/
|
||||
async function checkFeedEventsNotify(
|
||||
notifyFrequency: FeedChannelNotifyFrequency
|
||||
) {
|
||||
|
@ -1,14 +1,8 @@
|
||||
import { version } from '@tianji/shared';
|
||||
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}`;
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
(BigInt.prototype as any).toJSON = function () {
|
||||
const int = Number.parseInt(this.toString());
|
||||
return int ?? this.toString();
|
||||
|
@ -9,7 +9,6 @@ import { initCronjob } from './cronjob/index.js';
|
||||
import { logger } from './utils/logger.js';
|
||||
import { app } from './app.js';
|
||||
import { runMQWorker } from './mq/worker.js';
|
||||
import { initCounter } from './utils/prometheus/index.js';
|
||||
|
||||
const port = env.port;
|
||||
|
||||
@ -21,8 +20,6 @@ initSocketio(httpServer);
|
||||
|
||||
initCronjob();
|
||||
|
||||
initCounter();
|
||||
|
||||
runMQWorker();
|
||||
|
||||
monitorManager.startAll();
|
||||
|
@ -1,3 +1,7 @@
|
||||
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 { jwtSecret } from '../utils/common.js';
|
||||
|
||||
@ -10,6 +14,38 @@ export interface JWTPayload {
|
||||
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 {
|
||||
const token = jwt.sign(
|
||||
{
|
||||
@ -36,3 +72,9 @@ export function jwtVerify(token: string): JWTPayload {
|
||||
|
||||
return payload as JWTPayload;
|
||||
}
|
||||
|
||||
export function auth(): Handler {
|
||||
return passport.authenticate('jwt', {
|
||||
session: false,
|
||||
});
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
This folder is fork from https://github.com/PayU/prometheus-api-metrics
|
@ -1,132 +0,0 @@
|
||||
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);
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
'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,
|
||||
};
|
@ -19,7 +19,6 @@ export const workspaceDashboardLayoutSchema = z.object({
|
||||
export const workspaceSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
settings: z.record(z.string(), z.any()),
|
||||
});
|
||||
|
||||
export const userInfoSchema = z.object({
|
||||
|
@ -9,5 +9,4 @@ export const monitorPublicInfoSchema = MonitorModelSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
trendingMode: true,
|
||||
});
|
||||
|
@ -1,65 +0,0 @@
|
||||
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,
|
||||
}
|
||||
);
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { prisma } from '../_client.js';
|
||||
import { monitorPublicInfoSchema } from '../_schema/monitor.js';
|
||||
import { MonitorManager } from './manager.js';
|
||||
@ -68,57 +67,3 @@ export function getMonitorRecentData(
|
||||
})
|
||||
.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,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { Monitor } from '@prisma/client';
|
||||
import { Monitor, Notification } from '@prisma/client';
|
||||
import { prisma } from '../_client.js';
|
||||
import { MonitorRunner } from './runner.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { MonitorWithNotification } from './types.js';
|
||||
import { promMonitorRunnerCounter } from '../../utils/prometheus/client.js';
|
||||
|
||||
export type MonitorUpsertData = Pick<
|
||||
Monitor,
|
||||
@ -15,6 +13,8 @@ export type MonitorUpsertData = Pick<
|
||||
payload: Record<string, any>;
|
||||
};
|
||||
|
||||
type MonitorWithNotification = Monitor & { notifications: Notification[] };
|
||||
|
||||
export class MonitorManager {
|
||||
private monitorRunner: Record<string, MonitorRunner> = {};
|
||||
private isStarted = false;
|
||||
@ -64,7 +64,9 @@ export class MonitorManager {
|
||||
delete this.monitorRunner[monitor.id];
|
||||
}
|
||||
|
||||
const runner = await this.createRunner(monitor);
|
||||
const runner = (this.monitorRunner[monitor.id] = new MonitorRunner(
|
||||
monitor
|
||||
));
|
||||
runner.startMonitor();
|
||||
|
||||
return monitor;
|
||||
@ -110,7 +112,7 @@ export class MonitorManager {
|
||||
Promise.all(
|
||||
monitors.map(async (m) => {
|
||||
try {
|
||||
const runner = await this.createRunner(m);
|
||||
const runner = new MonitorRunner(m);
|
||||
this.monitorRunner[m.id] = runner;
|
||||
await runner.startMonitor();
|
||||
} catch (err) {
|
||||
@ -126,37 +128,11 @@ export class MonitorManager {
|
||||
return this.monitorRunner[monitorId];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
});
|
||||
createRunner(monitor: MonitorWithNotification) {
|
||||
const runner = (this.monitorRunner[monitor.id] = new MonitorRunner(
|
||||
workspace,
|
||||
monitor
|
||||
));
|
||||
|
||||
promMonitorRunnerCounter.set(Object.keys(this.monitorRunner).length);
|
||||
|
||||
return runner;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Notification, Workspace } from '@prisma/client';
|
||||
import { Monitor, Notification } from '@prisma/client';
|
||||
import { subscribeEventBus } from '../../ws/shared.js';
|
||||
import { prisma } from '../_client.js';
|
||||
import { monitorProviders } from './provider/index.js';
|
||||
@ -8,8 +8,6 @@ import { logger } from '../../utils/logger.js';
|
||||
import { token } from '../notification/token/index.js';
|
||||
import { ContentToken } from '../notification/token/type.js';
|
||||
import { createAuditLog } from '../auditLog.js';
|
||||
import { MonitorWithNotification } from './types.js';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* Class which actually run monitor data collect
|
||||
@ -19,14 +17,7 @@ export class MonitorRunner {
|
||||
timer: NodeJS.Timeout | null = null;
|
||||
retriedNum = 0;
|
||||
|
||||
constructor(
|
||||
public workspace: Workspace,
|
||||
public monitor: MonitorWithNotification
|
||||
) {}
|
||||
|
||||
getTimezone(): string {
|
||||
return get(this.workspace, ['settings', 'timezone']) || 'utc';
|
||||
}
|
||||
constructor(public monitor: Monitor & { notifications: Notification[] }) {}
|
||||
|
||||
/**
|
||||
* Start single monitor
|
||||
@ -83,9 +74,9 @@ export class MonitorRunner {
|
||||
);
|
||||
await this.notify(`[${monitor.name}] 🔴 Down`, [
|
||||
token.text(
|
||||
`[${monitor.name}] 🔴 Down\nTime: ${dayjs()
|
||||
.tz(this.getTimezone())
|
||||
.format('YYYY-MM-DD HH:mm:ss (z)')}`
|
||||
`[${monitor.name}] 🔴 Down\nTime: ${dayjs().format(
|
||||
'YYYY-MM-DD HH:mm:ss (z)'
|
||||
)}`
|
||||
),
|
||||
]);
|
||||
currentStatus = 'DOWN';
|
||||
@ -97,9 +88,9 @@ export class MonitorRunner {
|
||||
);
|
||||
await this.notify(`[${monitor.name}] ✅ Up`, [
|
||||
token.text(
|
||||
`[${monitor.name}] ✅ Up\nTime: ${dayjs()
|
||||
.tz(this.getTimezone())
|
||||
.format('YYYY-MM-DD HH:mm:ss (z)')}`
|
||||
`[${monitor.name}] ✅ Up\nTime: ${dayjs().format(
|
||||
'YYYY-MM-DD HH:mm:ss (z)'
|
||||
)}`
|
||||
),
|
||||
]);
|
||||
currentStatus = 'UP';
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { Monitor, Notification } from '@prisma/client';
|
||||
|
||||
export type MonitorWithNotification = Monitor & {
|
||||
notifications: Notification[];
|
||||
};
|
@ -1,5 +1,4 @@
|
||||
import { ServerStatusInfo } from '../../types/index.js';
|
||||
import { promServerCounter } from '../utils/prometheus/client.js';
|
||||
import { createSubscribeInitializer, subscribeEventBus } from '../ws/shared.js';
|
||||
import { isServerOnline } from '@tianji/shared';
|
||||
|
||||
@ -43,13 +42,6 @@ export function recordServerStatus(info: ServerStatusInfo) {
|
||||
payload,
|
||||
};
|
||||
|
||||
promServerCounter.set(
|
||||
{
|
||||
workspaceId,
|
||||
},
|
||||
Object.keys(serverMap[workspaceId]).length
|
||||
);
|
||||
|
||||
subscribeEventBus.emit(
|
||||
'onServerStatusUpdate',
|
||||
workspaceId,
|
||||
|
@ -5,9 +5,8 @@ import { jwtVerify } from '../middleware/auth.js';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { AdapterUser } from '@auth/core/adapters';
|
||||
import { md5, sha256 } from '../utils/common.js';
|
||||
import { md5 } from '../utils/common.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { promUserCounter } from '../utils/prometheus/client.js';
|
||||
|
||||
async function hashPassword(password: string) {
|
||||
return await bcryptjs.hash(password, 10);
|
||||
@ -42,7 +41,6 @@ export const createUserSelect = {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
settings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -89,8 +87,6 @@ export async function createAdminUser(username: string, password: string) {
|
||||
return user;
|
||||
});
|
||||
|
||||
promUserCounter.inc();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@ -133,8 +129,6 @@ export async function createUser(username: string, password: string) {
|
||||
return user;
|
||||
});
|
||||
|
||||
promUserCounter.inc();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@ -341,56 +335,3 @@ export async function leaveWorkspace(userId: string, workspaceId: string) {
|
||||
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;
|
||||
}
|
||||
|
@ -72,47 +72,3 @@ export async function getWorkspaceWebsiteDateRange(websiteId: string) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
@ -8,7 +8,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx watch --env-file=.env ./main.ts",
|
||||
"dev:debug": "tsx --inspect-brk --env-file=.env ./main.ts",
|
||||
"build": "tsc",
|
||||
"postinstall": "pnpm db:generate",
|
||||
"check:type": "tsc --noEmit --skipLibCheck",
|
||||
@ -26,7 +25,6 @@
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.34.1",
|
||||
"@auth/express": "^0.5.5",
|
||||
"@lemonsqueezy/lemonsqueezy.js": "^3.3.1",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@prisma/client": "5.14.0",
|
||||
"@tianji/shared": "workspace:^",
|
||||
@ -44,7 +42,6 @@
|
||||
"dayjs": "^1.11.9",
|
||||
"detect-browser": "^5.3.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"easy-currency-symbol": "^1.0.1",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-validator": "^7.0.1",
|
||||
@ -60,9 +57,9 @@
|
||||
"morgan": "^1.10.0",
|
||||
"nanoid": "^5.0.4",
|
||||
"nodemailer": "^6.9.8",
|
||||
"p-map": "4.0.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"ping": "^0.4.4",
|
||||
"prom-client": "^15.1.3",
|
||||
"puppeteer": "23.4.1",
|
||||
"request-ip": "^3.3.0",
|
||||
"socket.io": "^4.7.4",
|
||||
@ -91,6 +88,8 @@
|
||||
"@types/morgan": "^1.9.5",
|
||||
"@types/node": "^18.17.12",
|
||||
"@types/nodemailer": "^6.4.11",
|
||||
"@types/passport": "^1.0.12",
|
||||
"@types/passport-jwt": "^3.0.9",
|
||||
"@types/ping": "^0.4.2",
|
||||
"@types/request-ip": "^0.0.38",
|
||||
"@types/supertest": "^6.0.2",
|
||||
@ -98,6 +97,7 @@
|
||||
"@types/tcp-ping": "^0.1.5",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"execa": "^5.1.1",
|
||||
"p-map": "4.0.0",
|
||||
"prisma": "5.14.0",
|
||||
"prisma-json-types-generator": "3.0.3",
|
||||
"prisma-zod-generator": "0.8.13",
|
||||
|
@ -1,35 +0,0 @@
|
||||
-- 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");
|
@ -1,23 +0,0 @@
|
||||
-- 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');
|
@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "FeedChannel" ADD COLUMN "webhookSignature" VARCHAR(100) NOT NULL DEFAULT '';
|
@ -1,16 +0,0 @@
|
||||
-- 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;
|
@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "UserApiKey" ADD COLUMN "usage" INTEGER NOT NULL DEFAULT 0;
|
@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Workspace" ADD COLUMN "paused" BOOLEAN NOT NULL DEFAULT false;
|
@ -34,18 +34,6 @@ model User {
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
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 {
|
||||
@ -96,12 +84,9 @@ model Workspace {
|
||||
/// [CommonPayload]
|
||||
/// @zod.custom(imports.CommonPayloadSchema)
|
||||
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)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||
|
||||
subscription WorkspaceSubscription?
|
||||
|
||||
users WorkspacesOnUsers[]
|
||||
websites Website[]
|
||||
notifications Notification[]
|
||||
@ -131,49 +116,6 @@ model WorkspacesOnUsers {
|
||||
@@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 {
|
||||
id String @id @unique @default(cuid()) @db.VarChar(30)
|
||||
workspaceId String @db.VarChar(30)
|
||||
@ -601,7 +543,6 @@ model FeedChannel {
|
||||
id String @id @default(cuid()) @db.VarChar(30)
|
||||
workspaceId String @db.VarChar(30)
|
||||
name String
|
||||
webhookSignature String @default("") @db.VarChar(100)
|
||||
notifyFrequency FeedChannelNotifyFrequency @default(day)
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||
|
@ -7,7 +7,6 @@ export const FeedChannelModelSchema = z.object({
|
||||
id: z.string(),
|
||||
workspaceId: z.string(),
|
||||
name: z.string(),
|
||||
webhookSignature: z.string(),
|
||||
notifyFrequency: z.nativeEnum(FeedChannelNotifyFrequency),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
|
@ -1,13 +1,9 @@
|
||||
export * from "./user.js"
|
||||
export * from "./userapikey.js"
|
||||
export * from "./account.js"
|
||||
export * from "./session.js"
|
||||
export * from "./verificationtoken.js"
|
||||
export * from "./workspace.js"
|
||||
export * from "./workspacesonusers.js"
|
||||
export * from "./workspacesubscription.js"
|
||||
export * from "./lemonsqueezysubscription.js"
|
||||
export * from "./lemonsqueezywebhookevent.js"
|
||||
export * from "./website.js"
|
||||
export * from "./websitesession.js"
|
||||
export * from "./websiteevent.js"
|
||||
|
@ -1,16 +0,0 @@
|
||||
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(),
|
||||
})
|