Compare commits

..

10 Commits

281 changed files with 14273 additions and 16207 deletions

View File

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

View File

@ -1,229 +1,5 @@
## [1.16.5](https://github.com/msgbyte/tianji/compare/v1.16.4...v1.16.5) (2024-11-02)
### Features
* add webhookSignature in feed channel ([6b3631e](https://github.com/msgbyte/tianji/commit/6b3631eae186b9cacf64d0ddcfbb66378e041281))
### Bug Fixes
* add key to Fragment in map for monitor items ([9949b97](https://github.com/msgbyte/tianji/commit/9949b973bd63b4ad6b5e71f7b819442f505c09a6))
* retrieve date as string ([a8a47ed](https://github.com/msgbyte/tianji/commit/a8a47ed94dda87c3fe4cdecc0acb9a31f53f00a5))
### Others
* fix ci problem ([59b8746](https://github.com/msgbyte/tianji/commit/59b874644fd3427bc86cd2a7e948054e827de080))
* refactor status header and add typescript and translation support ([f637ade](https://github.com/msgbyte/tianji/commit/f637ade70f230fbf472bdee84105c9b284d6b8d4))
* update amount in stripe ([2725056](https://github.com/msgbyte/tianji/commit/272505669e450d882930cbf594dac39a879b2072))
* update webhooks signature api guide ([266b08f](https://github.com/msgbyte/tianji/commit/266b08f2da16d0457a5a44b4a7a251d28502abc9))
## [1.16.4](https://github.com/msgbyte/tianji/compare/v1.16.3...v1.16.4) (2024-10-27)
### Features
* add stripe feed integration ([09d0f02](https://github.com/msgbyte/tianji/commit/09d0f02d844159565e97bb64f076e0bbe218ce98))
### Others
* update currency symbols in feed ([98298c4](https://github.com/msgbyte/tianji/commit/98298c43670326b4e2300a6bbdeee3daa53f0eb3))
## [1.16.3](https://github.com/msgbyte/tianji/compare/v1.16.2...v1.16.3) (2024-10-24)
### Others
* fix ci problem and upgrade version ([1c5737e](https://github.com/msgbyte/tianji/commit/1c5737e588d19e0657be6437792cf4484b6fdddb))
## [1.16.2](https://github.com/msgbyte/tianji/compare/v1.16.1...v1.16.2) (2024-10-23)
### Features
* add prometheus report support ([fcb8f22](https://github.com/msgbyte/tianji/commit/fcb8f221168281ab710d3d3f12064a99d17b39e7))
### Bug Fixes
* fix a bug which will match incorrect path [#115](https://github.com/msgbyte/tianji/issues/115) ([79667a9](https://github.com/msgbyte/tianji/commit/79667a9644b78451400acb6a6bbf07b6ca61e6e0))
### Document
* update README ([1df32dc](https://github.com/msgbyte/tianji/commit/1df32dc2579f32649afd6c008512c1190a45fd9e))
### Others
* fix ci problem ([554f902](https://github.com/msgbyte/tianji/commit/554f9025847defe0b05492cf07a5dc8acc6c3685))
* update openapi document ([e402ee1](https://github.com/msgbyte/tianji/commit/e402ee1688bb77d83463ce70c5e730c97f68a695))
## [1.16.1](https://github.com/msgbyte/tianji/compare/v1.16.0...v1.16.1) (2024-10-20)
### Features
* add test notify ([4e3fd9d](https://github.com/msgbyte/tianji/commit/4e3fd9db64629f7721e6092b86b06144c47f521d))
* add timezone support [#114](https://github.com/msgbyte/tianji/issues/114) ([c7e20df](https://github.com/msgbyte/tianji/commit/c7e20df516bf3a991ce46c937223948bcdb6b8f0))
* add workspace settings manage ([3dca8fc](https://github.com/msgbyte/tianji/commit/3dca8fc27c82bd96dbab423b111e4de57f3b4bd8))
### Others
* update cronjob clear time ([83850f2](https://github.com/msgbyte/tianji/commit/83850f2981ded0b6624556ee3430f684752b8ea3))
## [1.16.0](https://github.com/msgbyte/tianji/compare/v1.15.8...v1.16.0) (2024-10-19)
### Features
* add click event for status page item which allow hide/show chart ([279e616](https://github.com/msgbyte/tianji/commit/279e616bee510ee5b0c5a3c9a3705a79efd5d3cb))
* add daily monitor data display for public ([dcff57f](https://github.com/msgbyte/tianji/commit/dcff57fe69273c7f9b3dd9c28e8acc9cb6e430a9))
* add monitor summary function ([bbb8d88](https://github.com/msgbyte/tianji/commit/bbb8d881168df695ccc70743f46320b39c1d7718))
* add MonitorLatestResponse and up status summary ([316b954](https://github.com/msgbyte/tianji/commit/316b95467d49b3ebe93d03006d4b90f9ca482262))
### Bug Fixes
* fix reporter memory leak problem [#103](https://github.com/msgbyte/tianji/issues/103) ([7f70557](https://github.com/msgbyte/tianji/commit/7f70557c776c35e4e01a5533d2c05cecc711e113))
### Others
* add border radius in smtp template ([f553f15](https://github.com/msgbyte/tianji/commit/f553f157dd9708d553c9d6cfca4d119a62d849c3))
* change public summary display logic ([e5e77db](https://github.com/msgbyte/tianji/commit/e5e77dbdeeeecb773237b84e3c671dd16e61d458))
* fix ci problem ([820b25b](https://github.com/msgbyte/tianji/commit/820b25baedc6fec02010ca19b43e4da99bf4b820))
* ignore unknown sentry log ([527f734](https://github.com/msgbyte/tianji/commit/527f734bc442458018d86df9a7e750a8e8de4495))
* let version text more prominent ([61980b3](https://github.com/msgbyte/tianji/commit/61980b37d3cecce32fa87b2b9810f4c715990a71))
* rename old tsconfig paths ([2a503ca](https://github.com/msgbyte/tianji/commit/2a503ca2501e705430c0c35cb7c8279927c1d4d5))
## [1.15.8](https://github.com/msgbyte/tianji/compare/v1.15.7...v1.15.8) (2024-10-13)
### Features
* add payload for feed event integration and send function ([572d96b](https://github.com/msgbyte/tianji/commit/572d96babb348858911105659bfe304e869915e4))
* add ping animation in website realtime visitor ([6da0e6f](https://github.com/msgbyte/tianji/commit/6da0e6f415e863448cd36246eb16e1f09dcd8a79))
* add plausible tracking(for testing) ([6474cef](https://github.com/msgbyte/tianji/commit/6474cefd896b36b872460786b11005b8deaf4436))
* add realtime datarange which can visit data more easy ([f3d8f55](https://github.com/msgbyte/tianji/commit/f3d8f5543d4e277fe34940ce98cd552dee45f2a8))
* add survey curl example code ([5d54ca1](https://github.com/msgbyte/tianji/commit/5d54ca1cbc01b69b9bd03d167edd0db242df350e))
* add survey webhook ([de57242](https://github.com/msgbyte/tianji/commit/de572426ebf99e99ff97ce49caf0b8ac13b68154))
* sdk add send feed function export ([f5933ec](https://github.com/msgbyte/tianji/commit/f5933ec0548fb4ac327152b6d7afe7ca2978bade))
* survey add webhook url field which can send webhook when receive any survey ([f00163b](https://github.com/msgbyte/tianji/commit/f00163b2f107bcf08d4ff398fa5dbd92ac36fda8))
* time event chart legend add some interaction ([4b78771](https://github.com/msgbyte/tianji/commit/4b7877155fd54416fb9231a02aca0e868aec97d2))
### Document
* add shacdn to website ([763810e](https://github.com/msgbyte/tianji/commit/763810e8b7e6cd41fdc0d83d28262dc7d747e4bf))
* add sitemap to improve SEO ([384224c](https://github.com/msgbyte/tianji/commit/384224cb624030522057df22527312957665d8e9))
* add website more language: de, fr, ja, zh-Hans ([7bda542](https://github.com/msgbyte/tianji/commit/7bda5420c5f22f6205d2dbd5086dd0bdbbc7f558))
* change code command line style ([8c5c417](https://github.com/msgbyte/tianji/commit/8c5c417a197531c187452fc4937d034d3bdc05a7))
* remove used blog directory ([3d9d032](https://github.com/msgbyte/tianji/commit/3d9d03296e9fb26eb363b2dba2f830f2eb9d58f3))
* resolve build problem with update source document content ([9e6e031](https://github.com/msgbyte/tianji/commit/9e6e03117cc3bd37058563e31817034876d35450))
* update depenpendency to resolve issue of docusaurus build ([de38363](https://github.com/msgbyte/tianji/commit/de38363315275ecbc7384c935c313940fca1d4fc))
* upgrade openapi ([1e57905](https://github.com/msgbyte/tianji/commit/1e57905f3239cc4fb3a949b469161dc9c8d2b40c))
### Others
* add CodeExample component ([29f184c](https://github.com/msgbyte/tianji/commit/29f184c15d36e42af750261f81ad67b49fe58c0b))
* comment sitemap to make sure its can build safe ([9b9799e](https://github.com/msgbyte/tianji/commit/9b9799ec6f846eb40762f5ca8cc0aa6c27fb7e02))
* fix ci problem ([a32f3d9](https://github.com/msgbyte/tianji/commit/a32f3d9824a09a2d8c9e97ba211ee5d44c8e2763))
* fix isolated-vm version ([43b4c9f](https://github.com/msgbyte/tianji/commit/43b4c9fe3763673dc54b61b01e50bc5b3d24a371))
* fix version of postman-code-generators ([eaffe3a](https://github.com/msgbyte/tianji/commit/eaffe3ab21022215a76c3441ef0b9b2c37386227))
* improve display of visitor map if data is too much ([9bc8c63](https://github.com/msgbyte/tianji/commit/9bc8c63fe2ea2ab080e7db3cf7d0c7636fabf8d1))
* migrate monitor data chart to recharts and remove @ant-design/charts ([c0e2ef0](https://github.com/msgbyte/tianji/commit/c0e2ef0fe8f5520a7b935eeeb44f6be9224e56a4))
* update ci run trigger path ([7322ad7](https://github.com/msgbyte/tianji/commit/7322ad741dcfc5c033b5057e1862e91d27244f7f))
* update pnpm lock file to resolve some magic problem ([064dbe9](https://github.com/msgbyte/tianji/commit/064dbe9985767b32492de4264d08c26206318cd4))
* update survey edit form ([a218c22](https://github.com/msgbyte/tianji/commit/a218c2239725deb5bcdee2e8d2de377e04dec941))
* upgrade @radix-ui/react-scroll-area version ([9d559b9](https://github.com/msgbyte/tianji/commit/9d559b93d16c130cf58649e0f12edf9e795ba8a5))
* upgrade @tianji/website docusaurus version ([e46f970](https://github.com/msgbyte/tianji/commit/e46f97097a593fe4bd5a8946237fd2f46fea69f6))
* use prebuilt rather than deploy build ([e51a880](https://github.com/msgbyte/tianji/commit/e51a88044fcef7727b2e7f17c5dd9eff08329cdc))
## [1.15.7](https://github.com/msgbyte/tianji/compare/v1.15.6...v1.15.7) (2024-10-03)
### Bug Fixes
* fix a problem which will make request list incorrect ([2d5a09c](https://github.com/msgbyte/tianji/commit/2d5a09c79cae48f62029b8767bae552376e68639))
### Document
* fix update code to new version ([1fe5009](https://github.com/msgbyte/tianji/commit/1fe50092bab5e0824c1b96b70d40d33f751f4135))
### Others
* split website from monorepo ([e09d7ee](https://github.com/msgbyte/tianji/commit/e09d7eef8788e27ac651f18dcf4d04de895a35ee))
* update workspace config and remove unused lock file ([7301eeb](https://github.com/msgbyte/tianji/commit/7301eeb82a4fdc4ae6853dbe98c1f1d7b9b79bca))
## [1.15.6](https://github.com/msgbyte/tianji/compare/v1.15.5...v1.15.6) (2024-10-02)
### Others
* add build dependency for build zeromq ([79b75f5](https://github.com/msgbyte/tianji/commit/79b75f55e39c057c330bc1fb0b3b15dc10e28a78))
* improve install package time in docker build static stage ([1be03cc](https://github.com/msgbyte/tianji/commit/1be03ccf532a7dd6d23334a5666dec34e2e68d77))
* update NODE_OPTIONS in static layer to make sure build can pass ([5eb7696](https://github.com/msgbyte/tianji/commit/5eb7696ead2da961d6ac5223a2badb48502142ba))
## [1.15.5](https://github.com/msgbyte/tianji/compare/v1.15.4...v1.15.5) (2024-10-01)
### Features
* add error message for lighthouse ([bb0c574](https://github.com/msgbyte/tianji/commit/bb0c57489347242300c6153ed3908d1822bb692c))
* add lighthouse score in database fields ([6c2a093](https://github.com/msgbyte/tianji/commit/6c2a0938423385d67309deefa67a3d971bf8d7c8))
* add webhook playground ([33a0a60](https://github.com/msgbyte/tianji/commit/33a0a60eee53d1ac08cc9accc2e96f06e56ebb52))
* add webhook playground entry ([92196e4](https://github.com/msgbyte/tianji/commit/92196e4e5bb9b183cfe85aad876115c0e17f824e))
* add zeromq to make sure lighthouse can only run one at same time ([50a3573](https://github.com/msgbyte/tianji/commit/50a35732ff202f2452b344c2df17aba677426ec3))
### Others
* improve avatar display timing for non-avatar user ([04dc1e9](https://github.com/msgbyte/tianji/commit/04dc1e98dd448c0fd6661559722cf594ab2e751e))
* refactor time event chart to recharts ([1337eaa](https://github.com/msgbyte/tianji/commit/1337eaa2c0ff55651a05878edd722ac1b46a5067))
* update style of website page card ([b778f8c](https://github.com/msgbyte/tianji/commit/b778f8c982f7df8328c2fffe67783b15cde51c15))
* upgrade shadcn cli and add recharts ([055f57e](https://github.com/msgbyte/tianji/commit/055f57e087f002b8f891053509e3cad865f1d52b))
## [1.15.4](https://github.com/msgbyte/tianji/compare/v1.15.3...v1.15.4) (2024-09-30)
### Features
* allow rename workspace ([63e6bfe](https://github.com/msgbyte/tianji/commit/63e6bfe0d1a989479a6c4658d01ea9d84fc84b45))
### Bug Fixes
* fix login view split incorrect if not any extra login way ([b16a7c3](https://github.com/msgbyte/tianji/commit/b16a7c3c2c203394c94ccfee8e829bc7685a2457))
* remove workspace name validation ([7c271dc](https://github.com/msgbyte/tianji/commit/7c271dc3c14fc6c751fb69b16adf6f08bfd5ac7b))
### Others
* add ignore in docker build ([ee72f74](https://github.com/msgbyte/tianji/commit/ee72f74e2c68c9baec500b78a3b994c8083abeed))
* add logger for lighthouse ([9d3e9d8](https://github.com/msgbyte/tianji/commit/9d3e9d89db40aad4a78df8b64ad3d7bfccb94d2e))
* add no sandbox args in puppeteer ([8b6a740](https://github.com/msgbyte/tianji/commit/8b6a74033c2838a6921c56990e13eedfbc8a559a))
* docker add puppeteer support ([23c6915](https://github.com/msgbyte/tianji/commit/23c691541db0a51b4765dd0943488def7741c0f4))
* downgrade alpine version to 3.19 to avoid issue ([e6df595](https://github.com/msgbyte/tianji/commit/e6df595af8ecddf734fe7e72a797921d84dad2c3))
* improve docker build and lighthouse config ([57ebaf6](https://github.com/msgbyte/tianji/commit/57ebaf6ad361cae3403263009b94566cc7de2293))
* improve websocket log ([b44e57d](https://github.com/msgbyte/tianji/commit/b44e57dde8d027eb05b7e8db20d490d2b62607cc))
* try to resolve no screenshot problem by remove single process. ([fe432f1](https://github.com/msgbyte/tianji/commit/fe432f13325adf5fb4cde3dbb2f4f1218cb789e7)), closes [/github.com/GoogleChrome/lighthouse/issues/11537#issuecomment-799895027](https://github.com/msgbyte//github.com/GoogleChrome/lighthouse/issues/11537/issues/issuecomment-799895027)
* unity esbuild version to resolve vulnerabilities which cause by esbuild ([bcc215c](https://github.com/msgbyte/tianji/commit/bcc215ca5d33126b368b58a9056d02fd93d5a99a))
* update dockerfile, carry back auto install dependency ([de09059](https://github.com/msgbyte/tianji/commit/de09059e6561a27e160f0e39e7987da8ad05edaa))
* update translation ([9c35bca](https://github.com/msgbyte/tianji/commit/9c35bca68508f2009434ebf578f0488a948e6b75))
* upgrade axios version to latest to resolve vulnerabilities ([d73fa10](https://github.com/msgbyte/tianji/commit/d73fa108978b3c965b07dda725fbe6ae20bc4140))
* upgrade puppeteer to make sure can fit with alpine image chromium version ([f59793d](https://github.com/msgbyte/tianji/commit/f59793d6f18625ad66b26d0a74aaa14c604ea812))
* upgrade puppeteer usage to fit new version ([1322741](https://github.com/msgbyte/tianji/commit/13227416c05e2eae5a1a99e5d5f3396679e83d89))
* upgrade puppeteer version to 23.4.1 ([e942769](https://github.com/msgbyte/tianji/commit/e942769af2e2570da71eb23f3ad489e8dcb72e95))
## [1.15.3](https://github.com/msgbyte/tianji/compare/v1.15.2...v1.15.3) (2024-09-24)

View File

@ -12,23 +12,8 @@ RUN cd reporter && go build .
FROM node:20-alpine3.19 AS base
RUN npm install -g pnpm@9.7.1
# For apprise
RUN apk add --update --no-cache python3 py3-pip g++ make
# For puppeteer
RUN apk upgrade --no-cache --available \
&& apk add --no-cache \
chromium-swiftshader \
ttf-freefont \
font-noto-emoji \
&& apk add --no-cache \
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
font-wqy-zenhei
# For zeromq
RUN apk add --update --no-cache curl cmake
# Tianji frontend ------------------------------
FROM base AS static
WORKDIR /app/tianji
@ -38,10 +23,9 @@ ARG VERSION
COPY . .
RUN pnpm install --filter @tianji/client... --config.dedupe-peer-dependents=false --frozen-lockfile
RUN pnpm install --frozen-lockfile
ENV VITE_VERSION=$VERSION
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN pnpm build:static
@ -54,6 +38,15 @@ ENV PUPPETEER_SKIP_DOWNLOAD=true
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
RUN apk upgrade --no-cache --available \
&& apk add --no-cache \
chromium-swiftshader \
ttf-freefont \
font-noto-emoji \
&& apk add --no-cache \
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
font-wqy-zenhei
COPY . .
RUN pnpm install --filter @tianji/server... --config.dedupe-peer-dependents=false

View File

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

View File

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

View File

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

View File

@ -1,13 +0,0 @@
import { $OpenApiTs, FeedService } from './open/client';
export async function sendFeed(
channelId: string,
payload: $OpenApiTs['/feed/{channelId}/send']['post']['req']['requestBody']
) {
const res = await FeedService.feedSendEvent({
channelId,
requestBody: payload,
});
return res;
}

View File

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

View File

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

View File

@ -51,21 +51,6 @@ export class UserService {
});
}
/**
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static userRegister(data: $OpenApiTs['/register']['post']['req']): CancelablePromise<$OpenApiTs['/register']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/register',
body: data.requestBody,
mediaType: 'application/json'
});
}
}
export class WorkspaceService {
@ -99,31 +84,16 @@ export class WorkspaceService {
});
}
/**
* @param data The data for the request.
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static workspaceRename(data: $OpenApiTs['/workspace//rename']['patch']['req']): CancelablePromise<$OpenApiTs['/workspace//rename']['patch']['res'][200]> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/workspace//rename',
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @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 +116,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
@ -477,24 +428,6 @@ export class WebsiteService {
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.websiteId
* @returns unknown Successful response
* @throws ApiError
*/
public static websiteDelete(data: $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}']['delete']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/{websiteId}']['delete']['res'][200]> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/workspace/{workspaceId}/website/{websiteId}',
path: {
workspaceId: data.workspaceId,
websiteId: data.websiteId
}
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
@ -516,68 +449,6 @@ export class WebsiteService {
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.websiteId
* @param data.requestBody
* @returns string Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static websiteGenerateLighthouseReport(data: $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport']['post']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport']['post']['res'][200] | $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/workspace/{workspaceId}/website/{websiteId}/generateLighthouseReport',
path: {
workspaceId: data.workspaceId,
websiteId: data.websiteId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.workspaceId
* @param data.websiteId
* @param data.limit
* @param data.cursor
* @returns unknown Successful response
* @throws ApiError
*/
public static websiteGetLighthouseReport(data: $OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/getLighthouseReport']['get']['req']): CancelablePromise<$OpenApiTs['/workspace/{workspaceId}/website/{websiteId}/getLighthouseReport']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/workspace/{workspaceId}/website/{websiteId}/getLighthouseReport',
path: {
workspaceId: data.workspaceId,
websiteId: data.websiteId
},
query: {
limit: data.limit,
cursor: data.cursor
}
});
}
/**
* @param data The data for the request.
* @param data.lighthouseId
* @returns unknown Successful response
* @throws ApiError
*/
public static websiteGetLighthouseJson(data: $OpenApiTs['/lighthouse/{lighthouseId}']['get']['req']): CancelablePromise<$OpenApiTs['/lighthouse/{lighthouseId}']['get']['res'][200]> {
return __request(OpenAPI, {
method: 'GET',
url: '/lighthouse/{lighthouseId}',
path: {
lighthouseId: data.lighthouseId
}
});
}
}
export class MonitorService {
@ -604,10 +475,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 +538,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 +605,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 +1048,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
@ -1481,7 +1316,6 @@ export class FeedService {
* @param data.channelId
* @param data.limit
* @param data.cursor
* @param data.archived
* @returns unknown Successful response
* @throws ApiError
*/
@ -1495,8 +1329,7 @@ export class FeedService {
},
query: {
limit: data.limit,
cursor: data.cursor,
archived: data.archived
cursor: data.cursor
}
});
}
@ -1527,10 +1360,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
@ -1557,86 +1390,6 @@ export class FeedService {
});
}
/**
* @param data The data for the request.
* @param data.channelId
* @param data.eventId
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static feedArchiveEvent(data: $OpenApiTs['/feed/{channelId}/{eventId}/archive']['patch']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/{eventId}/archive']['patch']['res'][200]> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/feed/{channelId}/{eventId}/archive',
path: {
channelId: data.channelId,
eventId: data.eventId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.channelId
* @param data.eventId
* @param data.requestBody
* @returns unknown Successful response
* @throws ApiError
*/
public static feedUnarchiveEvent(data: $OpenApiTs['/feed/{channelId}/{eventId}/unarchive']['patch']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/{eventId}/unarchive']['patch']['res'][200]> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/feed/{channelId}/{eventId}/unarchive',
path: {
channelId: data.channelId,
eventId: data.eventId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* @param data The data for the request.
* @param data.channelId
* @param data.requestBody
* @returns number Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static feedClearAllArchivedEvents(data: $OpenApiTs['/feed/{channelId}/clearAllArchivedEvents']['patch']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/clearAllArchivedEvents']['patch']['res'][200] | $OpenApiTs['/feed/{channelId}/clearAllArchivedEvents']['patch']['res'][200]> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/feed/{channelId}/clearAllArchivedEvents',
path: {
channelId: data.channelId
},
body: data.requestBody,
mediaType: 'application/json'
});
}
/**
* webhook playground
* @param data The data for the request.
* @param data.workspaceId
* @returns string Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static feedIntegrationPlayground(data: $OpenApiTs['/feed/playground/{workspaceId}']['post']['req']): CancelablePromise<$OpenApiTs['/feed/playground/{workspaceId}']['post']['res'][200] | $OpenApiTs['/feed/playground/{workspaceId}']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/feed/playground/{workspaceId}',
path: {
workspaceId: data.workspaceId
}
});
}
/**
* integrate with github webhook
* @param data The data for the request.
@ -1673,22 +1426,4 @@ export class FeedService {
});
}
/**
* integrate with sentry webhook
* @param data The data for the request.
* @param data.channelId
* @returns string Successful response
* @returns unknown Error response
* @throws ApiError
*/
public static feedIntegrationSentry(data: $OpenApiTs['/feed/{channelId}/sentry']['post']['req']): CancelablePromise<$OpenApiTs['/feed/{channelId}/sentry']['post']['res'][200] | $OpenApiTs['/feed/{channelId}/sentry']['post']['res'][200]> {
return __request(OpenAPI, {
method: 'POST',
url: '/feed/{channelId}/sentry',
path: {
channelId: data.channelId
}
});
}
}

View File

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

View File

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

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -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)

View File

@ -109,24 +109,7 @@ export function useSocket() {
return { socket, emit, subscribe };
}
export function useSocketSubscribe<K extends keyof SubscribeEventMap>(
name: K,
cb: (data: SubscribeEventData<K>) => void
) {
const { subscribe } = useSocket();
const fn = useEvent(cb);
useEffect(() => {
const unsubscribe = subscribe(name, fn);
return () => {
unsubscribe();
};
}, [name]);
}
export function useSocketSubscribeData<T>(
export function useSocketSubscribe<T>(
name: keyof SubscribeEventMap,
defaultData: T
): T {

View File

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

View File

@ -1,37 +0,0 @@
import React from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { CodeBlock } from './CodeBlock';
interface CodeExampleItem {
label: string;
code?: string;
element?: React.ReactNode;
}
interface CodeExampleProps {
className?: string;
example: Record<string, CodeExampleItem>;
}
export const CodeExample: React.FC<CodeExampleProps> = React.memo((props) => {
const keys = Object.keys(props.example);
return (
<Tabs className={props.className} defaultValue={keys[0]}>
<TabsList>
{keys.map((key) => (
<TabsTrigger key={key} value={key}>
{props.example[key].label}
</TabsTrigger>
))}
</TabsList>
{keys.map((key) => (
<TabsContent key={key} value={key}>
{props.example[key].element ?? (
<CodeBlock code={props.example[key].code ?? ''} />
)}
</TabsContent>
))}
</Tabs>
);
});
CodeExample.displayName = 'CodeExample';

View File

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

View File

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

View File

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

View File

@ -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',
}
)}
/>
))}

View File

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

View File

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

View File

@ -1,230 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from './ui/resizable';
import { cn } from '@/utils/style';
import { ScrollArea } from './ui/scroll-area';
import { Trans, useTranslation } from '@i18next-toolkit/react';
import { Empty } from 'antd';
import { Button } from './ui/button';
import copy from 'copy-to-clipboard';
import { toast } from 'sonner';
import { Code } from './Code';
import { useCurrentWorkspaceId } from '@/store/user';
import { Badge } from './ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { reverse, toPairs } from 'lodash-es';
import { CodeBlock } from './CodeBlock';
import dayjs from 'dayjs';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from './ui/dropdown-menu';
import { useEvent } from '@/hooks/useEvent';
import { useSocketSubscribeList } from '@/api/socketio';
import { Spinner } from './ui/spinner';
export const WebhookPlayground: React.FC = React.memo(() => {
const { t } = useTranslation();
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(
null
);
const workspaceId = useCurrentWorkspaceId();
const requestList = useSocketSubscribeList(
'onReceivePlaygroundWebhookRequest'
);
const selectedRequest = useMemo(() => {
return requestList.find((item) => item.id === selectedRequestId);
}, [selectedRequestId, requestList]);
const handleCopyAsCurl = useEvent(() => {
if (!selectedRequest) {
return;
}
const url = selectedRequest.url.startsWith('/')
? `${window.location.origin}${selectedRequest.url}`
: selectedRequest.url;
const command = [
'curl',
`-X ${selectedRequest.method}`,
`${toPairs(selectedRequest.headers)
.filter(([key]) => !['content-length'].includes(key.toLowerCase()))
.map(([key, value]) => `-H '${key}: ${value}'`)
.join(' ')}`,
`-d '${JSON.stringify(selectedRequest.body)}'`,
`'${url}'`,
].join(' ');
copy(command);
toast.success('Copied into your clipboard!');
});
const list = (
<ScrollArea className="flex-1">
<div className="flex flex-col gap-2 p-2">
{requestList.length === 0 && (
<div className="pt-10">
<Empty
description={t(
'Currently waiting for a new request from the remote server'
)}
/>
<div className="mt-2 flex justify-center text-center">
<Spinner size={24} />
</div>
</div>
)}
{reverse([...requestList]).map((item) => {
return (
<button
key={item.id}
className={cn(
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all',
selectedRequestId === item.id
? 'bg-gray-100 dark:!bg-gray-800'
: 'hover:bg-gray-50 dark:hover:bg-gray-900'
)}
onClick={() => {
setSelectedRequestId(item.id);
}}
>
<div className="flex w-full items-center justify-between gap-2">
<Badge>{item.method}</Badge>
<div className="text-xs opacity-80">
{dayjs(item.createdAt).fromNow()}
</div>
</div>
<div className="flex w-full items-center justify-between gap-1">
<div className="overflow-hidden text-ellipsis font-semibold">
{item.url}
</div>
</div>
</button>
);
})}
</div>
</ScrollArea>
);
const webhookUrl = `${window.location.origin}/open/feed/playground/${workspaceId}`;
const emptyContentFallback = (
<div className="pt-8">
<div>
<Trans>
Set the webhook URL to <Code children={webhookUrl} />, and keep this
window active. Once done, you will start receiving webhook requests
here.
</Trans>
</div>
<Button
className="mt-2"
size="sm"
onClick={() => {
copy(webhookUrl);
toast.success('Copied into your clipboard!');
}}
>
{t('Copy URL')}
</Button>
</div>
);
const copyBtn = (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">{t('Copy as')}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" side="bottom" align="end">
<DropdownMenuItem onClick={handleCopyAsCurl}>cURL</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
const content = selectedRequest ? (
<div className="flex flex-col gap-2">
<div>
<div className="flex gap-2">
<Badge>{selectedRequest.method}</Badge>
<div className="flex w-full flex-1 items-center justify-between gap-1 overflow-hidden text-ellipsis font-semibold">
{selectedRequest.url}
</div>
{copyBtn}
</div>
<div className="text-right text-xs opacity-80">
{dayjs(selectedRequest.createdAt).fromNow()}
</div>
</div>
<div className="grid grid-cols-1 gap-2 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>{t('Request Header')}</CardTitle>
</CardHeader>
<CardContent>
{toPairs(selectedRequest.headers).map(([key, value]) => {
return (
<div key={key} className="flex items-center justify-between">
<div className="overflow-hidden text-ellipsis font-semibold">
{key}
</div>
<div className="overflow-hidden text-ellipsis">{value}</div>
</div>
);
})}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('Request Body')}</CardTitle>
</CardHeader>
<CardContent>
<CodeBlock
height={600}
code={JSON.stringify(selectedRequest.body, null, 2)}
/>
</CardContent>
</Card>
</div>
</div>
) : (
emptyContentFallback
);
return (
<div className="h-full w-full">
<ResizablePanelGroup
direction="horizontal"
className="h-full items-stretch"
>
<ResizablePanel
defaultSize={30}
collapsible={false}
minSize={20}
maxSize={50}
className={cn('flex flex-col')}
>
{list}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel>
<ScrollArea className="h-full px-4 py-2">{content}</ScrollArea>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
});
WebhookPlayground.displayName = 'WebhookPlayground';

View File

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

View File

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

View File

@ -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"

View File

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

View File

@ -23,7 +23,7 @@ import {
useUserInfo,
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';
@ -76,7 +76,7 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
<Avatar size={props.isCollapsed ? 'sm' : 'default'}>
{userInfo?.avatar && <AvatarImage src={userInfo.avatar} />}
<AvatarFallback delayMs={userInfo?.avatar ? undefined : 0}>
<AvatarFallback>
{nickname.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
@ -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>

View File

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

View File

@ -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>
);
}

View File

@ -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',
},

View File

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

View File

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

View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}

View File

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

View File

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

View File

@ -2,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,

View File

@ -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: {

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@
"scripts": {
"dev": "vite --port 10000",
"build": "vite build",
"ui:add": "shadcn add",
"ui:add": "shadcn-ui add",
"check:type": "tsc --noEmit --skipLibCheck --module esnext",
"translation:extract": "i18next-toolkit extract",
"translation:scan": "i18next-toolkit scan",
@ -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",
@ -36,7 +37,7 @@
"@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-scroll-area": "1.2.0-rc.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
@ -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",
@ -88,7 +88,6 @@
"react-resizable-panels": "^2.0.12",
"react-router": "^6.15.0",
"react-router-dom": "^6.15.0",
"recharts": "^2.12.7",
"rehype-external-links": "^3.0.0",
"socket.io-client": "^4.7.2",
"sonner": "^1.4.3",
@ -107,7 +106,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",
@ -119,7 +117,7 @@
"autoprefixer": "^10.4.16",
"less": "^4.2.0",
"postcss": "^8.4.31",
"shadcn": "^2.1.0",
"shadcn-ui": "^0.8.0",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"vite": "^5.0.12",

View File

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 269 B

View File

@ -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}}",

View File

@ -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}}",

View File

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

View File

@ -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}}",

View File

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 450 B

View File

@ -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}}",

View File

Before

Width:  |  Height:  |  Size: 259 B

After

Width:  |  Height:  |  Size: 259 B

View File

@ -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?",

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -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}}",

View File

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 268 B

View File

@ -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}}",

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -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}}",

View File

@ -34,13 +34,10 @@ 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'
import { Route as FeedPlaygroundImport } from './routes/feed_/playground'
import { Route as FeedAddImport } from './routes/feed/add'
import { Route as WebsiteWebsiteIdIndexImport } from './routes/website/$websiteId/index'
import { Route as SurveySurveyIdIndexImport } from './routes/survey/$surveyId/index'
@ -168,21 +165,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,
@ -198,11 +185,6 @@ const MonitorAddRoute = MonitorAddImport.update({
getParentRoute: () => MonitorRoute,
} as any)
const FeedPlaygroundRoute = FeedPlaygroundImport.update({
path: '/feed/playground',
getParentRoute: () => rootRoute,
} as any)
const FeedAddRoute = FeedAddImport.update({
path: '/add',
getParentRoute: () => FeedRoute,
@ -308,10 +290,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof FeedAddImport
parentRoute: typeof FeedImport
}
'/feed/playground': {
preLoaderRoute: typeof FeedPlaygroundImport
parentRoute: typeof rootRoute
}
'/monitor/add': {
preLoaderRoute: typeof MonitorAddImport
parentRoute: typeof MonitorImport
@ -324,18 +302,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 +401,7 @@ export const routeTree = rootRoute.addChildren([
RegisterRoute,
ServerRoute,
SettingsRoute.addChildren([
SettingsApiKeyRoute,
SettingsAuditLogRoute,
SettingsBillingRoute,
SettingsNotificationsRoute,
SettingsProfileRoute,
SettingsUsageRoute,
@ -452,7 +420,6 @@ export const routeTree = rootRoute.addChildren([
WebsiteWebsiteIdConfigRoute,
WebsiteWebsiteIdIndexRoute,
]),
FeedPlaygroundRoute,
StatusSlugRoute,
])

View File

@ -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>
)}
/>

View File

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

View File

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

View File

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

View File

@ -2,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) {

View File

@ -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>
);
}

View File

@ -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}

View File

@ -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>
);
}

View File

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

View File

@ -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)}

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
};
});

View File

@ -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: {

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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),
});
}

View File

@ -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();
});
});

View File

@ -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,
};
});
}

View File

@ -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('');
});
});

View File

@ -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 '';
}
}

View File

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

View File

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

View File

@ -24,7 +24,7 @@ export default defineConfig({
},
clearScreen: false,
server: {
host: '127.0.0.1',
// host: '0.0.0.0',
proxy: {
'/socket.io': {
target: 'ws://localhost:12345',
@ -34,9 +34,6 @@ export default defineConfig({
'/trpc': {
target: 'http://localhost:12345',
},
'/open': {
target: 'http://localhost:12345',
},
'/lh': {
target: 'http://localhost:12345',
},

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