Compare commits
16 Commits
feat/lemon
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
162954606a | ||
|
3bf86b3e6e | ||
|
843a581d42 | ||
|
fffc989336 | ||
|
ea75ed7f88 | ||
|
34f9fe6957 | ||
|
71f75c27dd | ||
|
a12fa3e6fe | ||
|
ae5f5a97d9 | ||
|
31ad64cd95 | ||
|
1096e9ca9a | ||
|
b71bf6542e | ||
|
e4b98b1c36 | ||
|
fa1ff3b5f6 | ||
|
f0ddf6c5dd | ||
|
74d391afc1 |
133
pnpm-lock.yaml
@ -135,8 +135,8 @@ importers:
|
||||
specifier: ^3.3.4
|
||||
version: 3.3.4(react-hook-form@7.51.1(react@18.2.0))
|
||||
'@i18next-toolkit/react':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@types/react@18.2.78)(buffer@6.0.3)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
specifier: 2.0.0-rc.5
|
||||
version: 2.0.0-rc.5(@types/react@18.2.78)(buffer@6.0.3)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@loadable/component':
|
||||
specifier: ^5.16.3
|
||||
version: 5.16.3(react@18.2.0)
|
||||
@ -543,12 +543,9 @@ importers:
|
||||
nodemailer:
|
||||
specifier: ^6.9.8
|
||||
version: 6.9.8
|
||||
passport:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
passport-jwt:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
p-map:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
ping:
|
||||
specifier: ^0.4.4
|
||||
version: 0.4.4
|
||||
@ -634,12 +631,6 @@ importers:
|
||||
'@types/nodemailer':
|
||||
specifier: ^6.4.11
|
||||
version: 6.4.11
|
||||
'@types/passport':
|
||||
specifier: ^1.0.12
|
||||
version: 1.0.12
|
||||
'@types/passport-jwt':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9
|
||||
'@types/ping':
|
||||
specifier: ^0.4.2
|
||||
version: 0.4.2
|
||||
@ -661,9 +652,6 @@ importers:
|
||||
execa:
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1
|
||||
p-map:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
prisma:
|
||||
specifier: 5.14.0
|
||||
version: 5.14.0
|
||||
@ -2263,8 +2251,14 @@ packages:
|
||||
'@i18next-toolkit/extractor@1.1.0':
|
||||
resolution: {integrity: sha512-USq83a1XKKCRGqlaKBoNRuCImD1IDFCHMgDHs9686v3IpZ2wQdj/e11+cPaGX1UIjndZKULdQq4b0aZJyMrBfg==}
|
||||
|
||||
'@i18next-toolkit/react@1.1.0':
|
||||
resolution: {integrity: sha512-S9HFkBwCukCwRR18P4yhskzoBJwIJ2W182GQ9u5H69Guj3Sg4Lm0ghGk7VVALS4z3XmQNcJBzA+LWmLB2X5hIQ==}
|
||||
'@i18next-toolkit/react-core@1.1.0':
|
||||
resolution: {integrity: sha512-PkuBaIY8jLS0QKy1sjj0g0XAC7zLIDM8ckI5VZGY3feiCPjV6ZdFsuC3cJJwRH+LgOCTj49LurhBii5UXGBlVQ==}
|
||||
peerDependencies:
|
||||
'@types/react': ^18.2.55
|
||||
react: ^18.2.0
|
||||
|
||||
'@i18next-toolkit/react@2.0.0-rc.5':
|
||||
resolution: {integrity: sha512-ZiQaLwS3jnYgFrotDTPbcF74Wbs0JNw0DDouuY+qNegUt5QxLBf/sCymQsglt9C9u2KOU+2CjS3ZP5NWlRknOg==}
|
||||
peerDependencies:
|
||||
'@types/react': ^18.2.55
|
||||
react: ^18.2.0
|
||||
@ -2341,10 +2335,6 @@ packages:
|
||||
resolution: {integrity: sha512-gM/FdNsK3BlrD6JRrhmiyqBXQsCpzSUdKSoZwJMQfXqfqcK321og+uMssc6HYcygUMrGvPnNJyJ1RqZPFDrgtg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@ljharb/resumer@0.0.1':
|
||||
resolution: {integrity: sha512-skQiAOrCfO7vRTq53cxznMpks7wS1va95UCidALlOVWqvBAzwPVErwizDwoMqNVMEn1mDq0utxZd02eIrvF1lw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
'@ljharb/through@2.3.11':
|
||||
resolution: {integrity: sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -2449,24 +2439,28 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@14.1.3':
|
||||
resolution: {integrity: sha512-esk1RkRBLSIEp1qaQXv1+s6ZdYzuVCnDAZySpa62iFTMGTisCyNQmqyCTL9P+cLJ4N9FKCI3ojtSfsyPHJDQNw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@14.1.3':
|
||||
resolution: {integrity: sha512-8uOgRlYEYiKo0L8YGeS+3TudHVDWDjPVDUcST+z+dUzgBbTEwSSIaSgF/vkcC1T/iwl4QX9iuUyUdQEl0Kxalg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@14.1.3':
|
||||
resolution: {integrity: sha512-DX2zqz05ziElLoxskgHasaJBREC5Y9TJcbR2LYqu4r7naff25B4iXkfXWfcp69uD75/0URmmoSgT8JclJtrBoQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@14.1.3':
|
||||
resolution: {integrity: sha512-HjssFsCdsD4GHstXSQxsi2l70F/5FsRTRQp8xNgmQs15SxUfUJRvSI9qKny/jLkY3gLgiCR3+6A7wzzK0DBlfA==}
|
||||
@ -3351,6 +3345,7 @@ packages:
|
||||
resolution: {integrity: sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.9.5':
|
||||
resolution: {integrity: sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==}
|
||||
@ -3361,66 +3356,79 @@ packages:
|
||||
resolution: {integrity: sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.19.1':
|
||||
resolution: {integrity: sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.9.5':
|
||||
resolution: {integrity: sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.19.1':
|
||||
resolution: {integrity: sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.9.5':
|
||||
resolution: {integrity: sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.19.1':
|
||||
resolution: {integrity: sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.19.1':
|
||||
resolution: {integrity: sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.9.5':
|
||||
resolution: {integrity: sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.19.1':
|
||||
resolution: {integrity: sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.19.1':
|
||||
resolution: {integrity: sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.9.5':
|
||||
resolution: {integrity: sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.19.1':
|
||||
resolution: {integrity: sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.9.5':
|
||||
resolution: {integrity: sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.19.1':
|
||||
resolution: {integrity: sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==}
|
||||
@ -4278,15 +4286,6 @@ packages:
|
||||
'@types/parse5@6.0.3':
|
||||
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
||||
|
||||
'@types/passport-jwt@3.0.9':
|
||||
resolution: {integrity: sha512-5XJt+79emfgpuBvBQusUPylFIVtW1QVAAkTRwCbRJAmxUjmLtIqUU6V1ovpnHPu6Qut3mR5Juc+s7kd06roNTg==}
|
||||
|
||||
'@types/passport-strategy@0.2.35':
|
||||
resolution: {integrity: sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==}
|
||||
|
||||
'@types/passport@1.0.12':
|
||||
resolution: {integrity: sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==}
|
||||
|
||||
'@types/pbf@3.0.5':
|
||||
resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==}
|
||||
|
||||
@ -7295,6 +7294,9 @@ packages:
|
||||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
|
||||
i18next-browser-languagedetector@8.0.0:
|
||||
resolution: {integrity: sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==}
|
||||
|
||||
i18next-http-backend@2.4.3:
|
||||
resolution: {integrity: sha512-jo2M03O6n1/DNb51WSQ8PsQ0xEELzLZRdYUTbf17mLw3rVwnJF9hwNgMXvEFSxxb+N8dT+o0vtigA6s5mGWyPA==}
|
||||
|
||||
@ -9316,17 +9318,6 @@ packages:
|
||||
pascal-case@3.1.2:
|
||||
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
|
||||
|
||||
passport-jwt@4.0.1:
|
||||
resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==}
|
||||
|
||||
passport-strategy@1.0.0:
|
||||
resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
passport@0.7.0:
|
||||
resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
@ -9407,9 +9398,6 @@ packages:
|
||||
pathval@1.1.1:
|
||||
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
|
||||
|
||||
pause@0.0.1:
|
||||
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
|
||||
|
||||
pbf@3.2.1:
|
||||
resolution: {integrity: sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==}
|
||||
hasBin: true
|
||||
@ -15155,14 +15143,25 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- buffer
|
||||
|
||||
'@i18next-toolkit/react@1.1.0(@types/react@18.2.78)(buffer@6.0.3)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
'@i18next-toolkit/react-core@1.1.0(@types/react@18.2.78)(buffer@6.0.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@types/react': 18.2.78
|
||||
crc: 4.3.2(buffer@6.0.3)
|
||||
i18next: 23.10.0
|
||||
i18next-http-backend: 2.4.3(encoding@0.1.13)
|
||||
react: 18.2.0
|
||||
react-i18next: 14.0.5(i18next@23.10.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- buffer
|
||||
- react-dom
|
||||
- react-native
|
||||
|
||||
'@i18next-toolkit/react@2.0.0-rc.5(@types/react@18.2.78)(buffer@6.0.3)(encoding@0.1.13)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@i18next-toolkit/react-core': 1.1.0(@types/react@18.2.78)(buffer@6.0.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@types/react': 18.2.78
|
||||
i18next-browser-languagedetector: 8.0.0
|
||||
i18next-http-backend: 2.4.3(encoding@0.1.13)
|
||||
react: 18.2.0
|
||||
transitivePeerDependencies:
|
||||
- buffer
|
||||
- encoding
|
||||
@ -15298,10 +15297,6 @@ snapshots:
|
||||
|
||||
'@lemonsqueezy/lemonsqueezy.js@3.3.1': {}
|
||||
|
||||
'@ljharb/resumer@0.0.1':
|
||||
dependencies:
|
||||
'@ljharb/through': 2.3.11
|
||||
|
||||
'@ljharb/through@2.3.11':
|
||||
dependencies:
|
||||
call-bind: 1.0.7
|
||||
@ -17954,21 +17949,6 @@ snapshots:
|
||||
|
||||
'@types/parse5@6.0.3': {}
|
||||
|
||||
'@types/passport-jwt@3.0.9':
|
||||
dependencies:
|
||||
'@types/express': 4.17.17
|
||||
'@types/jsonwebtoken': 9.0.5
|
||||
'@types/passport-strategy': 0.2.35
|
||||
|
||||
'@types/passport-strategy@0.2.35':
|
||||
dependencies:
|
||||
'@types/express': 4.17.17
|
||||
'@types/passport': 1.0.12
|
||||
|
||||
'@types/passport@1.0.12':
|
||||
dependencies:
|
||||
'@types/express': 4.17.17
|
||||
|
||||
'@types/pbf@3.0.5': {}
|
||||
|
||||
'@types/ping@0.4.2': {}
|
||||
@ -21796,6 +21776,10 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
i18next-browser-languagedetector@8.0.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.0
|
||||
|
||||
i18next-http-backend@2.4.3(encoding@0.1.13):
|
||||
dependencies:
|
||||
cross-fetch: 4.0.0(encoding@0.1.13)
|
||||
@ -24342,19 +24326,6 @@ snapshots:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.6.2
|
||||
|
||||
passport-jwt@4.0.1:
|
||||
dependencies:
|
||||
jsonwebtoken: 9.0.2
|
||||
passport-strategy: 1.0.0
|
||||
|
||||
passport-strategy@1.0.0: {}
|
||||
|
||||
passport@0.7.0:
|
||||
dependencies:
|
||||
passport-strategy: 1.0.0
|
||||
pause: 0.0.1
|
||||
utils-merge: 1.0.1
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
path-dirname@1.0.2: {}
|
||||
@ -24416,8 +24387,6 @@ snapshots:
|
||||
|
||||
pathval@1.1.1: {}
|
||||
|
||||
pause@0.0.1: {}
|
||||
|
||||
pbf@3.2.1:
|
||||
dependencies:
|
||||
ieee754: 1.2.1
|
||||
|
@ -14,6 +14,8 @@ import {
|
||||
LuAreaChart,
|
||||
LuBellDot,
|
||||
LuFilePieChart,
|
||||
LuKanbanSquare,
|
||||
LuKeyRound,
|
||||
LuMonitorDot,
|
||||
LuSearch,
|
||||
LuServer,
|
||||
@ -171,6 +173,22 @@ 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>
|
||||
|
61
src/client/components/UsageCard.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardHeader } from './ui/card';
|
||||
import { formatNumber } from '@/utils/common';
|
||||
import { LuAlertCircle } from 'react-icons/lu';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import colors from 'tailwindcss/colors';
|
||||
|
||||
interface UsageCardProps {
|
||||
title: string;
|
||||
current: number;
|
||||
limit?: number;
|
||||
}
|
||||
export const UsageCard: React.FC<UsageCardProps> = React.memo((props) => {
|
||||
const { title, current, limit } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card className="relative h-full w-full overflow-hidden">
|
||||
{limit && (
|
||||
<div
|
||||
className="absolute h-full bg-black bg-opacity-5 dark:bg-white dark:bg-opacity-10"
|
||||
style={{ width: `${(current / limit) * 100}%` }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{limit && current > limit && (
|
||||
<div className="absolute right-2 top-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LuAlertCircle stroke={colors.red['500']} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div>
|
||||
{t(
|
||||
'Exceeded the limit, please upgrade your plan or your workspace will be paused soon.'
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardHeader className="text-muted-foreground">{title}</CardHeader>
|
||||
<CardContent>
|
||||
{limit && limit >= 0 ? (
|
||||
<div>
|
||||
<span className="text-2xl font-bold">{formatNumber(current)}</span>{' '}
|
||||
/ <span>{formatNumber(limit)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<span className="text-2xl font-bold">{formatNumber(current)}</span>{' '}
|
||||
/ <span>∞</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
UsageCard.displayName = 'UsageCard';
|
166
src/client/components/billing/SubscriptionSelection.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import { Check } from 'lucide-react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { defaultErrorHandler, trpc } from '@/api/trpc';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { cn } from '@/utils/style';
|
||||
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
||||
import { LuInfo } from 'react-icons/lu';
|
||||
|
||||
interface SubscriptionSelectionProps {
|
||||
tier: 'FREE' | 'PRO' | 'TEAM' | 'UNLIMITED' | undefined;
|
||||
}
|
||||
export const SubscriptionSelection: React.FC<SubscriptionSelectionProps> =
|
||||
React.memo((props) => {
|
||||
const { tier } = props;
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const checkoutMutation = trpc.billing.checkout.useMutation({
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const handleCheckoutSubscribe = useEvent(
|
||||
async (tier: 'free' | 'pro' | 'team') => {
|
||||
const { url } = await checkoutMutation.mutateAsync({
|
||||
workspaceId,
|
||||
tier,
|
||||
redirectUrl: location.href,
|
||||
});
|
||||
|
||||
location.href = url;
|
||||
}
|
||||
);
|
||||
|
||||
const plans = [
|
||||
{
|
||||
id: 'FREE',
|
||||
name: t('Free'),
|
||||
price: 0,
|
||||
features: [
|
||||
t('Basic trial'),
|
||||
t('Basic Usage'),
|
||||
t('Up to 3 websites'),
|
||||
t('Up to 3 surveys'),
|
||||
t('Up to 3 feed channels'),
|
||||
t('100K website events per month'),
|
||||
t('100K monitor execution per month'),
|
||||
t('10K feed event per month'),
|
||||
t('Discord Community Support'),
|
||||
],
|
||||
onClick: () => handleCheckoutSubscribe('free'),
|
||||
},
|
||||
{
|
||||
id: 'PRO',
|
||||
name: 'Pro',
|
||||
price: 19.99,
|
||||
features: [
|
||||
t('Sufficient for most situations'),
|
||||
t('Priority access to advanced features'),
|
||||
t('Up to 10 websites'),
|
||||
t('Up to 20 surveys'),
|
||||
t('Up to 20 feed channels'),
|
||||
t('1M website events per month'),
|
||||
t('1M monitor execution per month'),
|
||||
t('100K feed events per month'),
|
||||
t('Discord Community Support'),
|
||||
],
|
||||
onClick: () => handleCheckoutSubscribe('pro'),
|
||||
},
|
||||
{
|
||||
id: 'TEAM',
|
||||
name: 'Team',
|
||||
price: 99.99,
|
||||
features: [
|
||||
t('Fully sufficient'),
|
||||
t('Priority access to advanced features'),
|
||||
t('Unlimited websites'),
|
||||
t('Unlimited surveys'),
|
||||
t('Unlimited feed channels'),
|
||||
t('20M website events per month'),
|
||||
t('20M monitor execution per month'),
|
||||
t('1M feed events per month'),
|
||||
t('Priority email support'),
|
||||
],
|
||||
onClick: () => handleCheckoutSubscribe('team'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="mb-8 text-center text-3xl font-bold">
|
||||
{t('Subscription Plan')}
|
||||
</h1>
|
||||
|
||||
<Alert className="mb-4">
|
||||
<LuInfo className="h-4 w-4" />
|
||||
<AlertTitle>{t('Current Plan')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('Your Current Plan is:')}{' '}
|
||||
<span className="font-bold">{tier}</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{plans.map((plan) => {
|
||||
const isCurrent = plan.id === tier;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={plan.name}
|
||||
className={cn('flex flex-col', isCurrent && 'border-primary')}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription>${plan.price} per month</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow">
|
||||
<ul className="space-y-2">
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center">
|
||||
<Check className="mr-2 h-4 w-4 text-green-500" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{isCurrent ? (
|
||||
<Button className="w-full" disabled variant="outline">
|
||||
{t('Current')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={checkoutMutation.isLoading}
|
||||
onClick={plan.onClick}
|
||||
>
|
||||
{t('{{action}} to {{plan}}', {
|
||||
action:
|
||||
plans.indexOf(plan) <
|
||||
plans.findIndex((p) => p.id === tier)
|
||||
? t('Downgrade')
|
||||
: t('Upgrade'),
|
||||
plan: plan.name,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SubscriptionSelection.displayName = 'SubscriptionSelection';
|
@ -23,7 +23,7 @@ import {
|
||||
useUserInfo,
|
||||
useUserStore,
|
||||
} from '@/store/user';
|
||||
import { languages } from '@/utils/constants';
|
||||
import { languages } from '@/utils/i18n';
|
||||
import { useTranslation, setLanguage } from '@i18next-toolkit/react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { version } from '@/utils/env';
|
||||
|
59
src/client/components/ui/alert.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/utils/style"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
@ -22,6 +22,9 @@ 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();
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/** @type {import('@i18next-toolkit/cli').I18nextToolkitConfig} */
|
||||
const config = {
|
||||
locales: ['en', 'zh', 'jp', 'fr', 'de', 'pl', 'pt', 'ru'],
|
||||
locales: ['en', 'zh-CN', 'ja-JP', 'fr-FR', 'de-DE', 'pl-PL', 'pt-PT', 'ru-RU'],
|
||||
verbose: true,
|
||||
namespaces: ['translation'],
|
||||
translator: {
|
||||
|
3
src/client/init.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { initI18N } from './utils/i18n';
|
||||
|
||||
initI18N();
|
@ -1,5 +1,6 @@
|
||||
import './index.css';
|
||||
import './styles/global.less';
|
||||
import './init';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
@ -23,7 +23,7 @@
|
||||
"@bytemd/plugin-gfm": "^1.21.0",
|
||||
"@bytemd/react": "^1.21.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@i18next-toolkit/react": "^1.1.0",
|
||||
"@i18next-toolkit/react": "2.0.0-rc.5",
|
||||
"@loadable/component": "^5.16.3",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
|
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 269 B |
@ -91,6 +91,7 @@
|
||||
"k3e8b13f8": "Discord beitreten",
|
||||
"k3eaab921": "ÜberwachungsListe",
|
||||
"k3f36e17e": "Twitter folgen",
|
||||
"k406089a4": "Aktion",
|
||||
"k406e9ad8": "Bestätigen",
|
||||
"k41d3ce6c": "Ereignis wiederhergestellt",
|
||||
"k42347b91": "Website-Ereigniszählung",
|
||||
@ -99,6 +100,7 @@
|
||||
"k44186b66": "Zählung",
|
||||
"k44cad477": "(Aktuell)",
|
||||
"k45f80a27": "Erweitert",
|
||||
"k4727e4db": "Ablaufdatum",
|
||||
"k477b7ee4": "Teilweise Systemausfälle",
|
||||
"k47fe1f95": "Fügen Sie diesen Beispielcode zu Ihrem Projekt hinzu",
|
||||
"k48186ce": "Zurück zur Startseite",
|
||||
@ -130,9 +132,12 @@
|
||||
"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",
|
||||
@ -223,6 +228,7 @@
|
||||
"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?",
|
||||
@ -248,6 +254,7 @@
|
||||
"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",
|
||||
@ -269,6 +276,7 @@
|
||||
"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",
|
||||
@ -276,6 +284,7 @@
|
||||
"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",
|
||||
@ -314,6 +323,7 @@
|
||||
"kcd56f27b": "Zuletzt aktualisiert",
|
||||
"kcd643ef3": "Lade...",
|
||||
"kce77d0c1": "Zeitzone",
|
||||
"kcff78587": "Zuletzt verwendet am",
|
||||
"kd005f7a8": "Alle Feeds werden entfernt",
|
||||
"kd031b383": "Ansichten",
|
||||
"kd092de58": "Aktueller Arbeitsbereich:",
|
||||
@ -328,9 +338,11 @@
|
||||
"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",
|
@ -91,6 +91,7 @@
|
||||
"k3e8b13f8": "Join Discord",
|
||||
"k3eaab921": "Monitor List",
|
||||
"k3f36e17e": "Follow Twitter",
|
||||
"k406089a4": "Action",
|
||||
"k406e9ad8": "Confirm",
|
||||
"k41d3ce6c": "Event unarchived",
|
||||
"k42347b91": "Website Event Count",
|
||||
@ -99,6 +100,7 @@
|
||||
"k44186b66": "Count",
|
||||
"k44cad477": "(Current)",
|
||||
"k45f80a27": "Advanced",
|
||||
"k4727e4db": "Expired At",
|
||||
"k477b7ee4": "Partial System Outage",
|
||||
"k47fe1f95": "Add this example code into your project",
|
||||
"k48186ce": "Back to Homepage",
|
||||
@ -130,9 +132,12 @@
|
||||
"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",
|
||||
@ -223,6 +228,7 @@
|
||||
"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?",
|
||||
@ -248,6 +254,7 @@
|
||||
"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",
|
||||
@ -269,6 +276,7 @@
|
||||
"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",
|
||||
@ -276,6 +284,7 @@
|
||||
"kb5673707": "Last 7 days",
|
||||
"kb659c1bc": "Cert Exp.",
|
||||
"kb6d350b6": "Feed Channels",
|
||||
"kb7bf8869": "Api Keys",
|
||||
"kb7fa344a": "Select Feed Channel for send",
|
||||
"kb8de8c50": "BCC",
|
||||
"kbb31d3db": "Statistic Date",
|
||||
@ -314,6 +323,7 @@
|
||||
"kcd56f27b": "Last updated",
|
||||
"kcd643ef3": "Loading...",
|
||||
"kce77d0c1": "Timezone",
|
||||
"kcff78587": "Last Use At",
|
||||
"kd005f7a8": "All feed will be remove",
|
||||
"kd031b383": "Views",
|
||||
"kd092de58": "Current Workspace:",
|
||||
@ -328,9 +338,11 @@
|
||||
"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",
|
||||
|
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 267 B |
@ -91,6 +91,7 @@
|
||||
"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",
|
||||
@ -99,6 +100,7 @@
|
||||
"k44186b66": "Compte",
|
||||
"k44cad477": "(Actuel)",
|
||||
"k45f80a27": "Avancé",
|
||||
"k4727e4db": "Expiré À",
|
||||
"k477b7ee4": "Panne partielle du système",
|
||||
"k47fe1f95": "Ajoutez ce code d'exemple à votre projet",
|
||||
"k48186ce": "Retour à la page d'accueil",
|
||||
@ -130,9 +132,12 @@
|
||||
"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",
|
||||
@ -223,6 +228,7 @@
|
||||
"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 ?",
|
||||
@ -248,6 +254,7 @@
|
||||
"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",
|
||||
@ -269,6 +276,7 @@
|
||||
"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",
|
||||
@ -276,6 +284,7 @@
|
||||
"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",
|
||||
@ -314,6 +323,7 @@
|
||||
"kcd56f27b": "Dernière mise à jour",
|
||||
"kcd643ef3": "Chargement...",
|
||||
"kce77d0c1": "Fuseau horaire",
|
||||
"kcff78587": "Dernière Utilisation À",
|
||||
"kd005f7a8": "Tous les flux seront supprimés",
|
||||
"kd031b383": "Vues",
|
||||
"kd092de58": "Espace de travail actuel :",
|
||||
@ -328,9 +338,11 @@
|
||||
"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",
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 450 B |
@ -91,6 +91,7 @@
|
||||
"k3e8b13f8": "Discordに参加",
|
||||
"k3eaab921": "モニターリスト",
|
||||
"k3f36e17e": "Twitterをフォロー",
|
||||
"k406089a4": "アクション",
|
||||
"k406e9ad8": "確認",
|
||||
"k41d3ce6c": "イベントがアーカイブ解除されました",
|
||||
"k42347b91": "ウェブサイトイベント数",
|
||||
@ -99,6 +100,7 @@
|
||||
"k44186b66": "カウント",
|
||||
"k44cad477": "(現在)",
|
||||
"k45f80a27": "詳細",
|
||||
"k4727e4db": "期限切れ",
|
||||
"k477b7ee4": "部分的なシステム障害",
|
||||
"k47fe1f95": "このサンプルコードをプロジェクトに追加してください",
|
||||
"k48186ce": "ホームページに戻る",
|
||||
@ -130,9 +132,12 @@
|
||||
"k58267a45": "ソース",
|
||||
"k58f90514": "ボットトークン",
|
||||
"k593cf342": "このモニターを削除してもよろしいですか?",
|
||||
"k5a782f4b": "ウェブサイト数",
|
||||
"k5a839f71": "アップタイム",
|
||||
"k5b5be0d4": "現在の役割",
|
||||
"k5c18db28": "ステータスページ情報を変更",
|
||||
"k5d00536d": "コピー済み",
|
||||
"k5d49d751": "新しいAPIキーがクリップボードにコピーされました!",
|
||||
"k5eb87a8b": "開始",
|
||||
"k5ec0de4": "HTTPSモニタリングの場合、通知方法が割り当てられている場合、有効期限の1、3、7、14日前に通知が送信されます。",
|
||||
"k5ecf04b0": "ビュー",
|
||||
@ -223,6 +228,7 @@
|
||||
"k93458b98": "プレイグラウンド",
|
||||
"k951a939a": "ウェブサイト承認カウント",
|
||||
"k95f932a": "現在、リモートサーバーからの新しいリクエストを待機中です",
|
||||
"k97b02874": "ページ数",
|
||||
"k98f433ee": "からレポーターをダウンロード",
|
||||
"k9991c290": "コミュニティ",
|
||||
"k9a272ecf": "これはあなたのサーバーですか?",
|
||||
@ -248,6 +254,7 @@
|
||||
"ka6ee7455": "ウェブサイトID",
|
||||
"ka71c12e1": "2つのパスワードが一致しません",
|
||||
"ka765ad32": "通知",
|
||||
"ka7d8617e": "フィードチャンネル数",
|
||||
"ka7fe5937": "ディスク読み取り/書き込み",
|
||||
"ka8e41156": "検索して素早く移動",
|
||||
"ka90bc019": "アンインストール",
|
||||
@ -269,6 +276,7 @@
|
||||
"kb0e351e0": "更新されました",
|
||||
"kb114a2e8": "非推奨",
|
||||
"kb15a6374": "自分のドメインでステータスページを設定できます。たとえば、status.example.com",
|
||||
"kb2dded49": "キー",
|
||||
"kb320aac4": "{{dayNum}}日間監視",
|
||||
"kb35cde91": "検索",
|
||||
"kb35d71ed": "または",
|
||||
@ -276,6 +284,7 @@
|
||||
"kb5673707": "過去7日間",
|
||||
"kb659c1bc": "証明書の有効期限",
|
||||
"kb6d350b6": "フィードチャンネル",
|
||||
"kb7bf8869": "APIキー",
|
||||
"kb7fa344a": "送信するフィードチャンネルを選択",
|
||||
"kb8de8c50": "BCC",
|
||||
"kbb31d3db": "統計日",
|
||||
@ -314,6 +323,7 @@
|
||||
"kcd56f27b": "最終更新",
|
||||
"kcd643ef3": "読み込み中...",
|
||||
"kce77d0c1": "タイムゾーン",
|
||||
"kcff78587": "最終使用日時",
|
||||
"kd005f7a8": "すべてのフィードが削除されます",
|
||||
"kd031b383": "ビュー",
|
||||
"kd092de58": "現在のワークスペース:",
|
||||
@ -328,9 +338,11 @@
|
||||
"kd7279fa6": "コード",
|
||||
"kd7985726": "{{num}}人のユーザー",
|
||||
"kd92fa3e7": "ホスト名",
|
||||
"kdaa6ae2b": "モニター数",
|
||||
"kdaff25a6": "最新値を表示",
|
||||
"kdb61adbb": "オフラインを隠す",
|
||||
"kdbadcf43": "すべてのシステムが稼働中",
|
||||
"kdbe222b": "APIキー",
|
||||
"kdc10ee1a": "チームメンバーと協力するために新しいワークスペースを作成します。",
|
||||
"kdc15c5d": "データ",
|
||||
"kdc1bf80e": "URLは必須です",
|
Before Width: | Height: | Size: 259 B After Width: | Height: | Size: 259 B |
@ -91,6 +91,7 @@
|
||||
"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",
|
||||
@ -99,6 +100,7 @@
|
||||
"k44186b66": "Liczba",
|
||||
"k44cad477": "(Obecny)",
|
||||
"k45f80a27": "Zaawansowane",
|
||||
"k4727e4db": "Wygasło",
|
||||
"k477b7ee4": "Częściowa awaria systemu",
|
||||
"k47fe1f95": "Dodaj ten przykładowy kod do swojego projektu",
|
||||
"k48186ce": "Powrót do strony głównej",
|
||||
@ -130,9 +132,12 @@
|
||||
"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",
|
||||
@ -223,6 +228,7 @@
|
||||
"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?",
|
||||
@ -248,6 +254,7 @@
|
||||
"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",
|
||||
@ -269,6 +276,7 @@
|
||||
"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",
|
||||
@ -276,6 +284,7 @@
|
||||
"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",
|
||||
@ -314,6 +323,7 @@
|
||||
"kcd56f27b": "Ostatnia aktualizacja",
|
||||
"kcd643ef3": "Ładowanie...",
|
||||
"kce77d0c1": "Strefa czasowa",
|
||||
"kcff78587": "Ostatnie użycie",
|
||||
"kd005f7a8": "Wszystkie kanały informacyjne zostaną usunięte",
|
||||
"kd031b383": "Odsłony",
|
||||
"kd092de58": "Aktualna przestrzeń robocza:",
|
||||
@ -328,9 +338,11 @@
|
||||
"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",
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
@ -91,6 +91,7 @@
|
||||
"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",
|
||||
@ -99,6 +100,7 @@
|
||||
"k44186b66": "Contar",
|
||||
"k44cad477": "(Atual)",
|
||||
"k45f80a27": "Avançado",
|
||||
"k4727e4db": "Expirado Em",
|
||||
"k477b7ee4": "Interrupção Parcial do Sistema",
|
||||
"k47fe1f95": "Adicione este código de exemplo ao seu projeto",
|
||||
"k48186ce": "Voltar à página inicial",
|
||||
@ -130,9 +132,12 @@
|
||||
"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",
|
||||
@ -223,6 +228,7 @@
|
||||
"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?",
|
||||
@ -248,6 +254,7 @@
|
||||
"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",
|
||||
@ -269,6 +276,7 @@
|
||||
"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",
|
||||
@ -276,6 +284,7 @@
|
||||
"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",
|
||||
@ -314,6 +323,7 @@
|
||||
"kcd56f27b": "Última atualização",
|
||||
"kcd643ef3": "Carregando...",
|
||||
"kce77d0c1": "Fuso Horário",
|
||||
"kcff78587": "Último Uso Em",
|
||||
"kd005f7a8": "Todos os feeds serão removidos",
|
||||
"kd031b383": "Vistas",
|
||||
"kd092de58": "Espaço de Trabalho Atual:",
|
||||
@ -328,9 +338,11 @@
|
||||
"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",
|
Before Width: | Height: | Size: 268 B After Width: | Height: | Size: 268 B |
@ -91,6 +91,7 @@
|
||||
"k3e8b13f8": "Присоединяйтесь к Discord",
|
||||
"k3eaab921": "Список мониторинга",
|
||||
"k3f36e17e": "Подписаться на Twitter",
|
||||
"k406089a4": "Действие",
|
||||
"k406e9ad8": "Подтвердить",
|
||||
"k41d3ce6c": "Событие восстановлено",
|
||||
"k42347b91": "Количество событий на сайте",
|
||||
@ -99,6 +100,7 @@
|
||||
"k44186b66": "Количество",
|
||||
"k44cad477": "(Текущий)",
|
||||
"k45f80a27": "Расширенный",
|
||||
"k4727e4db": "Истекает",
|
||||
"k477b7ee4": "Частичный сбой системы",
|
||||
"k47fe1f95": "Добавьте этот пример кода в ваш проект",
|
||||
"k48186ce": "Вернуться на главную страницу",
|
||||
@ -130,9 +132,12 @@
|
||||
"k58267a45": "Источник",
|
||||
"k58f90514": "Токен бота",
|
||||
"k593cf342": "Вы уверены, что хотите удалить этот монитор?",
|
||||
"k5a782f4b": "Количество сайтов",
|
||||
"k5a839f71": "Время работы",
|
||||
"k5b5be0d4": "Текущая роль",
|
||||
"k5c18db28": "Изменить информацию на странице статуса",
|
||||
"k5d00536d": "Скопировано",
|
||||
"k5d49d751": "Новый API-ключ скопирован в буфер обмена!",
|
||||
"k5eb87a8b": "Старт",
|
||||
"k5ec0de4": "Для мониторинга HTTPS, если назначен любой метод уведомления, уведомления будут отправлены за 1, 3, 7 и 14 дней до истечения срока действия.",
|
||||
"k5ecf04b0": "Просмотр",
|
||||
@ -223,6 +228,7 @@
|
||||
"k93458b98": "Площадка",
|
||||
"k951a939a": "Количество принятых сайтом",
|
||||
"k95f932a": "В настоящее время ожидает нового запроса от удаленного сервера",
|
||||
"k97b02874": "Количество страниц",
|
||||
"k98f433ee": "Скачать репортер с",
|
||||
"k9991c290": "Сообщество",
|
||||
"k9a272ecf": "Это ваши серверы?",
|
||||
@ -248,6 +254,7 @@
|
||||
"ka6ee7455": "ID веб-сайта",
|
||||
"ka71c12e1": "Два пароля не совпадают",
|
||||
"ka765ad32": "Уведомления",
|
||||
"ka7d8617e": "Количество каналов ленты",
|
||||
"ka7fe5937": "Чтение/запись на диск",
|
||||
"ka8e41156": "Поиск и быстрый переход",
|
||||
"ka90bc019": "Удалить",
|
||||
@ -269,6 +276,7 @@
|
||||
"kb0e351e0": "Обновлено",
|
||||
"kb114a2e8": "Устаревший",
|
||||
"kb15a6374": "Вы можете настроить свою страницу статуса на своем собственном домене, например: status.example.com",
|
||||
"kb2dded49": "Ключ",
|
||||
"kb320aac4": "Мониторинг в течение {{dayNum}} дней",
|
||||
"kb35cde91": "Поиск",
|
||||
"kb35d71ed": "ИЛИ",
|
||||
@ -276,6 +284,7 @@
|
||||
"kb5673707": "Последние 7 дней",
|
||||
"kb659c1bc": "Истечение серт.",
|
||||
"kb6d350b6": "Каналы обратной связи",
|
||||
"kb7bf8869": "API-ключи",
|
||||
"kb7fa344a": "Выберите канал обратной связи для отправки",
|
||||
"kb8de8c50": "Скрытая копия",
|
||||
"kbb31d3db": "Дата статистики",
|
||||
@ -314,6 +323,7 @@
|
||||
"kcd56f27b": "Последнее обновление",
|
||||
"kcd643ef3": "Загрузка...",
|
||||
"kce77d0c1": "Часовой пояс",
|
||||
"kcff78587": "Последнее использование",
|
||||
"kd005f7a8": "Все ленты будут удалены",
|
||||
"kd031b383": "Просмотры",
|
||||
"kd092de58": "Текущее рабочее пространство:",
|
||||
@ -328,9 +338,11 @@
|
||||
"kd7279fa6": "Код",
|
||||
"kd7985726": "{{num}} пользователей",
|
||||
"kd92fa3e7": "Имя хоста",
|
||||
"kdaa6ae2b": "Количество мониторов",
|
||||
"kdaff25a6": "Показать последнее значение",
|
||||
"kdb61adbb": "Скрыть офлайн",
|
||||
"kdbadcf43": "Все системы работают",
|
||||
"kdbe222b": "API-ключ",
|
||||
"kdc10ee1a": "Создайте новое рабочее пространство для сотрудничества с членами команды.",
|
||||
"kdc15c5d": "Данные",
|
||||
"kdc1bf80e": "URL обязателен",
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@ -91,6 +91,7 @@
|
||||
"k3e8b13f8": "加入 Discord",
|
||||
"k3eaab921": "监控列表",
|
||||
"k3f36e17e": "关注 Twitter",
|
||||
"k406089a4": "操作",
|
||||
"k406e9ad8": "确认",
|
||||
"k41d3ce6c": "事件已取消归档",
|
||||
"k42347b91": "网站事件计数",
|
||||
@ -99,6 +100,7 @@
|
||||
"k44186b66": "计数",
|
||||
"k44cad477": "(当前)",
|
||||
"k45f80a27": "高级",
|
||||
"k4727e4db": "到期时间",
|
||||
"k477b7ee4": "部分系统故障",
|
||||
"k47fe1f95": "将此示例代码添加到您的项目中",
|
||||
"k48186ce": "返回首页",
|
||||
@ -130,9 +132,12 @@
|
||||
"k58267a45": "源",
|
||||
"k58f90514": "机器人令牌",
|
||||
"k593cf342": "您确定要删除这个监控器吗?",
|
||||
"k5a782f4b": "网站数量",
|
||||
"k5a839f71": "正常运行时间",
|
||||
"k5b5be0d4": "当前角色",
|
||||
"k5c18db28": "修改状态页面信息",
|
||||
"k5d00536d": "已复制",
|
||||
"k5d49d751": "新的 API 密钥已复制到您的剪贴板!",
|
||||
"k5eb87a8b": "开始",
|
||||
"k5ec0de4": "对于 HTTPS 监控,如果分配了任何通知方法,则将在到期前 1、3、7 和 14 天发送通知。",
|
||||
"k5ecf04b0": "查看",
|
||||
@ -223,6 +228,7 @@
|
||||
"k93458b98": "游乐场",
|
||||
"k951a939a": "网站接受计数",
|
||||
"k95f932a": "当前正在等待来自远程服务器的新请求",
|
||||
"k97b02874": "页面数量",
|
||||
"k98f433ee": "从这里下载报告器",
|
||||
"k9991c290": "社区",
|
||||
"k9a272ecf": "这是您的服务器吗?",
|
||||
@ -248,6 +254,7 @@
|
||||
"ka6ee7455": "网站ID",
|
||||
"ka71c12e1": "两次密码不一致",
|
||||
"ka765ad32": "通知",
|
||||
"ka7d8617e": "Feed 渠道数量",
|
||||
"ka7fe5937": "磁盘读/写",
|
||||
"ka8e41156": "搜索和快速跳转",
|
||||
"ka90bc019": "卸载",
|
||||
@ -269,6 +276,7 @@
|
||||
"kb0e351e0": "已刷新",
|
||||
"kb114a2e8": "已弃用",
|
||||
"kb15a6374": "您可以在自己的域名中配置您的状态页面,例如:status.example.com",
|
||||
"kb2dded49": "密钥",
|
||||
"kb320aac4": "已监控{{dayNum}}天",
|
||||
"kb35cde91": "搜索",
|
||||
"kb35d71ed": "或",
|
||||
@ -276,6 +284,7 @@
|
||||
"kb5673707": "最近7天",
|
||||
"kb659c1bc": "证书到期",
|
||||
"kb6d350b6": "馈送频道",
|
||||
"kb7bf8869": "API 密钥",
|
||||
"kb7fa344a": "选择要发送的馈送频道",
|
||||
"kb8de8c50": "密送",
|
||||
"kbb31d3db": "统计日期",
|
||||
@ -314,6 +323,7 @@
|
||||
"kcd56f27b": "最后更新",
|
||||
"kcd643ef3": "加载中...",
|
||||
"kce77d0c1": "时区",
|
||||
"kcff78587": "最后使用时间",
|
||||
"kd005f7a8": "所有订阅将被删除",
|
||||
"kd031b383": "视图",
|
||||
"kd092de58": "当前工作区:",
|
||||
@ -328,9 +338,11 @@
|
||||
"kd7279fa6": "代码",
|
||||
"kd7985726": "{{num}}个用户",
|
||||
"kd92fa3e7": "主机名",
|
||||
"kdaa6ae2b": "监控数量",
|
||||
"kdaff25a6": "显示最新值",
|
||||
"kdb61adbb": "隐藏离线",
|
||||
"kdbadcf43": "所有系统正常运行",
|
||||
"kdbe222b": "API 密钥",
|
||||
"kdc10ee1a": "创建一个新的工作区以与团队成员合作。",
|
||||
"kdc15c5d": "数据",
|
||||
"kdc1bf80e": "网址是必需的",
|
@ -34,6 +34,7 @@ import { Route as SettingsWorkspaceImport } from './routes/settings/workspace'
|
||||
import { Route as SettingsUsageImport } from './routes/settings/usage'
|
||||
import { Route as SettingsProfileImport } from './routes/settings/profile'
|
||||
import { Route as SettingsNotificationsImport } from './routes/settings/notifications'
|
||||
import { Route as SettingsBillingImport } from './routes/settings/billing'
|
||||
import { Route as SettingsAuditLogImport } from './routes/settings/auditLog'
|
||||
import { Route as SettingsApiKeyImport } from './routes/settings/apiKey'
|
||||
import { Route as PageAddImport } from './routes/page/add'
|
||||
@ -167,6 +168,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,
|
||||
@ -326,6 +332,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof SettingsAuditLogImport
|
||||
parentRoute: typeof SettingsImport
|
||||
}
|
||||
'/settings/billing': {
|
||||
preLoaderRoute: typeof SettingsBillingImport
|
||||
parentRoute: typeof SettingsImport
|
||||
}
|
||||
'/settings/notifications': {
|
||||
preLoaderRoute: typeof SettingsNotificationsImport
|
||||
parentRoute: typeof SettingsImport
|
||||
@ -423,6 +433,7 @@ export const routeTree = rootRoute.addChildren([
|
||||
SettingsRoute.addChildren([
|
||||
SettingsApiKeyRoute,
|
||||
SettingsAuditLogRoute,
|
||||
SettingsBillingRoute,
|
||||
SettingsNotificationsRoute,
|
||||
SettingsProfileRoute,
|
||||
SettingsUsageRoute,
|
||||
|
@ -2,6 +2,7 @@ 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 {
|
||||
@ -9,6 +10,7 @@ import {
|
||||
useNavigate,
|
||||
useRouterState,
|
||||
} from '@tanstack/react-router';
|
||||
import { compact } from 'lodash-es';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/settings')({
|
||||
@ -22,8 +24,9 @@ function PageComponent() {
|
||||
const pathname = useRouterState({
|
||||
select: (state) => state.location.pathname,
|
||||
});
|
||||
const { enableBilling } = useGlobalConfig();
|
||||
|
||||
const items = [
|
||||
const items = compact([
|
||||
{
|
||||
id: 'profile',
|
||||
title: t('Profile'),
|
||||
@ -54,7 +57,12 @@ function PageComponent() {
|
||||
title: t('Usage'),
|
||||
href: '/settings/usage',
|
||||
},
|
||||
];
|
||||
enableBilling && {
|
||||
id: 'billing',
|
||||
title: t('Billing'),
|
||||
href: '/settings/billing',
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname === Route.fullPath) {
|
||||
|
@ -2,11 +2,13 @@ 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 { trpc } from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import {
|
||||
defaultErrorHandler,
|
||||
defaultSuccessHandler,
|
||||
trpc,
|
||||
} from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId, useHasAdminPermission } from '../../store/user';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { last } from 'lodash-es';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
@ -14,6 +16,9 @@ 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,
|
||||
@ -24,8 +29,9 @@ function PageComponent() {
|
||||
const { t } = useTranslation();
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const hasAdminPermission = useHasAdminPermission();
|
||||
|
||||
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } =
|
||||
const { data, hasNextPage, fetchNextPage, isFetchingNextPage, refetch } =
|
||||
trpc.auditLog.fetchByCursor.useInfiniteQuery(
|
||||
{
|
||||
workspaceId,
|
||||
@ -35,6 +41,11 @@ function PageComponent() {
|
||||
}
|
||||
);
|
||||
|
||||
const clearMutation = trpc.auditLog.clear.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const allData = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
@ -69,7 +80,27 @@ function PageComponent() {
|
||||
});
|
||||
|
||||
return (
|
||||
<CommonWrapper header={<CommonHeader title={t('Audit Log')} />}>
|
||||
<CommonWrapper
|
||||
header={
|
||||
<CommonHeader
|
||||
title={t('Audit Log')}
|
||||
actions={
|
||||
<>
|
||||
{hasAdminPermission && (
|
||||
<AlertConfirm
|
||||
onConfirm={() => {
|
||||
clearMutation.mutateAsync({ workspaceId });
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button variant="outline" size="icon" Icon={LuTrash2} />
|
||||
</AlertConfirm>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="h-full overflow-hidden p-4">
|
||||
<SimpleVirtualList
|
||||
allData={allData}
|
||||
|
124
src/client/routes/settings/billing.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
defaultErrorHandler,
|
||||
defaultSuccessHandler,
|
||||
trpc,
|
||||
} from '../../api/trpc';
|
||||
import { useCurrentWorkspace, useCurrentWorkspaceId } from '../../store/user';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import dayjs from 'dayjs';
|
||||
import { formatNumber } from '@/utils/common';
|
||||
import { UsageCard } from '@/components/UsageCard';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { SubscriptionSelection } from '@/components/billing/SubscriptionSelection';
|
||||
|
||||
export const Route = createFileRoute('/settings/billing')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: PageComponent,
|
||||
});
|
||||
|
||||
function PageComponent() {
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { t } = useTranslation();
|
||||
const { data: currentTier } = trpc.billing.currentTier.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
const checkoutMutation = trpc.billing.checkout.useMutation({
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
const changePlanMutation = trpc.billing.changePlan.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
const cancelSubscriptionMutation =
|
||||
trpc.billing.cancelSubscription.useMutation({
|
||||
onSuccess: defaultSuccessHandler,
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const { data, refetch, isInitialLoading, isLoading } =
|
||||
trpc.billing.currentSubscription.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const handleChangeSubscribe = useEvent(
|
||||
async (tier: 'free' | 'pro' | 'team') => {
|
||||
await changePlanMutation.mutateAsync({
|
||||
workspaceId,
|
||||
tier,
|
||||
});
|
||||
|
||||
refetch();
|
||||
}
|
||||
);
|
||||
|
||||
const plan = data ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
loading={changePlanMutation.isLoading}
|
||||
onClick={() => handleChangeSubscribe('free')}
|
||||
>
|
||||
Change plan to Free
|
||||
</Button>
|
||||
<Button
|
||||
loading={changePlanMutation.isLoading}
|
||||
onClick={() => handleChangeSubscribe('pro')}
|
||||
>
|
||||
Change plan to Pro
|
||||
</Button>
|
||||
<Button
|
||||
loading={changePlanMutation.isLoading}
|
||||
onClick={() => handleChangeSubscribe('team')}
|
||||
>
|
||||
Change plan to Team
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
loading={cancelSubscriptionMutation.isLoading}
|
||||
onClick={() =>
|
||||
cancelSubscriptionMutation.mutateAsync({
|
||||
workspaceId,
|
||||
})
|
||||
}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<SubscriptionSelection tier={currentTier} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<CommonWrapper header={<CommonHeader title={t('Billing')} />}>
|
||||
<ScrollArea className="h-full overflow-hidden p-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<div>Current: {JSON.stringify(data)}</div>
|
||||
|
||||
<Button loading={isLoading} onClick={() => refetch()}>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
{isInitialLoading === false && plan}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CommonWrapper>
|
||||
);
|
||||
}
|
@ -10,6 +10,7 @@ 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,
|
||||
@ -24,12 +25,20 @@ function PageComponent() {
|
||||
[]
|
||||
);
|
||||
|
||||
const { data } = trpc.billing.usage.useQuery({
|
||||
const { data: serviceCountData } = trpc.workspace.getServiceCount.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const { data: billingUsageData } = trpc.billing.usage.useQuery({
|
||||
workspaceId,
|
||||
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">
|
||||
@ -45,50 +54,61 @@ function PageComponent() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Website Accepted Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.websiteAcceptedCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<UsageCard
|
||||
title={t('Website Count')}
|
||||
current={serviceCountData?.website ?? 0}
|
||||
limit={limit?.maxWebsiteCount}
|
||||
/>
|
||||
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Website Event Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.websiteEventCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<UsageCard
|
||||
title={t('Monitor Count')}
|
||||
current={serviceCountData?.monitor ?? 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('Survey Count')}
|
||||
current={serviceCountData?.survey ?? 0}
|
||||
/>
|
||||
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Survey Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.surveyCount ?? 0)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<UsageCard
|
||||
title={t('Page Count')}
|
||||
current={serviceCountData?.page ?? 0}
|
||||
/>
|
||||
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="text-muted-foreground">
|
||||
{t('Feed Event Count')}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{formatNumber(data?.feedEventCount ?? 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}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { setupI18nInstance } from '@i18next-toolkit/react';
|
||||
|
||||
export const languages = [
|
||||
{
|
||||
label: 'English',
|
||||
@ -5,31 +7,36 @@ export const languages = [
|
||||
},
|
||||
{
|
||||
label: 'Deutsch',
|
||||
key: 'de',
|
||||
key: 'de-DE',
|
||||
},
|
||||
{
|
||||
label: 'Français',
|
||||
key: 'fr',
|
||||
key: 'fr-FR',
|
||||
},
|
||||
{
|
||||
label: '日本語',
|
||||
key: 'jp',
|
||||
key: 'ja-JP',
|
||||
},
|
||||
{
|
||||
label: 'Polski',
|
||||
key: 'pl',
|
||||
key: 'pl-PL',
|
||||
},
|
||||
{
|
||||
label: 'Português',
|
||||
key: 'pt',
|
||||
key: 'pt-PT',
|
||||
},
|
||||
{
|
||||
label: 'Русский',
|
||||
key: 'ru',
|
||||
key: 'ru-RU',
|
||||
},
|
||||
|
||||
{
|
||||
label: '简体中文',
|
||||
key: 'zh',
|
||||
key: 'zh-CN',
|
||||
},
|
||||
];
|
||||
|
||||
export function initI18N() {
|
||||
setupI18nInstance({
|
||||
supportedLngs: languages.map((l) => l.key),
|
||||
});
|
||||
}
|
@ -2,7 +2,6 @@ import express from 'express';
|
||||
import 'express-async-errors';
|
||||
import compression from 'compression';
|
||||
import swaggerUI from 'swagger-ui-express';
|
||||
import passport from 'passport';
|
||||
import morgan from 'morgan';
|
||||
import { websiteRouter } from './router/website.js';
|
||||
import { telemetryRouter } from './router/telemetry.js';
|
||||
@ -38,7 +37,6 @@ app.use(
|
||||
},
|
||||
})
|
||||
);
|
||||
app.use(passport.initialize());
|
||||
app.use(morgan('tiny'));
|
||||
app.use(cors());
|
||||
|
||||
|
@ -9,6 +9,7 @@ import { token } from '../model/notification/token/index.js';
|
||||
import pMap from 'p-map';
|
||||
import { sendFeedEventsNotify } from '../model/feed/event.js';
|
||||
import { get } from 'lodash-es';
|
||||
import { checkWorkspaceUsage } from '../model/billing/cronjob.js';
|
||||
|
||||
type WebsiteEventCountSqlReturn = {
|
||||
workspace_id: string;
|
||||
@ -29,6 +30,10 @@ export function initCronjob() {
|
||||
checkFeedEventsNotify(FeedChannelNotifyFrequency.day),
|
||||
]);
|
||||
|
||||
if (env.billing.enable) {
|
||||
await checkWorkspaceUsage();
|
||||
}
|
||||
|
||||
logger.info('Daily cronjob completed');
|
||||
} catch (err) {
|
||||
logger.error('Daily cronjob error:', err);
|
||||
@ -386,6 +391,9 @@ async function dailyHTTPCertCheckNotify() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check feed events notify
|
||||
*/
|
||||
async function checkFeedEventsNotify(
|
||||
notifyFrequency: FeedChannelNotifyFrequency
|
||||
) {
|
||||
|
@ -1,7 +1,3 @@
|
||||
import { findUser } from '../model/user.js';
|
||||
import passport from 'passport';
|
||||
import { Handler } from 'express';
|
||||
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { jwtSecret } from '../utils/common.js';
|
||||
|
||||
@ -14,38 +10,6 @@ export interface JWTPayload {
|
||||
role: string;
|
||||
}
|
||||
|
||||
passport.use(
|
||||
new JwtStrategy(
|
||||
{
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: jwtSecret,
|
||||
issuer: jwtIssuer,
|
||||
audience: jwtAudience,
|
||||
},
|
||||
function (jwt_payload, done) {
|
||||
findUser(jwt_payload.id)
|
||||
.then((user) => {
|
||||
if (user) {
|
||||
done(null, user);
|
||||
} else {
|
||||
done(null, false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
done(err);
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
passport.serializeUser(function (user: any, cb) {
|
||||
cb(null, { id: user.id, username: user.username });
|
||||
});
|
||||
|
||||
passport.deserializeUser(function (user: any, cb) {
|
||||
cb(null, user);
|
||||
});
|
||||
|
||||
export function jwtSign(payload: JWTPayload): string {
|
||||
const token = jwt.sign(
|
||||
{
|
||||
@ -72,9 +36,3 @@ export function jwtVerify(token: string): JWTPayload {
|
||||
|
||||
return payload as JWTPayload;
|
||||
}
|
||||
|
||||
export function auth(): Handler {
|
||||
return passport.authenticate('jwt', {
|
||||
session: false,
|
||||
});
|
||||
}
|
||||
|
65
src/server/model/billing/cronjob.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import pMap from 'p-map';
|
||||
import { prisma } from '../_client.js';
|
||||
import { WorkspaceSubscriptionTier } from '@prisma/client';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { getTierLimit } from './limit.js';
|
||||
import { getWorkspaceUsage, pauseWorkspace } from './workspace.js';
|
||||
import dayjs from 'dayjs';
|
||||
import { getWorkspaceServiceCount } from '../workspace.js';
|
||||
|
||||
/**
|
||||
* Check workspace usage
|
||||
* if over limit, pause workspace
|
||||
*/
|
||||
export async function checkWorkspaceUsage() {
|
||||
logger.info('[checkWorkspaceUsage] Start run checkWorkspaceUsage');
|
||||
|
||||
const workspaces = await prisma.workspace.findMany({
|
||||
where: {
|
||||
paused: false,
|
||||
},
|
||||
include: {
|
||||
subscription: true,
|
||||
},
|
||||
});
|
||||
|
||||
await pMap(
|
||||
workspaces,
|
||||
async (workspace) => {
|
||||
const tier =
|
||||
workspace.subscription?.tier ?? WorkspaceSubscriptionTier.FREE;
|
||||
|
||||
if (tier === WorkspaceSubscriptionTier.UNLIMITED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [usage, serviceCount] = await Promise.all([
|
||||
getWorkspaceUsage(
|
||||
workspace.id,
|
||||
dayjs().startOf('month').valueOf(),
|
||||
dayjs().valueOf()
|
||||
),
|
||||
getWorkspaceServiceCount(workspace.id),
|
||||
]);
|
||||
|
||||
const limit = getTierLimit(tier);
|
||||
|
||||
const overUsage =
|
||||
serviceCount.website > limit.maxWebsiteCount ||
|
||||
usage.websiteEventCount > limit.maxWebsiteEventCount ||
|
||||
usage.monitorExecutionCount > limit.maxMonitorExecutionCount ||
|
||||
usage.websiteEventCount > limit.maxWebsiteEventCount ||
|
||||
usage.surveyCount > limit.maxSurveyCount ||
|
||||
serviceCount.feed > limit.maxFeedChannelCount ||
|
||||
usage.feedEventCount > limit.maxFeedEventCount;
|
||||
|
||||
if (overUsage) {
|
||||
// pause workspace
|
||||
await pauseWorkspace(workspace.id);
|
||||
}
|
||||
},
|
||||
{
|
||||
concurrency: 5,
|
||||
}
|
||||
);
|
||||
}
|
@ -1,19 +1,22 @@
|
||||
import { TierType } from './types.js';
|
||||
import { WorkspaceSubscriptionTier } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface TierLimit {
|
||||
maxWebsiteCount: number;
|
||||
maxWebsiteEventCount: number;
|
||||
maxMonitorExecutionCount: number;
|
||||
maxSurveyCount: number;
|
||||
maxFeedChannelCount: number;
|
||||
maxFeedEventCount: number;
|
||||
}
|
||||
export const TierLimitSchema = z.object({
|
||||
maxWebsiteCount: z.number(),
|
||||
maxWebsiteEventCount: z.number(),
|
||||
maxMonitorExecutionCount: z.number(),
|
||||
maxSurveyCount: z.number(),
|
||||
maxFeedChannelCount: z.number(),
|
||||
maxFeedEventCount: z.number(),
|
||||
});
|
||||
|
||||
type TierLimit = z.infer<typeof TierLimitSchema>;
|
||||
|
||||
/**
|
||||
* Limit, Every month
|
||||
*/
|
||||
export function getTierLimit(tier: TierType): TierLimit {
|
||||
if (tier === 'free') {
|
||||
export function getTierLimit(tier: WorkspaceSubscriptionTier): TierLimit {
|
||||
if (tier === WorkspaceSubscriptionTier.FREE) {
|
||||
return {
|
||||
maxWebsiteCount: 3,
|
||||
maxWebsiteEventCount: 100_000,
|
||||
@ -24,7 +27,7 @@ export function getTierLimit(tier: TierType): TierLimit {
|
||||
};
|
||||
}
|
||||
|
||||
if (tier === 'pro') {
|
||||
if (tier === WorkspaceSubscriptionTier.PRO) {
|
||||
return {
|
||||
maxWebsiteCount: 10,
|
||||
maxWebsiteEventCount: 1_000_000,
|
||||
@ -35,7 +38,7 @@ export function getTierLimit(tier: TierType): TierLimit {
|
||||
};
|
||||
}
|
||||
|
||||
if (tier === 'team') {
|
||||
if (tier === WorkspaceSubscriptionTier.TEAM) {
|
||||
return {
|
||||
maxWebsiteCount: -1,
|
||||
maxWebsiteEventCount: 20_000_000,
|
||||
@ -46,6 +49,7 @@ export function getTierLimit(tier: TierType): TierLimit {
|
||||
};
|
||||
}
|
||||
|
||||
// Unlimited
|
||||
return {
|
||||
maxWebsiteCount: -1,
|
||||
maxWebsiteEventCount: -1,
|
||||
|
@ -1 +0,0 @@
|
||||
export type TierType = 'free' | 'pro' | 'team' | 'unlimited';
|
56
src/server/model/billing/workspace.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { WorkspaceSubscriptionTier } from '@prisma/client';
|
||||
import { prisma } from '../_client.js';
|
||||
|
||||
export async function getWorkspaceUsage(
|
||||
workspaceId: string,
|
||||
startAt: number,
|
||||
endAt: number
|
||||
) {
|
||||
const res = await prisma.workspaceDailyUsage.aggregate({
|
||||
where: {
|
||||
workspaceId,
|
||||
date: {
|
||||
gte: new Date(startAt),
|
||||
lte: new Date(endAt),
|
||||
},
|
||||
},
|
||||
_sum: {
|
||||
websiteAcceptedCount: true,
|
||||
websiteEventCount: true,
|
||||
monitorExecutionCount: true,
|
||||
surveyCount: true,
|
||||
feedEventCount: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
websiteAcceptedCount: res._sum.websiteAcceptedCount ?? 0,
|
||||
websiteEventCount: res._sum.websiteEventCount ?? 0,
|
||||
monitorExecutionCount: res._sum.monitorExecutionCount ?? 0,
|
||||
surveyCount: res._sum.surveyCount ?? 0,
|
||||
feedEventCount: res._sum.feedEventCount ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWorkspaceSubscription(
|
||||
workspaceId: string
|
||||
): Promise<WorkspaceSubscriptionTier> {
|
||||
const subscription = await prisma.workspaceSubscription.findFirst({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return subscription?.tier ?? WorkspaceSubscriptionTier.FREE;
|
||||
}
|
||||
|
||||
export async function pauseWorkspace(workspaceId: string) {
|
||||
await prisma.workspace.update({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
},
|
||||
data: {
|
||||
paused: true,
|
||||
},
|
||||
});
|
||||
}
|
@ -90,7 +90,7 @@ export async function getMonitorSummaryWithDay(
|
||||
"MonitorData"
|
||||
WHERE
|
||||
"monitorId" = ${monitorId} AND
|
||||
"createdAt" >= CURRENT_DATE - INTERVAL '${beforeDay} days'
|
||||
"createdAt" >= CURRENT_DATE - INTERVAL '1 day' * ${beforeDay}
|
||||
GROUP BY
|
||||
DATE("createdAt")
|
||||
ORDER BY
|
||||
|
@ -72,3 +72,47 @@ export async function getWorkspaceWebsiteDateRange(websiteId: string) {
|
||||
min: res._min.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWorkspaceServiceCount(workspaceId: string) {
|
||||
const [website, monitor, telemetry, page, survey, feed] = await Promise.all([
|
||||
prisma.website.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
prisma.monitor.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
prisma.telemetry.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
prisma.monitorStatusPage.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
prisma.survey.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
prisma.feedChannel.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
website,
|
||||
monitor,
|
||||
telemetry,
|
||||
page,
|
||||
survey,
|
||||
feed,
|
||||
};
|
||||
}
|
||||
|
@ -60,8 +60,7 @@
|
||||
"morgan": "^1.10.0",
|
||||
"nanoid": "^5.0.4",
|
||||
"nodemailer": "^6.9.8",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"p-map": "4.0.0",
|
||||
"ping": "^0.4.4",
|
||||
"prom-client": "^15.1.3",
|
||||
"puppeteer": "23.4.1",
|
||||
@ -92,8 +91,6 @@
|
||||
"@types/morgan": "^1.9.5",
|
||||
"@types/node": "^18.17.12",
|
||||
"@types/nodemailer": "^6.4.11",
|
||||
"@types/passport": "^1.0.12",
|
||||
"@types/passport-jwt": "^3.0.9",
|
||||
"@types/ping": "^0.4.2",
|
||||
"@types/request-ip": "^0.0.38",
|
||||
"@types/supertest": "^6.0.2",
|
||||
@ -101,7 +98,6 @@
|
||||
"@types/tcp-ping": "^0.1.5",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"execa": "^5.1.1",
|
||||
"p-map": "4.0.0",
|
||||
"prisma": "5.14.0",
|
||||
"prisma-json-types-generator": "3.0.3",
|
||||
"prisma-zod-generator": "0.8.13",
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Workspace" ADD COLUMN "paused" BOOLEAN NOT NULL DEFAULT false;
|
@ -96,6 +96,7 @@ model Workspace {
|
||||
/// [CommonPayload]
|
||||
/// @zod.custom(imports.CommonPayloadSchema)
|
||||
settings Json @default("{}")
|
||||
paused Boolean @default(false) // if workspace over billing, its will marked as pause and not receive and input.
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||
|
||||
|
@ -20,6 +20,7 @@ export const WorkspaceModelSchema = z.object({
|
||||
* [CommonPayload]
|
||||
*/
|
||||
settings: imports.CommonPayloadSchema,
|
||||
paused: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
})
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import { OpenApiMetaInfo, router, workspaceProcedure } from '../trpc.js';
|
||||
import {
|
||||
OpenApiMetaInfo,
|
||||
router,
|
||||
workspaceAdminProcedure,
|
||||
workspaceProcedure,
|
||||
} from '../trpc.js';
|
||||
import { OPENAPI_TAG } from '../../utils/const.js';
|
||||
import { WorkspaceAuditLogModelSchema } from '../../prisma/zod/index.js';
|
||||
import { prisma } from '../../model/_client.js';
|
||||
@ -46,6 +51,24 @@ export const auditLogRouter = router({
|
||||
nextCursor,
|
||||
};
|
||||
}),
|
||||
clear: workspaceAdminProcedure
|
||||
.meta(
|
||||
buildAuditLogOpenapi({
|
||||
method: 'DELETE',
|
||||
path: '/clear',
|
||||
description: 'clear all workspace audit log',
|
||||
})
|
||||
)
|
||||
.output(z.void())
|
||||
.mutation(async ({ input }) => {
|
||||
const { workspaceId } = input;
|
||||
|
||||
await prisma.workspaceAuditLog.deleteMany({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
function buildAuditLogOpenapi(meta: OpenApiMetaInfo): OpenApiMeta {
|
||||
|
@ -16,6 +16,12 @@ import {
|
||||
SubscriptionTierType,
|
||||
} from '../../model/billing/index.js';
|
||||
import { LemonSqueezySubscriptionModelSchema } from '../../prisma/zod/lemonsqueezysubscription.js';
|
||||
import {
|
||||
getWorkspaceSubscription,
|
||||
getWorkspaceUsage,
|
||||
} from '../../model/billing/workspace.js';
|
||||
import { getTierLimit, TierLimitSchema } from '../../model/billing/limit.js';
|
||||
import { WorkspaceSubscriptionTier } from '@prisma/client';
|
||||
|
||||
export const billingRouter = router({
|
||||
usage: workspaceProcedure
|
||||
@ -44,30 +50,36 @@ export const billingRouter = router({
|
||||
.query(async ({ input }) => {
|
||||
const { workspaceId, startAt, endAt } = input;
|
||||
|
||||
const res = await prisma.workspaceDailyUsage.aggregate({
|
||||
where: {
|
||||
workspaceId,
|
||||
date: {
|
||||
gte: new Date(startAt),
|
||||
lte: new Date(endAt),
|
||||
},
|
||||
},
|
||||
_sum: {
|
||||
websiteAcceptedCount: true,
|
||||
websiteEventCount: true,
|
||||
monitorExecutionCount: true,
|
||||
surveyCount: true,
|
||||
feedEventCount: true,
|
||||
},
|
||||
});
|
||||
return getWorkspaceUsage(workspaceId, startAt, endAt);
|
||||
}),
|
||||
limit: workspaceProcedure
|
||||
.meta(
|
||||
buildBillingOpenapi({
|
||||
method: 'GET',
|
||||
path: '/limit',
|
||||
description: 'get workspace subscription limit',
|
||||
})
|
||||
)
|
||||
.output(TierLimitSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { workspaceId } = input;
|
||||
const tier = await getWorkspaceSubscription(workspaceId);
|
||||
|
||||
return {
|
||||
websiteAcceptedCount: res._sum.websiteAcceptedCount ?? 0,
|
||||
websiteEventCount: res._sum.websiteEventCount ?? 0,
|
||||
monitorExecutionCount: res._sum.monitorExecutionCount ?? 0,
|
||||
surveyCount: res._sum.surveyCount ?? 0,
|
||||
feedEventCount: res._sum.feedEventCount ?? 0,
|
||||
};
|
||||
return getTierLimit(tier);
|
||||
}),
|
||||
currentTier: workspaceProcedure
|
||||
.meta(
|
||||
buildBillingOpenapi({
|
||||
method: 'GET',
|
||||
path: '/currentTier',
|
||||
description: 'get workspace current tier',
|
||||
})
|
||||
)
|
||||
.output(z.nativeEnum(WorkspaceSubscriptionTier))
|
||||
.query(({ input }) => {
|
||||
const { workspaceId } = input;
|
||||
|
||||
return getWorkspaceSubscription(workspaceId);
|
||||
}),
|
||||
currentSubscription: workspaceProcedure
|
||||
.meta(
|
||||
@ -102,7 +114,7 @@ export const billingRouter = router({
|
||||
checkout: workspaceOwnerProcedure
|
||||
.input(
|
||||
z.object({
|
||||
tier: z.string(),
|
||||
tier: z.enum(['free', 'pro', 'team']),
|
||||
redirectUrl: z.string().optional(),
|
||||
})
|
||||
)
|
||||
@ -117,7 +129,7 @@ export const billingRouter = router({
|
||||
const checkout = await createCheckoutBilling(
|
||||
workspaceId,
|
||||
userId,
|
||||
input.tier as SubscriptionTierType,
|
||||
input.tier,
|
||||
redirectUrl
|
||||
);
|
||||
|
||||
|
@ -24,9 +24,10 @@ export const globalRouter = router({
|
||||
disableAnonymousTelemetry: z.boolean(),
|
||||
customTrackerScriptName: z.string().optional(),
|
||||
authProvider: z.array(z.string()),
|
||||
enableBilling: z.boolean(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
.query(async () => {
|
||||
return {
|
||||
allowRegister: env.allowRegister,
|
||||
websiteId: env.websiteId,
|
||||
@ -36,6 +37,7 @@ export const globalRouter = router({
|
||||
disableAnonymousTelemetry: env.disableAnonymousTelemetry,
|
||||
customTrackerScriptName: env.customTrackerScriptName,
|
||||
authProvider: env.auth.provider,
|
||||
enableBilling: env.billing.enable,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
@ -28,6 +28,7 @@ import { WorkspacesOnUsersModelSchema } from '../../prisma/zod/workspacesonusers
|
||||
import { monitorManager } from '../../model/monitor/index.js';
|
||||
import { get, merge } from 'lodash-es';
|
||||
import { promWorkspaceCounter } from '../../utils/prometheus/client.js';
|
||||
import { getWorkspaceServiceCount } from '../../model/workspace.js';
|
||||
|
||||
export const workspaceRouter = router({
|
||||
create: protectProedure
|
||||
@ -382,39 +383,8 @@ export const workspaceRouter = router({
|
||||
.query(async ({ input }) => {
|
||||
const { workspaceId } = input;
|
||||
|
||||
const [website, monitor, telemetry, page, survey, feed] =
|
||||
await Promise.all([
|
||||
prisma.website.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
prisma.monitor.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
prisma.telemetry.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
prisma.monitorStatusPage.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
prisma.survey.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
prisma.feedChannel.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const { website, monitor, telemetry, page, survey, feed } =
|
||||
await getWorkspaceServiceCount(workspaceId);
|
||||
|
||||
const server = getServerCount(workspaceId);
|
||||
|
||||
|
@ -47,6 +47,7 @@ export const env = {
|
||||
},
|
||||
},
|
||||
billing: {
|
||||
enable: checkEnvTrusty(process.env.ENABLE_BILLING),
|
||||
lemonSqueezy: {
|
||||
signatureSecret: process.env.LEMON_SQUEEZY_SIGNATURE_SECRET ?? '',
|
||||
apiKey: process.env.LEMON_SQUEEZY_API_KEY ?? '',
|
||||
|
@ -205,11 +205,6 @@ const config: Config = {
|
||||
defer: true,
|
||||
'data-website-id': 'clopxgjr6050tqn5dzxo7pjac',
|
||||
},
|
||||
{
|
||||
src: 'https://plausible.io/js/script.outbound-links.js',
|
||||
defer: true,
|
||||
'data-domain': 'msgbyte.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|