Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed79ae0920 | ||
|
|
6b71daa88e | ||
|
|
880b424622 | ||
|
|
1ff2f2fe0a | ||
|
|
6e914e69ea | ||
|
|
5a35286a0f | ||
|
|
0d7e296a6c | ||
|
|
32fbe8b8c3 | ||
|
|
79f8947742 | ||
|
|
ab81c76395 | ||
|
|
71b8fd90b0 | ||
|
|
fc31c53321 | ||
|
|
241ac1f44f | ||
|
|
42cd6847a8 | ||
|
|
48328c0e94 | ||
|
|
ddfcb09a12 | ||
|
|
447e1f7fe5 | ||
|
|
5c19e5ee78 | ||
|
|
0e532f16d4 | ||
|
|
7a0da57663 | ||
|
|
1dff917027 | ||
|
|
8e3bca65ab | ||
|
|
f39b32c42d | ||
|
|
20f290e9a7 | ||
|
|
6d892b7635 | ||
|
|
5424a9bba3 | ||
|
|
5f8951b40f | ||
|
|
5705a9f1d8 | ||
|
|
2ae19d7e1b | ||
|
|
8e004c42af | ||
|
|
0c7a552cad | ||
|
|
8ab9548d39 | ||
|
|
388992bd08 | ||
|
|
508ebc36b0 | ||
|
|
70462337dc | ||
|
|
b021900fb8 | ||
|
|
581ba9b55c | ||
|
|
019f13f2cb | ||
|
|
0782966617 | ||
|
|
6a6c16dbb2 | ||
|
|
0c765d5ce5 | ||
|
|
0a3ff275ae | ||
|
|
f606526974 | ||
|
|
a9dfc64af8 | ||
|
|
98c4d2e3cf | ||
|
|
cb2964565e | ||
|
|
c6461d2635 |
No files matched your search
@@ -12,22 +12,20 @@ Checkmate is an open-source uptime and infrastructure monitoring application. It
|
||||
```bash
|
||||
cd client
|
||||
npm install
|
||||
npm run dev -- --port 10001 --strictPort # Local dev port is 10001 (5173 is used by another project on this machine)
|
||||
npm run dev # Start dev server at http://localhost:5173
|
||||
npm run build # TypeScript check + production build
|
||||
npm run lint # ESLint (strict, max-warnings 0)
|
||||
npm run format # Prettier formatting
|
||||
npm run format-check # Check formatting
|
||||
```
|
||||
|
||||
Server `.env` on this machine is configured with `CLIENT_HOST="http://localhost:10001"` to match the client dev port. If you change the client port, keep `.env` in sync.
|
||||
|
||||
### Server (Node.js/Express)
|
||||
```bash
|
||||
cd server
|
||||
npm install
|
||||
npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345
|
||||
npm run build # TypeScript compile + path alias resolution
|
||||
npm run test # Run Jest tests with coverage
|
||||
npm run test # Run Mocha tests with c8 coverage
|
||||
npm run lint # ESLint v9
|
||||
npm run lint-fix # Auto-fix lint issues
|
||||
npm run format # Prettier formatting
|
||||
@@ -159,26 +157,6 @@ When working on anything related to check scheduling, incident lifecycle, or not
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Coding conventions (mandatory)
|
||||
Read `docs/coding-conventions.md` before touching any `.tsx` or `.ts` file. The doc covers both frontend and backend rules, all enforced in code review.
|
||||
|
||||
**Universal — rule 0:** look at peer files first and follow the established pattern. Don't sneak in novel shapes.
|
||||
|
||||
**Frontend (`client/src`):**
|
||||
1. Prefer MUI native props over `sx` (e.g. `color={…}`, `bgcolor={…}`, `mt={…}` — not `sx={{ color, bgcolor, mt }}`).
|
||||
2. Use the full theme path for colors: `color={theme.palette.text.secondary}`, never `color="text.secondary"` (greppability).
|
||||
3. No hardcoded literals — use `LAYOUT.*`, `typographyLevels.*`, `theme.shape.borderRadius`, `theme.palette.*`.
|
||||
4. Use `useTheme()` inside components; don't `import { theme } from "@/Utils/Theme/Theme"`.
|
||||
5. Pair runtime tuples with derived types: `const X = [...] as const; type X = (typeof X)[number]`.
|
||||
|
||||
**Backend (`server/src`):**
|
||||
6. Layering: Controller → Service → Repository → Mongoose. No DB calls outside repositories.
|
||||
7. Provider conventions: DI for stdlib clients, `SERVICE_NAME` + `TIMEOUT_MS` constants, `timeRequest()` helper, inline `setTimeout` race for timeout, outer try/catch into `AppError`.
|
||||
8. Mongoose schema fields with closed value sets must declare `enum` reusing the same `types/*` const tuple.
|
||||
9. Provider tests at `server/test/unit/providers/network/<name>.test.ts`, `from "@jest/globals"`, `testStatusProviderContract`, inline `setup()` per test (not `beforeEach`).
|
||||
10. Centralize validation enums in `types/*` const tuples; never inline `z.enum([…])` more than once.
|
||||
11. New entity fields must land in the validator's `*ResponseSchema` so the auto-generated OpenAPI spec sees them.
|
||||
|
||||
### Internationalization
|
||||
All user-facing strings must use the translation function:
|
||||
```javascript
|
||||
@@ -196,13 +174,12 @@ t('your.key') // Never hardcode UI strings
|
||||
- Both use ESLint with strict settings
|
||||
|
||||
### Testing
|
||||
Server tests use Jest (with `--experimental-vm-modules` for ESM):
|
||||
Server tests use Mocha + Chai + Sinon:
|
||||
```bash
|
||||
npm test # Run all tests with coverage
|
||||
npm test -- -t "pattern" # Run tests matching name pattern
|
||||
npm test -- path/to/file.test.ts # Run a specific file
|
||||
npm test # Run all tests with coverage
|
||||
npm test -- --grep "pattern" # Run specific tests
|
||||
```
|
||||
Test files: `server/test/**/*.test.ts`
|
||||
Test files: `server/tests/**/*.test.js`
|
||||
|
||||
## Database Models
|
||||
|
||||
|
||||
+16
-2
@@ -255,6 +255,20 @@ docker rm uptime_database_mongo
|
||||
|
||||
Start with [good first issues](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
|
||||
|
||||
### Improve the documentation?
|
||||
|
||||
Docs live in [checkmate-documentation](https://github.com/bluewave-labs/checkmate-documentation). You can fix typos, add guides, or explain features better.
|
||||
|
||||
### Help with translations?
|
||||
|
||||
We use [PoEditor](https://poeditor.com) for translations. You can:
|
||||
|
||||
- [Sign up and join your language team](https://poeditor.com/join/project/lRUoGZFCsJ).
|
||||
- Translate UI strings.
|
||||
- Ask questions on Discord in the relevant #translations channel.
|
||||
|
||||
Make sure all new UI strings in code use `t('key')`.
|
||||
|
||||
### Submit a pull request?
|
||||
|
||||
Follow the [pull request checklist](#pull-request-checklist). Your PR should:
|
||||
@@ -269,7 +283,7 @@ Follow the [pull request checklist](#pull-request-checklist). Your PR should:
|
||||
|
||||
## Code guidelines
|
||||
|
||||
- Use ESLint and Prettier. Run `npm run lint` and `npm run format-check` in both `client` and `server`. If `format-check` reports issues, fix them with `npm run format` before committing.
|
||||
- Use ESLint and Prettier (`npm run lint`).
|
||||
- Follow naming conventions: `camelCase` for variables, `PascalCase` for components, `UPPER_CASE` for constants.
|
||||
- No hard-coded strings — use `t('your.key')` for everything visible.
|
||||
- Use the shared theme and components. No magic numbers or hardcoded styles.
|
||||
@@ -290,7 +304,7 @@ Before submitting your pull request, please confirm the following:
|
||||
- You used the shared theme for any styling — no magic numbers or inline styles.
|
||||
- The pull request addresses only one issue or topic.
|
||||
- You added screenshots or a video for any UI-related changes.
|
||||
- Your code passes `npm run lint`, `npm run format-check`, and `npm run build` in both `client` and `server` with no errors.
|
||||
- Your code passes linting and has no TypeScript errors.
|
||||
|
||||
If one or more of these are missing, we may ask you to update your pull request before we can merge it.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||

|
||||

|
||||

|
||||
[](https://www.bestpractices.dev/projects/9901)
|
||||
[](https://deepwiki.com/bluewave-labs/checkmate)
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
@@ -15,10 +16,7 @@
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<br>
|
||||
|
||||
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
|
||||
|
||||
This repository contains both the frontend and the backend of Checkmate, an open-source, self-hosted monitoring tool for tracking server hardware, uptime, response times, and incidents in real-time with beautiful visualizations. Checkmate regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
|
||||
|
||||
@@ -26,6 +24,8 @@ Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/c
|
||||
|
||||
Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks.
|
||||
|
||||
**If you would like to sponsor a feature, [see this link](https://checkmate.so/sponsored-features).**
|
||||
|
||||
## 📚 Table of contents
|
||||
|
||||
- [📦 Demo](#demo)
|
||||
@@ -38,6 +38,7 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
|
||||
- [🏗️ Tech stack](#tech-stack)
|
||||
- [🔗 A few links](#a-few-links)
|
||||
- [🤝 Contributing](#contributing)
|
||||
- [💰 Our sponsors](#our-sponsors)
|
||||
|
||||
|
||||
## Demo
|
||||
@@ -88,17 +89,30 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||
## Features
|
||||
|
||||
- Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5)
|
||||
- Several monitoring options: Uptime, Docker, Ping, SSL, Port, Game server
|
||||
- Website monitoring
|
||||
- Page speed monitoring
|
||||
- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
|
||||
- Selective disk monitoring with mountpoint selection
|
||||
- Docker monitoring
|
||||
- Ping monitoring
|
||||
- SSL monitoring
|
||||
- Port monitoring
|
||||
- Game server monitoring (3.0)
|
||||
- Incidents at a glance
|
||||
- Status pages with 4 beautiful themes
|
||||
- E-mail, Webhooks, Discord, Slack, PagerDuty, Matrix, Microsoft Teams, Telegram, Pushover, Twilio (SMS) notifications
|
||||
- Status pages
|
||||
- E-mail, Webhooks, Discord and Slack notifications
|
||||
- Scheduled maintenance
|
||||
- JSON query monitoring
|
||||
- Multi-language support for Arabic, Chinese (Simplified), Chinese (Traditional, Taiwan), Czech, English, Finnish, French, German, Japanese, Portuguese (Brazil), Russian, Spanish, Thai, Turkish, Ukrainian, and Vietnamese
|
||||
- Multi-language support for English, German, Japanese, Portuguese (Brazil), Russian, Turkish, Ukrainian, Vietnamese, Chinese (Traditional, Taiwan)
|
||||
|
||||
**Short term roadmap:**
|
||||
|
||||
- Plugins that will help Checkmate get any information from a remote service (e.g database, etc)
|
||||
- Better notifications
|
||||
- Network monitoring
|
||||
- ..and a few more features
|
||||
|
||||
If you would like to sponsor an additional feature, [see this page](https://checkmate.so/sponsored-features).
|
||||
|
||||
## Monitor Lifecycle
|
||||
|
||||
@@ -166,3 +180,8 @@ Here's how you can contribute:
|
||||
|
||||
[](https://star-history.com/#bluewave-labs/Checkmate&Date)
|
||||
|
||||
## Our sponsors
|
||||
|
||||
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
|
||||
|
||||
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
|
||||
@@ -1,104 +1,3 @@
|
||||
// MUI sx keys that have a native (system) prop equivalent.
|
||||
// Flagged when found inside an sx={{ ... }} object literal — see frontend-conventions.md rule #1.
|
||||
const SX_NATIVE_EQUIVALENT_KEYS = [
|
||||
"color",
|
||||
"bgcolor",
|
||||
"backgroundColor",
|
||||
"borderRadius",
|
||||
"border",
|
||||
"borderTop",
|
||||
"borderBottom",
|
||||
"borderLeft",
|
||||
"borderRight",
|
||||
"width",
|
||||
"height",
|
||||
"minHeight",
|
||||
"maxWidth",
|
||||
"minWidth",
|
||||
"maxHeight",
|
||||
"p",
|
||||
"pt",
|
||||
"pb",
|
||||
"px",
|
||||
"py",
|
||||
"pl",
|
||||
"pr",
|
||||
"m",
|
||||
"mt",
|
||||
"mb",
|
||||
"mx",
|
||||
"my",
|
||||
"ml",
|
||||
"mr",
|
||||
"display",
|
||||
"textAlign",
|
||||
"fontWeight",
|
||||
"fontSize",
|
||||
"lineHeight",
|
||||
"gap",
|
||||
];
|
||||
|
||||
// MUI components that accept system props as top-level attributes.
|
||||
// sx={{ color, p, mt, ... }} on these can be hoisted into native props.
|
||||
// Dialog*, Drawer*, Menu*, Tab*, Toolbar etc. only accept sx — don't flag those.
|
||||
const SX_HOISTABLE_COMPONENTS = [
|
||||
"Box",
|
||||
"Stack",
|
||||
"Typography",
|
||||
"Grid",
|
||||
"Container",
|
||||
"Paper",
|
||||
"Card",
|
||||
"CardContent",
|
||||
"CardActions",
|
||||
"CardHeader",
|
||||
"Divider",
|
||||
"Link",
|
||||
"Chip",
|
||||
];
|
||||
|
||||
const sxKeySelector = SX_HOISTABLE_COMPONENTS.flatMap((component) =>
|
||||
SX_NATIVE_EQUIVALENT_KEYS.map(
|
||||
(k) =>
|
||||
`JSXOpeningElement[name.name='${component}'] > JSXAttribute[name.name='sx'] > JSXExpressionContainer > ObjectExpression > Property[key.name='${k}']`
|
||||
)
|
||||
).join(", ");
|
||||
|
||||
const FRONTEND_CONVENTION_RULES = {
|
||||
// Rule #2 — color/bgcolor/borderColor as a string shorthand like "text.secondary".
|
||||
// Catches: <Typography color="text.secondary" /> — should be color={theme.palette.text.secondary}.
|
||||
"no-restricted-syntax": [
|
||||
"warn",
|
||||
{
|
||||
selector:
|
||||
"JSXAttribute[name.name=/^(color|bgcolor|borderColor)$/] > Literal[value=/\\./]",
|
||||
message:
|
||||
'Use the full theme path instead of a string shorthand. e.g. color={theme.palette.text.secondary} not color="text.secondary". See docs/frontend-conventions.md rule #2.',
|
||||
},
|
||||
{
|
||||
// Rule #1 — sx={{ key: ... }} where key has a native prop equivalent.
|
||||
selector: sxKeySelector,
|
||||
message:
|
||||
"Hoist this key out of `sx` onto the component as a native MUI prop (e.g. color={...}, mt={...}, width={...}). See docs/frontend-conventions.md rule #1.",
|
||||
},
|
||||
{
|
||||
// Rule #4 — importing the singleton theme from the Theme module.
|
||||
// Catches: import { theme } from "@/Utils/Theme/Theme" — should be useTheme() inside the component.
|
||||
selector:
|
||||
"ImportDeclaration[source.value='@/Utils/Theme/Theme'] > ImportSpecifier[imported.name='theme']",
|
||||
message:
|
||||
"Don't import { theme } from @/Utils/Theme/Theme — use the useTheme() hook inside the component. See docs/frontend-conventions.md rule #4.",
|
||||
},
|
||||
{
|
||||
// Rule #3 (subset) — Typography fontSize={13} is the body default; drop it.
|
||||
selector:
|
||||
"JSXOpeningElement[name.name='Typography'] JSXAttribute[name.name='fontSize'] > JSXExpressionContainer > Literal[value=13]",
|
||||
message:
|
||||
"fontSize={13} is the Typography body default — drop the prop. For other sizes use typographyLevels tokens. See docs/frontend-conventions.md rule #3.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
@@ -116,43 +15,9 @@ module.exports = {
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"react/no-unescaped-entities": "off",
|
||||
...FRONTEND_CONVENTION_RULES,
|
||||
},
|
||||
globals: {
|
||||
__APP_VERSION__: "readonly",
|
||||
process: "readonly",
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
],
|
||||
plugins: ["@typescript-eslint", "react-refresh"],
|
||||
rules: {
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"react/no-unescaped-entities": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||
],
|
||||
// TS handles these; eslint duplicates create noise on existing files.
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
...FRONTEND_CONVENTION_RULES,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
Generated
+30
-386
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "3.7.1",
|
||||
"version": "3.6.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "client",
|
||||
"version": "3.7.1",
|
||||
"version": "3.6.1",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@mui/x-date-pickers": "7.29.4",
|
||||
"@reduxjs/toolkit": "2.7.0",
|
||||
"@types/maplibre-gl": "^1.13.2",
|
||||
"axios": "^1.15.2",
|
||||
"axios": "^1.7.4",
|
||||
"dayjs": "1.11.13",
|
||||
"html2canvas": "^1.4.1",
|
||||
"human-interval": "2.0.1",
|
||||
@@ -49,8 +49,6 @@
|
||||
"@types/node": "24.5.2",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
@@ -59,7 +57,7 @@
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^6.4.2"
|
||||
"vite": "6.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -2352,238 +2350,6 @@
|
||||
"version": "0.0.6",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
|
||||
"integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
"@typescript-eslint/type-utils": "7.18.0",
|
||||
"@typescript-eslint/utils": "7.18.0",
|
||||
"@typescript-eslint/visitor-keys": "7.18.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^8.56.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
|
||||
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
"@typescript-eslint/typescript-estree": "7.18.0",
|
||||
"@typescript-eslint/visitor-keys": "7.18.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.56.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
|
||||
"integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
"@typescript-eslint/visitor-keys": "7.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
|
||||
"integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "7.18.0",
|
||||
"@typescript-eslint/utils": "7.18.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.56.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
|
||||
"integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
|
||||
"integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
"@typescript-eslint/visitor-keys": "7.18.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
|
||||
"integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
"@typescript-eslint/typescript-estree": "7.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.56.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
|
||||
"integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"dev": true,
|
||||
@@ -2791,16 +2557,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/array.prototype.findlast": {
|
||||
"version": "1.2.5",
|
||||
"dev": true,
|
||||
@@ -2925,14 +2681,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
|
||||
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros": {
|
||||
@@ -3239,9 +2995,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig/node_modules/yaml": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
|
||||
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -3516,19 +3272,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-type": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"dev": true,
|
||||
@@ -4071,36 +3814,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.2",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-glob/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
@@ -4191,16 +3904,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"version": "3.3.3",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"version": "1.15.11",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -4461,27 +4170,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/globby": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-union": "^2.1.0",
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.2.9",
|
||||
"ignore": "^5.2.0",
|
||||
"merge2": "^1.4.1",
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
@@ -5317,9 +5005,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"version": "4.17.23",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
@@ -5451,16 +5137,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -5476,9 +5152,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -5887,9 +5563,7 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"version": "4.0.3",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -5920,9 +5594,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
||||
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
||||
"version": "8.5.6",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -6014,19 +5686,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
|
||||
"integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
"version": "1.1.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
@@ -6726,16 +6393,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
|
||||
@@ -7136,19 +6793,6 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
||||
"integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"license": "0BSD"
|
||||
@@ -7380,9 +7024,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
|
||||
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -7663,9 +7307,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
|
||||
+4
-7
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "3.7.1",
|
||||
"version": "3.6.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"build-dev": "tsc -b && vite build --mode development",
|
||||
"lint": "eslint . --ext js,jsx,ts,tsx --report-unused-disable-directives",
|
||||
"lint-strict": "eslint . --ext js,jsx,ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check ."
|
||||
@@ -27,7 +26,7 @@
|
||||
"@mui/x-date-pickers": "7.29.4",
|
||||
"@reduxjs/toolkit": "2.7.0",
|
||||
"@types/maplibre-gl": "^1.13.2",
|
||||
"axios": "^1.15.2",
|
||||
"axios": "^1.7.4",
|
||||
"dayjs": "1.11.13",
|
||||
"html2canvas": "^1.4.1",
|
||||
"human-interval": "2.0.1",
|
||||
@@ -58,8 +57,6 @@
|
||||
"@types/node": "24.5.2",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
@@ -68,6 +65,6 @@
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "^6.4.2"
|
||||
"vite": "6.4.1"
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { getResponseColor } from "@/Utils/DataUtils";
|
||||
import { HeatmapResponseTimeTooltip } from "@/Components/common/charts/HeatmapResponseTimeTooltip";
|
||||
import type { SxProps } from "@mui/material/styles";
|
||||
import type { ResponsiveStyleValue } from "@mui/system";
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
|
||||
interface PlaceholderCheck {
|
||||
status: "placeholder";
|
||||
responseTime: 0;
|
||||
@@ -32,11 +32,11 @@ export const HeatmapResponseTime = ({
|
||||
|
||||
if (!checks || checks.length === 0) return null;
|
||||
|
||||
const latestChecks = checks.slice(-MAX_RECENT_CHECKS);
|
||||
const latestChecks = checks.slice(-25);
|
||||
|
||||
let data: Array<CheckSnapshot | PlaceholderCheck>;
|
||||
if (latestChecks.length !== MAX_RECENT_CHECKS) {
|
||||
const placeholders = Array(MAX_RECENT_CHECKS - latestChecks.length).fill({
|
||||
if (latestChecks.length !== 25) {
|
||||
const placeholders = Array(25 - latestChecks.length).fill({
|
||||
status: "placeholder" as const,
|
||||
});
|
||||
data = [...latestChecks, ...placeholders];
|
||||
@@ -44,13 +44,13 @@ export const HeatmapResponseTime = ({
|
||||
data = latestChecks;
|
||||
}
|
||||
return (
|
||||
<Box width={"100%"}>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Box
|
||||
width={"100%"}
|
||||
display={"grid"}
|
||||
gap={gap}
|
||||
sx={{
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
width: "100%",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(25, 1fr)",
|
||||
gap: gap,
|
||||
alignItems: "stretch",
|
||||
}}
|
||||
>
|
||||
@@ -82,50 +82,50 @@ export const HeatmapResponseTime = ({
|
||||
check={check}
|
||||
>
|
||||
<Box
|
||||
display={"grid"}
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateRows: "auto auto",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display={"flex"}
|
||||
gap={theme.spacing(2)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={"100%"}
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
aspectRatio: "10",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
bgcolor={statusBg}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
bgcolor: statusBg,
|
||||
borderRadius: theme.spacing(0.5),
|
||||
...availabilityCellSx,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
pointerEvents: "none",
|
||||
borderRadius: theme.spacing(0.5),
|
||||
boxShadow: `inset 0 0 0 2px ${statusBorder}`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
width={"100%"}
|
||||
bgcolor={respBg}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{
|
||||
width: "100%",
|
||||
aspectRatio: "1 / 1",
|
||||
bgcolor: respBg,
|
||||
borderRadius: theme.spacing(0.5),
|
||||
...responseCellSx,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { CheckSnapshot } from "@/Types/Check";
|
||||
import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
|
||||
interface HistogramResponseTimeProps {
|
||||
checks: CheckSnapshot[];
|
||||
@@ -58,11 +57,8 @@ export const HistogramResponseTime = ({
|
||||
if (!Array.isArray(checks) || checks.length === 0) return null;
|
||||
|
||||
const data =
|
||||
checks.length !== MAX_RECENT_CHECKS
|
||||
? [
|
||||
...checks,
|
||||
...Array(MAX_RECENT_CHECKS - checks.length).fill({ status: "placeholder" }),
|
||||
]
|
||||
checks.length !== 25
|
||||
? [...checks, ...Array(25 - checks.length).fill({ status: "placeholder" })]
|
||||
: checks;
|
||||
|
||||
const chartHeight = typeof height === "number" ? `${height}px` : height;
|
||||
@@ -90,17 +86,17 @@ export const HistogramResponseTime = ({
|
||||
<Stack
|
||||
direction={statsPosition === "left" ? "row-reverse" : "row"}
|
||||
alignItems="center"
|
||||
width={"100%"}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box
|
||||
width={"100%"}
|
||||
display={"grid"}
|
||||
gap={gridGap}
|
||||
height={chartHeight}
|
||||
sx={{
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
width: "100%",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(25, 1fr)",
|
||||
gap: gridGap,
|
||||
alignItems: "end",
|
||||
height: chartHeight,
|
||||
}}
|
||||
>
|
||||
{data.map((check, index) => {
|
||||
@@ -112,23 +108,23 @@ export const HistogramResponseTime = ({
|
||||
: theme.palette.error.light;
|
||||
const bar = (
|
||||
<Box
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
bgcolor={theme.palette.action.hover}
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: theme.spacing(1),
|
||||
bgcolor: theme.palette.action.hover,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={"100%"}
|
||||
height={isPlaceholder ? 0 : heightPct}
|
||||
bgcolor={barColor}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: isPlaceholder ? 0 : heightPct,
|
||||
bgcolor: barColor,
|
||||
transition: "height 500ms cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
}}
|
||||
/>
|
||||
@@ -137,8 +133,8 @@ export const HistogramResponseTime = ({
|
||||
|
||||
return isPlaceholder ? (
|
||||
<Box
|
||||
height={"100%"}
|
||||
key={index}
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
{bar}
|
||||
</Box>
|
||||
|
||||
@@ -9,14 +9,13 @@ import {
|
||||
EmptyMonitorFallback,
|
||||
} from "@/Components/design-elements/Fallback";
|
||||
import { EmptyState } from "@/Components/design-elements/EmptyState";
|
||||
import { NoticeBanner } from "@/Components/design-elements/NoticeBanner";
|
||||
import { Breadcrumb } from "@/Components/design-elements/Breadcrumb";
|
||||
import { PageHeader } from "@/Components/design-elements/PageHeader";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { LanguageSelector, SwitchTheme } from "@/Components/inputs";
|
||||
|
||||
import type { StackProps } from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTheme, alpha } from "@mui/material/styles";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Typography } from "@mui/material";
|
||||
@@ -24,30 +23,60 @@ import { Typography } from "@mui/material";
|
||||
export const PageSpeedKeyPriorityFallback = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const alertBg = alpha(theme.palette.warning.main, 0.08);
|
||||
const alertBorder = alpha(theme.palette.warning.main, 0.45);
|
||||
const alertIcon = theme.palette.warning.main;
|
||||
return (
|
||||
<EmptyState
|
||||
fullscreen
|
||||
title={t("pages.pageSpeed.fallback.title")}
|
||||
description={t("pages.pageSpeed.fallback.description")}
|
||||
alert={
|
||||
<NoticeBanner severity="warning">
|
||||
<Trans
|
||||
i18nKey="common.alerts.pageSpeedApiKey.content"
|
||||
components={{
|
||||
settingsLink: (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/settings"
|
||||
color={theme.palette.primary.main}
|
||||
fontWeight={600}
|
||||
sx={{
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: theme.spacing(LAYOUT.MD),
|
||||
borderRadius: 1,
|
||||
border: `1px solid ${alertBorder}`,
|
||||
backgroundColor: alertBg,
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
color: alertIcon,
|
||||
fontSize: 18,
|
||||
lineHeight: 1,
|
||||
mt: "2px",
|
||||
}}
|
||||
/>
|
||||
</NoticeBanner>
|
||||
>
|
||||
⚠
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{ color: theme.palette.text.primary, lineHeight: 1.55, fontSize: 13 }}
|
||||
>
|
||||
<Trans
|
||||
i18nKey="common.alerts.pageSpeedApiKey.content"
|
||||
components={{
|
||||
settingsLink: (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/settings"
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||
return (
|
||||
<Stack>
|
||||
<Typography
|
||||
fontSize={typographyLevels.xxl}
|
||||
fontSize={typographyLevels.xl}
|
||||
fontWeight={500}
|
||||
overflow={"hidden"}
|
||||
textOverflow={"ellipsis"}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme, alpha } from "@mui/material/styles";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
|
||||
export const Severities = ["info", "warning", "error"] as const;
|
||||
export type Severity = (typeof Severities)[number];
|
||||
|
||||
const SEVERITY_GLYPH: Record<Severity, string> = {
|
||||
info: "ⓘ",
|
||||
warning: "⚠",
|
||||
error: "✕",
|
||||
};
|
||||
|
||||
interface NoticeBannerProps {
|
||||
severity?: Severity;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const NoticeBanner = ({ severity = "info", children }: NoticeBannerProps) => {
|
||||
const theme = useTheme();
|
||||
const tone = theme.palette[severity].main;
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
width={"100%"}
|
||||
p={theme.spacing(LAYOUT.MD)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
border={`1px solid ${alpha(tone, 0.45)}`}
|
||||
bgcolor={alpha(tone, 0.08)}
|
||||
textAlign={"left"}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
color={tone}
|
||||
fontSize={typographyLevels.xl}
|
||||
lineHeight={1}
|
||||
mt={LAYOUT.XXS}
|
||||
aria-hidden
|
||||
>
|
||||
{SEVERITY_GLYPH[severity]}
|
||||
</Box>
|
||||
<Typography
|
||||
color={theme.palette.text.primary}
|
||||
lineHeight={1.55}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -53,7 +53,7 @@ export const ConfigBox = ({
|
||||
rightContent,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: React.ReactNode;
|
||||
subtitle: string;
|
||||
leftContent?: React.ReactNode;
|
||||
rightContent: React.ReactNode;
|
||||
}) => {
|
||||
|
||||
@@ -35,9 +35,6 @@ import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
export type Header<T> = {
|
||||
id: number | string;
|
||||
content: React.ReactNode;
|
||||
mobileLabel?: React.ReactNode;
|
||||
hideMobileLabel?: boolean;
|
||||
align?: "left" | "center" | "right" | "justify" | "inherit";
|
||||
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
|
||||
render: (row: T) => React.ReactNode;
|
||||
};
|
||||
@@ -112,24 +109,6 @@ export function DataTable<
|
||||
key={key}
|
||||
>
|
||||
{headers.map((header) => {
|
||||
if (header.hideMobileLabel) {
|
||||
return (
|
||||
<Grid2
|
||||
container
|
||||
key={header.id}
|
||||
>
|
||||
<Grid2
|
||||
size={12}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
{header.render(row)}
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid2
|
||||
container
|
||||
@@ -144,7 +123,7 @@ export function DataTable<
|
||||
component="div"
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{header.mobileLabel ?? header.content}
|
||||
{header.content}
|
||||
</Typography>
|
||||
</Grid2>
|
||||
<Grid2
|
||||
@@ -214,7 +193,7 @@ export function DataTable<
|
||||
{headers.map((header, idx) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={header.align ?? (idx === 0 ? "left" : "center")}
|
||||
align={idx === 0 ? "left" : "center"}
|
||||
key={header.id}
|
||||
>
|
||||
{header.content}
|
||||
@@ -243,7 +222,7 @@ export function DataTable<
|
||||
{headers.map((header, index) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={header.align ?? (index === 0 ? "left" : "center")}
|
||||
align={index === 0 ? "left" : "center"}
|
||||
key={header.id}
|
||||
onClick={
|
||||
header.onClick ? (e) => header.onClick!(e, row) : undefined
|
||||
|
||||
@@ -22,6 +22,5 @@ export * from "./Gauge";
|
||||
export * from "./Tabs";
|
||||
export * from "./SplitBox";
|
||||
export * from "./TextLink";
|
||||
export * from "./NoticeBanner";
|
||||
export * from "./OfflineBanner";
|
||||
export * from "./Avatar";
|
||||
@@ -1,43 +1,19 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import type { ButtonProps } from "@mui/material/Button";
|
||||
import { useTheme, darken } from "@mui/material/styles";
|
||||
import { HOVER } from "@/Utils/Theme/constants";
|
||||
|
||||
const PALETTE_COLORS = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"error",
|
||||
"warning",
|
||||
"info",
|
||||
"success",
|
||||
] as const;
|
||||
type PaletteColor = (typeof PALETTE_COLORS)[number];
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const hoverOverrides: Record<string, unknown> = { boxShadow: "none" };
|
||||
|
||||
if (props.variant === "outlined") {
|
||||
hoverOverrides.borderColor = theme.palette.text.secondary;
|
||||
}
|
||||
|
||||
if (props.variant === "contained") {
|
||||
const colorKey = (props.color ?? "primary") as PaletteColor;
|
||||
const palette = theme.palette[colorKey];
|
||||
if (palette && typeof palette === "object" && "main" in palette) {
|
||||
hoverOverrides.backgroundColor = darken(palette.main, HOVER.DARKEN);
|
||||
}
|
||||
}
|
||||
|
||||
const variantSx =
|
||||
const outlinedSx =
|
||||
props.variant === "outlined"
|
||||
? {
|
||||
color: theme.palette.text.primary,
|
||||
borderColor: theme.palette.divider,
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.text.secondary,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
@@ -50,8 +26,10 @@ export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
boxShadow: "none",
|
||||
"&:hover": hoverOverrides,
|
||||
...variantSx,
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
...outlinedSx,
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -2,12 +2,10 @@ import Dialog from "@mui/material/Dialog";
|
||||
import type { DialogProps } from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import { Button } from "@/Components/inputs";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
@@ -18,6 +16,7 @@ export const DialogInput = ({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
confirmColor = "primary",
|
||||
cancelColor = "error",
|
||||
loading = false,
|
||||
cancelText,
|
||||
confirmText,
|
||||
@@ -32,6 +31,7 @@ export const DialogInput = ({
|
||||
onConfirm?(item: any): any;
|
||||
onCancel?(item: any): any;
|
||||
confirmColor?: "error" | "primary";
|
||||
cancelColor?: "error" | "primary";
|
||||
loading?: boolean;
|
||||
cancelText?: string;
|
||||
confirmText?: string;
|
||||
@@ -41,7 +41,6 @@ export const DialogInput = ({
|
||||
additionalButtons?: ReactNode;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -51,48 +50,16 @@ export const DialogInput = ({
|
||||
if (reason !== "backdropClick" && onCancel) onCancel(undefined);
|
||||
}}
|
||||
>
|
||||
{title && (
|
||||
<DialogTitle
|
||||
pb={theme.spacing(LAYOUT.XS)}
|
||||
bgcolor={theme.palette.action.hover}
|
||||
borderBottom={`1px solid ${theme.palette.divider}`}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="h2"
|
||||
fontWeight={600}
|
||||
display={"block"}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{content && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={theme.palette.text.secondary}
|
||||
mt={theme.spacing(LAYOUT.XS)}
|
||||
display={"block"}
|
||||
>
|
||||
{content}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogTitle>
|
||||
)}
|
||||
{children && (
|
||||
<DialogContent sx={{ pt: theme.spacing(LAYOUT.MD) }}>
|
||||
<Box pt={theme.spacing(LAYOUT.XS)}>{children}</Box>
|
||||
</DialogContent>
|
||||
)}
|
||||
<DialogActions
|
||||
sx={{
|
||||
p: theme.spacing(LAYOUT.MD),
|
||||
pt: theme.spacing(LAYOUT.SM),
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
{title && <DialogTitle sx={{ fontSize: typographyLevels.l }}>{title}</DialogTitle>}
|
||||
<DialogContent>
|
||||
{content && <DialogContentText>{content}</DialogContentText>}
|
||||
{children}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
color={cancelColor}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{cancelText ?? t("common.buttons.cancel")}
|
||||
|
||||
@@ -15,9 +15,9 @@ export const FieldLabel = ({ children, required = false, htmlFor }: FieldLabelPr
|
||||
return (
|
||||
<Typography
|
||||
component="label"
|
||||
variant="body1"
|
||||
htmlFor={htmlFor}
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
marginBottom: theme.spacing(2),
|
||||
|
||||
@@ -5,7 +5,6 @@ import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { FieldLabel } from "./FieldLabel";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface TextInputProps extends Omit<TextFieldProps, "label"> {
|
||||
fieldLabel?: string;
|
||||
@@ -13,53 +12,47 @@ interface TextInputProps extends Omit<TextFieldProps, "label"> {
|
||||
}
|
||||
|
||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(
|
||||
{ fieldLabel, required, sx, ...props },
|
||||
{ fieldLabel, required, ...props },
|
||||
ref
|
||||
) {
|
||||
const theme = useTheme();
|
||||
|
||||
const innerSx = {
|
||||
width: "100%",
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: 34,
|
||||
fontSize: typographyLevels.base,
|
||||
overflow: "hidden",
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"& .MuiFormHelperText-root": {
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
"& input:-webkit-autofill, & input:-webkit-autofill:hover, & input:-webkit-autofill:focus, & input:-webkit-autofill:active":
|
||||
{
|
||||
WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.default} inset !important`,
|
||||
WebkitTextFillColor: `${theme.palette.text.primary} !important`,
|
||||
caretColor: theme.palette.text.primary,
|
||||
transition: "background-color 5000s ease-in-out 0s",
|
||||
},
|
||||
};
|
||||
|
||||
const input = (
|
||||
<TextField
|
||||
{...props}
|
||||
inputRef={ref}
|
||||
sx={fieldLabel ? innerSx : { ...innerSx, ...sx }}
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: 34,
|
||||
fontSize: typographyLevels.base,
|
||||
overflow: "hidden",
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"& .MuiFormHelperText-root": {
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
"& input:-webkit-autofill, & input:-webkit-autofill:hover, & input:-webkit-autofill:focus, & input:-webkit-autofill:active":
|
||||
{
|
||||
WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.default} inset !important`,
|
||||
WebkitTextFillColor: `${theme.palette.text.primary} !important`,
|
||||
caretColor: theme.palette.text.primary,
|
||||
transition: "background-color 5000s ease-in-out 0s",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (fieldLabel) {
|
||||
return (
|
||||
<Stack
|
||||
spacing={theme.spacing(LAYOUT.XXS)}
|
||||
sx={sx}
|
||||
>
|
||||
<Stack spacing={theme.spacing(2)}>
|
||||
<FieldLabel required={required}>{fieldLabel}</FieldLabel>
|
||||
{input}
|
||||
</Stack>
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Slide from "@mui/material/Slide";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { useSidebar } from "@/Hooks/useSidebar";
|
||||
|
||||
interface BulkActionsBarProps {
|
||||
selectedCount: number;
|
||||
onCancel: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BulkActionsBar = ({
|
||||
selectedCount,
|
||||
onCancel,
|
||||
children,
|
||||
}: BulkActionsBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isOpen = selectedCount > 0;
|
||||
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const { width, collapsedWidth, transition } = useSidebar();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
bottom: theme.spacing(LAYOUT.XS),
|
||||
left: isSmall ? collapsedWidth : width,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
pointerEvents: "none",
|
||||
zIndex: theme.zIndex.snackbar,
|
||||
transition: transition.replace("width", "left"),
|
||||
}}
|
||||
>
|
||||
<Slide
|
||||
direction="up"
|
||||
in={isOpen}
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
>
|
||||
<Paper
|
||||
elevation={6}
|
||||
sx={{
|
||||
pointerEvents: "auto",
|
||||
px: LAYOUT.MD,
|
||||
py: LAYOUT.XS,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: LAYOUT.XS,
|
||||
boxShadow: theme.shadows[8],
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={LAYOUT.XXS}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={600}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{t("pages.common.monitors.actions.bulkSelected", {
|
||||
count: selectedCount,
|
||||
})}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onCancel}
|
||||
aria-label={t("common.buttons.cancel")}
|
||||
>
|
||||
<X size={18} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={LAYOUT.XXS}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Slide>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -14,4 +14,3 @@ export * from "./charts/HistogramPageSpeedDetails";
|
||||
export * from "./charts/HistogramPageSpeedDetailsTooltip";
|
||||
export * from "./charts/HistogramInfrastructure";
|
||||
export * from "./HeaderMonitorsSummary";
|
||||
export * from "./BulkActionsBar";
|
||||
@@ -89,9 +89,7 @@ const uiSlice = createSlice({
|
||||
},
|
||||
|
||||
setTimezone: (state, action: PayloadAction<{ timezone: string }>) => {
|
||||
if (action.payload.timezone) {
|
||||
state.timezone = action.payload.timezone;
|
||||
}
|
||||
state.timezone = action.payload.timezone;
|
||||
},
|
||||
setLanguage: (state, action: PayloadAction<string>) => {
|
||||
state.language = action.payload;
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { post } from "@/Utils/ApiClient";
|
||||
import { useToast } from "@/Hooks/UseToast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logger } from "@/Utils/logger";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
interface ApiResponse {
|
||||
success: boolean;
|
||||
msg: string;
|
||||
data: Monitor[];
|
||||
}
|
||||
|
||||
interface UseBulkMonitorActionsReturn {
|
||||
selectedRows: string[];
|
||||
setSelectedRows: (rows: string[]) => void;
|
||||
handleBulkPause: () => Promise<void>;
|
||||
handleBulkResume: () => Promise<void>;
|
||||
handleCancelSelection: () => void;
|
||||
}
|
||||
|
||||
export const useBulkMonitorActions = (
|
||||
refetch: () => void,
|
||||
page?: number
|
||||
): UseBulkMonitorActionsReturn => {
|
||||
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
||||
const { toastSuccess, toastError, toastInfo } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Clear selection when page changes
|
||||
useEffect(() => {
|
||||
setSelectedRows([]);
|
||||
}, [page]);
|
||||
|
||||
const executeBulkAction = async (pause: boolean) => {
|
||||
try {
|
||||
const res = await post<ApiResponse>("/monitors/bulk/pause", {
|
||||
monitorIds: selectedRows,
|
||||
pause,
|
||||
});
|
||||
|
||||
const affectedCount = res.data?.data?.length ?? 0;
|
||||
|
||||
if (affectedCount === 0) {
|
||||
const key = pause
|
||||
? "pages.common.monitors.bulkPause.alreadyPaused"
|
||||
: "pages.common.monitors.bulkPause.alreadyRunning";
|
||||
toastInfo(t(key, { count: selectedRows.length }));
|
||||
} else {
|
||||
const key = pause
|
||||
? "pages.common.monitors.bulkPause.paused"
|
||||
: "pages.common.monitors.bulkPause.resumed";
|
||||
toastSuccess(t(key, { count: affectedCount }));
|
||||
}
|
||||
|
||||
setSelectedRows([]);
|
||||
refetch();
|
||||
} catch (err: unknown) {
|
||||
let errMsg = "An error occurred";
|
||||
|
||||
if (axios.isAxiosError(err)) {
|
||||
errMsg = err.response?.data?.msg || err.message || errMsg;
|
||||
} else if (err instanceof Error) {
|
||||
errMsg = err.message;
|
||||
}
|
||||
|
||||
logger.error("Bulk pause/resume failed", err instanceof Error ? err : undefined, {
|
||||
pause,
|
||||
});
|
||||
toastError(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkPause = async () => {
|
||||
await executeBulkAction(true);
|
||||
};
|
||||
|
||||
const handleBulkResume = async () => {
|
||||
await executeBulkAction(false);
|
||||
};
|
||||
|
||||
const handleCancelSelection = () => {
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
return {
|
||||
selectedRows,
|
||||
setSelectedRows,
|
||||
handleBulkPause,
|
||||
handleBulkResume,
|
||||
handleCancelSelection,
|
||||
};
|
||||
};
|
||||
@@ -18,7 +18,6 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
|
||||
systemEmailHost: data?.systemEmailHost || "",
|
||||
systemEmailUser: data?.systemEmailUser || "",
|
||||
systemEmailAddress: data?.systemEmailAddress || "",
|
||||
systemEmailDisplayName: data?.systemEmailDisplayName || "",
|
||||
systemEmailConnectionHost: data?.systemEmailConnectionHost || "localhost",
|
||||
systemEmailTLSServername: data?.systemEmailTLSServername || "",
|
||||
systemEmailPort: data?.systemEmailPort,
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { statusPageSchema, type StatusPageFormData } from "@/Validation/statusPage";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
import {
|
||||
DEFAULT_STATUS_PAGE_THEME,
|
||||
DEFAULT_STATUS_PAGE_THEME_MODE,
|
||||
} from "@/Types/StatusPage";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import { resolveTimezone } from "@/Utils/TimeUtils";
|
||||
|
||||
interface UseStatusPageFormOptions {
|
||||
data?: StatusPage | null;
|
||||
@@ -31,7 +26,7 @@ export const useStatusPageForm = ({
|
||||
const defaults: StatusPageFormData = {
|
||||
companyName: data?.companyName || "",
|
||||
url: data?.url || generateDefaultUrl(),
|
||||
timezone: resolveTimezone(data?.timezone),
|
||||
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
type: data?.type || ["uptime"],
|
||||
color: data?.color || "#13715B",
|
||||
monitors: data?.monitors || [],
|
||||
@@ -41,8 +36,6 @@ export const useStatusPageForm = ({
|
||||
showAdminLoginLink: data?.showAdminLoginLink ?? false,
|
||||
showInfrastructure: data?.showInfrastructure ?? false,
|
||||
customCSS: data?.customCSS || "",
|
||||
theme: data?.theme ?? DEFAULT_STATUS_PAGE_THEME,
|
||||
themeMode: data?.themeMode ?? DEFAULT_STATUS_PAGE_THEME_MODE,
|
||||
logo: transformLogo(data?.logo),
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useAddTeamMemberForm } from "@/Hooks/useAddTeamMemberForm";
|
||||
import type { AddTeamMemberFormData } from "@/Validation/addTeamMember";
|
||||
import type { UserRole, User } from "@/Types/User";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface AddTeamMemberDialogProps {
|
||||
open: boolean;
|
||||
@@ -83,40 +82,38 @@ export const AddTeamMemberDialog = ({
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<Controller
|
||||
name="firstName"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("common.form.name.option.firstName.label")}
|
||||
placeholder={t("common.form.name.option.firstName.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="lastName"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("common.form.name.option.lastName.label")}
|
||||
placeholder={t("common.form.name.option.lastName.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(4)}
|
||||
>
|
||||
<Controller
|
||||
name="firstName"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("common.form.name.option.firstName.label")}
|
||||
placeholder={t("common.form.name.option.firstName.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="lastName"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("common.form.name.option.lastName.label")}
|
||||
placeholder={t("common.form.name.option.lastName.placeholder")}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
@@ -154,41 +151,36 @@ export const AddTeamMemberDialog = ({
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.auth.common.form.option.password.label")}
|
||||
placeholder={t("pages.auth.common.form.option.password.placeholder")}
|
||||
type="password"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="confirm"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.auth.common.form.option.confirmPassword.label")}
|
||||
placeholder={t("pages.auth.common.form.option.password.placeholder")}
|
||||
type="password"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.auth.common.form.option.password.label")}
|
||||
placeholder={t("pages.auth.common.form.option.password.placeholder")}
|
||||
type="password"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="confirm"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fieldLabel={t("pages.auth.common.form.option.confirmPassword.label")}
|
||||
placeholder={t("pages.auth.common.form.option.password.placeholder")}
|
||||
type="password"
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { UserRole } from "@/Types/User";
|
||||
import { useInviteForm } from "@/Hooks/useInviteForm";
|
||||
import type { InviteFormData } from "@/Validation/invite";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
|
||||
|
||||
@@ -96,7 +95,10 @@ export const InviteTeamMemberDialog = ({
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(4)}
|
||||
>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
|
||||
@@ -136,8 +136,8 @@ const getGeneralSettingsConfig = (
|
||||
showIgnoreTls: false,
|
||||
},
|
||||
hardware: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.captureUrl.label"),
|
||||
urlPlaceholder: t("pages.createMonitor.form.general.option.captureUrl.placeholder"),
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
urlPlaceholder: t("pages.createMonitor.form.general.option.url.placeholder"),
|
||||
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
|
||||
showUrl: true,
|
||||
showPort: false,
|
||||
@@ -359,20 +359,7 @@ const CreateMonitorPage = () => {
|
||||
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.general.title")}
|
||||
subtitle={
|
||||
<Trans
|
||||
i18nKey={`pages.createMonitor.form.general.description.${watchedType}`}
|
||||
components={{
|
||||
gamedigLink: (
|
||||
<Link
|
||||
href="https://github.com/gamedig/node-gamedig/blob/master/GAMES_LIST.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
subtitle={t(`pages.createMonitor.form.general.description.${watchedType}`)}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||
{/* URL/Host/Container field - not shown for hardware */}
|
||||
|
||||
@@ -20,24 +20,6 @@ interface CardDetailsProps {
|
||||
sx?: object;
|
||||
}
|
||||
|
||||
const SectionHeading = ({ children }: { children: React.ReactNode }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const Cell = ({ children }: { children: React.ReactNode }) => (
|
||||
<Typography variant="body1">{children}</Typography>
|
||||
);
|
||||
|
||||
export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@@ -51,18 +33,22 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
sx={sx}
|
||||
>
|
||||
<SectionHeading>{t("pages.incidents.dialog.details.title")}</SectionHeading>
|
||||
<Typography textTransform={"uppercase"}>
|
||||
{t("pages.incidents.dialog.details.title")}
|
||||
</Typography>
|
||||
<BaseBox padding={LAYOUT.MD}>
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
<SectionHeading>{t("pages.incidents.dialog.details.overview")}</SectionHeading>
|
||||
<Typography textTransform={"uppercase"}>
|
||||
{t("pages.incidents.dialog.details.overview")}
|
||||
</Typography>
|
||||
<Divider />
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(LAYOUT.MD)}
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.status")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={2}>{t("pages.incidents.dialog.details.status")}</Grid>
|
||||
<Grid size={10}>
|
||||
<ValueLabel
|
||||
value={incident.status ? "negative" : "positive"}
|
||||
@@ -75,17 +61,15 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
</Grid>
|
||||
{monitor && (
|
||||
<>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.monitor")}</Cell>
|
||||
</Grid>
|
||||
<Grid size={2}>{t("pages.incidents.dialog.details.monitor")}</Grid>
|
||||
<Grid size={10}>
|
||||
<Cell>{monitor.name ?? "N/A"}</Cell>
|
||||
<Typography>{monitor.name ?? "N/A"}</Typography>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.url")}</Cell>
|
||||
<Typography>{t("pages.incidents.dialog.details.url")}</Typography>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Cell>{monitor.url ?? "N/A"}</Cell>
|
||||
<Typography>{monitor.url ?? "N/A"}</Typography>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
@@ -94,48 +78,51 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
</BaseBox>
|
||||
<BaseBox padding={LAYOUT.MD}>
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
<SectionHeading>{t("pages.incidents.dialog.details.analysis")}</SectionHeading>
|
||||
<Typography textTransform={"uppercase"}>
|
||||
{t("pages.incidents.dialog.details.analysis")}
|
||||
</Typography>
|
||||
<Divider />
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(LAYOUT.MD)}
|
||||
>
|
||||
<Grid size={6}>
|
||||
<Cell>{t("pages.incidents.dialog.details.timeline")}</Cell>
|
||||
<Typography>{t("pages.incidents.dialog.details.timeline")}</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Cell>{t("pages.incidents.dialog.details.detailsLabel")}</Cell>
|
||||
<Typography>{t("pages.incidents.dialog.details.detailsLabel")}</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Divider />
|
||||
<Divider></Divider>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Divider />
|
||||
<Divider></Divider>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.startedAt")}</Cell>
|
||||
<Typography>{t("pages.incidents.dialog.details.startedAt")}</Typography>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Cell>
|
||||
<Typography>
|
||||
{formatDateWithTz(incident.startTime, "D MMM YYYY, h:mm A", uiTimezone)}
|
||||
</Cell>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.statusCode")}</Cell>
|
||||
<Typography>{t("pages.incidents.dialog.details.statusCode")}</Typography>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Cell>{incident.statusCode ?? "N/A"}</Cell>
|
||||
<Typography>{incident.statusCode ?? "N/A"}</Typography>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.downtime")}</Cell>
|
||||
<Typography>{t("pages.incidents.dialog.details.downtime")}</Typography>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Cell>{getIncidentsDuration(incident)}</Cell>
|
||||
<Typography>{getIncidentsDuration(incident)}</Typography>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.message")}</Cell>
|
||||
<Typography>{t("pages.incidents.dialog.details.message")}</Typography>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<Cell>{incident.message ?? "N/A"}</Cell>
|
||||
<Typography>{incident.message ?? "N/A"}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
@@ -143,53 +130,60 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
||||
{!incident.status && (
|
||||
<BaseBox padding={LAYOUT.MD}>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<SectionHeading>
|
||||
<Typography textTransform={"uppercase"}>
|
||||
{t("pages.incidents.dialog.details.resolutionDetails")}
|
||||
</SectionHeading>
|
||||
</Typography>
|
||||
<Divider />
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(LAYOUT.MD)}
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.resolvedAt")}</Cell>
|
||||
<Typography>{t("pages.incidents.dialog.details.resolvedAt")}</Typography>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Cell>
|
||||
<Typography>
|
||||
{incident.endTime
|
||||
? formatDateWithTz(incident.endTime, "D MMM YYYY, h:mm A", uiTimezone)
|
||||
: "N/A"}
|
||||
</Cell>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.resolutionType")}</Cell>
|
||||
<Typography>
|
||||
{t("pages.incidents.dialog.details.resolutionType")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Cell>
|
||||
<Typography>
|
||||
{incident.resolutionType
|
||||
? t(
|
||||
`pages.incidents.dialog.details.resolutionTypes.${incident.resolutionType}`
|
||||
)
|
||||
: "N/A"}
|
||||
</Cell>
|
||||
</Typography>
|
||||
</Grid>
|
||||
{incident.resolvedBy && (
|
||||
<>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.resolvedBy")}</Cell>
|
||||
<Typography>
|
||||
{t("pages.incidents.dialog.details.resolvedBy")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Cell>{incident.resolvedByEmail ?? incident.resolvedBy}</Cell>
|
||||
<Typography>
|
||||
{incident.resolvedByEmail ?? incident.resolvedBy}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{incident.comment && (
|
||||
<>
|
||||
<Grid size={2}>
|
||||
<Cell>{t("pages.incidents.dialog.details.comment")}</Cell>
|
||||
<Typography>{t("pages.incidents.dialog.details.comment")}</Typography>
|
||||
</Grid>
|
||||
<Grid size={10}>
|
||||
<Cell>{incident.comment}</Cell>
|
||||
<Typography>{incident.comment}</Typography>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { BaseBox, Icon, ValueLabel } from "@/Components/design-elements";
|
||||
import { BaseBox, ValueLabel } from "@/Components/design-elements";
|
||||
import { CircleCheck, TriangleAlert, Bell, Wrench, Globe } from "lucide-react";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
@@ -24,7 +25,6 @@ const SummaryItem = ({ icon, label, value }: SummaryItemProps) => {
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(2)}
|
||||
minHeight={32}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -32,10 +32,10 @@ const SummaryItem = ({ icon, label, value }: SummaryItemProps) => {
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
{icon}
|
||||
<Typography variant="body1">{label}</Typography>
|
||||
<Typography variant="body2">{label}</Typography>
|
||||
</Stack>
|
||||
<Typography
|
||||
variant="body1"
|
||||
variant="body2"
|
||||
fontWeight={600}
|
||||
>
|
||||
{value}
|
||||
@@ -67,11 +67,15 @@ export const SummaryCard = ({
|
||||
>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.secondary}
|
||||
sx={{
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Divider />
|
||||
{children}
|
||||
</Stack>
|
||||
</BaseBox>
|
||||
@@ -93,13 +97,10 @@ export const SummaryCardActiveIncidents = ({
|
||||
const activeCount = summary.totalActive;
|
||||
const hasActive = activeCount > 0;
|
||||
const color = hasActive ? theme.palette.error.main : theme.palette.success.main;
|
||||
const icon = (
|
||||
<Box sx={{ color, display: "inline-flex" }}>
|
||||
<Icon
|
||||
icon={hasActive ? TriangleAlert : CircleCheck}
|
||||
size={32}
|
||||
/>
|
||||
</Box>
|
||||
const icon = hasActive ? (
|
||||
<TriangleAlert color={color} />
|
||||
) : (
|
||||
<CircleCheck color={color} />
|
||||
);
|
||||
const msg = t("pages.incidents.summaryCard.activeIncidents.active", {
|
||||
count: activeCount,
|
||||
@@ -132,7 +133,10 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
|
||||
container
|
||||
alignItems="center"
|
||||
spacing={2}
|
||||
sx={{ width: "100%", minHeight: 32 }}
|
||||
sx={{
|
||||
width: "100%",
|
||||
py: theme.spacing(0.5),
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
size={{ xs: 12, lg: 5 }}
|
||||
@@ -143,7 +147,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
|
||||
gap: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Icon icon={Globe} />
|
||||
<Globe />
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={500}
|
||||
@@ -155,7 +159,9 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
|
||||
|
||||
<Grid
|
||||
size={{ xs: 12, md: 6, lg: 3 }}
|
||||
sx={{ display: "flex" }}
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<ValueLabel
|
||||
value={incident.status ? "negative" : "positive"}
|
||||
@@ -167,7 +173,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
|
||||
size={{ xs: 12, md: 6, lg: 4 }}
|
||||
sx={{
|
||||
textAlign: { xs: "left", md: "right" },
|
||||
fontWeight: 600,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{duration}</Typography>
|
||||
@@ -184,17 +190,22 @@ export const SummaryCardLatestIncidents = ({
|
||||
summary,
|
||||
}: SummaryCardLatestIncidentsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const latestIncidents = summary?.latestIncidents ?? [];
|
||||
|
||||
return (
|
||||
<SummaryCard title={t("pages.incidents.summaryCard.latestIncidents.title")}>
|
||||
{latestIncidents.slice(0, 3).map((incident) => (
|
||||
<SummaryIncidentItem
|
||||
key={incident.id}
|
||||
incident={incident}
|
||||
/>
|
||||
))}
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
{latestIncidents.slice(0, 3).map((incident, index) => (
|
||||
<Box key={incident.id}>
|
||||
<SummaryIncidentItem incident={incident} />
|
||||
{index < latestIncidents.length - 1 && (
|
||||
<Divider sx={{ mt: theme.spacing(2) }} />
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</SummaryCard>
|
||||
);
|
||||
};
|
||||
@@ -213,17 +224,17 @@ export const SummaryCardStats = ({ summary }: SummaryCardStatsProps) => {
|
||||
return (
|
||||
<SummaryCard title={t("pages.incidents.summaryCard.incidentStats.title")}>
|
||||
<SummaryItem
|
||||
icon={<Icon icon={Bell} />}
|
||||
icon={<Bell size={18} />}
|
||||
label={t("pages.incidents.summaryCard.incidentStats.totalIncidents")}
|
||||
value={summary?.total || 0}
|
||||
/>
|
||||
<SummaryItem
|
||||
icon={<Icon icon={TriangleAlert} />}
|
||||
icon={<TriangleAlert size={18} />}
|
||||
label={t("pages.incidents.summaryCard.incidentStats.mostAffectedMonitor")}
|
||||
value={mostAffected}
|
||||
/>
|
||||
<SummaryItem
|
||||
icon={<Icon icon={Wrench} />}
|
||||
icon={<Wrench size={18} />}
|
||||
label={t("pages.incidents.summaryCard.incidentStats.avgResolutionTime")}
|
||||
value={
|
||||
summary.total > 0
|
||||
|
||||
@@ -75,7 +75,7 @@ export const ControlsIncidentFilter = ({
|
||||
variant="contained"
|
||||
onClick={onClearFilters}
|
||||
>
|
||||
{t("common.buttons.clearFilters")}
|
||||
{t("pages.incidents.filters.clearFilters")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import { Dialog, TextField } from "@/Components/inputs";
|
||||
import { usePut } from "@/Hooks/UseApi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
interface DialogResolutionProps {
|
||||
open: boolean;
|
||||
@@ -17,6 +19,7 @@ export const DialogResolution = ({
|
||||
onResolved,
|
||||
}: DialogResolutionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [comment, setComment] = useState("");
|
||||
const { put: resolveIncident, loading: isResolving } = usePut();
|
||||
|
||||
@@ -47,19 +50,22 @@ export const DialogResolution = ({
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
confirmColor="error"
|
||||
cancelColor="primary"
|
||||
loading={isResolving}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<TextField
|
||||
fieldLabel={t("pages.incidents.dialog.resolveIncident.option.comment.label")}
|
||||
placeholder={t(
|
||||
"pages.incidents.dialog.resolveIncident.option.comment.placeholder"
|
||||
)}
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
<Box sx={{ mt: theme.spacing(4) }}>
|
||||
<TextField
|
||||
fieldLabel={t("pages.incidents.dialog.resolveIncident.option.comment.label")}
|
||||
placeholder={t(
|
||||
"pages.incidents.dialog.resolveIncident.option.comment.placeholder"
|
||||
)}
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -15,7 +15,6 @@ import { HeaderTimeRange } from "@/Components/common";
|
||||
import { ControlsIncidentFilter } from "@/Pages/Incidents/Components/ControlsIncidentFilter";
|
||||
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import type { Incident, IncidentsResponse, IncidentSummary } from "@/Types/Incident";
|
||||
@@ -187,9 +186,8 @@ const IncidentsPage = () => {
|
||||
{activeIncidentsCount > 0 && (
|
||||
<>
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.secondary}
|
||||
mb={theme.spacing(LAYOUT.XS)}
|
||||
variant="h6"
|
||||
sx={{ mb: theme.spacing(4), textTransform: "uppercase" }}
|
||||
>
|
||||
{t("pages.incidents.table.activeIncidents")}
|
||||
</Typography>
|
||||
@@ -210,9 +208,8 @@ const IncidentsPage = () => {
|
||||
)}
|
||||
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.secondary}
|
||||
mb={theme.spacing(LAYOUT.XS)}
|
||||
variant="h6"
|
||||
sx={{ mb: theme.spacing(4), textTransform: "uppercase" }}
|
||||
>
|
||||
{t("pages.incidents.table.resolvedIncidents")}
|
||||
</Typography>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { NoticeBanner } from "@/Components/design-elements";
|
||||
import { HistogramInfrastructure } from "@/Components/monitors";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -8,7 +6,6 @@ import type { HardwareCheckStats } from "@/Types/Monitor";
|
||||
import { useMemo } from "react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
const formatBytesToMB = (value: number) => `${(value / (1024 * 1024)).toFixed(2)} MB`;
|
||||
|
||||
@@ -63,11 +60,6 @@ const getChartConfigs = (
|
||||
return configs;
|
||||
};
|
||||
|
||||
const hasAnyNetworkTraffic = (checks: HardwareCheckStats[]): boolean =>
|
||||
checks.some((c) =>
|
||||
c.net?.some((iface) => iface.bytesSentPerSecond > 0 || iface.deltaBytesRecv > 0)
|
||||
);
|
||||
|
||||
export const InfraNetworkCharts = ({
|
||||
checks,
|
||||
dateRange,
|
||||
@@ -82,43 +74,35 @@ export const InfraNetworkCharts = ({
|
||||
() => getChartConfigs(theme, checks, t),
|
||||
[theme, checks, t]
|
||||
);
|
||||
const showNoTrafficNotice = chartConfigs.length > 0 && !hasAnyNetworkTraffic(checks);
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
{showNoTrafficNotice && (
|
||||
<NoticeBanner severity="warning">
|
||||
{t("pages.infrastructure.charts.labels.netNoTraffic")}
|
||||
</NoticeBanner>
|
||||
)}
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(LAYOUT.MD)}
|
||||
>
|
||||
{chartConfigs.map((config) => {
|
||||
return (
|
||||
<Grid
|
||||
size={isSmall ? 12 : 6}
|
||||
key={`${config.type}-${config.interfaceName ?? config.idx ?? ""}`}
|
||||
>
|
||||
<HistogramInfrastructure
|
||||
dateRange={dateRange}
|
||||
title={config.title}
|
||||
type={config.type}
|
||||
idx={config.idx}
|
||||
checks={checks}
|
||||
xKey="bucketDate"
|
||||
dataKeys={config.dataKeys}
|
||||
gradient={true}
|
||||
gradientStartColor={config.gradientStartColor}
|
||||
gradientEndColor="#ffffff"
|
||||
strokeColor={config.strokeColor}
|
||||
yAxisFormatter={formatBytesToMB}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Stack>
|
||||
<Grid
|
||||
container
|
||||
spacing={theme.spacing(8)}
|
||||
>
|
||||
{chartConfigs.map((config) => {
|
||||
return (
|
||||
<Grid
|
||||
size={isSmall ? 12 : 6}
|
||||
key={`${config.type}-${config.interfaceName ?? config.idx ?? ""}`}
|
||||
>
|
||||
<HistogramInfrastructure
|
||||
dateRange={dateRange}
|
||||
title={config.title}
|
||||
type={config.type}
|
||||
idx={config.idx}
|
||||
checks={checks}
|
||||
xKey="bucketDate"
|
||||
dataKeys={config.dataKeys}
|
||||
gradient={true}
|
||||
gradientStartColor={config.gradientStartColor}
|
||||
gradientEndColor="#ffffff"
|
||||
strokeColor={config.strokeColor}
|
||||
yAxisFormatter={formatBytesToMB}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Table, Pagination, StatusLabel, Gauge } from "@/Components/design-elements";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
@@ -13,24 +12,6 @@ import { useNavigate } from "react-router-dom";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import { Checkbox } from "@/Components/inputs";
|
||||
|
||||
interface InfraMonitorsTableProps {
|
||||
monitors: Monitor[];
|
||||
refetch: () => void;
|
||||
setSelectedMonitor: (monitor: Monitor | null) => void;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
rowsPerPage: number;
|
||||
setRowsPerPage: (rowsPerPage: number) => void;
|
||||
selectedRows?: string[];
|
||||
onSelectionChange?: (selected: string[]) => void;
|
||||
}
|
||||
|
||||
export const InfraMonitorsTable = ({
|
||||
monitors,
|
||||
@@ -45,32 +26,29 @@ export const InfraMonitorsTable = ({
|
||||
setPage,
|
||||
rowsPerPage,
|
||||
setRowsPerPage,
|
||||
selectedRows = [],
|
||||
onSelectionChange,
|
||||
}: InfraMonitorsTableProps) => {
|
||||
}: {
|
||||
monitors: Monitor[];
|
||||
refetch: Function;
|
||||
setSelectedMonitor: Function;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
rowsPerPage: number;
|
||||
setRowsPerPage: (rowsPerPage: number) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const navigate = useNavigate();
|
||||
const { post } = usePost<Record<string, never>, Monitor>();
|
||||
|
||||
const selectedSet = new Set(selectedRows);
|
||||
const isAllSelected =
|
||||
monitors.length > 0 && monitors.every((m) => selectedSet.has(m.id));
|
||||
const isSomeSelected = selectedRows.length > 0 && !isAllSelected;
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
onSelectionChange?.(checked ? monitors.map((m) => m.id) : []);
|
||||
};
|
||||
|
||||
const handleSelectRow = (id: string, checked: boolean) => {
|
||||
if (!onSelectionChange) return;
|
||||
onSelectionChange(
|
||||
checked ? [...selectedRows, id] : selectedRows.filter((rowId) => rowId !== id)
|
||||
);
|
||||
};
|
||||
|
||||
const isRowSelected = (id: string) => selectedSet.has(id);
|
||||
const {
|
||||
post,
|
||||
// loading: isPatching,
|
||||
// error: postError,
|
||||
} = usePost<any, Monitor>();
|
||||
|
||||
const handlePageChange = (
|
||||
_e: React.MouseEvent<HTMLButtonElement> | null,
|
||||
@@ -87,7 +65,7 @@ export const InfraMonitorsTable = ({
|
||||
setRowsPerPage(value);
|
||||
};
|
||||
|
||||
const handleSort = (e: React.MouseEvent, field: string) => {
|
||||
const handleSort = (e: any, field: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (sortField === field) {
|
||||
@@ -123,6 +101,13 @@ export const InfraMonitorsTable = ({
|
||||
navigate(`/infrastructure/configure/${monitor.id}`);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// id: 5,
|
||||
// label: "Clone",
|
||||
// action: () => {
|
||||
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: 6,
|
||||
label:
|
||||
@@ -167,35 +152,8 @@ export const InfraMonitorsTable = ({
|
||||
</Box>
|
||||
);
|
||||
const headers: Header<Monitor>[] = [
|
||||
{
|
||||
id: "selection",
|
||||
content: (
|
||||
<Checkbox
|
||||
aria-label={t("pages.common.monitors.actions.selectAll")}
|
||||
indeterminate={isSomeSelected}
|
||||
checked={isAllSelected}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
/>
|
||||
),
|
||||
hideMobileLabel: true,
|
||||
render: (row) => (
|
||||
<Checkbox
|
||||
aria-label={t("pages.common.monitors.actions.selectMonitor", {
|
||||
name: row.name,
|
||||
})}
|
||||
checked={isRowSelected(row.id)}
|
||||
onChange={(e) => {
|
||||
handleSelectRow(row.id, e.target.checked);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
),
|
||||
onClick: (e) => e?.stopPropagation(),
|
||||
},
|
||||
{
|
||||
id: "name",
|
||||
align: "left",
|
||||
mobileLabel: t("common.table.headers.name"),
|
||||
content: (
|
||||
<Typography
|
||||
component="div"
|
||||
@@ -215,20 +173,18 @@ export const InfraMonitorsTable = ({
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
mobileLabel: t("common.table.headers.status"),
|
||||
content: (
|
||||
<Stack
|
||||
<Typography
|
||||
component="div"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
onClick={(e) => handleSort(e, "status")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<Box width={theme.spacing(8)} />
|
||||
{t("common.table.headers.status")}
|
||||
{renderSortIcon(sortField === "status")}
|
||||
</Stack>
|
||||
</Typography>
|
||||
),
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status} />;
|
||||
@@ -292,11 +248,6 @@ export const InfraMonitorsTable = ({
|
||||
onRowClick={(row) => {
|
||||
navigate(`/infrastructure/${row.id}`);
|
||||
}}
|
||||
getRowSx={(row) => ({
|
||||
backgroundColor: isRowSelected(row.id)
|
||||
? theme.palette.action.selected
|
||||
: "inherit",
|
||||
})}
|
||||
/>
|
||||
<Pagination
|
||||
component="div"
|
||||
|
||||
@@ -2,13 +2,8 @@ import Stack from "@mui/material/Stack";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { MonitorBasePageWithStates } from "@/Components/design-elements";
|
||||
import { HeaderCreate } from "@/Components/common";
|
||||
import {
|
||||
ControlsFilter,
|
||||
HeaderMonitorsSummary,
|
||||
BulkActionsBar,
|
||||
} from "@/Components/monitors";
|
||||
import { TextField, Dialog, Button } from "@/Components/inputs";
|
||||
import { Play, Pause } from "lucide-react";
|
||||
import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/monitors";
|
||||
import { TextField, Dialog } from "@/Components/inputs";
|
||||
|
||||
import { useGet, useDelete } from "@/Hooks/UseApi";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
@@ -21,7 +16,6 @@ import { setRowsPerPage } from "@/Features/UI/uiSlice.js";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { InfraMonitorsTable } from "./Components/MonitorsTable";
|
||||
import useDebounce from "@/Hooks/useDebounce";
|
||||
import { useBulkMonitorActions } from "@/Hooks/useBulkMonitorActions";
|
||||
|
||||
const InfrastructureMonitors = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -43,7 +37,6 @@ const InfrastructureMonitors = () => {
|
||||
const [sortField, setSortField] = useState<string>("");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
|
||||
|
||||
const isDialogOpen = Boolean(selectedMonitor);
|
||||
|
||||
const debouncedSearch = useDebounce<string>(search, 300);
|
||||
@@ -104,15 +97,6 @@ const InfrastructureMonitors = () => {
|
||||
const { summary, count } = monitorsWithChecksData ?? { summary: null, count: 0 };
|
||||
const isLoading = monitorsWithChecksLoading;
|
||||
|
||||
// Bulk actions
|
||||
const {
|
||||
selectedRows,
|
||||
setSelectedRows,
|
||||
handleBulkPause,
|
||||
handleBulkResume,
|
||||
handleCancelSelection,
|
||||
} = useBulkMonitorActions(refetch, page);
|
||||
|
||||
// Check if any filters are active
|
||||
const hasActiveFilters = Boolean(selectedStatus || selectedState || search);
|
||||
|
||||
@@ -175,29 +159,6 @@ const InfrastructureMonitors = () => {
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{!isLoading && (
|
||||
<BulkActionsBar
|
||||
selectedCount={selectedRows.length}
|
||||
onCancel={handleCancelSelection}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Play size={16} />}
|
||||
onClick={handleBulkResume}
|
||||
>
|
||||
{t("common.buttons.resume")}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Pause size={16} />}
|
||||
onClick={handleBulkPause}
|
||||
>
|
||||
{t("common.buttons.pause")}
|
||||
</Button>
|
||||
</BulkActionsBar>
|
||||
)}
|
||||
|
||||
<InfraMonitorsTable
|
||||
monitors={monitorsWithChecksData?.monitors || []}
|
||||
refetch={refetch}
|
||||
@@ -219,8 +180,6 @@ const InfrastructureMonitors = () => {
|
||||
);
|
||||
setPage(0);
|
||||
}}
|
||||
selectedRows={selectedRows}
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { QueueData } from "@/Types/Queue";
|
||||
import { Metrics } from "@/Pages/Logs/components/Metrics";
|
||||
import { Button } from "@/Components/inputs";
|
||||
import { EmptyState } from "@/Components/design-elements";
|
||||
import { SPACING } from "@/Utils/Theme/constants";
|
||||
|
||||
export const TabQueue = () => {
|
||||
const theme = useTheme();
|
||||
@@ -20,7 +19,7 @@ export const TabQueue = () => {
|
||||
isLoading,
|
||||
// error,
|
||||
refetch,
|
||||
} = useGet<QueueData>("/queue/all-metrics", {}, { refreshInterval: 1000 });
|
||||
} = useGet<QueueData>("/queue/all-metrics", {}, { refreshInterval: 5000 });
|
||||
|
||||
const { post, loading: isFlushing } = usePost();
|
||||
|
||||
@@ -50,22 +49,32 @@ export const TabQueue = () => {
|
||||
<Stack gap={theme.spacing(1)}>
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.secondary}
|
||||
color="text.secondary"
|
||||
>
|
||||
{t("pages.logs.jobQueue")}
|
||||
</Typography>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<Trans
|
||||
i18nKey="pages.logs.jobQueueExplainer"
|
||||
components={{
|
||||
highlight: (
|
||||
<Box
|
||||
component="span"
|
||||
bgcolor={theme.palette.rowStatus.running}
|
||||
color={theme.palette.text.primary}
|
||||
px={SPACING.SM}
|
||||
py={SPACING.XXS}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
theme.palette.mode === "dark"
|
||||
? "rgba(19, 113, 91, 0.18)"
|
||||
: "#ECF7F2",
|
||||
color: theme.palette.text.primary,
|
||||
px: 1,
|
||||
py: 0.25,
|
||||
borderRadius: 0.5,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
@@ -78,11 +87,16 @@ export const TabQueue = () => {
|
||||
<Stack gap={theme.spacing(1)}>
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.secondary}
|
||||
color="text.secondary"
|
||||
>
|
||||
{t("pages.logs.failedJobs")}
|
||||
</Typography>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
{t("pages.logs.failedJobsExplainer")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -30,22 +30,6 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
|
||||
};
|
||||
|
||||
const headers: Header<QueueJobWithId>[] = [
|
||||
{
|
||||
id: "state",
|
||||
content: t("pages.logs.table.headers.state"),
|
||||
render: (row) => {
|
||||
if (row.lockedAt) {
|
||||
return t("common.labels.running");
|
||||
}
|
||||
if (row.monitorActive === true) {
|
||||
return t("common.labels.active");
|
||||
}
|
||||
if (row.monitorActive === false) {
|
||||
return t("common.labels.paused");
|
||||
}
|
||||
return t("common.labels.idle");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "id",
|
||||
content: t("common.table.headers.monitorId"),
|
||||
@@ -63,10 +47,7 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
|
||||
id: "url",
|
||||
content: t("common.table.headers.url"),
|
||||
render: (row) => (
|
||||
<Typography
|
||||
title={row.monitorUrl ?? ""}
|
||||
sx={cellSx}
|
||||
>
|
||||
<Typography title={row.monitorUrl ?? ""} sx={cellSx}>
|
||||
{row.monitorUrl}
|
||||
</Typography>
|
||||
),
|
||||
@@ -79,15 +60,9 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
|
||||
{
|
||||
id: "interval",
|
||||
content: t("common.table.headers.interval"),
|
||||
render: (row) => {
|
||||
let interval;
|
||||
if (row.monitorId.toString().includes("geo")) {
|
||||
interval = row.monitorGeoInterval ?? 0;
|
||||
} else {
|
||||
interval = row.repeat ?? 0;
|
||||
}
|
||||
return <Typography sx={cellSx}>{prettyMilliseconds(interval)}</Typography>;
|
||||
},
|
||||
render: (row) => (
|
||||
<Typography sx={cellSx}>{prettyMilliseconds(row.monitorInterval ?? 0)}</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "lastRun",
|
||||
@@ -95,10 +70,7 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
|
||||
render: (row) => {
|
||||
const v = formatTimestamp(row.lastRunAt) ?? "-";
|
||||
return (
|
||||
<Typography
|
||||
title={v}
|
||||
sx={cellSx}
|
||||
>
|
||||
<Typography title={v} sx={cellSx}>
|
||||
{v}
|
||||
</Typography>
|
||||
);
|
||||
@@ -118,10 +90,7 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
|
||||
render: (row) => {
|
||||
const v = formatTimestamp(row.lockedAt) ?? "-";
|
||||
return (
|
||||
<Typography
|
||||
title={v}
|
||||
sx={cellSx}
|
||||
>
|
||||
<Typography title={v} sx={cellSx}>
|
||||
{v}
|
||||
</Typography>
|
||||
);
|
||||
@@ -129,16 +98,16 @@ export const TableJobs = ({ jobs }: TableJobsProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
const runningBg = isDark ? "rgba(19, 113, 91, 0.18)" : "#ECF7F2";
|
||||
|
||||
return (
|
||||
<Table
|
||||
headers={headers}
|
||||
data={jobsWithId}
|
||||
getRowSx={(row) => ({
|
||||
...(row.lockedAt && {
|
||||
"& td": { backgroundColor: theme.palette.rowStatus.running },
|
||||
}),
|
||||
...(row.monitorActive === false && {
|
||||
"& td": { backgroundColor: theme.palette.rowStatus.paused },
|
||||
"& td": { backgroundColor: runningBg },
|
||||
}),
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,6 @@ import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { HeatmapResponseTime, HistogramResponseTime } from "@/Components/common";
|
||||
import type { CheckSnapshot } from "@/Types/Check";
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
|
||||
interface DummyChartProps {
|
||||
chartType: string;
|
||||
@@ -11,7 +10,7 @@ interface DummyChartProps {
|
||||
|
||||
const generateDummyChecks = (): CheckSnapshot[] => {
|
||||
const checks: CheckSnapshot[] = [];
|
||||
for (let i = 0; i < MAX_RECENT_CHECKS; i++) {
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const isUp = Math.random() > 0.1;
|
||||
const responseTime = Math.floor(Math.random() * 80) + 20;
|
||||
checks.push({
|
||||
|
||||
@@ -128,8 +128,8 @@ export const SettingsPage = () => {
|
||||
timezoneOptions.find((tz) => tz.id === selectedTimezoneId) ?? null;
|
||||
|
||||
const handleTimezoneChange = (newValue: Timezone | null) => {
|
||||
if (!newValue?.id) return;
|
||||
dispatch(setTimezone({ timezone: newValue.id }));
|
||||
const newId = newValue?.id ?? "";
|
||||
dispatch(setTimezone({ timezone: newId }));
|
||||
};
|
||||
|
||||
const handleModeChange = (e: SelectChangeEvent<ThemeMode>) => {
|
||||
@@ -182,9 +182,6 @@ export const SettingsPage = () => {
|
||||
systemEmailRequireTLS: formValues.systemEmailRequireTLS,
|
||||
systemEmailRejectUnauthorized: formValues.systemEmailRejectUnauthorized,
|
||||
...(formValues.systemEmailUser && { systemEmailUser: formValues.systemEmailUser }),
|
||||
...(formValues.systemEmailDisplayName && {
|
||||
systemEmailDisplayName: formValues.systemEmailDisplayName,
|
||||
}),
|
||||
...(formValues.systemEmailTLSServername && {
|
||||
systemEmailTLSServername: formValues.systemEmailTLSServername,
|
||||
}),
|
||||
@@ -580,65 +577,44 @@ export const SettingsPage = () => {
|
||||
href="https://nodemailer.com/smtp/"
|
||||
target="_blank"
|
||||
/>
|
||||
{(() => {
|
||||
const address = form.watch("systemEmailAddress") || "";
|
||||
const displayName = form.watch("systemEmailDisplayName")?.trim();
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
component="pre"
|
||||
p={2}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
bgcolor={theme.palette.action.hover}
|
||||
sx={{
|
||||
fontFamily: theme.typography.fontFamilyMonospace,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{JSON.stringify(
|
||||
{
|
||||
host: form.watch("systemEmailHost") || "",
|
||||
port: form.watch("systemEmailPort") || "",
|
||||
secure: form.watch("systemEmailSecure") ?? false,
|
||||
auth: {
|
||||
user: form.watch("systemEmailUser") || address,
|
||||
pass: "<your_password>",
|
||||
},
|
||||
name: form.watch("systemEmailConnectionHost") || "localhost",
|
||||
pool: form.watch("systemEmailPool") ?? false,
|
||||
tls: {
|
||||
rejectUnauthorized:
|
||||
form.watch("systemEmailRejectUnauthorized") ?? true,
|
||||
ignoreTLS: form.watch("systemEmailIgnoreTLS") ?? false,
|
||||
requireTLS: form.watch("systemEmailRequireTLS") ?? false,
|
||||
servername: form.watch("systemEmailTLSServername") || "",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</code>
|
||||
</Box>
|
||||
{address && (
|
||||
<Box
|
||||
component="pre"
|
||||
p={2}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
bgcolor={theme.palette.action.hover}
|
||||
sx={{
|
||||
fontFamily: theme.typography.fontFamilyMonospace,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{`From: ${displayName ? `"${displayName}" <${address}>` : address}`}
|
||||
</code>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
<Box
|
||||
component="pre"
|
||||
sx={{
|
||||
fontFamily: theme.typography.fontFamilyMonospace,
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
overflow: "auto",
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{JSON.stringify(
|
||||
{
|
||||
host: form.watch("systemEmailHost") || "",
|
||||
port: form.watch("systemEmailPort") || "",
|
||||
secure: form.watch("systemEmailSecure") ?? false,
|
||||
auth: {
|
||||
user:
|
||||
form.watch("systemEmailUser") ||
|
||||
form.watch("systemEmailAddress") ||
|
||||
"",
|
||||
pass: "<your_password>",
|
||||
},
|
||||
name: form.watch("systemEmailConnectionHost") || "localhost",
|
||||
pool: form.watch("systemEmailPool") ?? false,
|
||||
tls: {
|
||||
rejectUnauthorized:
|
||||
form.watch("systemEmailRejectUnauthorized") ?? true,
|
||||
ignoreTLS: form.watch("systemEmailIgnoreTLS") ?? false,
|
||||
requireTLS: form.watch("systemEmailRequireTLS") ?? false,
|
||||
servername: form.watch("systemEmailTLSServername") || "",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</code>
|
||||
</Box>
|
||||
</Stack>
|
||||
}
|
||||
rightContent={
|
||||
@@ -704,24 +680,6 @@ export const SettingsPage = () => {
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Email Display Name (Optional) */}
|
||||
<Controller
|
||||
name="systemEmailDisplayName"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
fieldLabel={t("pages.settings.form.email.option.displayName.label")}
|
||||
placeholder={t(
|
||||
"pages.settings.form.email.option.displayName.placeholder"
|
||||
)}
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Email User (Optional) */}
|
||||
<Controller
|
||||
name="systemEmailUser"
|
||||
@@ -1016,13 +974,7 @@ export const SettingsPage = () => {
|
||||
setIsDemoMonitorsDialogOpen(false);
|
||||
}}
|
||||
loading={isDeletingAllMonitors}
|
||||
confirmColor="error"
|
||||
confirmText={t("common.buttons.removeMonitors")}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
{t("pages.settings.form.removeMonitors.dialog.paragraph")}
|
||||
</Typography>
|
||||
</Dialog>
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { RadioWithDescription, Select } from "@/Components/inputs";
|
||||
import {
|
||||
STATUS_PAGE_THEMES,
|
||||
STATUS_PAGE_THEME_MODES,
|
||||
type StatusPageTheme,
|
||||
type StatusPageThemeMode,
|
||||
} from "@/Types/StatusPage";
|
||||
|
||||
interface Props {
|
||||
theme: StatusPageTheme;
|
||||
themeMode: StatusPageThemeMode;
|
||||
onThemeChange: (value: StatusPageTheme) => void;
|
||||
onThemeModeChange: (value: StatusPageThemeMode) => void;
|
||||
}
|
||||
|
||||
export const ThemePicker = ({
|
||||
theme,
|
||||
themeMode,
|
||||
onThemeChange,
|
||||
onThemeModeChange,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const muiTheme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack spacing={muiTheme.spacing(6)}>
|
||||
<RadioGroup
|
||||
value={theme}
|
||||
onChange={(_, value) => onThemeChange(value as StatusPageTheme)}
|
||||
>
|
||||
<Stack spacing={muiTheme.spacing(4)}>
|
||||
{STATUS_PAGE_THEMES.map((option) => (
|
||||
<RadioWithDescription
|
||||
key={option}
|
||||
value={option}
|
||||
label={t(`pages.statusPages.form.theme.options.${option}.name`)}
|
||||
description={t(
|
||||
`pages.statusPages.form.theme.options.${option}.description`
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
|
||||
<Select
|
||||
fieldLabel={t("pages.statusPages.form.themeMode.label")}
|
||||
value={themeMode}
|
||||
onChange={(e) => onThemeModeChange(e.target.value as StatusPageThemeMode)}
|
||||
>
|
||||
{STATUS_PAGE_THEME_MODES.map((mode) => (
|
||||
<MenuItem
|
||||
key={mode}
|
||||
value={mode}
|
||||
>
|
||||
{t(`pages.statusPages.form.themeMode.${mode}`)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -32,10 +32,8 @@ import type { MonitorDisplayType, StatusPageResponse } from "@/Types/StatusPage"
|
||||
import { getMonitorTypeLabel } from "@/Types/StatusPage";
|
||||
import timezones from "@/Utils/timezones.json";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { mutate } from "swr";
|
||||
import axios from "axios";
|
||||
import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls";
|
||||
import { ThemePicker } from "./Components/ThemePicker";
|
||||
|
||||
const monitorsUrl = (() => {
|
||||
const params = new URLSearchParams();
|
||||
@@ -50,9 +48,6 @@ interface TimezoneOption {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const buildStatusPageKey = (slug: string | null | undefined) =>
|
||||
slug ? `/status-page/${slug}?type=uptime&type=infrastructure` : null;
|
||||
|
||||
const CreateStatusPage = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -63,7 +58,9 @@ const CreateStatusPage = () => {
|
||||
|
||||
// Fetch existing status page data when configuring
|
||||
const { data: statusPageData, isLoading: isLoadingStatusPage } =
|
||||
useGet<StatusPageResponse>(isCreate ? null : buildStatusPageKey(url));
|
||||
useGet<StatusPageResponse>(
|
||||
isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure`
|
||||
);
|
||||
|
||||
const { data: monitorsResponse } = useGet<Monitor[]>(monitorsUrl);
|
||||
const monitors = monitorsResponse ?? [];
|
||||
@@ -142,8 +139,6 @@ const CreateStatusPage = () => {
|
||||
fd.append("showUptimePercentage", String(data.showUptimePercentage));
|
||||
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
|
||||
fd.append("showInfrastructure", String(data.showInfrastructure));
|
||||
if (data.theme) fd.append("theme", data.theme);
|
||||
if (data.themeMode) fd.append("themeMode", data.themeMode);
|
||||
|
||||
data.monitors.forEach((monitorId) => {
|
||||
fd.append("monitors[]", monitorId);
|
||||
@@ -183,10 +178,6 @@ const CreateStatusPage = () => {
|
||||
}
|
||||
|
||||
if (result) {
|
||||
await mutate(buildStatusPageKey(data.url));
|
||||
if (!isCreate && url !== data.url) {
|
||||
await mutate(buildStatusPageKey(url));
|
||||
}
|
||||
navigate(`/status/${data.url}`);
|
||||
}
|
||||
};
|
||||
@@ -447,30 +438,6 @@ const CreateStatusPage = () => {
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.statusPages.form.theme.title")}
|
||||
subtitle={t("pages.statusPages.form.theme.description")}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="theme"
|
||||
control={control}
|
||||
render={({ field: themeField }) => (
|
||||
<Controller
|
||||
name="themeMode"
|
||||
control={control}
|
||||
render={({ field: modeField }) => (
|
||||
<ThemePicker
|
||||
theme={themeField.value ?? "refined"}
|
||||
themeMode={modeField.value ?? "auto"}
|
||||
onThemeChange={themeField.onChange}
|
||||
onThemeModeChange={modeField.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title={t("pages.statusPages.form.features.title")}
|
||||
subtitle={t("pages.statusPages.form.features.description")}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Settings, ExternalLink } from "lucide-react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
|
||||
interface HeaderStatusPageControlsProps {
|
||||
isAdmin: boolean;
|
||||
@@ -50,7 +50,7 @@ export const HeaderStatusPageControls = ({
|
||||
<Typography
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`,
|
||||
`/status/public/${statusPage.url}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Gauge } from "@/Components/design-elements";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import Box from "@mui/material/Box";
|
||||
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||
|
||||
const GAUGE_RADIUS = 60;
|
||||
const GAUGE_STROKE_WIDTH = 12;
|
||||
const PERCENTAGE_MULTIPLIER = 100;
|
||||
|
||||
interface StatusPageMonitor extends Monitor {
|
||||
checks?: Monitor["recentChecks"];
|
||||
}
|
||||
|
||||
interface MetricDetail {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface MetricConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
hasData: boolean;
|
||||
progress: number;
|
||||
details: MetricDetail[];
|
||||
}
|
||||
|
||||
const MetricDetailRow = ({ label, value }: MetricDetail) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="body2">{value}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
interface MetricItemProps {
|
||||
label: string;
|
||||
progress: number;
|
||||
details?: MetricDetail[];
|
||||
}
|
||||
|
||||
const MetricItem = ({ label, progress, details }: MetricItemProps) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
return (
|
||||
<Grid
|
||||
size={isSmall ? 12 : 4}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
gap: theme.spacing(SPACING.LG),
|
||||
padding: theme.spacing(LAYOUT.XS),
|
||||
borderRight: isSmall ? "none" : `1px solid ${theme.palette.divider}`,
|
||||
borderBottom: isSmall ? `1px solid ${theme.palette.divider}` : "none",
|
||||
"&:last-child": {
|
||||
borderRight: "none",
|
||||
borderBottom: "none",
|
||||
paddingBottom: theme.spacing(SPACING.LG),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
>
|
||||
<Gauge
|
||||
progress={progress}
|
||||
radius={GAUGE_RADIUS}
|
||||
strokeWidth={GAUGE_STROKE_WIDTH}
|
||||
/>
|
||||
<Typography variant="body2">{label}</Typography>
|
||||
</Box>
|
||||
{details && details.length > 0 && (
|
||||
<Box
|
||||
width="100%"
|
||||
paddingX={theme.spacing(LAYOUT.LG)}
|
||||
>
|
||||
{details.map((detail) => (
|
||||
<MetricDetailRow
|
||||
key={detail.label}
|
||||
label={detail.label}
|
||||
value={detail.value}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
type LatestCheck = NonNullable<Monitor["recentChecks"]>[number];
|
||||
|
||||
const buildCpuMetric = (
|
||||
check: LatestCheck,
|
||||
t: (key: string) => string
|
||||
): MetricConfig | null => {
|
||||
if (!check.cpu || typeof check.cpu.usage_percent !== "number") {
|
||||
return null;
|
||||
}
|
||||
const usagePercent = (check.cpu.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER;
|
||||
return {
|
||||
key: "cpu",
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.cpu"),
|
||||
hasData: true,
|
||||
progress: usagePercent,
|
||||
details: [
|
||||
{
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.usage"),
|
||||
value: `${usagePercent.toFixed(2)}%`,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const buildMemoryMetric = (
|
||||
check: LatestCheck,
|
||||
t: (key: string) => string
|
||||
): MetricConfig | null => {
|
||||
if (
|
||||
!check.memory ||
|
||||
typeof check.memory.usage_percent !== "number" ||
|
||||
typeof check.memory.used_bytes !== "number" ||
|
||||
typeof check.memory.total_bytes !== "number"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: "memory",
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.memory"),
|
||||
hasData: true,
|
||||
progress: (check.memory.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER,
|
||||
details: [
|
||||
{
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.used"),
|
||||
value: prettyBytes(check.memory.used_bytes ?? 0),
|
||||
},
|
||||
{
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.total"),
|
||||
value: prettyBytes(check.memory.total_bytes ?? 0),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const buildDiskMetric = (
|
||||
check: LatestCheck,
|
||||
t: (key: string) => string
|
||||
): MetricConfig | null => {
|
||||
if (!check.disk || check.disk.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const disks = check.disk;
|
||||
const avgUsagePercent =
|
||||
(disks.reduce((acc, disk) => acc + (disk?.usage_percent ?? 0), 0) / disks.length) *
|
||||
PERCENTAGE_MULTIPLIER;
|
||||
const totalUsedBytes = disks.reduce((acc, disk) => acc + (disk?.used_bytes ?? 0), 0);
|
||||
const totalTotalBytes = disks.reduce((acc, disk) => acc + (disk?.total_bytes ?? 0), 0);
|
||||
|
||||
return {
|
||||
key: "disk",
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.disk"),
|
||||
hasData: true,
|
||||
progress: avgUsagePercent,
|
||||
details: [
|
||||
{
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.used"),
|
||||
value: prettyBytes(totalUsedBytes),
|
||||
},
|
||||
{
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.total"),
|
||||
value: prettyBytes(totalTotalBytes),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0];
|
||||
|
||||
if (!latestCheck) {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{t("pages.statusPages.monitorsList.noData")}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const metrics: MetricConfig[] = [
|
||||
buildCpuMetric(latestCheck, t),
|
||||
buildMemoryMetric(latestCheck, t),
|
||||
buildDiskMetric(latestCheck, t),
|
||||
].filter((m): m is MetricConfig => m !== null);
|
||||
|
||||
if (metrics.length === 0) {
|
||||
return (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{t("pages.statusPages.monitorsList.noData")}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
padding={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
{metrics.map(({ key, label, progress, details }) => (
|
||||
<MetricItem
|
||||
key={key}
|
||||
label={label}
|
||||
progress={progress}
|
||||
details={details}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
|
||||
import { StatusLabel, BaseBox } from "@/Components/design-elements";
|
||||
import { SwitchComponent } from "@/Components/inputs";
|
||||
import { InfrastructureMetrics } from "@/Pages/StatusPage/Status/Components/InfrastructureMetrics";
|
||||
|
||||
import { useTheme, type Theme } from "@mui/material/styles";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState } from "react";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
import { getMonitorTypeLabel } from "@/Types/StatusPage";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||
|
||||
interface StatusPageMonitor extends Monitor {
|
||||
checks?: Monitor["recentChecks"];
|
||||
}
|
||||
|
||||
interface MonitorsListProps {
|
||||
statusPage: StatusPage;
|
||||
monitors: StatusPageMonitor[];
|
||||
}
|
||||
|
||||
const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
|
||||
const bg =
|
||||
monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light;
|
||||
return {
|
||||
backgroundColor: bg,
|
||||
color: theme.palette.background.paper,
|
||||
padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
};
|
||||
};
|
||||
|
||||
const MonitorHeader = ({
|
||||
monitor,
|
||||
showURL,
|
||||
}: {
|
||||
monitor: StatusPageMonitor;
|
||||
showURL: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
mb={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(SPACING.LG)}
|
||||
mb={theme.spacing(SPACING.SM)}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={getMonitorBadgeStyles(monitor.type ?? "", theme)}
|
||||
>
|
||||
{getMonitorTypeLabel(monitor.type, t)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{showURL && monitor.url && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.text.secondary}
|
||||
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||
>
|
||||
{monitor.url}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<StatusLabel status={monitor.status} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const MonitorContent = ({
|
||||
monitor,
|
||||
statusPage,
|
||||
chartType,
|
||||
}: {
|
||||
monitor: StatusPageMonitor;
|
||||
statusPage: StatusPage;
|
||||
chartType: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (monitor.type === "hardware") {
|
||||
if (statusPage.showInfrastructure === false) return null;
|
||||
return <InfrastructureMetrics monitor={monitor} />;
|
||||
}
|
||||
|
||||
if (statusPage.showCharts === false) return null;
|
||||
|
||||
const checks = monitor.checks?.slice().reverse() ?? [];
|
||||
return (
|
||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1, mb: theme.spacing(SPACING.LG) }}>
|
||||
{chartType === "histogram" ? (
|
||||
<HistogramResponseTime
|
||||
height={{ xs: 50, md: 100 }}
|
||||
gap={{ xs: theme.spacing(SPACING.SM), md: theme.spacing(LAYOUT.SM) }}
|
||||
checks={checks}
|
||||
/>
|
||||
) : (
|
||||
<HeatmapResponseTime checks={checks} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const showURL = useSelector((state: RootState) => state.ui?.showURL);
|
||||
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
{statusPage.showCharts && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
>
|
||||
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
|
||||
<SwitchComponent
|
||||
dualOption
|
||||
value={chartType}
|
||||
checked={chartType === "histogram"}
|
||||
onChange={(e) => setChartType(e.target.checked ? "histogram" : "heatmap")}
|
||||
/>
|
||||
<Typography>
|
||||
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{monitors.map((monitor) => (
|
||||
<BaseBox
|
||||
key={monitor.id}
|
||||
padding={theme.spacing(LAYOUT.MD)}
|
||||
>
|
||||
<MonitorHeader
|
||||
monitor={monitor}
|
||||
showURL={showURL}
|
||||
/>
|
||||
<MonitorContent
|
||||
monitor={monitor}
|
||||
statusPage={statusPage}
|
||||
chartType={chartType}
|
||||
/>
|
||||
</BaseBox>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
Loader,
|
||||
PauseCircle,
|
||||
ShieldAlert,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { Theme } from "@mui/material";
|
||||
import type { Monitor, MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
interface StatusDisplay {
|
||||
icon: JSX.Element;
|
||||
msg?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => {
|
||||
const monitorsStatus: StatusDisplay = {
|
||||
icon: <AlertTriangle size={24} />,
|
||||
};
|
||||
|
||||
// Handle empty monitors array
|
||||
if (monitors.length === 0) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.noMonitors");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <CircleX size={24} />;
|
||||
return monitorsStatus;
|
||||
}
|
||||
|
||||
const allOf = (...statuses: MonitorStatus[]) =>
|
||||
monitors.every((m) => statuses.includes(m.status));
|
||||
const someOf = (...statuses: MonitorStatus[]) =>
|
||||
monitors.some((m) => statuses.includes(m.status));
|
||||
const noneOf = (...statuses: MonitorStatus[]) =>
|
||||
monitors.every((m) => !statuses.includes(m.status));
|
||||
|
||||
// All monitors in a single state
|
||||
if (allOf("up")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allUp");
|
||||
monitorsStatus.color = theme.palette.success.main;
|
||||
monitorsStatus.icon = <CircleCheck size={24} />;
|
||||
} else if (allOf("breached")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allBreached");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
monitorsStatus.icon = <ShieldAlert size={24} />;
|
||||
} else if (allOf("maintenance")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allMaintenance");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <Wrench size={24} />;
|
||||
} else if (allOf("down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allDown");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
monitorsStatus.icon = <CircleX size={24} />;
|
||||
} else if (allOf("paused")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allPaused");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <PauseCircle size={24} />;
|
||||
} else if (allOf("initializing")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allInitializing");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <Loader size={24} />;
|
||||
|
||||
// Breached takes highest priority in mixed states
|
||||
} else if (someOf("breached") && someOf("down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.breachedAndDown");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
monitorsStatus.icon = <ShieldAlert size={24} />;
|
||||
} else if (someOf("breached")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.breached");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
monitorsStatus.icon = <ShieldAlert size={24} />;
|
||||
|
||||
// Maintenance combinations
|
||||
} else if (someOf("maintenance") && someOf("down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.maintenanceAndDown");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
monitorsStatus.icon = <Wrench size={24} />;
|
||||
} else if (someOf("maintenance") && noneOf("down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.maintenance");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <Wrench size={24} />;
|
||||
|
||||
// Degraded (some down, no maintenance/breached)
|
||||
} else if (someOf("down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.degraded");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <AlertTriangle size={24} />;
|
||||
|
||||
// Some Paused
|
||||
} else if (someOf("paused")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.partiallyPaused");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <PauseCircle size={24} />;
|
||||
|
||||
// Initializing
|
||||
} else if (someOf("initializing")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.initializing");
|
||||
monitorsStatus.color = theme.palette.info.main;
|
||||
monitorsStatus.icon = <Loader size={24} />;
|
||||
} else {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.unknown");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
}
|
||||
|
||||
return monitorsStatus;
|
||||
};
|
||||
|
||||
interface StatusBarProps {
|
||||
monitors: Monitor[];
|
||||
}
|
||||
|
||||
export const StatusBar = ({ monitors }: StatusBarProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const monitorsStatus = getMonitorStatus(monitors, theme, t);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={theme.spacing(2)}
|
||||
height={theme.spacing(30)}
|
||||
bgcolor={monitorsStatus.color}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
>
|
||||
{monitorsStatus.icon}
|
||||
<Typography>{monitorsStatus.msg}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +1,18 @@
|
||||
import { BasePage, BaseFallback } from "@/Components/design-elements";
|
||||
import { StatusBar } from "@/Pages/StatusPage/Status/Components/StatusBar";
|
||||
import { MonitorsList } from "@/Pages/StatusPage/Status/Components/MonitorsList";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Link } from "react-router-dom";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { resolveStatusPageTheme } from "@/Types/StatusPage";
|
||||
import {
|
||||
PUBLIC_STATUS_PAGE_PREFIX,
|
||||
type StatusPageResponse,
|
||||
type StatusPageTheme,
|
||||
} from "@/Types/StatusPage";
|
||||
import { HeaderStatusPageControls } from "@/Pages/StatusPage/Status/Components/HeaderStatusPageControls";
|
||||
import { StatusPageThemeProvider } from "@/Pages/StatusPage/Status/themes/StatusPageThemeProvider";
|
||||
import {
|
||||
BaseStatusPage,
|
||||
type ThemeConfig,
|
||||
} from "@/Pages/StatusPage/Status/themes/shared/BaseStatusPage";
|
||||
import { BrowserFrame } from "@/Pages/StatusPage/Status/themes/BrowserFrame";
|
||||
import { refinedStyles } from "@/Pages/StatusPage/Status/themes/refined/styles";
|
||||
import { RefinedHeader } from "@/Pages/StatusPage/Status/themes/refined/RefinedHeader";
|
||||
import { RefinedHero } from "@/Pages/StatusPage/Status/themes/refined/RefinedHero";
|
||||
import { modernStyles } from "@/Pages/StatusPage/Status/themes/modern/styles";
|
||||
import { ModernHeader } from "@/Pages/StatusPage/Status/themes/modern/ModernHeader";
|
||||
import { ModernHero } from "@/Pages/StatusPage/Status/themes/modern/ModernHero";
|
||||
import { boldStyles } from "@/Pages/StatusPage/Status/themes/bold/styles";
|
||||
import { BoldHeader } from "@/Pages/StatusPage/Status/themes/bold/BoldHeader";
|
||||
import { BoldHero } from "@/Pages/StatusPage/Status/themes/bold/BoldHero";
|
||||
import { editorialStyles } from "@/Pages/StatusPage/Status/themes/editorial/styles";
|
||||
import { EditorialHeader } from "@/Pages/StatusPage/Status/themes/editorial/EditorialHeader";
|
||||
import { EditorialHero } from "@/Pages/StatusPage/Status/themes/editorial/EditorialHero";
|
||||
|
||||
const THEME_CONFIGS: Record<StatusPageTheme, ThemeConfig<any>> = {
|
||||
refined: {
|
||||
createStyles: refinedStyles,
|
||||
HeaderSlot: RefinedHeader,
|
||||
HeroSlot: RefinedHero,
|
||||
},
|
||||
modern: {
|
||||
createStyles: modernStyles,
|
||||
HeaderSlot: ModernHeader,
|
||||
HeroSlot: ModernHero,
|
||||
overallStatusOptions: { iconSize: 20 },
|
||||
},
|
||||
bold: {
|
||||
createStyles: boldStyles,
|
||||
HeaderSlot: BoldHeader,
|
||||
HeroSlot: BoldHero,
|
||||
overallStatusOptions: { iconSize: 18 },
|
||||
},
|
||||
editorial: {
|
||||
createStyles: editorialStyles,
|
||||
HeaderSlot: EditorialHeader,
|
||||
HeroSlot: EditorialHero,
|
||||
overallStatusOptions: { allUpKey: "pages.statusPages.editorial.allUp" },
|
||||
},
|
||||
};
|
||||
import type { StatusPageResponse } from "@/Types/StatusPage";
|
||||
import { HeaderStatusPageControls } from "./Components/HeaderStatusPageControls";
|
||||
|
||||
const StatusPageView = () => {
|
||||
const theme = useTheme();
|
||||
@@ -66,8 +20,8 @@ const StatusPageView = () => {
|
||||
const { url } = useParams();
|
||||
const isAdmin = useIsAdmin();
|
||||
const location = useLocation();
|
||||
|
||||
const isPublic = location.pathname.startsWith(PUBLIC_STATUS_PAGE_PREFIX);
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const isPublic = location.pathname.startsWith("/status/public");
|
||||
|
||||
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
|
||||
|
||||
@@ -111,50 +65,48 @@ const StatusPageView = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const themeConfig = THEME_CONFIGS[resolveStatusPageTheme(statusPage.theme)];
|
||||
const themedRenderer = (
|
||||
<BaseStatusPage
|
||||
statusPage={statusPage}
|
||||
monitors={monitors}
|
||||
config={themeConfig}
|
||||
/>
|
||||
);
|
||||
|
||||
// Public route: render directly on the viewport, themed background covers everything.
|
||||
let sx: React.CSSProperties = {};
|
||||
if (isPublic) {
|
||||
return (
|
||||
<StatusPageThemeProvider
|
||||
theme={statusPage.theme}
|
||||
themeMode={statusPage.themeMode}
|
||||
timezone={statusPage.timezone}
|
||||
paintBody
|
||||
>
|
||||
{themedRenderer}
|
||||
</StatusPageThemeProvider>
|
||||
);
|
||||
sx.paddingTop = theme.spacing(20);
|
||||
sx.paddingLeft = isSmall ? "5vw" : "20vw";
|
||||
sx.paddingRight = isSmall ? "5vw" : "20vw";
|
||||
}
|
||||
|
||||
const publicUrl = `${window.location.origin}${PUBLIC_STATUS_PAGE_PREFIX}/${statusPage.url}`;
|
||||
const logoSrc = statusPage.logo?.data
|
||||
? `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
loading={isLoading}
|
||||
error={error}
|
||||
breadcrumbOverride={undefined}
|
||||
sx={{ flex: 1, minHeight: 0 }}
|
||||
sx={sx}
|
||||
breadcrumbOverride={isPublic ? [] : undefined}
|
||||
>
|
||||
<HeaderStatusPageControls
|
||||
isAdmin={isAdmin}
|
||||
statusPage={statusPage}
|
||||
isPublic={false}
|
||||
isPublic={isPublic}
|
||||
/>
|
||||
{logoSrc && (
|
||||
<Box
|
||||
component="img"
|
||||
src={logoSrc}
|
||||
alignSelf={"flex-start"}
|
||||
alt={statusPage.companyName}
|
||||
sx={{
|
||||
maxHeight: 120,
|
||||
maxWidth: "100%",
|
||||
objectFit: "contain",
|
||||
mb: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<StatusBar monitors={monitors} />
|
||||
<MonitorsList
|
||||
statusPage={statusPage}
|
||||
monitors={monitors}
|
||||
/>
|
||||
<StatusPageThemeProvider
|
||||
theme={statusPage.theme}
|
||||
themeMode={statusPage.themeMode}
|
||||
timezone={statusPage.timezone}
|
||||
transparent
|
||||
>
|
||||
<BrowserFrame url={publicUrl}>{themedRenderer}</BrowserFrame>
|
||||
</StatusPageThemeProvider>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useStatusPageTheme } from "./StatusPageThemeProvider";
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const BrowserFrame = ({ url, children }: Props) => {
|
||||
const { tokens } = useStatusPageTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
minHeight: "calc(100vh - 180px)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
borderRadius: "12px",
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
background: tokens.surface,
|
||||
color: tokens.text,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1.25,
|
||||
padding: "10px 14px",
|
||||
background: tokens.bg,
|
||||
borderBottom: `1px solid ${tokens.border}`,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", gap: 0.75 }}>
|
||||
<Box
|
||||
sx={{ width: 12, height: 12, borderRadius: "50%", background: "#ff5f57" }}
|
||||
/>
|
||||
<Box
|
||||
sx={{ width: 12, height: 12, borderRadius: "50%", background: "#febc2e" }}
|
||||
/>
|
||||
<Box
|
||||
sx={{ width: 12, height: 12, borderRadius: "50%", background: "#28c840" }}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
padding: "4px 12px",
|
||||
marginLeft: 2,
|
||||
borderRadius: "6px",
|
||||
background: tokens.surface,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
fontFamily: "ui-monospace, Menlo, monospace",
|
||||
fontSize: 12,
|
||||
color: tokens.textMuted,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{url}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, minHeight: 0 }}>{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,153 +0,0 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import {
|
||||
DEFAULT_STATUS_PAGE_THEME,
|
||||
resolveStatusPageTheme,
|
||||
resolveStatusPageThemeMode,
|
||||
type StatusPageTheme,
|
||||
type StatusPageThemeMode,
|
||||
} from "@/Types/StatusPage";
|
||||
import { resolveTimezone } from "@/Utils/TimeUtils";
|
||||
import { themeTokens, type StatusPageThemeTokens } from "./tokens";
|
||||
|
||||
type ResolvedMode = "light" | "dark";
|
||||
|
||||
interface StatusPageThemeContextValue {
|
||||
theme: StatusPageTheme;
|
||||
mode: ResolvedMode;
|
||||
tokens: StatusPageThemeTokens;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
const StatusPageThemeContext = createContext<StatusPageThemeContextValue | null>(null);
|
||||
|
||||
const resolveSystemMode = (): ResolvedMode => {
|
||||
if (typeof window === "undefined" || !window.matchMedia) return "light";
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
};
|
||||
|
||||
interface Props {
|
||||
theme?: StatusPageTheme;
|
||||
themeMode?: StatusPageThemeMode;
|
||||
timezone?: string;
|
||||
/**
|
||||
* When true, paint the document body with the theme background so it
|
||||
* covers beyond the provider's wrapper (useful on the public route
|
||||
* where no admin shell sits behind). Defaults to false for admin
|
||||
* previews that already have an app background.
|
||||
*/
|
||||
paintBody?: boolean;
|
||||
/**
|
||||
* When true, the provider's wrapper div does NOT paint its own
|
||||
* background or set a min-height. Use this when the wrapper is
|
||||
* inside a clipped container (e.g. BrowserFrame) that already sets
|
||||
* the visible background, so the wrapper's bg doesn't bleed past the
|
||||
* container's rounded corners.
|
||||
*/
|
||||
transparent?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const StatusPageThemeProvider = ({
|
||||
theme,
|
||||
themeMode,
|
||||
timezone,
|
||||
paintBody = false,
|
||||
transparent = false,
|
||||
children,
|
||||
}: Props) => {
|
||||
const resolvedTheme = resolveStatusPageTheme(theme);
|
||||
const resolvedThemeMode = resolveStatusPageThemeMode(themeMode);
|
||||
const resolvedTimezone = resolveTimezone(timezone);
|
||||
|
||||
const [systemMode, setSystemMode] = useState<ResolvedMode>(resolveSystemMode);
|
||||
|
||||
useEffect(() => {
|
||||
if (resolvedThemeMode !== "auto") return;
|
||||
if (typeof window === "undefined" || !window.matchMedia) return;
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
// Reconcile on first paint whenever we (re-)enter auto mode — only
|
||||
// when it disagrees with the stored value, so we don't trigger an
|
||||
// extra render on the common hot path.
|
||||
const next: ResolvedMode = mq.matches ? "dark" : "light";
|
||||
setSystemMode((prev) => (prev === next ? prev : next));
|
||||
const handler = (e: MediaQueryListEvent) =>
|
||||
setSystemMode(e.matches ? "dark" : "light");
|
||||
mq.addEventListener("change", handler);
|
||||
return () => mq.removeEventListener("change", handler);
|
||||
}, [resolvedThemeMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!paintBody || typeof document === "undefined") return;
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
const prev = {
|
||||
htmlBg: html.style.background,
|
||||
bodyBg: body.style.background,
|
||||
bodyMargin: body.style.margin,
|
||||
htmlColorScheme: html.style.colorScheme,
|
||||
};
|
||||
const resolvedMode = resolvedThemeMode === "auto" ? systemMode : resolvedThemeMode;
|
||||
const bg = themeTokens[resolvedTheme][resolvedMode].bg;
|
||||
html.style.background = bg;
|
||||
body.style.background = bg;
|
||||
body.style.margin = "0";
|
||||
html.style.colorScheme = resolvedMode;
|
||||
return () => {
|
||||
html.style.background = prev.htmlBg;
|
||||
body.style.background = prev.bodyBg;
|
||||
body.style.margin = prev.bodyMargin;
|
||||
html.style.colorScheme = prev.htmlColorScheme;
|
||||
};
|
||||
}, [paintBody, resolvedTheme, resolvedThemeMode, systemMode]);
|
||||
|
||||
const resolvedMode: ResolvedMode =
|
||||
resolvedThemeMode === "auto" ? systemMode : resolvedThemeMode;
|
||||
const tokens = themeTokens[resolvedTheme][resolvedMode];
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
theme: resolvedTheme,
|
||||
mode: resolvedMode,
|
||||
tokens,
|
||||
timezone: resolvedTimezone,
|
||||
}),
|
||||
[resolvedTheme, resolvedMode, tokens, resolvedTimezone]
|
||||
);
|
||||
|
||||
return (
|
||||
<StatusPageThemeContext.Provider value={value}>
|
||||
<div
|
||||
style={
|
||||
transparent
|
||||
? undefined
|
||||
: {
|
||||
background: tokens.bg,
|
||||
minHeight: "100vh",
|
||||
}
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</StatusPageThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useStatusPageTheme = (): StatusPageThemeContextValue => {
|
||||
const ctx = useContext(StatusPageThemeContext);
|
||||
if (!ctx) {
|
||||
return {
|
||||
theme: DEFAULT_STATUS_PAGE_THEME,
|
||||
mode: resolveSystemMode(),
|
||||
tokens: themeTokens[DEFAULT_STATUS_PAGE_THEME][resolveSystemMode()],
|
||||
timezone: resolveTimezone(),
|
||||
};
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { monoFirstChar } from "@/Pages/StatusPage/Status/themes/shared/overallStatus";
|
||||
import type { SlotProps } from "@/Pages/StatusPage/Status/themes/shared/BaseStatusPage";
|
||||
import type { BoldStyles } from "@/Pages/StatusPage/Status/themes/bold/styles";
|
||||
|
||||
export const BoldHeader = ({ statusPage, logoSrc, styles }: SlotProps<BoldStyles>) => (
|
||||
<Box sx={styles.brand}>
|
||||
{logoSrc ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={logoSrc}
|
||||
alt={statusPage.companyName}
|
||||
sx={styles.logoImg}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={styles.logoConic}>{monoFirstChar(statusPage.companyName)}</Box>
|
||||
)}
|
||||
{statusPage.companyName}
|
||||
</Box>
|
||||
);
|
||||
@@ -1,32 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SlotProps } from "@/Pages/StatusPage/Status/themes/shared/BaseStatusPage";
|
||||
import type { BoldStyles } from "@/Pages/StatusPage/Status/themes/bold/styles";
|
||||
|
||||
export const BoldHero = ({ overall, monitorCount, styles }: SlotProps<BoldStyles>) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box sx={styles.hero}>
|
||||
<Box
|
||||
component="h1"
|
||||
sx={styles.heroTitle}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.heroCheck(overall.tone)}
|
||||
>
|
||||
{overall.icon}
|
||||
</Box>
|
||||
{overall.message}
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,405 +0,0 @@
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
import type { StatusPageThemeTokens } from "../tokens";
|
||||
import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus";
|
||||
import { BOLD_SANS_STACK, MONO_STACK } from "../shared/fontStacks";
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
|
||||
export type BoldHeatCell = "fast" | "med" | "slow" | "down" | "empty";
|
||||
export type BoldBarKind = "up" | "down" | "empty";
|
||||
export type BoldGaugeFill = "ok" | "warm" | "hot";
|
||||
|
||||
export interface BoldStyles {
|
||||
page: SxProps<Theme>;
|
||||
top: SxProps<Theme>;
|
||||
brand: SxProps<Theme>;
|
||||
logoConic: SxProps<Theme>;
|
||||
logoImg: SxProps<Theme>;
|
||||
hero: SxProps<Theme>;
|
||||
heroTitle: SxProps<Theme>;
|
||||
heroCheck: (tone: OverallTone) => SxProps<Theme>;
|
||||
heroSub: SxProps<Theme>;
|
||||
chartSwitchWrap: SxProps<Theme>;
|
||||
chartSwitch: SxProps<Theme>;
|
||||
chartSwitchButton: (active: boolean) => SxProps<Theme>;
|
||||
monitorList: SxProps<Theme>;
|
||||
card: SxProps<Theme>;
|
||||
cardRow: SxProps<Theme>;
|
||||
cardLeft: SxProps<Theme>;
|
||||
monitorName: SxProps<Theme>;
|
||||
monitorMeta: SxProps<Theme>;
|
||||
pill: SxProps<Theme>;
|
||||
pillHardware: SxProps<Theme>;
|
||||
monitorUrl: SxProps<Theme>;
|
||||
badge: (tone: OverallTone) => SxProps<Theme>;
|
||||
heatmap: SxProps<Theme>;
|
||||
heatmapCell: (kind: BoldHeatCell) => SxProps<Theme>;
|
||||
histogram: SxProps<Theme>;
|
||||
bar: (kind: BoldBarKind, heightPct: number) => SxProps<Theme>;
|
||||
chartStats: SxProps<Theme>;
|
||||
infra: SxProps<Theme>;
|
||||
infraEmpty: SxProps<Theme>;
|
||||
gauge: SxProps<Theme>;
|
||||
gaugeLabel: SxProps<Theme>;
|
||||
gaugeValue: SxProps<Theme>;
|
||||
gaugeBar: SxProps<Theme>;
|
||||
gaugeFill: (level: BoldGaugeFill, widthPct: number) => SxProps<Theme>;
|
||||
gaugeSub: SxProps<Theme>;
|
||||
footer: SxProps<Theme>;
|
||||
}
|
||||
|
||||
export const boldStyles = (
|
||||
tokens: StatusPageThemeTokens,
|
||||
isDark: boolean
|
||||
): BoldStyles => {
|
||||
const heatCellBg: Record<BoldHeatCell, string> = {
|
||||
fast: tokens.up,
|
||||
med: `color-mix(in srgb, ${tokens.up} 70%, #ffffff 30%)`,
|
||||
slow: tokens.warn,
|
||||
down: tokens.down,
|
||||
empty: tokens.border,
|
||||
};
|
||||
const heatCellShadow: Record<BoldHeatCell, string> = {
|
||||
fast: `0 0 6px color-mix(in srgb, ${tokens.up} 18%, transparent)`,
|
||||
med: `0 0 6px color-mix(in srgb, ${tokens.up} 18%, transparent)`,
|
||||
slow: `0 0 6px color-mix(in srgb, ${tokens.warn} 20%, transparent)`,
|
||||
down: `0 0 6px color-mix(in srgb, ${tokens.down} 20%, transparent)`,
|
||||
empty: "none",
|
||||
};
|
||||
|
||||
const barBg: Record<BoldBarKind, string> = {
|
||||
up: tokens.up,
|
||||
down: tokens.down,
|
||||
empty: tokens.border,
|
||||
};
|
||||
|
||||
const gaugeFillBg: Record<BoldGaugeFill, string> = {
|
||||
ok: `linear-gradient(90deg, ${tokens.up}, color-mix(in srgb, ${tokens.up} 70%, #ffffff 30%))`,
|
||||
warm: `linear-gradient(90deg, ${tokens.warn}, #fbbf24)`,
|
||||
hot: `linear-gradient(90deg, ${tokens.down}, #fb7185)`,
|
||||
};
|
||||
|
||||
const heroBgLight = `linear-gradient(135deg, color-mix(in srgb, ${tokens.up} 8%, transparent), transparent 60%), ${tokens.surface}`;
|
||||
const heroBgDark = `linear-gradient(135deg, color-mix(in srgb, ${tokens.up} 16%, transparent), transparent 60%), ${tokens.surface}`;
|
||||
|
||||
const heroGlowLight = `radial-gradient(400px 200px at 20% 30%, color-mix(in srgb, ${tokens.up} 14%, transparent), transparent 60%), radial-gradient(300px 200px at 90% 80%, color-mix(in srgb, ${tokens.upStrong} 10%, transparent), transparent 60%)`;
|
||||
const heroGlowDark = `radial-gradient(400px 200px at 20% 30%, color-mix(in srgb, ${tokens.up} 32%, transparent), transparent 60%), radial-gradient(300px 200px at 90% 80%, color-mix(in srgb, ${tokens.upStrong} 22%, transparent), transparent 60%)`;
|
||||
|
||||
return {
|
||||
page: {
|
||||
flex: "1 0 auto",
|
||||
maxWidth: 1040,
|
||||
width: "100%",
|
||||
mx: "auto",
|
||||
p: "56px 24px 96px",
|
||||
fontFamily: BOLD_SANS_STACK,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.55,
|
||||
color: tokens.text,
|
||||
WebkitFontSmoothing: "antialiased",
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
mb: "36px",
|
||||
},
|
||||
brand: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "14px",
|
||||
fontWeight: 700,
|
||||
fontSize: 16,
|
||||
letterSpacing: "-0.01em",
|
||||
color: tokens.text,
|
||||
},
|
||||
logoConic: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: "12px",
|
||||
background: `conic-gradient(from 140deg, ${tokens.up}, ${tokens.upStrong}, #0f766e, ${tokens.up})`,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
color: "#05070a",
|
||||
fontWeight: 900,
|
||||
fontSize: 14,
|
||||
boxShadow: `0 8px 24px color-mix(in srgb, ${tokens.up} 30%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.2)`,
|
||||
},
|
||||
logoImg: { maxHeight: 36, maxWidth: 140, objectFit: "contain" },
|
||||
|
||||
hero: {
|
||||
position: "relative",
|
||||
borderRadius: "22px",
|
||||
padding: "36px 36px 32px",
|
||||
mb: "28px",
|
||||
background: isDark ? heroBgDark : heroBgLight,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
overflow: "hidden",
|
||||
"&::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: isDark ? heroGlowDark : heroGlowLight,
|
||||
pointerEvents: "none",
|
||||
opacity: 0.8,
|
||||
zIndex: 0,
|
||||
...(isDark ? { mixBlendMode: "screen" } : {}),
|
||||
},
|
||||
"& > *": { position: "relative", zIndex: 1 },
|
||||
},
|
||||
heroTitle: {
|
||||
m: 0,
|
||||
fontSize: 34,
|
||||
fontWeight: 800,
|
||||
letterSpacing: "-0.02em",
|
||||
lineHeight: 1.1,
|
||||
color: tokens.text,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
},
|
||||
heroCheck: (tone) => ({
|
||||
display: "inline-grid",
|
||||
placeItems: "center",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "50%",
|
||||
background: toneColor(tone, tokens),
|
||||
color: tone === "down" ? "#fff" : "#05070a",
|
||||
flexShrink: 0,
|
||||
boxShadow: `0 0 0 6px ${toneSoft(tone, tokens)}`,
|
||||
}),
|
||||
heroSub: { mt: "10px", mb: 0, color: tokens.textMuted, fontSize: 14 },
|
||||
|
||||
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "16px" },
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "999px",
|
||||
background: tokens.surface,
|
||||
p: "3px",
|
||||
gap: "2px",
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
border: 0,
|
||||
background: active ? tokens.upSoft : "transparent",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 11,
|
||||
padding: "8px 18px",
|
||||
cursor: "pointer",
|
||||
color: active ? tokens.up : tokens.textMuted,
|
||||
fontWeight: 700,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.12em",
|
||||
transition: "background 0.2s, color 0.2s",
|
||||
borderRadius: "999px",
|
||||
"&:hover": { color: active ? tokens.up : tokens.text },
|
||||
}),
|
||||
|
||||
monitorList: {
|
||||
listStyle: "none",
|
||||
m: 0,
|
||||
p: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "14px",
|
||||
},
|
||||
card: {
|
||||
background: tokens.surface,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "18px",
|
||||
overflow: "hidden",
|
||||
transition: "transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1), border-color 0.2s",
|
||||
"&:hover": {
|
||||
transform: "translateY(-3px)",
|
||||
borderColor: `color-mix(in srgb, ${tokens.up} 40%, ${tokens.border})`,
|
||||
},
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
gap: "24px",
|
||||
p: "22px 28px",
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
monitorName: {
|
||||
fontWeight: 700,
|
||||
fontSize: 17,
|
||||
letterSpacing: "-0.01em",
|
||||
color: tokens.text,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
mt: "6px",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
pill: {
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.1em",
|
||||
color: tokens.textMuted,
|
||||
background: `color-mix(in srgb, ${tokens.surface} 80%, ${tokens.bg})`,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
padding: "3px 10px",
|
||||
borderRadius: "999px",
|
||||
fontWeight: 700,
|
||||
},
|
||||
pillHardware: {
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.1em",
|
||||
padding: "3px 10px",
|
||||
borderRadius: "999px",
|
||||
fontWeight: 700,
|
||||
color: tokens.up,
|
||||
borderColor: `color-mix(in srgb, ${tokens.up} 40%, transparent)`,
|
||||
border: `1px solid color-mix(in srgb, ${tokens.up} 40%, transparent)`,
|
||||
background: tokens.upSoft,
|
||||
},
|
||||
monitorUrl: {
|
||||
fontSize: 12,
|
||||
color: tokens.textMuted,
|
||||
fontFamily: MONO_STACK,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: 300,
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
fontSize: 11,
|
||||
fontWeight: 800,
|
||||
padding: "6px 14px",
|
||||
borderRadius: "999px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "7px",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
whiteSpace: "nowrap",
|
||||
background: toneSoft(tone, tokens),
|
||||
color: toneColor(tone, tokens),
|
||||
"&::before": {
|
||||
content: '""',
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
background: "currentColor",
|
||||
boxShadow: "0 0 8px currentColor",
|
||||
},
|
||||
}),
|
||||
|
||||
heatmap: {
|
||||
padding: "0 28px 22px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
height: 48,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
borderRadius: "3px",
|
||||
background: heatCellBg[kind],
|
||||
boxShadow: heatCellShadow[kind],
|
||||
opacity: kind === "empty" ? 0.4 : 1,
|
||||
transition: "transform 0.15s",
|
||||
"&:hover": { transform: "scaleY(1.2)" },
|
||||
}),
|
||||
|
||||
histogram: {
|
||||
padding: "0 28px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
alignItems: "flex-end",
|
||||
height: 48,
|
||||
},
|
||||
bar: (kind, heightPct) => ({
|
||||
background: barBg[kind],
|
||||
borderRadius: "3px",
|
||||
minHeight: 4,
|
||||
opacity: kind === "empty" ? 0.4 : 1,
|
||||
height: `${heightPct}%`,
|
||||
}),
|
||||
chartStats: {
|
||||
padding: "0 28px 22px",
|
||||
fontSize: 11,
|
||||
color: tokens.textMuted,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
fontWeight: 700,
|
||||
letterSpacing: "0.05em",
|
||||
},
|
||||
|
||||
infra: {
|
||||
padding: "18px 28px 26px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", md: "repeat(3, 1fr)" },
|
||||
gap: "16px",
|
||||
},
|
||||
infraEmpty: {
|
||||
padding: "18px 28px 26px",
|
||||
color: tokens.textMuted,
|
||||
fontSize: 13,
|
||||
},
|
||||
gauge: {
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "16px",
|
||||
p: "16px 18px",
|
||||
background: `color-mix(in srgb, ${tokens.surface} 80%, ${tokens.bg})`,
|
||||
},
|
||||
gaugeLabel: {
|
||||
fontSize: 10,
|
||||
color: tokens.textMuted,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.12em",
|
||||
fontWeight: 700,
|
||||
},
|
||||
gaugeValue: {
|
||||
fontSize: 30,
|
||||
fontWeight: 800,
|
||||
letterSpacing: "-0.02em",
|
||||
mt: "6px",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
color: tokens.text,
|
||||
},
|
||||
gaugeBar: {
|
||||
height: 6,
|
||||
background: tokens.border,
|
||||
borderRadius: "3px",
|
||||
overflow: "hidden",
|
||||
mt: "12px",
|
||||
},
|
||||
gaugeFill: (level, widthPct) => ({
|
||||
display: "block",
|
||||
height: "100%",
|
||||
background: gaugeFillBg[level],
|
||||
borderRadius: "3px",
|
||||
transition: "width 0.9s cubic-bezier(0.2, 0.8, 0.2, 1)",
|
||||
width: `${Math.max(0, Math.min(100, widthPct))}%`,
|
||||
}),
|
||||
gaugeSub: {
|
||||
fontSize: 11,
|
||||
color: tokens.textMuted,
|
||||
mt: "10px",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
},
|
||||
|
||||
footer: {
|
||||
textAlign: "center",
|
||||
color: tokens.textMuted,
|
||||
fontSize: 12,
|
||||
mt: "52px",
|
||||
"& a": {
|
||||
color: tokens.up,
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: "3px",
|
||||
fontWeight: 800,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SlotProps } from "@/Pages/StatusPage/Status/themes/shared/BaseStatusPage";
|
||||
import type { EditorialStyles } from "@/Pages/StatusPage/Status/themes/editorial/styles";
|
||||
|
||||
export const EditorialHeader = ({
|
||||
statusPage,
|
||||
logoSrc,
|
||||
styles,
|
||||
}: SlotProps<EditorialStyles>) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box sx={styles.brandWrap}>
|
||||
{logoSrc && (
|
||||
<Box
|
||||
component="img"
|
||||
src={logoSrc}
|
||||
alt={statusPage.companyName}
|
||||
sx={styles.logoImg}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
component="h2"
|
||||
sx={styles.brandEyebrow}
|
||||
>
|
||||
{statusPage.companyName}
|
||||
</Box>
|
||||
<Box
|
||||
component="h1"
|
||||
sx={styles.brandTitle}
|
||||
>
|
||||
{t("pages.statusPages.editorial.reportTitle")}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SlotProps } from "@/Pages/StatusPage/Status/themes/shared/BaseStatusPage";
|
||||
import type { EditorialStyles } from "@/Pages/StatusPage/Status/themes/editorial/styles";
|
||||
|
||||
export const EditorialHero = ({
|
||||
overall,
|
||||
monitorCount,
|
||||
styles,
|
||||
}: SlotProps<EditorialStyles>) => {
|
||||
const { t } = useTranslation();
|
||||
const todayLabel = useMemo(
|
||||
() =>
|
||||
new Date().toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.statusLine}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.statusDot(overall.tone)}
|
||||
/>
|
||||
{overall.message}
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.dateline}
|
||||
>
|
||||
{todayLabel} ·{" "}
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,341 +0,0 @@
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
import type { StatusPageThemeTokens } from "../tokens";
|
||||
import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus";
|
||||
import {
|
||||
EDITORIAL_SECONDARY_SANS_STACK,
|
||||
MONO_STACK,
|
||||
SERIF_STACK,
|
||||
} from "../shared/fontStacks";
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
|
||||
export type EditorialHeatCell = "fast" | "med" | "slow" | "down" | "empty";
|
||||
export type EditorialBarKind = "up" | "down" | "empty";
|
||||
export type EditorialGaugeFill = "ok" | "warm" | "hot";
|
||||
|
||||
export interface EditorialStyles {
|
||||
page: SxProps<Theme>;
|
||||
top: SxProps<Theme>;
|
||||
brandWrap: SxProps<Theme>;
|
||||
logoImg: SxProps<Theme>;
|
||||
brandEyebrow: SxProps<Theme>;
|
||||
brandTitle: SxProps<Theme>;
|
||||
statusLine: SxProps<Theme>;
|
||||
statusDot: (tone: OverallTone) => SxProps<Theme>;
|
||||
dateline: SxProps<Theme>;
|
||||
chartSwitchWrap: SxProps<Theme>;
|
||||
chartSwitch: SxProps<Theme>;
|
||||
chartSwitchButton: (active: boolean) => SxProps<Theme>;
|
||||
monitorList: SxProps<Theme>;
|
||||
card: SxProps<Theme>;
|
||||
cardRow: SxProps<Theme>;
|
||||
cardLeft: SxProps<Theme>;
|
||||
monitorName: SxProps<Theme>;
|
||||
monitorMeta: SxProps<Theme>;
|
||||
pill: SxProps<Theme>;
|
||||
pillHardware: SxProps<Theme>;
|
||||
monitorUrl: SxProps<Theme>;
|
||||
badge: (tone: OverallTone) => SxProps<Theme>;
|
||||
heatmap: SxProps<Theme>;
|
||||
heatmapCell: (kind: EditorialHeatCell) => SxProps<Theme>;
|
||||
histogram: SxProps<Theme>;
|
||||
bar: (kind: EditorialBarKind, heightPct: number) => SxProps<Theme>;
|
||||
chartStats: SxProps<Theme>;
|
||||
infra: SxProps<Theme>;
|
||||
infraEmpty: SxProps<Theme>;
|
||||
gauge: SxProps<Theme>;
|
||||
gaugeLabel: SxProps<Theme>;
|
||||
gaugeValue: SxProps<Theme>;
|
||||
gaugeBar: SxProps<Theme>;
|
||||
gaugeFill: (level: EditorialGaugeFill, widthPct: number) => SxProps<Theme>;
|
||||
gaugeSub: SxProps<Theme>;
|
||||
footer: SxProps<Theme>;
|
||||
}
|
||||
|
||||
export const editorialStyles = (
|
||||
tokens: StatusPageThemeTokens,
|
||||
_isDark: boolean
|
||||
): EditorialStyles => {
|
||||
const heatCellBg: Record<EditorialHeatCell, string> = {
|
||||
fast: tokens.up,
|
||||
med: `color-mix(in srgb, ${tokens.up} 60%, ${tokens.bg})`,
|
||||
slow: tokens.warn,
|
||||
down: tokens.down,
|
||||
empty: tokens.border,
|
||||
};
|
||||
|
||||
const barBg: Record<EditorialBarKind, string> = {
|
||||
up: tokens.up,
|
||||
down: tokens.down,
|
||||
empty: tokens.border,
|
||||
};
|
||||
|
||||
const gaugeFillBg: Record<EditorialGaugeFill, string> = {
|
||||
ok: tokens.up,
|
||||
warm: tokens.warn,
|
||||
hot: tokens.down,
|
||||
};
|
||||
|
||||
return {
|
||||
page: {
|
||||
flex: "1 0 auto",
|
||||
maxWidth: 760,
|
||||
width: "100%",
|
||||
mx: "auto",
|
||||
p: "64px 24px 96px",
|
||||
fontFamily: SERIF_STACK,
|
||||
fontSize: 15,
|
||||
lineHeight: 1.6,
|
||||
color: tokens.text,
|
||||
WebkitFontSmoothing: "antialiased",
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "space-between",
|
||||
mb: "48px",
|
||||
borderBottom: `2px solid ${tokens.text}`,
|
||||
pb: "18px",
|
||||
},
|
||||
brandWrap: { display: "flex", flexDirection: "column" },
|
||||
logoImg: { maxHeight: 48, maxWidth: 160, objectFit: "contain", mb: "8px" },
|
||||
brandEyebrow: {
|
||||
m: 0,
|
||||
fontSize: 11,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.3em",
|
||||
fontWeight: 700,
|
||||
color: tokens.textMuted,
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
},
|
||||
brandTitle: {
|
||||
mt: "6px",
|
||||
mb: 0,
|
||||
fontSize: 40,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.02em",
|
||||
lineHeight: 1,
|
||||
color: tokens.text,
|
||||
},
|
||||
|
||||
statusLine: {
|
||||
fontSize: 22,
|
||||
fontWeight: 400,
|
||||
m: 0,
|
||||
mb: "8px",
|
||||
letterSpacing: "-0.01em",
|
||||
lineHeight: 1.35,
|
||||
color: tokens.text,
|
||||
fontFamily: SERIF_STACK,
|
||||
},
|
||||
statusDot: (tone) => ({
|
||||
display: "inline-block",
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: "50%",
|
||||
background: toneColor(tone, tokens),
|
||||
mr: "10px",
|
||||
verticalAlign: "middle",
|
||||
}),
|
||||
dateline: {
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
fontSize: 11,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.12em",
|
||||
color: tokens.textMuted,
|
||||
m: 0,
|
||||
mb: "56px",
|
||||
},
|
||||
|
||||
chartSwitchWrap: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
mb: "20px",
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
},
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
border: `1px solid ${tokens.text}`,
|
||||
"& > button + button": { borderLeft: `1px solid ${tokens.text}` },
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
fontFamily: "inherit",
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.2em",
|
||||
padding: "6px 14px",
|
||||
border: 0,
|
||||
background: active ? tokens.text : "transparent",
|
||||
color: active ? tokens.bg : tokens.text,
|
||||
cursor: "pointer",
|
||||
fontWeight: 700,
|
||||
}),
|
||||
|
||||
monitorList: {
|
||||
listStyle: "none",
|
||||
m: 0,
|
||||
p: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
card: {
|
||||
background: "transparent",
|
||||
border: 0,
|
||||
borderBottom: `1px solid ${tokens.border}`,
|
||||
mb: 0,
|
||||
py: "20px",
|
||||
"&:last-of-type": { borderBottom: 0 },
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "baseline",
|
||||
gap: "20px",
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
monitorName: {
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.015em",
|
||||
color: tokens.text,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
fontSize: 12,
|
||||
color: tokens.textMuted,
|
||||
mt: "6px",
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
},
|
||||
pill: {
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.12em",
|
||||
fontWeight: 700,
|
||||
color: tokens.textMuted,
|
||||
},
|
||||
pillHardware: {
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.12em",
|
||||
fontWeight: 700,
|
||||
color: tokens.up,
|
||||
},
|
||||
monitorUrl: {
|
||||
fontFamily: MONO_STACK,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: 280,
|
||||
fontSize: 12,
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
padding: "4px 10px",
|
||||
borderRadius: 0,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.15em",
|
||||
whiteSpace: "nowrap",
|
||||
background: toneSoft(tone, tokens),
|
||||
color: toneColor(tone, tokens),
|
||||
}),
|
||||
|
||||
heatmap: {
|
||||
mt: "16px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "2px",
|
||||
height: 40,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
background: heatCellBg[kind],
|
||||
opacity: kind === "empty" ? 0.5 : 1,
|
||||
}),
|
||||
|
||||
histogram: {
|
||||
mt: "16px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "2px",
|
||||
alignItems: "flex-end",
|
||||
height: 40,
|
||||
},
|
||||
bar: (kind, heightPct) => ({
|
||||
background: barBg[kind],
|
||||
minHeight: 3,
|
||||
opacity: kind === "empty" ? 0.5 : 1,
|
||||
height: `${heightPct}%`,
|
||||
}),
|
||||
chartStats: {
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
fontSize: 11,
|
||||
color: tokens.textMuted,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.1em",
|
||||
},
|
||||
|
||||
infra: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", md: "repeat(3, 1fr)" },
|
||||
gap: "24px",
|
||||
mt: "20px",
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
},
|
||||
infraEmpty: { mt: "20px", color: tokens.textMuted, fontSize: 13 },
|
||||
gauge: {},
|
||||
gaugeLabel: {
|
||||
fontSize: 10,
|
||||
color: tokens.textMuted,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.18em",
|
||||
fontWeight: 700,
|
||||
},
|
||||
gaugeValue: {
|
||||
fontSize: 28,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.02em",
|
||||
mt: "4px",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
fontFamily: SERIF_STACK,
|
||||
color: tokens.text,
|
||||
},
|
||||
gaugeBar: { height: 2, background: tokens.border, mt: "10px" },
|
||||
gaugeFill: (level, widthPct) => ({
|
||||
display: "block",
|
||||
height: "100%",
|
||||
background: gaugeFillBg[level],
|
||||
transition: "width 0.8s",
|
||||
width: `${Math.max(0, Math.min(100, widthPct))}%`,
|
||||
}),
|
||||
gaugeSub: {
|
||||
fontSize: 11,
|
||||
color: tokens.textMuted,
|
||||
mt: "8px",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
},
|
||||
|
||||
footer: {
|
||||
textAlign: "center",
|
||||
color: tokens.textMuted,
|
||||
fontSize: 11,
|
||||
mt: "56px",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.2em",
|
||||
fontFamily: EDITORIAL_SECONDARY_SANS_STACK,
|
||||
"& a": {
|
||||
color: tokens.text,
|
||||
textDecoration: "none",
|
||||
borderBottom: `1px solid ${tokens.text}`,
|
||||
pb: "2px",
|
||||
fontWeight: 700,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { monoFirstChar } from "@/Pages/StatusPage/Status/themes/shared/overallStatus";
|
||||
import type { SlotProps } from "@/Pages/StatusPage/Status/themes/shared/BaseStatusPage";
|
||||
import type { ModernStyles } from "@/Pages/StatusPage/Status/themes/modern/styles";
|
||||
|
||||
export const ModernHeader = ({
|
||||
statusPage,
|
||||
logoSrc,
|
||||
styles,
|
||||
}: SlotProps<ModernStyles>) => (
|
||||
<Box sx={styles.brand}>
|
||||
{logoSrc ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={logoSrc}
|
||||
alt={statusPage.companyName}
|
||||
sx={styles.logoImg}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={styles.logoGrad}>{monoFirstChar(statusPage.companyName)}</Box>
|
||||
)}
|
||||
{statusPage.companyName}
|
||||
</Box>
|
||||
);
|
||||
@@ -1,34 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SlotProps } from "@/Pages/StatusPage/Status/themes/shared/BaseStatusPage";
|
||||
import type { ModernStyles } from "@/Pages/StatusPage/Status/themes/modern/styles";
|
||||
|
||||
export const ModernHero = ({
|
||||
overall,
|
||||
monitorCount,
|
||||
styles,
|
||||
}: SlotProps<ModernStyles>) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box sx={styles.hero}>
|
||||
<Box sx={styles.pulse(overall.tone)} />
|
||||
<Box sx={styles.statusCopy}>
|
||||
<Box
|
||||
component="h1"
|
||||
sx={styles.heroTitle}
|
||||
>
|
||||
{overall.message}
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,429 +0,0 @@
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
import { keyframes } from "@mui/system";
|
||||
import type { StatusPageThemeTokens } from "../tokens";
|
||||
import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus";
|
||||
import { MONO_STACK, SANS_STACK } from "../shared/fontStacks";
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
|
||||
export type ModernHeatCell = "fast" | "med" | "slow" | "down" | "empty";
|
||||
export type ModernBarKind = "up" | "down" | "empty";
|
||||
export type ModernGaugeFill = "ok" | "warm" | "hot";
|
||||
|
||||
export interface ModernStyles {
|
||||
page: SxProps<Theme>;
|
||||
top: SxProps<Theme>;
|
||||
brand: SxProps<Theme>;
|
||||
logoGrad: SxProps<Theme>;
|
||||
logoImg: SxProps<Theme>;
|
||||
hero: SxProps<Theme>;
|
||||
pulse: (tone: OverallTone) => SxProps<Theme>;
|
||||
statusCopy: SxProps<Theme>;
|
||||
heroTitle: SxProps<Theme>;
|
||||
heroSub: SxProps<Theme>;
|
||||
heroIcon: (tone: OverallTone) => SxProps<Theme>;
|
||||
chartSwitchWrap: SxProps<Theme>;
|
||||
chartSwitch: SxProps<Theme>;
|
||||
chartSwitchButton: (active: boolean) => SxProps<Theme>;
|
||||
monitorList: SxProps<Theme>;
|
||||
card: SxProps<Theme>;
|
||||
cardRow: SxProps<Theme>;
|
||||
cardLeft: SxProps<Theme>;
|
||||
monitorName: SxProps<Theme>;
|
||||
monitorMeta: SxProps<Theme>;
|
||||
pill: SxProps<Theme>;
|
||||
pillHardware: SxProps<Theme>;
|
||||
monitorUrl: SxProps<Theme>;
|
||||
badge: (tone: OverallTone) => SxProps<Theme>;
|
||||
heatmap: SxProps<Theme>;
|
||||
heatmapCell: (kind: ModernHeatCell) => SxProps<Theme>;
|
||||
histogram: SxProps<Theme>;
|
||||
bar: (kind: ModernBarKind, heightPct: number) => SxProps<Theme>;
|
||||
chartStats: SxProps<Theme>;
|
||||
infra: SxProps<Theme>;
|
||||
infraEmpty: SxProps<Theme>;
|
||||
gauge: SxProps<Theme>;
|
||||
gaugeLabel: SxProps<Theme>;
|
||||
gaugeValue: SxProps<Theme>;
|
||||
gaugeBar: SxProps<Theme>;
|
||||
gaugeFill: (level: ModernGaugeFill, widthPct: number) => SxProps<Theme>;
|
||||
gaugeSub: SxProps<Theme>;
|
||||
footer: SxProps<Theme>;
|
||||
}
|
||||
|
||||
const lightShadow =
|
||||
"0 2px 4px rgba(16, 24, 40, 0.04), 0 12px 32px rgba(16, 24, 40, 0.06)";
|
||||
const darkShadow = "0 2px 6px rgba(0, 0, 0, 0.5), 0 20px 40px rgba(0, 0, 0, 0.4)";
|
||||
const lightShadowHover =
|
||||
"0 4px 8px rgba(16, 24, 40, 0.05), 0 16px 40px rgba(16, 24, 40, 0.08)";
|
||||
const darkShadowHover = "0 4px 10px rgba(0, 0, 0, 0.55), 0 24px 48px rgba(0, 0, 0, 0.45)";
|
||||
|
||||
const pillBase = {
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: "0.08em",
|
||||
padding: "3px 9px",
|
||||
borderRadius: "999px",
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
const pulseKeyframes = keyframes`
|
||||
0% { transform: scale(1); opacity: 0.5; }
|
||||
100% { transform: scale(2.4); opacity: 0; }
|
||||
`;
|
||||
|
||||
const fadeInKeyframes = keyframes`
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
`;
|
||||
|
||||
export const modernStyles = (
|
||||
tokens: StatusPageThemeTokens,
|
||||
isDark: boolean
|
||||
): ModernStyles => {
|
||||
const cardShadow = isDark ? darkShadow : lightShadow;
|
||||
const cardHoverShadow = isDark ? darkShadowHover : lightShadowHover;
|
||||
|
||||
const heatCellBg: Record<ModernHeatCell, string> = {
|
||||
fast: `linear-gradient(180deg, ${tokens.up}, ${tokens.upStrong})`,
|
||||
med: `linear-gradient(180deg, color-mix(in srgb, ${tokens.up} 70%, #ffffff 30%), ${tokens.up})`,
|
||||
slow: `linear-gradient(180deg, ${tokens.warn}, ${tokens.degraded})`,
|
||||
down: `linear-gradient(180deg, ${tokens.down}, color-mix(in srgb, ${tokens.down} 70%, #000000 30%))`,
|
||||
empty: tokens.border,
|
||||
};
|
||||
|
||||
const barBg: Record<ModernBarKind, string> = {
|
||||
up: tokens.up,
|
||||
down: tokens.down,
|
||||
empty: tokens.border,
|
||||
};
|
||||
|
||||
const gaugeFillBg: Record<ModernGaugeFill, string> = {
|
||||
ok: `linear-gradient(90deg, ${tokens.up}, ${tokens.upStrong})`,
|
||||
warm: `linear-gradient(90deg, ${tokens.warn}, ${tokens.degraded})`,
|
||||
hot: `linear-gradient(90deg, ${tokens.down}, color-mix(in srgb, ${tokens.down} 70%, #000000 30%))`,
|
||||
};
|
||||
|
||||
return {
|
||||
page: {
|
||||
flex: "1 0 auto",
|
||||
maxWidth: 980,
|
||||
width: "100%",
|
||||
mx: "auto",
|
||||
p: "56px 20px 80px",
|
||||
fontFamily: SANS_STACK,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.5,
|
||||
color: tokens.text,
|
||||
WebkitFontSmoothing: "antialiased",
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
mb: "32px",
|
||||
},
|
||||
brand: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
fontWeight: 700,
|
||||
fontSize: 15,
|
||||
letterSpacing: "-0.01em",
|
||||
color: tokens.text,
|
||||
},
|
||||
logoGrad: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "10px",
|
||||
background: `linear-gradient(135deg, ${tokens.up}, ${tokens.upStrong})`,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
color: "#fff",
|
||||
fontWeight: 700,
|
||||
fontSize: 14,
|
||||
boxShadow: `0 4px 10px color-mix(in srgb, ${tokens.up} 40%, transparent)`,
|
||||
},
|
||||
logoImg: { maxHeight: 32, maxWidth: 120, objectFit: "contain" },
|
||||
|
||||
hero: {
|
||||
position: "relative",
|
||||
borderRadius: "20px",
|
||||
padding: "28px 32px",
|
||||
mb: "24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
background: tokens.surface,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
boxShadow: cardShadow,
|
||||
},
|
||||
pulse: (tone) => {
|
||||
const c = toneColor(tone, tokens);
|
||||
return {
|
||||
position: "relative",
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: "50%",
|
||||
background: c,
|
||||
color: c,
|
||||
flexShrink: 0,
|
||||
"&::before, &::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
borderRadius: "50%",
|
||||
background: "currentColor",
|
||||
opacity: 0.5,
|
||||
animation: `${pulseKeyframes} 2s ease-out infinite`,
|
||||
},
|
||||
"&::after": { animationDelay: "1s" },
|
||||
"@media (prefers-reduced-motion: reduce)": {
|
||||
"&::before, &::after": { animation: "none" },
|
||||
},
|
||||
};
|
||||
},
|
||||
statusCopy: { flex: 1, minWidth: 0 },
|
||||
heroTitle: {
|
||||
m: 0,
|
||||
mb: "4px",
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.02em",
|
||||
color: tokens.text,
|
||||
},
|
||||
heroSub: { m: 0, color: tokens.textMuted, fontSize: 13 },
|
||||
heroIcon: (tone) => ({
|
||||
color: toneColor(tone, tokens),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}),
|
||||
|
||||
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "14px" },
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "999px",
|
||||
background: tokens.surface,
|
||||
p: "3px",
|
||||
gap: "2px",
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
border: 0,
|
||||
background: active ? tokens.upSoft : "transparent",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 11,
|
||||
padding: "6px 16px",
|
||||
cursor: "pointer",
|
||||
color: active ? tokens.up : tokens.textMuted,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
transition: "background 0.2s, color 0.2s",
|
||||
borderRadius: "999px",
|
||||
"&:hover": { color: active ? tokens.up : tokens.text },
|
||||
}),
|
||||
|
||||
monitorList: {
|
||||
listStyle: "none",
|
||||
m: 0,
|
||||
p: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "14px",
|
||||
},
|
||||
card: {
|
||||
background: tokens.surface,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "16px",
|
||||
boxShadow: cardShadow,
|
||||
overflow: "hidden",
|
||||
transition: "transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.2s",
|
||||
opacity: 0,
|
||||
transform: "translateY(8px)",
|
||||
animation: `${fadeInKeyframes} 0.5s forwards`,
|
||||
"&:nth-of-type(1)": { animationDelay: "0.05s" },
|
||||
"&:nth-of-type(2)": { animationDelay: "0.1s" },
|
||||
"&:nth-of-type(3)": { animationDelay: "0.15s" },
|
||||
"&:nth-of-type(4)": { animationDelay: "0.2s" },
|
||||
"&:nth-of-type(5)": { animationDelay: "0.25s" },
|
||||
"&:nth-of-type(n+6)": { animationDelay: "0.3s" },
|
||||
"&:hover": { transform: "translateY(-2px)", boxShadow: cardHoverShadow },
|
||||
"@media (prefers-reduced-motion: reduce)": {
|
||||
animation: "none",
|
||||
opacity: 1,
|
||||
transform: "none",
|
||||
},
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
p: "18px 24px",
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
monitorName: {
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
letterSpacing: "-0.005em",
|
||||
color: tokens.text,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
mt: "6px",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
pill: {
|
||||
...pillBase,
|
||||
color: tokens.textMuted,
|
||||
background: tokens.bg,
|
||||
},
|
||||
pillHardware: {
|
||||
...pillBase,
|
||||
color: tokens.up,
|
||||
background: tokens.upSoft,
|
||||
},
|
||||
monitorUrl: {
|
||||
fontSize: 12,
|
||||
color: tokens.textMuted,
|
||||
fontFamily: MONO_STACK,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: 280,
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
padding: "5px 12px",
|
||||
borderRadius: "999px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
whiteSpace: "nowrap",
|
||||
background: toneSoft(tone, tokens),
|
||||
color: toneColor(tone, tokens),
|
||||
"&::before": {
|
||||
content: '""',
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
background: "currentColor",
|
||||
},
|
||||
}),
|
||||
|
||||
heatmap: {
|
||||
padding: "0 24px 20px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
height: 46,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
borderRadius: "3px",
|
||||
background: heatCellBg[kind],
|
||||
opacity: kind === "empty" ? 0.4 : 1,
|
||||
transition: "transform 0.15s",
|
||||
"&:hover": { transform: "scaleY(1.2)" },
|
||||
}),
|
||||
|
||||
histogram: {
|
||||
padding: "0 24px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
alignItems: "flex-end",
|
||||
height: 46,
|
||||
},
|
||||
bar: (kind, heightPct) => ({
|
||||
background: barBg[kind],
|
||||
borderRadius: "3px",
|
||||
minHeight: 4,
|
||||
opacity: kind === "empty" ? 0.4 : 1,
|
||||
height: `${heightPct}%`,
|
||||
transition: "transform 0.15s",
|
||||
"&:hover": { transform: "scaleY(1.08)" },
|
||||
}),
|
||||
chartStats: {
|
||||
padding: "0 24px 20px",
|
||||
fontSize: 11,
|
||||
color: tokens.textMuted,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
fontWeight: 600,
|
||||
},
|
||||
|
||||
infra: {
|
||||
padding: "16px 24px 22px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", md: "repeat(3, 1fr)" },
|
||||
gap: "14px",
|
||||
},
|
||||
infraEmpty: {
|
||||
padding: "16px 24px 22px",
|
||||
color: tokens.textMuted,
|
||||
fontSize: 13,
|
||||
},
|
||||
gauge: {
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "14px",
|
||||
p: "14px 16px",
|
||||
background: `linear-gradient(135deg, ${tokens.bg}, ${tokens.surface})`,
|
||||
},
|
||||
gaugeLabel: {
|
||||
fontSize: 10,
|
||||
color: tokens.textMuted,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.1em",
|
||||
fontWeight: 700,
|
||||
},
|
||||
gaugeValue: {
|
||||
fontSize: 26,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.02em",
|
||||
mt: "4px",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
color: tokens.text,
|
||||
},
|
||||
gaugeBar: {
|
||||
height: 6,
|
||||
background: tokens.border,
|
||||
borderRadius: "3px",
|
||||
overflow: "hidden",
|
||||
mt: "10px",
|
||||
},
|
||||
gaugeFill: (level, widthPct) => ({
|
||||
display: "block",
|
||||
height: "100%",
|
||||
background: gaugeFillBg[level],
|
||||
borderRadius: "3px",
|
||||
transition: "width 0.8s cubic-bezier(0.2, 0.8, 0.2, 1)",
|
||||
width: `${Math.max(0, Math.min(100, widthPct))}%`,
|
||||
}),
|
||||
gaugeSub: {
|
||||
fontSize: 11,
|
||||
color: tokens.textMuted,
|
||||
mt: "8px",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
},
|
||||
|
||||
footer: {
|
||||
textAlign: "center",
|
||||
color: tokens.textMuted,
|
||||
fontSize: 12,
|
||||
mt: "48px",
|
||||
"& a": {
|
||||
color: tokens.up,
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: "3px",
|
||||
fontWeight: 700,
|
||||
"&:hover": { color: tokens.upStrong },
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { monoFirstChar } from "@/Pages/StatusPage/Status/themes/shared/overallStatus";
|
||||
import type { SlotProps } from "@/Pages/StatusPage/Status/themes/shared/BaseStatusPage";
|
||||
import type { RefinedStyles } from "@/Pages/StatusPage/Status/themes/refined/styles";
|
||||
|
||||
export const RefinedHeader = ({
|
||||
statusPage,
|
||||
logoSrc,
|
||||
styles,
|
||||
}: SlotProps<RefinedStyles>) => (
|
||||
<Box sx={styles.brand}>
|
||||
{logoSrc ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={logoSrc}
|
||||
alt={statusPage.companyName}
|
||||
sx={styles.logoImg}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={styles.logoMono}>{monoFirstChar(statusPage.companyName)}</Box>
|
||||
)}
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.company}
|
||||
>
|
||||
{statusPage.companyName}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -1,34 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SlotProps } from "@/Pages/StatusPage/Status/themes/shared/BaseStatusPage";
|
||||
import type { RefinedStyles } from "@/Pages/StatusPage/Status/themes/refined/styles";
|
||||
|
||||
export const RefinedHero = ({
|
||||
overall,
|
||||
monitorCount,
|
||||
styles,
|
||||
}: SlotProps<RefinedStyles>) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box sx={styles.hero}>
|
||||
<Box sx={styles.statusDot(overall.tone)} />
|
||||
<Box sx={styles.statusCopy}>
|
||||
<Box
|
||||
component="h1"
|
||||
sx={styles.heroTitle}
|
||||
>
|
||||
{overall.message}
|
||||
</Box>
|
||||
<Box
|
||||
component="p"
|
||||
sx={styles.heroSub}
|
||||
>
|
||||
{t("pages.statusPages.statusBar.monitoringSummary", {
|
||||
count: monitorCount,
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={styles.heroIcon(overall.tone)}>{overall.icon}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,368 +0,0 @@
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
import type { StatusPageThemeTokens } from "../tokens";
|
||||
import { type OverallTone, toneColor, toneSoft } from "../shared/overallStatus";
|
||||
import { MONO_STACK, SANS_STACK } from "../shared/fontStacks";
|
||||
|
||||
export type RefinedHeatCell = "fast" | "med" | "slow" | "down" | "empty";
|
||||
export type RefinedBarKind = "up" | "down" | "empty";
|
||||
export type RefinedGaugeFill = "ok" | "warm" | "hot";
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
|
||||
export interface RefinedStyles {
|
||||
page: SxProps<Theme>;
|
||||
top: SxProps<Theme>;
|
||||
brand: SxProps<Theme>;
|
||||
logoMono: SxProps<Theme>;
|
||||
logoImg: SxProps<Theme>;
|
||||
company: SxProps<Theme>;
|
||||
hero: SxProps<Theme>;
|
||||
statusDot: (tone: OverallTone) => SxProps<Theme>;
|
||||
statusCopy: SxProps<Theme>;
|
||||
heroTitle: SxProps<Theme>;
|
||||
heroSub: SxProps<Theme>;
|
||||
heroIcon: (tone: OverallTone) => SxProps<Theme>;
|
||||
chartSwitchWrap: SxProps<Theme>;
|
||||
chartSwitch: SxProps<Theme>;
|
||||
chartSwitchButton: (active: boolean) => SxProps<Theme>;
|
||||
monitorList: SxProps<Theme>;
|
||||
card: SxProps<Theme>;
|
||||
cardRow: SxProps<Theme>;
|
||||
cardLeft: SxProps<Theme>;
|
||||
monitorName: SxProps<Theme>;
|
||||
monitorMeta: SxProps<Theme>;
|
||||
pill: SxProps<Theme>;
|
||||
pillHardware: SxProps<Theme>;
|
||||
monitorUrl: SxProps<Theme>;
|
||||
badge: (tone: OverallTone) => SxProps<Theme>;
|
||||
heatmap: SxProps<Theme>;
|
||||
heatmapCell: (kind: RefinedHeatCell) => SxProps<Theme>;
|
||||
histogram: SxProps<Theme>;
|
||||
bar: (kind: RefinedBarKind, heightPct: number) => SxProps<Theme>;
|
||||
chartStats: SxProps<Theme>;
|
||||
infra: SxProps<Theme>;
|
||||
infraEmpty: SxProps<Theme>;
|
||||
gauge: SxProps<Theme>;
|
||||
gaugeLabel: SxProps<Theme>;
|
||||
gaugeValue: SxProps<Theme>;
|
||||
gaugeBar: SxProps<Theme>;
|
||||
gaugeFill: (level: RefinedGaugeFill, widthPct: number) => SxProps<Theme>;
|
||||
gaugeSub: SxProps<Theme>;
|
||||
footer: SxProps<Theme>;
|
||||
}
|
||||
|
||||
const cardShadow = "0 1px 2px rgba(10, 16, 32, 0.06), 0 6px 18px rgba(10, 16, 32, 0.08)";
|
||||
const cardShadowHover =
|
||||
"0 2px 4px rgba(16, 24, 40, 0.06), 0 10px 24px rgba(16, 24, 40, 0.06)";
|
||||
|
||||
const pillBase = {
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: "0.08em",
|
||||
padding: "2px 8px",
|
||||
borderRadius: "999px",
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
export const refinedStyles = (
|
||||
tokens: StatusPageThemeTokens,
|
||||
_isDark: boolean
|
||||
): RefinedStyles => {
|
||||
const heatCellBg: Record<RefinedHeatCell, string> = {
|
||||
fast: tokens.up,
|
||||
med: `color-mix(in srgb, ${tokens.up} 60%, #ffffff 40%)`,
|
||||
slow: tokens.warn,
|
||||
down: tokens.down,
|
||||
empty: tokens.border,
|
||||
};
|
||||
|
||||
const barBg: Record<RefinedBarKind, string> = {
|
||||
up: tokens.up,
|
||||
down: tokens.down,
|
||||
empty: tokens.border,
|
||||
};
|
||||
|
||||
const gaugeFillBg: Record<RefinedGaugeFill, string> = {
|
||||
ok: tokens.up,
|
||||
warm: tokens.warn,
|
||||
hot: tokens.down,
|
||||
};
|
||||
|
||||
return {
|
||||
page: {
|
||||
flex: "1 0 auto",
|
||||
maxWidth: 960,
|
||||
width: "100%",
|
||||
mx: "auto",
|
||||
p: "48px 20px 80px",
|
||||
fontFamily: SANS_STACK,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.5,
|
||||
color: tokens.text,
|
||||
WebkitFontSmoothing: "antialiased",
|
||||
},
|
||||
|
||||
top: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
mb: "28px",
|
||||
},
|
||||
brand: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
fontWeight: 600,
|
||||
letterSpacing: "-0.01em",
|
||||
color: tokens.text,
|
||||
},
|
||||
logoMono: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: "8px",
|
||||
background: tokens.up,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
color: "#fff",
|
||||
fontWeight: 700,
|
||||
fontSize: 13,
|
||||
},
|
||||
logoImg: { maxHeight: 32, maxWidth: 120, objectFit: "contain" },
|
||||
company: { fontSize: 14 },
|
||||
|
||||
hero: {
|
||||
background: tokens.surface,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: tokens.radius,
|
||||
padding: "22px 24px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
boxShadow: cardShadow,
|
||||
mb: "20px",
|
||||
},
|
||||
statusDot: (tone) => ({
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: "50%",
|
||||
background: toneColor(tone, tokens),
|
||||
boxShadow: `0 0 0 4px ${toneSoft(tone, tokens)}`,
|
||||
flexShrink: 0,
|
||||
}),
|
||||
statusCopy: { flex: 1, minWidth: 0 },
|
||||
heroTitle: {
|
||||
m: 0,
|
||||
mb: "2px",
|
||||
fontSize: 17,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "-0.01em",
|
||||
color: tokens.text,
|
||||
},
|
||||
heroSub: { m: 0, color: tokens.textMuted, fontSize: 13 },
|
||||
heroIcon: (tone) => ({
|
||||
color: toneColor(tone, tokens),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}),
|
||||
|
||||
chartSwitchWrap: { display: "flex", justifyContent: "flex-end", mb: "12px" },
|
||||
chartSwitch: {
|
||||
display: "inline-flex",
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "8px",
|
||||
background: tokens.surface,
|
||||
p: "3px",
|
||||
gap: "2px",
|
||||
},
|
||||
chartSwitchButton: (active) => ({
|
||||
border: 0,
|
||||
background: active ? tokens.upSoft : "transparent",
|
||||
fontFamily: "inherit",
|
||||
fontSize: 11,
|
||||
padding: "5px 14px",
|
||||
cursor: "pointer",
|
||||
color: active ? tokens.up : tokens.textMuted,
|
||||
borderRadius: "5px",
|
||||
transition: "background 0.15s ease, color 0.15s ease",
|
||||
fontWeight: active ? 600 : 500,
|
||||
"&:hover": { color: active ? tokens.up : tokens.text },
|
||||
}),
|
||||
|
||||
monitorList: {
|
||||
listStyle: "none",
|
||||
m: 0,
|
||||
p: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "12px",
|
||||
},
|
||||
card: {
|
||||
background: tokens.surface,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: tokens.radius,
|
||||
boxShadow: cardShadow,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
transition: "transform 0.15s, box-shadow 0.15s",
|
||||
"&:hover": { transform: "translateY(-1px)", boxShadow: cardShadowHover },
|
||||
},
|
||||
cardRow: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
p: "16px 20px",
|
||||
},
|
||||
cardLeft: { minWidth: 0 },
|
||||
monitorName: {
|
||||
fontWeight: 600,
|
||||
fontSize: 14,
|
||||
letterSpacing: "-0.005em",
|
||||
color: tokens.text,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
monitorMeta: {
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
mt: "4px",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
pill: {
|
||||
...pillBase,
|
||||
color: tokens.textMuted,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
},
|
||||
pillHardware: {
|
||||
...pillBase,
|
||||
color: tokens.up,
|
||||
border: `1px solid ${tokens.border}`,
|
||||
background: tokens.upSoft,
|
||||
},
|
||||
monitorUrl: {
|
||||
fontSize: 12,
|
||||
color: tokens.textMuted,
|
||||
fontFamily: MONO_STACK,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: 280,
|
||||
},
|
||||
|
||||
badge: (tone) => ({
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
padding: "4px 10px",
|
||||
borderRadius: "999px",
|
||||
whiteSpace: "nowrap",
|
||||
background: toneSoft(tone, tokens),
|
||||
color: toneColor(tone, tokens),
|
||||
}),
|
||||
|
||||
heatmap: {
|
||||
padding: "0 20px 16px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
height: 42,
|
||||
},
|
||||
heatmapCell: (kind) => ({
|
||||
borderRadius: "2px",
|
||||
background: heatCellBg[kind],
|
||||
opacity: kind === "empty" ? 0.4 : 1,
|
||||
transition: "transform 0.15s",
|
||||
"&:hover": { transform: "scaleY(1.15)" },
|
||||
}),
|
||||
|
||||
histogram: {
|
||||
padding: "0 20px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${MAX_RECENT_CHECKS}, 1fr)`,
|
||||
gap: "3px",
|
||||
alignItems: "flex-end",
|
||||
height: 42,
|
||||
},
|
||||
bar: (kind, heightPct) => ({
|
||||
background: barBg[kind],
|
||||
borderRadius: "2px",
|
||||
minHeight: 3,
|
||||
opacity: kind === "empty" ? 0.4 : 1,
|
||||
height: `${heightPct}%`,
|
||||
}),
|
||||
chartStats: {
|
||||
padding: "0 20px 16px",
|
||||
fontSize: 11,
|
||||
color: tokens.textMuted,
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
},
|
||||
|
||||
infra: {
|
||||
padding: "14px 20px 18px",
|
||||
display: "grid",
|
||||
gridTemplateColumns: { xs: "1fr", md: "repeat(3, 1fr)" },
|
||||
gap: "12px",
|
||||
},
|
||||
infraEmpty: {
|
||||
padding: "14px 20px 18px",
|
||||
color: tokens.textMuted,
|
||||
fontSize: 13,
|
||||
},
|
||||
gauge: {
|
||||
border: `1px solid ${tokens.border}`,
|
||||
borderRadius: "10px",
|
||||
p: "12px 14px",
|
||||
background: tokens.bg,
|
||||
},
|
||||
gaugeLabel: {
|
||||
fontSize: 11,
|
||||
color: tokens.textMuted,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
fontWeight: 600,
|
||||
},
|
||||
gaugeValue: {
|
||||
fontSize: 20,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "-0.01em",
|
||||
mt: "2px",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
color: tokens.text,
|
||||
},
|
||||
gaugeBar: {
|
||||
height: 4,
|
||||
background: tokens.border,
|
||||
borderRadius: "2px",
|
||||
overflow: "hidden",
|
||||
mt: "8px",
|
||||
},
|
||||
gaugeFill: (level, widthPct) => ({
|
||||
display: "block",
|
||||
height: "100%",
|
||||
background: gaugeFillBg[level],
|
||||
borderRadius: "2px",
|
||||
transition: "width 0.6s",
|
||||
width: `${Math.max(0, Math.min(100, widthPct))}%`,
|
||||
}),
|
||||
gaugeSub: {
|
||||
fontSize: 11,
|
||||
color: tokens.textMuted,
|
||||
mt: "6px",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
},
|
||||
|
||||
footer: {
|
||||
textAlign: "center",
|
||||
color: tokens.textMuted,
|
||||
fontSize: 12,
|
||||
mt: "40px",
|
||||
"& a": {
|
||||
color: tokens.up,
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: "3px",
|
||||
fontWeight: 600,
|
||||
"&:hover": { color: tokens.upStrong || tokens.up },
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,250 +0,0 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
import { getMonitorTypeLabel } from "@/Types/StatusPage";
|
||||
import type { StatusPageThemeTokens } from "@/Pages/StatusPage/Status/themes/tokens";
|
||||
import {
|
||||
ThemedHeatmap,
|
||||
type HeatCellKind,
|
||||
} from "@/Pages/StatusPage/Status/themes/shared/ThemedHeatmap";
|
||||
import {
|
||||
ThemedHistogram,
|
||||
type BarKind,
|
||||
} from "@/Pages/StatusPage/Status/themes/shared/ThemedHistogram";
|
||||
import {
|
||||
ThemedInfrastructure,
|
||||
type GaugeFillLevel,
|
||||
} from "@/Pages/StatusPage/Status/themes/shared/ThemedInfrastructure";
|
||||
import {
|
||||
type OverallStatus,
|
||||
type OverallTone,
|
||||
monitorBadgeTone,
|
||||
resolveOverallStatus,
|
||||
statusBadgeKey,
|
||||
} from "@/Pages/StatusPage/Status/themes/shared/overallStatus";
|
||||
import { useStatusPageTheme } from "@/Pages/StatusPage/Status/themes/StatusPageThemeProvider";
|
||||
|
||||
type StatusPageMonitor = Monitor & { checks?: Monitor["recentChecks"] };
|
||||
|
||||
export interface BaseStyles {
|
||||
page: SxProps<Theme>;
|
||||
top: SxProps<Theme>;
|
||||
chartSwitchWrap: SxProps<Theme>;
|
||||
chartSwitch: SxProps<Theme>;
|
||||
chartSwitchButton: (active: boolean) => SxProps<Theme>;
|
||||
monitorList: SxProps<Theme>;
|
||||
card: SxProps<Theme>;
|
||||
cardRow: SxProps<Theme>;
|
||||
cardLeft: SxProps<Theme>;
|
||||
monitorName: SxProps<Theme>;
|
||||
monitorMeta: SxProps<Theme>;
|
||||
pill: SxProps<Theme>;
|
||||
pillHardware: SxProps<Theme>;
|
||||
monitorUrl: SxProps<Theme>;
|
||||
badge: (tone: OverallTone) => SxProps<Theme>;
|
||||
heatmap: SxProps<Theme>;
|
||||
heatmapCell: (kind: HeatCellKind) => SxProps<Theme>;
|
||||
histogram: SxProps<Theme>;
|
||||
bar: (kind: BarKind, heightPct: number) => SxProps<Theme>;
|
||||
chartStats: SxProps<Theme>;
|
||||
infra: SxProps<Theme>;
|
||||
infraEmpty: SxProps<Theme>;
|
||||
gauge: SxProps<Theme>;
|
||||
gaugeLabel: SxProps<Theme>;
|
||||
gaugeValue: SxProps<Theme>;
|
||||
gaugeBar: SxProps<Theme>;
|
||||
gaugeFill: (level: GaugeFillLevel, widthPct: number) => SxProps<Theme>;
|
||||
gaugeSub: SxProps<Theme>;
|
||||
footer: SxProps<Theme>;
|
||||
}
|
||||
|
||||
export interface SlotProps<S extends BaseStyles = BaseStyles> {
|
||||
statusPage: StatusPage;
|
||||
logoSrc: string | null;
|
||||
overall: OverallStatus;
|
||||
monitorCount: number;
|
||||
styles: S;
|
||||
}
|
||||
|
||||
export interface ThemeConfig<S extends BaseStyles = BaseStyles> {
|
||||
createStyles: (tokens: StatusPageThemeTokens, isDark: boolean) => S;
|
||||
HeaderSlot: React.ComponentType<SlotProps<S>>;
|
||||
HeroSlot: React.ComponentType<SlotProps<S>>;
|
||||
overallStatusOptions?: { iconSize?: number; allUpKey?: string };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
statusPage: StatusPage;
|
||||
monitors: StatusPageMonitor[];
|
||||
config: ThemeConfig<any>;
|
||||
}
|
||||
|
||||
export const BaseStatusPage = ({ statusPage, monitors, config }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { tokens, mode } = useStatusPageTheme();
|
||||
const styles = useMemo(
|
||||
() => config.createStyles(tokens, mode === "dark"),
|
||||
[config, tokens, mode]
|
||||
);
|
||||
const [chartMode, setChartMode] = useState<"heatmap" | "histogram">("heatmap");
|
||||
|
||||
const overall = resolveOverallStatus(monitors, t, config.overallStatusOptions);
|
||||
const logoSrc = statusPage.logo?.data
|
||||
? `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`
|
||||
: null;
|
||||
|
||||
const { HeaderSlot, HeroSlot } = config;
|
||||
|
||||
return (
|
||||
<Box sx={styles.page}>
|
||||
<Box
|
||||
component="header"
|
||||
sx={styles.top}
|
||||
>
|
||||
<HeaderSlot
|
||||
statusPage={statusPage}
|
||||
logoSrc={logoSrc}
|
||||
overall={overall}
|
||||
monitorCount={monitors.length}
|
||||
styles={styles}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<HeroSlot
|
||||
statusPage={statusPage}
|
||||
logoSrc={logoSrc}
|
||||
overall={overall}
|
||||
monitorCount={monitors.length}
|
||||
styles={styles}
|
||||
/>
|
||||
|
||||
{statusPage.showCharts && (
|
||||
<Box sx={styles.chartSwitchWrap}>
|
||||
<Box
|
||||
sx={styles.chartSwitch}
|
||||
role="radiogroup"
|
||||
>
|
||||
<Box
|
||||
component="button"
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={chartMode === "heatmap"}
|
||||
onClick={() => setChartMode("heatmap")}
|
||||
sx={styles.chartSwitchButton(chartMode === "heatmap")}
|
||||
>
|
||||
{t("pages.statusPages.monitorsList.chartTypeHeatmap")}
|
||||
</Box>
|
||||
<Box
|
||||
component="button"
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={chartMode === "histogram"}
|
||||
onClick={() => setChartMode("histogram")}
|
||||
sx={styles.chartSwitchButton(chartMode === "histogram")}
|
||||
>
|
||||
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Stack
|
||||
component="ul"
|
||||
sx={styles.monitorList}
|
||||
>
|
||||
{monitors.map((monitor) => {
|
||||
const isHardware = monitor.type === "hardware";
|
||||
const showInfra = isHardware && statusPage.showInfrastructure !== false;
|
||||
const showChart = !isHardware && statusPage.showCharts !== false;
|
||||
const badgeTone = monitorBadgeTone(monitor.status);
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="li"
|
||||
key={monitor.id}
|
||||
sx={styles.card}
|
||||
>
|
||||
<Box sx={styles.cardRow}>
|
||||
<Box sx={styles.cardLeft}>
|
||||
<Box sx={styles.monitorName}>{monitor.name}</Box>
|
||||
<Box sx={styles.monitorMeta}>
|
||||
<Box
|
||||
component="span"
|
||||
sx={isHardware ? styles.pillHardware : styles.pill}
|
||||
>
|
||||
{getMonitorTypeLabel(monitor.type, t)}
|
||||
</Box>
|
||||
{monitor.url && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.monitorUrl}
|
||||
title={monitor.url}
|
||||
>
|
||||
{monitor.url}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={styles.badge(badgeTone)}
|
||||
>
|
||||
{t(statusBadgeKey[monitor.status])}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{showInfra && (
|
||||
<ThemedInfrastructure
|
||||
monitor={monitor}
|
||||
sxApi={{
|
||||
containerSx: styles.infra,
|
||||
emptySx: styles.infraEmpty,
|
||||
gaugeSx: styles.gauge,
|
||||
gaugeLabelSx: styles.gaugeLabel,
|
||||
gaugeValueSx: styles.gaugeValue,
|
||||
gaugeBarSx: styles.gaugeBar,
|
||||
gaugeFillSx: styles.gaugeFill,
|
||||
gaugeSubSx: styles.gaugeSub,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showChart &&
|
||||
(chartMode === "heatmap" ? (
|
||||
<ThemedHeatmap
|
||||
checks={monitor.recentChecks ?? []}
|
||||
containerSx={styles.heatmap}
|
||||
cellSx={styles.heatmapCell}
|
||||
/>
|
||||
) : (
|
||||
<ThemedHistogram
|
||||
checks={monitor.recentChecks ?? []}
|
||||
containerSx={styles.histogram}
|
||||
barSx={styles.bar}
|
||||
statsSx={styles.chartStats}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
<Box
|
||||
component="footer"
|
||||
sx={styles.footer}
|
||||
>
|
||||
{t("pages.statusPages.footer.poweredBy")}{" "}
|
||||
<a
|
||||
href="https://checkmate.so"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Checkmate
|
||||
</a>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { CheckSnapshot } from "@/Types/Check";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
import { useStatusPageTheme } from "../StatusPageThemeProvider";
|
||||
|
||||
const CELLS = MAX_RECENT_CHECKS;
|
||||
|
||||
export type HeatCellKind = "fast" | "med" | "slow" | "down" | "empty";
|
||||
|
||||
interface Props {
|
||||
checks: CheckSnapshot[];
|
||||
containerSx: SxProps<Theme>;
|
||||
cellSx: (kind: HeatCellKind) => SxProps<Theme>;
|
||||
}
|
||||
|
||||
const classify = (check: CheckSnapshot): Exclude<HeatCellKind, "empty"> => {
|
||||
if (!check.status) return "down";
|
||||
const rt = check.responseTime ?? 0;
|
||||
if (rt > 500) return "slow";
|
||||
if (rt > 250) return "med";
|
||||
return "fast";
|
||||
};
|
||||
|
||||
export const ThemedHeatmap = ({ checks, containerSx, cellSx }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { timezone } = useStatusPageTheme();
|
||||
|
||||
const source = checks.slice(-CELLS);
|
||||
const padded: (CheckSnapshot | null)[] = [
|
||||
...source,
|
||||
...Array.from({ length: Math.max(0, CELLS - source.length) }, () => null),
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={containerSx}
|
||||
role="img"
|
||||
aria-label={t("pages.statusPages.monitorsList.chart.heatmapAria")}
|
||||
>
|
||||
{padded.map((check, i) => {
|
||||
if (!check) {
|
||||
return (
|
||||
<Box
|
||||
key={`empty-${i}`}
|
||||
sx={cellSx("empty")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const kind = classify(check);
|
||||
const tooltipContent = (
|
||||
<Stack gap={0.25}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
fontWeight={600}
|
||||
>
|
||||
{check.status
|
||||
? `${check.responseTime} ms`
|
||||
: t("pages.statusPages.monitorsList.chart.downTooltip")}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{formatDateWithTz(check.createdAt, "ddd, MMMM D, YYYY, HH:mm A", timezone)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Tooltip
|
||||
key={check.id ?? i}
|
||||
title={tooltipContent}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Box
|
||||
sx={cellSx(kind)}
|
||||
aria-label={`${check.responseTime} ms, ${check.status ? "up" : "down"}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { CheckSnapshot } from "@/Types/Check";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import { MAX_RECENT_CHECKS } from "@/Types/Monitor";
|
||||
import { useStatusPageTheme } from "../StatusPageThemeProvider";
|
||||
const CELLS = MAX_RECENT_CHECKS;
|
||||
const MIN_HEIGHT_PCT = 6;
|
||||
|
||||
export type BarKind = "up" | "down" | "empty";
|
||||
|
||||
interface Props {
|
||||
checks: CheckSnapshot[];
|
||||
containerSx: SxProps<Theme>;
|
||||
barSx: (kind: BarKind, heightPct: number) => SxProps<Theme>;
|
||||
statsSx: SxProps<Theme>;
|
||||
statsGap?: number;
|
||||
}
|
||||
|
||||
const tone = (check: CheckSnapshot): Exclude<BarKind, "empty"> =>
|
||||
check.status ? "up" : "down";
|
||||
|
||||
export const ThemedHistogram = ({
|
||||
checks,
|
||||
containerSx,
|
||||
barSx,
|
||||
statsSx,
|
||||
statsGap = 1,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { timezone } = useStatusPageTheme();
|
||||
|
||||
const { padded, max, avg, peak } = useMemo(() => {
|
||||
const source = checks.slice(-CELLS);
|
||||
const out: (CheckSnapshot | null)[] = [
|
||||
...source,
|
||||
...Array.from({ length: Math.max(0, CELLS - source.length) }, () => null),
|
||||
];
|
||||
const valid = out.filter((c): c is CheckSnapshot => c !== null && c.responseTime > 0);
|
||||
const maxRt = valid.length ? Math.max(...valid.map((c) => c.responseTime)) : 1;
|
||||
const avgRt = valid.length
|
||||
? Math.round(valid.reduce((s, c) => s + c.responseTime, 0) / valid.length)
|
||||
: 0;
|
||||
return { padded: out, max: maxRt, avg: avgRt, peak: valid.length ? maxRt : 0 };
|
||||
}, [checks]);
|
||||
|
||||
return (
|
||||
<Stack gap={statsGap}>
|
||||
<Box sx={containerSx}>
|
||||
{padded.map((check, i) => {
|
||||
if (!check) {
|
||||
return (
|
||||
<Box
|
||||
key={`empty-${i}`}
|
||||
sx={barSx("empty", MIN_HEIGHT_PCT)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const height = Math.max(
|
||||
MIN_HEIGHT_PCT,
|
||||
Math.round((check.responseTime / max) * 100)
|
||||
);
|
||||
const tooltipContent = (
|
||||
<Stack gap={0.25}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
fontWeight={600}
|
||||
>
|
||||
{check.status
|
||||
? `${check.responseTime} ms`
|
||||
: t("pages.statusPages.monitorsList.chart.downTooltip")}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{formatDateWithTz(
|
||||
check.createdAt,
|
||||
"ddd, MMMM D, YYYY, HH:mm A",
|
||||
timezone
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Tooltip
|
||||
key={check.id ?? i}
|
||||
title={tooltipContent}
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Box
|
||||
sx={barSx(tone(check), height)}
|
||||
aria-label={`${check.responseTime} ms`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
sx={statsSx}
|
||||
>
|
||||
<span>{t("pages.statusPages.monitorsList.chart.avg", { value: avg })}</span>
|
||||
<span>{t("pages.statusPages.monitorsList.chart.max", { value: peak })}</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import type { SxProps, Theme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
export type GaugeFillLevel = "ok" | "warm" | "hot";
|
||||
|
||||
export interface InfraSx {
|
||||
containerSx: SxProps<Theme>;
|
||||
emptySx: SxProps<Theme>;
|
||||
gaugeSx: SxProps<Theme>;
|
||||
gaugeLabelSx: SxProps<Theme>;
|
||||
gaugeValueSx: SxProps<Theme>;
|
||||
gaugeBarSx: SxProps<Theme>;
|
||||
gaugeFillSx: (level: GaugeFillLevel, widthPct: number) => SxProps<Theme>;
|
||||
gaugeSubSx: SxProps<Theme>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
monitor: Monitor & { checks?: Monitor["recentChecks"] };
|
||||
sxApi: InfraSx;
|
||||
}
|
||||
|
||||
interface Gauge {
|
||||
key: string;
|
||||
label: string;
|
||||
value: number;
|
||||
sub: string;
|
||||
}
|
||||
|
||||
const PCT = 100;
|
||||
|
||||
const heatLevel = (value: number): GaugeFillLevel =>
|
||||
value > 85 ? "hot" : value > 70 ? "warm" : "ok";
|
||||
|
||||
export const ThemedInfrastructure = ({ monitor, sxApi }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const latest = monitor.recentChecks?.[0] ?? monitor.checks?.[0];
|
||||
|
||||
const renderEmpty = () => (
|
||||
<Box sx={sxApi.emptySx}>{t("pages.statusPages.monitorsList.noData")}</Box>
|
||||
);
|
||||
|
||||
if (!latest) return renderEmpty();
|
||||
|
||||
const gauges: Gauge[] = [];
|
||||
|
||||
if (typeof latest.cpu?.usage_percent === "number") {
|
||||
const pct = latest.cpu.usage_percent * PCT;
|
||||
gauges.push({
|
||||
key: "cpu",
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.cpu"),
|
||||
value: pct,
|
||||
sub: `${pct.toFixed(0)}%`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
typeof latest.memory?.usage_percent === "number" &&
|
||||
typeof latest.memory?.used_bytes === "number" &&
|
||||
typeof latest.memory?.total_bytes === "number"
|
||||
) {
|
||||
gauges.push({
|
||||
key: "memory",
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.memory"),
|
||||
value: latest.memory.usage_percent * PCT,
|
||||
sub: `${prettyBytes(latest.memory.used_bytes)} / ${prettyBytes(latest.memory.total_bytes)}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (latest.disk && latest.disk.length > 0) {
|
||||
const disks = latest.disk;
|
||||
const avg =
|
||||
(disks.reduce((acc, d) => acc + (d?.usage_percent ?? 0), 0) / disks.length) * PCT;
|
||||
const used = disks.reduce((acc, d) => acc + (d?.used_bytes ?? 0), 0);
|
||||
const total = disks.reduce((acc, d) => acc + (d?.total_bytes ?? 0), 0);
|
||||
gauges.push({
|
||||
key: "disk",
|
||||
label: t("pages.statusPages.monitorsList.infrastructure.disk"),
|
||||
value: avg,
|
||||
sub: `${prettyBytes(used)} / ${prettyBytes(total)}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (gauges.length === 0) return renderEmpty();
|
||||
|
||||
return (
|
||||
<Box sx={sxApi.containerSx}>
|
||||
{gauges.map((g) => (
|
||||
<Box
|
||||
key={g.key}
|
||||
sx={sxApi.gaugeSx}
|
||||
>
|
||||
<Box sx={sxApi.gaugeLabelSx}>{g.label}</Box>
|
||||
<Box sx={sxApi.gaugeValueSx}>{g.value.toFixed(0)}%</Box>
|
||||
<Box sx={sxApi.gaugeBarSx}>
|
||||
<Box sx={sxApi.gaugeFillSx(heatLevel(g.value), Math.min(100, g.value))} />
|
||||
</Box>
|
||||
<Box sx={sxApi.gaugeSubSx}>{g.sub}</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
export const SANS_STACK =
|
||||
'-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif';
|
||||
|
||||
export const BOLD_SANS_STACK = `ui-sans-serif, ${SANS_STACK}`;
|
||||
|
||||
export const SERIF_STACK = 'Georgia, "Iowan Old Style", "Palatino Linotype", serif';
|
||||
|
||||
export const EDITORIAL_SECONDARY_SANS_STACK =
|
||||
'-apple-system, "Helvetica Neue", Arial, sans-serif';
|
||||
|
||||
export const MONO_STACK = "ui-monospace, Menlo, monospace";
|
||||
@@ -1,167 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
Loader,
|
||||
PauseCircle,
|
||||
ShieldAlert,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import type { Monitor, MonitorStatus } from "@/Types/Monitor";
|
||||
import type { StatusPageThemeTokens } from "../tokens";
|
||||
|
||||
export type OverallTone = "up" | "warn" | "down";
|
||||
|
||||
export const toneColor = (tone: OverallTone, t: StatusPageThemeTokens): string =>
|
||||
tone === "down" ? t.down : tone === "warn" ? t.warn : t.up;
|
||||
|
||||
export const toneSoft = (tone: OverallTone, t: StatusPageThemeTokens): string =>
|
||||
tone === "down" ? t.downSoft : tone === "warn" ? t.warnSoft : t.upSoft;
|
||||
|
||||
export interface OverallStatus {
|
||||
tone: OverallTone;
|
||||
message: string;
|
||||
icon: ReactNode;
|
||||
}
|
||||
|
||||
type StatusPageMonitor = Pick<Monitor, "status">;
|
||||
|
||||
interface Options {
|
||||
iconSize?: number;
|
||||
// Optional override for the `allUp` message key (editorial uses a more
|
||||
// formal sentence).
|
||||
allUpKey?: string;
|
||||
}
|
||||
|
||||
export const resolveOverallStatus = (
|
||||
monitors: StatusPageMonitor[],
|
||||
t: (key: string) => string,
|
||||
options: Options = {}
|
||||
): OverallStatus => {
|
||||
const size = options.iconSize ?? 18;
|
||||
const allUpMessage = t(options.allUpKey ?? "pages.statusPages.statusBar.allUp");
|
||||
|
||||
if (monitors.length === 0) {
|
||||
return {
|
||||
tone: "warn",
|
||||
message: t("pages.statusPages.statusBar.noMonitors"),
|
||||
icon: <CircleX size={size} />,
|
||||
};
|
||||
}
|
||||
|
||||
const allOf = (...statuses: MonitorStatus[]) =>
|
||||
monitors.every((m) => statuses.includes(m.status));
|
||||
const someOf = (...statuses: MonitorStatus[]) =>
|
||||
monitors.some((m) => statuses.includes(m.status));
|
||||
const noneOf = (...statuses: MonitorStatus[]) =>
|
||||
monitors.every((m) => !statuses.includes(m.status));
|
||||
|
||||
if (allOf("up")) {
|
||||
return { tone: "up", message: allUpMessage, icon: <CircleCheck size={size} /> };
|
||||
}
|
||||
if (allOf("breached")) {
|
||||
return {
|
||||
tone: "down",
|
||||
message: t("pages.statusPages.statusBar.allBreached"),
|
||||
icon: <ShieldAlert size={size} />,
|
||||
};
|
||||
}
|
||||
if (allOf("maintenance")) {
|
||||
return {
|
||||
tone: "warn",
|
||||
message: t("pages.statusPages.statusBar.allMaintenance"),
|
||||
icon: <Wrench size={size} />,
|
||||
};
|
||||
}
|
||||
if (allOf("down")) {
|
||||
return {
|
||||
tone: "down",
|
||||
message: t("pages.statusPages.statusBar.allDown"),
|
||||
icon: <CircleX size={size} />,
|
||||
};
|
||||
}
|
||||
if (allOf("paused")) {
|
||||
return {
|
||||
tone: "warn",
|
||||
message: t("pages.statusPages.statusBar.allPaused"),
|
||||
icon: <PauseCircle size={size} />,
|
||||
};
|
||||
}
|
||||
if (allOf("initializing")) {
|
||||
return {
|
||||
tone: "warn",
|
||||
message: t("pages.statusPages.statusBar.allInitializing"),
|
||||
icon: <Loader size={size} />,
|
||||
};
|
||||
}
|
||||
if (someOf("breached") && someOf("down")) {
|
||||
return {
|
||||
tone: "down",
|
||||
message: t("pages.statusPages.statusBar.breachedAndDown"),
|
||||
icon: <ShieldAlert size={size} />,
|
||||
};
|
||||
}
|
||||
if (someOf("breached")) {
|
||||
return {
|
||||
tone: "down",
|
||||
message: t("pages.statusPages.statusBar.breached"),
|
||||
icon: <ShieldAlert size={size} />,
|
||||
};
|
||||
}
|
||||
if (someOf("maintenance") && someOf("down")) {
|
||||
return {
|
||||
tone: "down",
|
||||
message: t("pages.statusPages.statusBar.maintenanceAndDown"),
|
||||
icon: <Wrench size={size} />,
|
||||
};
|
||||
}
|
||||
if (someOf("maintenance") && noneOf("down")) {
|
||||
return {
|
||||
tone: "warn",
|
||||
message: t("pages.statusPages.statusBar.maintenance"),
|
||||
icon: <Wrench size={size} />,
|
||||
};
|
||||
}
|
||||
if (someOf("down")) {
|
||||
return {
|
||||
tone: "warn",
|
||||
message: t("pages.statusPages.statusBar.degraded"),
|
||||
icon: <AlertTriangle size={size} />,
|
||||
};
|
||||
}
|
||||
if (someOf("paused")) {
|
||||
return {
|
||||
tone: "warn",
|
||||
message: t("pages.statusPages.statusBar.partiallyPaused"),
|
||||
icon: <PauseCircle size={size} />,
|
||||
};
|
||||
}
|
||||
if (someOf("initializing")) {
|
||||
return {
|
||||
tone: "up",
|
||||
message: t("pages.statusPages.statusBar.initializing"),
|
||||
icon: <Loader size={size} />,
|
||||
};
|
||||
}
|
||||
return {
|
||||
tone: "warn",
|
||||
message: t("pages.statusPages.statusBar.unknown"),
|
||||
icon: <AlertTriangle size={size} />,
|
||||
};
|
||||
};
|
||||
|
||||
export const statusBadgeKey: Record<MonitorStatus, string> = {
|
||||
up: "pages.statusPages.monitorsList.status.up",
|
||||
down: "pages.statusPages.monitorsList.status.down",
|
||||
breached: "pages.statusPages.monitorsList.status.breached",
|
||||
maintenance: "pages.statusPages.monitorsList.status.maintenance",
|
||||
paused: "pages.statusPages.monitorsList.status.paused",
|
||||
initializing: "pages.statusPages.monitorsList.status.initializing",
|
||||
};
|
||||
|
||||
export const monoFirstChar = (s?: string): string =>
|
||||
(s?.trim().charAt(0) || "?").toUpperCase();
|
||||
|
||||
export const monitorBadgeTone = (status: MonitorStatus): OverallTone =>
|
||||
status === "up" ? "up" : status === "down" || status === "breached" ? "down" : "warn";
|
||||
@@ -1,199 +0,0 @@
|
||||
import type { StatusPageTheme } from "@/Types/StatusPage";
|
||||
|
||||
export interface StatusPageThemeTokens {
|
||||
bg: string;
|
||||
surface: string;
|
||||
border: string;
|
||||
text: string;
|
||||
textMuted: string;
|
||||
up: string;
|
||||
upStrong: string;
|
||||
upSoft: string;
|
||||
degraded: string;
|
||||
degradedSoft: string;
|
||||
down: string;
|
||||
downSoft: string;
|
||||
warn: string;
|
||||
warnSoft: string;
|
||||
radius: string;
|
||||
fontFamily?: string;
|
||||
headingFontFamily?: string;
|
||||
headingWeight?: number;
|
||||
headingLetterSpacing?: string;
|
||||
chartBarRadius?: string;
|
||||
pulseStatusDot?: boolean;
|
||||
staggeredCardFadeIn?: boolean;
|
||||
conicLogo?: boolean;
|
||||
heroSize?: "default" | "large";
|
||||
cardStyle?: "card" | "hairline";
|
||||
}
|
||||
|
||||
type ThemeVariants = { light: StatusPageThemeTokens; dark: StatusPageThemeTokens };
|
||||
|
||||
const refined: ThemeVariants = {
|
||||
light: {
|
||||
bg: "#f4f6fa",
|
||||
surface: "#ffffff",
|
||||
border: "#dce1ea",
|
||||
text: "#0a1020",
|
||||
textMuted: "#4b5768",
|
||||
up: "#0f8a6d",
|
||||
upStrong: "#0c7359",
|
||||
upSoft: "#d6f1e6",
|
||||
degraded: "#c2630a",
|
||||
degradedSoft: "#fde7bf",
|
||||
down: "#d11f2f",
|
||||
downSoft: "#fdd9dc",
|
||||
warn: "#e08a0b",
|
||||
warnSoft: "#fdebc5",
|
||||
radius: "12px",
|
||||
},
|
||||
dark: {
|
||||
bg: "#070b11",
|
||||
surface: "#131a24",
|
||||
border: "#253142",
|
||||
text: "#f1f5fb",
|
||||
textMuted: "#9aa7bd",
|
||||
up: "#2fd7a2",
|
||||
upStrong: "#1fb487",
|
||||
upSoft: "rgba(47,215,162,0.15)",
|
||||
degraded: "#e69138",
|
||||
degradedSoft: "rgba(230,145,56,0.18)",
|
||||
down: "#ff5b6b",
|
||||
downSoft: "rgba(255,91,107,0.18)",
|
||||
warn: "#f0a837",
|
||||
warnSoft: "rgba(240,168,55,0.18)",
|
||||
radius: "12px",
|
||||
},
|
||||
};
|
||||
|
||||
const modern: ThemeVariants = {
|
||||
light: {
|
||||
bg: "#fafbfc",
|
||||
surface: "#ffffff",
|
||||
border: "#eceef2",
|
||||
text: "#0b1220",
|
||||
textMuted: "#6b7280",
|
||||
up: "#10a37f",
|
||||
upStrong: "#0b8a6a",
|
||||
upSoft: "#d7f3e9",
|
||||
degraded: "#d97706",
|
||||
degradedSoft: "#fef3c7",
|
||||
down: "#dc2626",
|
||||
downSoft: "#fee2e2",
|
||||
warn: "#f59e0b",
|
||||
warnSoft: "#fef3c7",
|
||||
radius: "16px",
|
||||
pulseStatusDot: true,
|
||||
staggeredCardFadeIn: true,
|
||||
},
|
||||
dark: {
|
||||
bg: "#0a0d12",
|
||||
surface: "#10151c",
|
||||
border: "#1b2430",
|
||||
text: "#e8edf5",
|
||||
textMuted: "#8a95a8",
|
||||
up: "#10a37f",
|
||||
upStrong: "#0b8a6a",
|
||||
upSoft: "rgba(16,163,127,0.18)",
|
||||
degraded: "#d97706",
|
||||
degradedSoft: "rgba(217,119,6,0.22)",
|
||||
down: "#dc2626",
|
||||
downSoft: "rgba(220,38,38,0.22)",
|
||||
warn: "#f59e0b",
|
||||
warnSoft: "rgba(245,158,11,0.2)",
|
||||
radius: "16px",
|
||||
pulseStatusDot: true,
|
||||
staggeredCardFadeIn: true,
|
||||
},
|
||||
};
|
||||
|
||||
const bold: ThemeVariants = {
|
||||
light: {
|
||||
bg: "#f5f6f9",
|
||||
surface: "#ffffff",
|
||||
border: "#e6e8ee",
|
||||
text: "#0b1220",
|
||||
textMuted: "#6b7280",
|
||||
up: "#10a37f",
|
||||
upStrong: "#0b8a6a",
|
||||
upSoft: "rgba(16,163,127,0.12)",
|
||||
degraded: "#f59e0b",
|
||||
degradedSoft: "rgba(245,158,11,0.14)",
|
||||
down: "#f43f5e",
|
||||
downSoft: "rgba(244,63,94,0.12)",
|
||||
warn: "#f59e0b",
|
||||
warnSoft: "rgba(245,158,11,0.14)",
|
||||
radius: "18px",
|
||||
heroSize: "large",
|
||||
conicLogo: true,
|
||||
},
|
||||
dark: {
|
||||
bg: "#05070a",
|
||||
surface: "#0c1117",
|
||||
border: "#1a2330",
|
||||
text: "#eef2f7",
|
||||
textMuted: "#7a8699",
|
||||
up: "#22d3a6",
|
||||
upStrong: "#10a37f",
|
||||
upSoft: "rgba(34,211,166,0.14)",
|
||||
degraded: "#f59e0b",
|
||||
degradedSoft: "rgba(245,158,11,0.14)",
|
||||
down: "#f43f5e",
|
||||
downSoft: "rgba(244,63,94,0.16)",
|
||||
warn: "#f59e0b",
|
||||
warnSoft: "rgba(245,158,11,0.14)",
|
||||
radius: "18px",
|
||||
heroSize: "large",
|
||||
conicLogo: true,
|
||||
},
|
||||
};
|
||||
|
||||
const editorial: ThemeVariants = {
|
||||
light: {
|
||||
bg: "#fbfaf7",
|
||||
surface: "#ffffff",
|
||||
// Darkened from mockup's #e9e4d8 (~1.3:1 on #fbfaf7) to pass WCAG AA 3:1 for UI borders.
|
||||
border: "#d4cdb8",
|
||||
text: "#1a1a1a",
|
||||
textMuted: "#6b675e",
|
||||
up: "#2c6a4f",
|
||||
upStrong: "#205239",
|
||||
upSoft: "#dfeee4",
|
||||
degraded: "#a46200",
|
||||
degradedSoft: "#f7ecd4",
|
||||
down: "#9a2a2a",
|
||||
downSoft: "#f5dede",
|
||||
warn: "#a46200",
|
||||
warnSoft: "#f7ecd4",
|
||||
radius: "6px",
|
||||
headingFontFamily: 'Georgia, "Iowan Old Style", "Palatino Linotype", serif',
|
||||
cardStyle: "hairline",
|
||||
},
|
||||
dark: {
|
||||
bg: "#141310",
|
||||
surface: "#1c1a15",
|
||||
border: "#3a352b",
|
||||
text: "#f0ece3",
|
||||
textMuted: "#9e988a",
|
||||
up: "#6fbf96",
|
||||
upStrong: "#4fa37a",
|
||||
upSoft: "rgba(44,106,79,0.22)",
|
||||
degraded: "#d79a3b",
|
||||
degradedSoft: "rgba(164,98,0,0.22)",
|
||||
down: "#d87a7a",
|
||||
downSoft: "rgba(154,42,42,0.22)",
|
||||
warn: "#d79a3b",
|
||||
warnSoft: "rgba(164,98,0,0.22)",
|
||||
radius: "6px",
|
||||
headingFontFamily: 'Georgia, "Iowan Old Style", "Palatino Linotype", serif',
|
||||
cardStyle: "hairline",
|
||||
},
|
||||
};
|
||||
|
||||
export const themeTokens: Record<StatusPageTheme, ThemeVariants> = {
|
||||
refined,
|
||||
modern,
|
||||
bold,
|
||||
editorial,
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
|
||||
interface StatusPagesTableProps {
|
||||
data: StatusPage[];
|
||||
@@ -53,7 +53,7 @@ export const StatusPagesTable = ({
|
||||
const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => {
|
||||
if (row.isPublished) {
|
||||
e.stopPropagation();
|
||||
const url = `${PUBLIC_STATUS_PAGE_PREFIX}/${row.url}`;
|
||||
const url = `/status/public/${row.url}`;
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,24 +20,6 @@ import { useSelector } from "react-redux";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { ActionMenuItem } from "@/Components/actions-menu";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { Checkbox } from "@/Components/inputs";
|
||||
|
||||
interface MonitorTableProps {
|
||||
monitors: Monitor[];
|
||||
refetch: () => void;
|
||||
setSelectedMonitor: (monitor: Monitor | null) => void;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
rowsPerPage: number;
|
||||
setRowsPerPage: (rowsPerPage: number) => void;
|
||||
selectedRows?: string[];
|
||||
onSelectionChange?: (selected: string[]) => void;
|
||||
}
|
||||
|
||||
export const MonitorTable = ({
|
||||
monitors,
|
||||
@@ -52,34 +34,31 @@ export const MonitorTable = ({
|
||||
setPage,
|
||||
rowsPerPage,
|
||||
setRowsPerPage,
|
||||
selectedRows = [],
|
||||
onSelectionChange,
|
||||
}: MonitorTableProps) => {
|
||||
}: {
|
||||
monitors: Monitor[];
|
||||
refetch: Function;
|
||||
setSelectedMonitor: (monitor: Monitor | null) => void;
|
||||
sortField: string;
|
||||
setSortField: (field: string) => void;
|
||||
sortOrder: "asc" | "desc";
|
||||
setSortOrder: (order: "asc" | "desc") => void;
|
||||
count: number;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
rowsPerPage: number;
|
||||
setRowsPerPage: (rowsPerPage: number) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const chartType = useSelector((state: RootState) => state.ui?.chartType ?? "histogram");
|
||||
const { post } = usePost<Record<string, never>, Monitor>();
|
||||
const {
|
||||
post,
|
||||
// loading: isPosting,
|
||||
// error: postError,
|
||||
} = usePost<any, Monitor>();
|
||||
|
||||
const selectedSet = new Set(selectedRows);
|
||||
const isAllSelected =
|
||||
monitors.length > 0 && monitors.every((m) => selectedSet.has(m.id));
|
||||
const isSomeSelected = selectedRows.length > 0 && !isAllSelected;
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
onSelectionChange?.(checked ? monitors.map((m) => m.id) : []);
|
||||
};
|
||||
|
||||
const handleSelectRow = (id: string, checked: boolean) => {
|
||||
if (!onSelectionChange) return;
|
||||
onSelectionChange(
|
||||
checked ? [...selectedRows, id] : selectedRows.filter((rowId) => rowId !== id)
|
||||
);
|
||||
};
|
||||
|
||||
const isRowSelected = (id: string) => selectedSet.has(id);
|
||||
|
||||
const handleSort = (e: React.MouseEvent, field: string) => {
|
||||
const handleSort = (e: any, field: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (sortField === field) {
|
||||
@@ -131,6 +110,13 @@ export const MonitorTable = ({
|
||||
navigate(`/uptime/configure/${monitor.id}`);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// id: 5,
|
||||
// label: "Clone",
|
||||
// action: () => {
|
||||
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: 6,
|
||||
label:
|
||||
@@ -174,35 +160,8 @@ export const MonitorTable = ({
|
||||
</Box>
|
||||
);
|
||||
const headers: Header<Monitor>[] = [
|
||||
{
|
||||
id: "selection",
|
||||
content: (
|
||||
<Checkbox
|
||||
aria-label={t("pages.common.monitors.actions.selectAll")}
|
||||
indeterminate={isSomeSelected}
|
||||
checked={isAllSelected}
|
||||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||
/>
|
||||
),
|
||||
hideMobileLabel: true,
|
||||
render: (row) => (
|
||||
<Checkbox
|
||||
aria-label={t("pages.common.monitors.actions.selectMonitor", {
|
||||
name: row.name,
|
||||
})}
|
||||
checked={isRowSelected(row.id)}
|
||||
onChange={(e) => {
|
||||
handleSelectRow(row.id, e.target.checked);
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
),
|
||||
onClick: (e) => e?.stopPropagation(),
|
||||
},
|
||||
{
|
||||
id: "name",
|
||||
align: "left",
|
||||
mobileLabel: t("common.table.headers.name"),
|
||||
content: (
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
@@ -222,7 +181,6 @@ export const MonitorTable = ({
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
mobileLabel: t("common.table.headers.status"),
|
||||
content: (
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
@@ -254,7 +212,6 @@ export const MonitorTable = ({
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
mobileLabel: t("common.table.headers.type"),
|
||||
content: (
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
@@ -294,11 +251,6 @@ export const MonitorTable = ({
|
||||
onRowClick={(row) => {
|
||||
navigate(`/uptime/${row.id}`);
|
||||
}}
|
||||
getRowSx={(row) => ({
|
||||
backgroundColor: isRowSelected(row.id)
|
||||
? theme.palette.action.selected
|
||||
: "inherit",
|
||||
})}
|
||||
/>
|
||||
<Pagination
|
||||
component="div"
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import {
|
||||
ControlsFilter,
|
||||
HeaderMonitorsSummary,
|
||||
BulkActionsBar,
|
||||
} from "@/Components/monitors";
|
||||
import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/monitors";
|
||||
import { MonitorBasePageWithStates } from "@/Components/design-elements";
|
||||
import { TextField, Dialog, Button } from "@/Components/inputs";
|
||||
import { TextField, Dialog } from "@/Components/inputs";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable";
|
||||
import { HeaderCreate } from "@/Components/common";
|
||||
import { Play, Pause } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
@@ -21,7 +16,6 @@ import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import type { RootState } from "@/Types/state";
|
||||
import { useTheme } from "@mui/material";
|
||||
import useDebounce from "@/Hooks/useDebounce";
|
||||
import { useBulkMonitorActions } from "@/Hooks/useBulkMonitorActions";
|
||||
|
||||
const UptimeMonitorsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -43,7 +37,6 @@ const UptimeMonitorsPage = () => {
|
||||
const [sortField, setSortField] = useState<string>("");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
|
||||
|
||||
const isDialogOpen = Boolean(selectedMonitor);
|
||||
|
||||
const debouncedSearch = useDebounce<string>(search, 300);
|
||||
@@ -109,15 +102,6 @@ const UptimeMonitorsPage = () => {
|
||||
count,
|
||||
} = monitorsWithChecksData ?? { monitors: null, summary: null, count: 0 };
|
||||
|
||||
// Bulk actions
|
||||
const {
|
||||
selectedRows,
|
||||
setSelectedRows,
|
||||
handleBulkPause,
|
||||
handleBulkResume,
|
||||
handleCancelSelection,
|
||||
} = useBulkMonitorActions(refetch, page);
|
||||
|
||||
// Delete hook
|
||||
const { deleteFn, loading: isDeleting } = useDelete();
|
||||
|
||||
@@ -193,29 +177,6 @@ const UptimeMonitorsPage = () => {
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{!isLoading && (
|
||||
<BulkActionsBar
|
||||
selectedCount={selectedRows.length}
|
||||
onCancel={handleCancelSelection}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Play size={16} />}
|
||||
onClick={handleBulkResume}
|
||||
>
|
||||
{t("common.buttons.resume")}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Pause size={16} />}
|
||||
onClick={handleBulkPause}
|
||||
>
|
||||
{t("common.buttons.pause")}
|
||||
</Button>
|
||||
</BulkActionsBar>
|
||||
)}
|
||||
|
||||
<MonitorTable
|
||||
monitors={monitorsWithChecks || []}
|
||||
refetch={refetch}
|
||||
@@ -237,8 +198,6 @@ const UptimeMonitorsPage = () => {
|
||||
);
|
||||
setPage(0);
|
||||
}}
|
||||
selectedRows={selectedRows}
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
|
||||
@@ -3,8 +3,6 @@ import type { PageSpeedGroupedCheck } from "@/Types/Check";
|
||||
import type { GeoContinent } from "@/Types/GeoCheck";
|
||||
export type { GeoContinent } from "@/Types/GeoCheck";
|
||||
|
||||
export const MAX_RECENT_CHECKS = 50;
|
||||
|
||||
export const MonitorTypes = [
|
||||
"http",
|
||||
"ping",
|
||||
|
||||
@@ -23,8 +23,6 @@ export interface QueueJobSummary {
|
||||
monitorUrl: string | null;
|
||||
monitorType: MonitorType | null;
|
||||
monitorInterval: number | null;
|
||||
monitorGeoInterval: number | null;
|
||||
monitorActive: boolean | null;
|
||||
active: boolean;
|
||||
lockedAt: number | null;
|
||||
runCount: number;
|
||||
@@ -34,7 +32,6 @@ export interface QueueJobSummary {
|
||||
lastFinishedAt: number | null;
|
||||
lastRunTook: number | null;
|
||||
lastFailedAt: number | null;
|
||||
repeat: number | null;
|
||||
}
|
||||
|
||||
export interface QueueData {
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface Settings {
|
||||
systemEmailHost?: string;
|
||||
systemEmailPort?: number;
|
||||
systemEmailAddress?: string;
|
||||
systemEmailDisplayName?: string;
|
||||
systemEmailUser?: string;
|
||||
systemEmailConnectionHost?: string;
|
||||
systemEmailTLSServername?: string;
|
||||
|
||||
@@ -21,25 +21,6 @@ export const getMonitorTypeLabel = (
|
||||
return key ? t(key) : type;
|
||||
};
|
||||
|
||||
export const STATUS_PAGE_THEMES = ["refined", "modern", "bold", "editorial"] as const;
|
||||
export type StatusPageTheme = (typeof STATUS_PAGE_THEMES)[number];
|
||||
export const DEFAULT_STATUS_PAGE_THEME: StatusPageTheme = "refined";
|
||||
|
||||
export const STATUS_PAGE_THEME_MODES = ["auto", "light", "dark"] as const;
|
||||
export type StatusPageThemeMode = (typeof STATUS_PAGE_THEME_MODES)[number];
|
||||
export const DEFAULT_STATUS_PAGE_THEME_MODE: StatusPageThemeMode = "auto";
|
||||
|
||||
export const resolveStatusPageTheme = (
|
||||
value: string | null | undefined
|
||||
): StatusPageTheme =>
|
||||
STATUS_PAGE_THEMES.find((t) => t === value) ?? DEFAULT_STATUS_PAGE_THEME;
|
||||
|
||||
export const resolveStatusPageThemeMode = (
|
||||
value: string | null | undefined
|
||||
): StatusPageThemeMode =>
|
||||
STATUS_PAGE_THEME_MODES.find((m) => m === value) ?? DEFAULT_STATUS_PAGE_THEME_MODE;
|
||||
export const PUBLIC_STATUS_PAGE_PREFIX = "/status/public";
|
||||
|
||||
export interface StatusPage {
|
||||
id: string;
|
||||
userId: string;
|
||||
@@ -62,8 +43,6 @@ export interface StatusPage {
|
||||
showAdminLoginLink: boolean;
|
||||
showInfrastructure: boolean;
|
||||
customCSS: string;
|
||||
theme: StatusPageTheme;
|
||||
themeMode: StatusPageThemeMode;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { alpha } from "@mui/material/styles";
|
||||
|
||||
const typographyBase = 13;
|
||||
|
||||
export const typographyLevels = {
|
||||
base: typographyBase, // 13px
|
||||
xs: `${(typographyBase - 4) / 16}rem`, // 9px
|
||||
s: `${(typographyBase - 2) / 16}rem`, // 11px
|
||||
m: `${typographyBase / 16}rem`, // 13px
|
||||
l: `${(typographyBase + 2) / 16}rem`, // 15px
|
||||
xl: `${(typographyBase + 5) / 16}rem`, // 18px
|
||||
xxl: `${(typographyBase + 10) / 16}rem`, // 23px
|
||||
base: typographyBase,
|
||||
xs: `${(typographyBase - 4) / 16}rem`,
|
||||
s: `${(typographyBase - 2) / 16}rem`,
|
||||
m: `${typographyBase / 16}rem`,
|
||||
l: `${(typographyBase + 2) / 16}rem`,
|
||||
xl: `${(typographyBase + 10) / 16}rem`,
|
||||
};
|
||||
|
||||
export const colors = {
|
||||
@@ -19,9 +16,6 @@ export const colors = {
|
||||
gray850: "#1c1c21",
|
||||
brandGreen: "#13715B",
|
||||
brandGreenLight: "#4DAF94",
|
||||
green200: "#ECF7F2",
|
||||
yellow200: "#FFF4E5",
|
||||
orange500: "#FFA500",
|
||||
};
|
||||
|
||||
export const lightPalette = {
|
||||
@@ -34,10 +28,6 @@ export const lightPalette = {
|
||||
sidebar: {
|
||||
accent: colors.brandGreen,
|
||||
},
|
||||
rowStatus: {
|
||||
running: colors.green200,
|
||||
paused: colors.yellow200,
|
||||
},
|
||||
};
|
||||
|
||||
export const darkPalette = {
|
||||
@@ -54,8 +44,4 @@ export const darkPalette = {
|
||||
sidebar: {
|
||||
accent: colors.brandGreenLight,
|
||||
},
|
||||
rowStatus: {
|
||||
running: alpha(colors.brandGreen, 0.18),
|
||||
paused: alpha(colors.orange500, 0.18),
|
||||
},
|
||||
};
|
||||
@@ -15,11 +15,9 @@ declare module "@mui/material/styles" {
|
||||
}
|
||||
interface Palette {
|
||||
sidebar: { accent: string };
|
||||
rowStatus: { running: string; paused: string };
|
||||
}
|
||||
interface PaletteOptions {
|
||||
sidebar?: { accent: string };
|
||||
rowStatus?: { running: string; paused: string };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +58,7 @@ export const theme = (mode: string, palette: any) =>
|
||||
fontFamilyMonospace,
|
||||
fontSize: typographyLevels.base,
|
||||
h1: {
|
||||
fontSize: typographyLevels.xxl,
|
||||
fontSize: typographyLevels.xl,
|
||||
fontWeight: 500,
|
||||
},
|
||||
h2: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export const SPACING = {
|
||||
XXS: 0.25,
|
||||
XS: 0.5,
|
||||
SM: 1,
|
||||
MD: 1.5,
|
||||
@@ -9,7 +8,6 @@ export const SPACING = {
|
||||
} as const;
|
||||
|
||||
export const LAYOUT = {
|
||||
XXS: 2,
|
||||
XS: 4,
|
||||
SM: 6,
|
||||
MD: 8,
|
||||
@@ -17,7 +15,3 @@ export const LAYOUT = {
|
||||
XL: 12,
|
||||
XXL: 16,
|
||||
} as const;
|
||||
|
||||
export const HOVER = {
|
||||
DARKEN: 0.06, // This is a coefficient for darkening function
|
||||
} as const;
|
||||
@@ -15,33 +15,12 @@ export const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
||||
export const MS_PER_DAY = 24 * MS_PER_HOUR;
|
||||
export const MS_PER_WEEK = MS_PER_DAY * 7;
|
||||
|
||||
const resolveSystemTimezone = (): string => {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
||||
} catch {
|
||||
return "UTC";
|
||||
}
|
||||
};
|
||||
|
||||
export const SYSTEM_TIMEZONE = resolveSystemTimezone();
|
||||
|
||||
export const resolveTimezone = (preferred?: string | null): string =>
|
||||
preferred?.trim() || SYSTEM_TIMEZONE;
|
||||
|
||||
export const formatDateWithTz = (
|
||||
timestamp: string,
|
||||
format: string,
|
||||
timezone?: string
|
||||
) => {
|
||||
export const formatDateWithTz = (timestamp: string, format: string, timezone: string) => {
|
||||
if (!timestamp) {
|
||||
return "Unknown time";
|
||||
}
|
||||
const tz = resolveTimezone(timezone);
|
||||
try {
|
||||
return dayjs(timestamp).tz(tz).format(format);
|
||||
} catch {
|
||||
return dayjs(timestamp).utc().format(format);
|
||||
}
|
||||
const formattedDate = dayjs(timestamp).tz(timezone).format(format);
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
export const tickDateFormatLookup = (range: string) => {
|
||||
|
||||
@@ -32,11 +32,6 @@ export const settingsSchema = z.object({
|
||||
.or(z.literal(""))
|
||||
.transform((val) => (val === "" ? null : val.toLowerCase().trim()))
|
||||
.optional(),
|
||||
systemEmailDisplayName: z
|
||||
.string()
|
||||
.max(100, "Display name must be 100 characters or fewer")
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
.optional(),
|
||||
systemEmailUser: z
|
||||
.string()
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { STATUS_PAGE_THEMES, STATUS_PAGE_THEME_MODES } from "@/Types/StatusPage";
|
||||
|
||||
export const statusPageSchema = z.object({
|
||||
companyName: z
|
||||
@@ -26,8 +25,6 @@ export const statusPageSchema = z.object({
|
||||
showAdminLoginLink: z.boolean(),
|
||||
showInfrastructure: z.boolean(),
|
||||
customCSS: z.string().optional(),
|
||||
theme: z.enum(STATUS_PAGE_THEMES).optional(),
|
||||
themeMode: z.enum(STATUS_PAGE_THEME_MODES).optional(),
|
||||
logo: z
|
||||
.object({
|
||||
data: z.string(),
|
||||
|
||||
+49
-102
@@ -13,7 +13,7 @@
|
||||
"content": "تحذير: لم تقم بإضافة مفتاح Google PageSpeed API بعد. قم بزيارة <settingsLink>الإعدادات</settingsLink> لإضافته. بدونه، لن تعمل مراقبة PageSpeed."
|
||||
}
|
||||
},
|
||||
"appName": "Checkmate",
|
||||
"appName": "",
|
||||
"breadcrumbs": {
|
||||
"details": "التفاصيل",
|
||||
"home": "الرئيسية"
|
||||
@@ -35,11 +35,11 @@
|
||||
"sendInvite": "إرسال دعوة",
|
||||
"test": "اختبار",
|
||||
"testNotifications": "اختبار الإشعارات",
|
||||
"toggleTheme": "تبديل الوضع الفاتح والداكن",
|
||||
"toggleTheme": "",
|
||||
"flushQueue": "تفريغ قائمة الانتظار",
|
||||
"notFound": "الانتقال إلى لوحة التحكم الرئيسية",
|
||||
"resetPassword": "إعادة تعيين كلمة المرور",
|
||||
"reset": "إعادة تعيين",
|
||||
"resetPassword": "",
|
||||
"reset": "",
|
||||
"clear": "مسح",
|
||||
"addDemo": "إضافة مراقبات تجريبية",
|
||||
"removeMonitors": "إزالة المراقبات",
|
||||
@@ -53,8 +53,8 @@
|
||||
"labels": {
|
||||
"averageResponseTime": "متوسط وقت الاستجابة",
|
||||
"downtime": "وقت التوقف",
|
||||
"high": "مرتفع",
|
||||
"low": "منخفض",
|
||||
"high": "",
|
||||
"low": "",
|
||||
"uptime": "وقت التشغيل"
|
||||
},
|
||||
"histogram": {
|
||||
@@ -86,12 +86,12 @@
|
||||
"name": {
|
||||
"option": {
|
||||
"firstName": {
|
||||
"label": "الاسم الأول",
|
||||
"placeholder": "أدخل اسمك الأول"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
},
|
||||
"lastName": {
|
||||
"label": "اسم العائلة",
|
||||
"placeholder": "أدخل اسم عائلتك"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -107,14 +107,14 @@
|
||||
"table": {
|
||||
"empty": "لا يوجد شيء هنا",
|
||||
"headers": {
|
||||
"actions": "إجراءات",
|
||||
"actions": "",
|
||||
"dateTime": "التاريخ والوقت",
|
||||
"message": "الرسالة",
|
||||
"monitor": "المراقب",
|
||||
"monitorId": "معرّف المراقب",
|
||||
"name": "الاسم",
|
||||
"status": "الحالة",
|
||||
"type": "النوع",
|
||||
"type": "",
|
||||
"url": "الرابط",
|
||||
"interval": "الفاصل الزمني",
|
||||
"active": "نشط",
|
||||
@@ -125,17 +125,17 @@
|
||||
"components": {
|
||||
"imageUpload": {
|
||||
"clickToUpload": "انقر للرفع",
|
||||
"dragAndDrop": "أو اسحب وأسقط",
|
||||
"supportedFormats": "الصيغ المدعومة",
|
||||
"maxSize": "الحجم الأقصى",
|
||||
"dragAndDrop": "",
|
||||
"supportedFormats": "",
|
||||
"maxSize": "",
|
||||
"orDragAndDrop": "أو اسحب وأفلت",
|
||||
"errors": {
|
||||
"invalidFileSize": "حجم الملف كبير جدًا!",
|
||||
"invalidFileFormat": "تنسيق الملف غير مدعوم!"
|
||||
"invalidFileSize": "",
|
||||
"invalidFileFormat": ""
|
||||
}
|
||||
},
|
||||
"headerStatusPageControls": {
|
||||
"publicLink": "رابط عام"
|
||||
"publicLink": ""
|
||||
},
|
||||
"headerTimeRange": {
|
||||
"labels": {
|
||||
@@ -155,26 +155,26 @@
|
||||
"serverUnreachable": "تعذر الوصول إلى الخادم",
|
||||
"retry": "إعادة المحاولة",
|
||||
"retrying": "جارٍ إعادة المحاولة...",
|
||||
"reconnected": "تمت استعادة الاتصال"
|
||||
"reconnected": ""
|
||||
},
|
||||
"sidebar": {
|
||||
"menu": {
|
||||
"uptime": "وقت التشغيل",
|
||||
"pagespeed": "Pagespeed",
|
||||
"infrastructure": "البنية التحتية",
|
||||
"notifications": "الإشعارات",
|
||||
"uptime": "",
|
||||
"pagespeed": "",
|
||||
"infrastructure": "",
|
||||
"notifications": "",
|
||||
"checks": "الفحوصات",
|
||||
"incidents": "الحوادث",
|
||||
"statusPages": "صفحات الحالة",
|
||||
"maintenance": "الصيانة",
|
||||
"logs": "السجلات",
|
||||
"settings": "الإعدادات"
|
||||
"incidents": "",
|
||||
"statusPages": "",
|
||||
"maintenance": "",
|
||||
"logs": "",
|
||||
"settings": ""
|
||||
},
|
||||
"bottomMenu": {
|
||||
"support": "الدعم",
|
||||
"discussions": "المناقشات",
|
||||
"docs": "الوثائق",
|
||||
"changelog": "سجل التغييرات"
|
||||
"support": "",
|
||||
"discussions": "",
|
||||
"docs": "",
|
||||
"changelog": ""
|
||||
},
|
||||
"accountMenu": {
|
||||
"profile": "الملف الشخصي",
|
||||
@@ -186,13 +186,13 @@
|
||||
"description": "اطلع على أحدث الإصدارات وساعد في تنمية المجتمع على GitHub"
|
||||
},
|
||||
"authFooter": {
|
||||
"navControls": "عناصر التحكم",
|
||||
"logOut": "تسجيل الخروج",
|
||||
"navControls": "",
|
||||
"logOut": "",
|
||||
"roles": {
|
||||
"superAdmin": "مسؤول أعلى",
|
||||
"admin": "مسؤول",
|
||||
"user": "مستخدم",
|
||||
"demoUser": "مستخدم تجريبي"
|
||||
"demoUser": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -259,15 +259,15 @@
|
||||
"team": {
|
||||
"filter": {
|
||||
"placeholder": "تصفية حسب الدور",
|
||||
"all": "كل الأدوار",
|
||||
"all": "",
|
||||
"admin": "مسؤول",
|
||||
"member": "عضو"
|
||||
"member": ""
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"email": "البريد الإلكتروني",
|
||||
"role": "الدور",
|
||||
"created": "تم الإنشاء في"
|
||||
"created": ""
|
||||
}
|
||||
},
|
||||
"addMember": {
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "مرحباً بعودتك إلى Checkmate!",
|
||||
"subtitle": "سجّل للبدء",
|
||||
"submit": "تسجيل",
|
||||
"setupNotice": "هذه الصفحة متاحة فقط أثناء الإعداد الأولي. بمجرد تسجيل المسؤول الأعلى، يمكن دعوة مستخدمين إضافيين من إعدادات الفريق."
|
||||
"submit": "تسجيل"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "نسيت كلمة المرور؟",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "دعوة عضو",
|
||||
"openSite": "فتح الموقع",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"resume": "استئناف",
|
||||
"selectAll": "تحديد جميع الشاشات",
|
||||
"selectMonitor": "تحديد {{name}}",
|
||||
"bulkSelected_one": "{{count}} شاشة محددة",
|
||||
"bulkSelected_other": "{{count}} شاشات محددة"
|
||||
"resume": "استئناف"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "نشط منذ",
|
||||
"certificateExpiry": "انتهاء الشهادة",
|
||||
"lastCheck": "آخر فحص",
|
||||
"lastResponseTime": "آخر وقت استجابة",
|
||||
"serviceIsDown": "الخدمة معطلة"
|
||||
"lastResponseTime": "آخر وقت استجابة"
|
||||
},
|
||||
"status": {
|
||||
"down": "متوقف",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "البنية التحتية",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "تم إيقاف {{count}} شاشة مؤقتاً",
|
||||
"paused_other": "تم إيقاف {{count}} شاشات مؤقتاً",
|
||||
"resumed_one": "تم استئناف {{count}} شاشة",
|
||||
"resumed_other": "تم استئناف {{count}} شاشات",
|
||||
"alreadyPaused_one": "الشاشة المحددة متوقفة مؤقتاً بالفعل",
|
||||
"alreadyPaused_other": "جميع الشاشات المحددة متوقفة مؤقتاً بالفعل",
|
||||
"alreadyRunning_one": "الشاشة المحددة قيد التشغيل بالفعل",
|
||||
"alreadyRunning_other": "جميع الشاشات المحددة قيد التشغيل بالفعل"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "رابط WebSocket",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "نقطة نهاية Capture",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "الإعدادات العامة",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "",
|
||||
"port": "",
|
||||
"docker": "",
|
||||
"game": "أدخل عنوان IP أو اسم المضيف ورقم المنفذ للاختبار (مثل 192.168.1.100 أو example.com) واختر نوع اللعبة. تتطلب بعض الألعاب تفعيل استعلامات الخادم في ملف الإعدادات الخاص بها — يُرجى مراجعة وثائق خادم اللعبة. اطّلع على <gamedigLink>قائمة الألعاب المدعومة</gamedigLink> لمعرفة تفاصيل البروتوكول.",
|
||||
"game": "",
|
||||
"pagespeed": "تتبع أداء تحميل الصفحة ومؤشرات Core Web Vitals ودرجات التحسين لموقعك.",
|
||||
"grpc": "أدخل اسم المضيف ومنفذ خادم gRPC، وحدد اختيارياً اسم خدمة لفحص الصحة، وأضف اسم عرض.",
|
||||
"websocket": "أدخل رابط WebSocket للمراقبة (مثال: wss://example.com/socket) وأضف اسم عرض.",
|
||||
"hardware": "راقب استخدام وحدة المعالجة المركزية والذاكرة والقرص ودرجة الحرارة لبنيتك التحتية. يجب أن يتضمن عنوان URL لنقطة نهاية Capture المسار الكامل /api/v1/metrics (مثال: http://192.168.1.10:5001/api/v1/metrics). بالنسبة لمفتاح التفويض، الصق سلسلة الرمز فقط — لا تُضمّن كلمة 'Bearer'."
|
||||
"hardware": "مراقبة استخدام CPU والذاكرة والقرص ودرجة الحرارة للبنية التحتية الخاصة بك."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "استخدام الذاكرة",
|
||||
"netBytesRecv": "{{name}} - البايتات المستلمة",
|
||||
"netBytesSent": "{{name}} - البايتات المرسلة",
|
||||
"netNoTraffic": "Capture يبلغ عن عدم وجود حركة بيانات على أي واجهة. هذا أمر طبيعي عندما يعمل Capture داخل حاوية لا ترى سوى واجهات الاسترجاع — قم بتشغيل Capture مباشرة على المضيف لرؤية نشاط الشبكة الحقيقي.",
|
||||
"temp": "الحرارة"
|
||||
}
|
||||
},
|
||||
@@ -846,9 +825,9 @@
|
||||
"description": "ستظهر هنا نشاط الخادم والتحذيرات والأخطاء أثناء تشغيل Checkmate."
|
||||
},
|
||||
"tabs": {
|
||||
"diagnostics": "التشخيص",
|
||||
"logs": "السجلات",
|
||||
"queue": "قائمة المهام"
|
||||
"diagnostics": "",
|
||||
"logs": "",
|
||||
"queue": ""
|
||||
},
|
||||
"logLevelSelect": {
|
||||
"label": "مستوى السجل"
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "معرّف المحادثة",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "إعدادات Pushover",
|
||||
"description": "أدخل رمز التطبيق ومفتاح المستخدم في Pushover.",
|
||||
"optionAppToken": "رمز التطبيق",
|
||||
"placeholderAppToken": "أدخل رمز التطبيق",
|
||||
"optionUserKey": "مفتاح المستخدم",
|
||||
"placeholderUserKey": "أدخل مفتاح المستخدم"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "إعدادات Twilio",
|
||||
"description": "أدخل بيانات اعتماد حساب Twilio لإرسال الرسائل النصية.",
|
||||
"optionAccountSid": "معرف الحساب (SID)",
|
||||
"placeholderAccountSid": "أدخل معرف الحساب",
|
||||
"optionAuthToken": "رمز المصادقة",
|
||||
"placeholderAuthToken": "أدخل رمز المصادقة",
|
||||
"optionFromNumber": "من الرقم",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "إلى الرقم",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1255,12 +1214,11 @@
|
||||
"description": "إضافة مراقبات نموذجية لأغراض العرض التوضيحي."
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "إعادة ضبط النظام",
|
||||
"title": "إعادة تعيين النظام",
|
||||
"description": "إزالة جميع المراقبات من نظامك.",
|
||||
"dialog": {
|
||||
"title": "إزالة جميع المراقبات؟",
|
||||
"description": "لا يمكن التراجع عن هذا الإجراء.",
|
||||
"paragraph": "سيتم أيضًا إزالة جميع سجلات الفحص والحوادث ونوافذ الصيانة المجدولة وأي صفحات حالة كانت تعتمد على هذه المراقبات. صدّر المراقبات أولًا للاحتفاظ بنسخة احتياطية من بياناتها."
|
||||
"description": "لا يمكن التراجع عن هذا."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "جميع الأنظمة متوقفة",
|
||||
"allUp": "جميع الأنظمة تعمل بشكل طبيعي",
|
||||
"degraded": "بعض الأنظمة تواجه مشكلات",
|
||||
"unknown": "تعذر تحديد حالة النظام",
|
||||
"allBreached": "تجاوزت جميع الأنظمة الحدود",
|
||||
"allMaintenance": "جميع الأنظمة قيد الصيانة",
|
||||
"allPaused": "جميع الأنظمة متوقفة مؤقتًا",
|
||||
"allInitializing": "جميع الأنظمة قيد التهيئة",
|
||||
"breachedAndDown": "تجاوزت بعض الأنظمة الحدود وهي معطلة",
|
||||
"breached": "تجاوزت بعض الأنظمة الحدود",
|
||||
"maintenanceAndDown": "بعض الأنظمة معطلة أثناء الصيانة",
|
||||
"maintenance": "بعض الأنظمة قيد الصيانة",
|
||||
"partiallyPaused": "بعض الأنظمة متوقفة مؤقتًا",
|
||||
"initializing": "بعض الأنظمة قيد التهيئة",
|
||||
"noMonitors": "لم يتم تكوين أي شاشة"
|
||||
"unknown": "تعذر تحديد حالة النظام"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
+42
-95
@@ -39,7 +39,7 @@
|
||||
"flushQueue": "Vyprázdnit frontu",
|
||||
"notFound": "Přejít na hlavní přehled",
|
||||
"resetPassword": "Obnovit heslo",
|
||||
"reset": "Resetovat",
|
||||
"reset": "",
|
||||
"clear": "Vymazat",
|
||||
"addDemo": "Přidat ukázkové monitory",
|
||||
"removeMonitors": "Odebrat monitory",
|
||||
@@ -53,8 +53,8 @@
|
||||
"labels": {
|
||||
"averageResponseTime": "Průměrná doba odezvy",
|
||||
"downtime": "Výpadek",
|
||||
"high": "Vysoké",
|
||||
"low": "Nízké",
|
||||
"high": "",
|
||||
"low": "",
|
||||
"uptime": "Dostupnost"
|
||||
},
|
||||
"histogram": {
|
||||
@@ -107,14 +107,14 @@
|
||||
"table": {
|
||||
"empty": "Zde nic není",
|
||||
"headers": {
|
||||
"actions": "Akce",
|
||||
"actions": "",
|
||||
"dateTime": "Datum a čas",
|
||||
"message": "Zpráva",
|
||||
"monitor": "Monitor",
|
||||
"monitorId": "ID monitoru",
|
||||
"name": "Název",
|
||||
"status": "Stav",
|
||||
"type": "Typ",
|
||||
"type": "",
|
||||
"url": "URL",
|
||||
"interval": "Interval",
|
||||
"active": "Aktivní",
|
||||
@@ -125,17 +125,17 @@
|
||||
"components": {
|
||||
"imageUpload": {
|
||||
"clickToUpload": "Klikněte pro nahrání",
|
||||
"dragAndDrop": "nebo přetáhněte",
|
||||
"supportedFormats": "Podporované formáty",
|
||||
"maxSize": "Max. velikost",
|
||||
"dragAndDrop": "",
|
||||
"supportedFormats": "",
|
||||
"maxSize": "",
|
||||
"orDragAndDrop": "nebo přetáhněte soubor",
|
||||
"errors": {
|
||||
"invalidFileSize": "Soubor je příliš velký!",
|
||||
"invalidFileFormat": "Nepodporovaný formát souboru!"
|
||||
"invalidFileSize": "",
|
||||
"invalidFileFormat": ""
|
||||
}
|
||||
},
|
||||
"headerStatusPageControls": {
|
||||
"publicLink": "Veřejný odkaz"
|
||||
"publicLink": ""
|
||||
},
|
||||
"headerTimeRange": {
|
||||
"labels": {
|
||||
@@ -159,22 +159,22 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"menu": {
|
||||
"uptime": "Dostupnost",
|
||||
"pagespeed": "Pagespeed",
|
||||
"infrastructure": "Infrastruktura",
|
||||
"notifications": "Oznámení",
|
||||
"uptime": "",
|
||||
"pagespeed": "",
|
||||
"infrastructure": "",
|
||||
"notifications": "",
|
||||
"checks": "Kontroly",
|
||||
"incidents": "Incidenty",
|
||||
"statusPages": "Stavové stránky",
|
||||
"maintenance": "Údržba",
|
||||
"logs": "Záznamy",
|
||||
"settings": "Nastavení"
|
||||
"incidents": "",
|
||||
"statusPages": "",
|
||||
"maintenance": "",
|
||||
"logs": "",
|
||||
"settings": ""
|
||||
},
|
||||
"bottomMenu": {
|
||||
"support": "Podpora",
|
||||
"discussions": "Diskuze",
|
||||
"docs": "Dokumentace",
|
||||
"changelog": "Záznam změn"
|
||||
"support": "",
|
||||
"discussions": "",
|
||||
"docs": "",
|
||||
"changelog": ""
|
||||
},
|
||||
"accountMenu": {
|
||||
"profile": "Profil",
|
||||
@@ -186,13 +186,13 @@
|
||||
"description": "Sledujte nejnovější vydání a pomozte rozšířit komunitu na GitHubu"
|
||||
},
|
||||
"authFooter": {
|
||||
"navControls": "Ovládací prvky",
|
||||
"logOut": "Odhlásit se",
|
||||
"navControls": "",
|
||||
"logOut": "",
|
||||
"roles": {
|
||||
"superAdmin": "Superadministrátor",
|
||||
"admin": "Administrátor",
|
||||
"user": "Uživatel",
|
||||
"demoUser": "Demo uživatel"
|
||||
"demoUser": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -259,15 +259,15 @@
|
||||
"team": {
|
||||
"filter": {
|
||||
"placeholder": "Filtrovat podle role",
|
||||
"all": "Všechny role",
|
||||
"all": "",
|
||||
"admin": "Administrátor",
|
||||
"member": "Člen"
|
||||
"member": ""
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"email": "Email",
|
||||
"role": "Role",
|
||||
"created": "Vytvořeno"
|
||||
"created": ""
|
||||
}
|
||||
},
|
||||
"addMember": {
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "Vítejte v Checkmate!",
|
||||
"subtitle": "Zaregistrujte se a začněte",
|
||||
"submit": "Registrovat",
|
||||
"setupNotice": "Tato stránka je k dispozici pouze během počátečního nastavení. Po zaregistrování super administrátora lze pozvat další uživatele z nastavení týmu."
|
||||
"submit": "Registrovat"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Zapomněli jste heslo?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "Pozvat člena",
|
||||
"openSite": "Otevřít stránku",
|
||||
"pause": "Pozastavit",
|
||||
"resume": "Obnovit",
|
||||
"selectAll": "Vybrat všechny monitory",
|
||||
"selectMonitor": "Vybrat {{name}}",
|
||||
"bulkSelected_one": "Vybrán {{count}} monitor",
|
||||
"bulkSelected_other": "Vybráno {{count}} monitorů"
|
||||
"resume": "Obnovit"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Aktivní po dobu",
|
||||
"certificateExpiry": "Expirace certifikátu",
|
||||
"lastCheck": "Poslední kontrola",
|
||||
"lastResponseTime": "Poslední doba odezvy",
|
||||
"serviceIsDown": "Služba je nedostupná"
|
||||
"lastResponseTime": "Poslední doba odezvy"
|
||||
},
|
||||
"status": {
|
||||
"down": "nedostupný",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "Infrastruktura",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "{{count}} monitor pozastaven",
|
||||
"paused_other": "{{count}} monitorů pozastaveno",
|
||||
"resumed_one": "{{count}} monitor obnoven",
|
||||
"resumed_other": "{{count}} monitorů obnoveno",
|
||||
"alreadyPaused_one": "Vybraný monitor je již pozastaven",
|
||||
"alreadyPaused_other": "Všechny vybrané monitory jsou již pozastaveny",
|
||||
"alreadyRunning_one": "Vybraný monitor již běží",
|
||||
"alreadyRunning_other": "Všechny vybrané monitory již běží"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Endpoint Capture",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "Obecná nastavení",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "",
|
||||
"port": "",
|
||||
"docker": "",
|
||||
"game": "Zadejte IP adresu nebo název hostitele a číslo portu pro ping (např. 192.168.1.100 nebo example.com) a vyberte typ hry. Některé hry vyžadují povolení dotazů na server v jejich konfiguračním souboru — viz dokumentace vašeho herního serveru. Podrobnosti o protokolu najdete v <gamedigLink>seznamu podporovaných her</gamedigLink>.",
|
||||
"game": "",
|
||||
"pagespeed": "Sledujte výkon načítání stránky, metriky Core Web Vitals a hodnocení optimalizace vašeho webu.",
|
||||
"grpc": "Zadejte název hostitele a port gRPC serveru, volitelně zadejte název služby pro kontrolu stavu a přidejte zobrazovaný název.",
|
||||
"websocket": "Zadejte WebSocket URL ke sledování (např. wss://example.com/socket) a přidejte zobrazovaný název.",
|
||||
"hardware": "Sledujte využití CPU, paměti, disku a teplotu vaší infrastruktury. URL endpointu Capture musí obsahovat celou cestu /api/v1/metrics (např. http://192.168.1.10:5001/api/v1/metrics). Do autorizačního klíče vložte pouze řetězec tokenu — nezahrnujte 'Bearer'."
|
||||
"hardware": "Sledujte využití CPU, paměti, disku a teplotu vaší infrastruktury."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "Využití paměti",
|
||||
"netBytesRecv": "{{name}} - přijaté bajty",
|
||||
"netBytesSent": "{{name}} - odeslané bajty",
|
||||
"netNoTraffic": "Capture hlásí nulový provoz na všech rozhraních. To je normální, když Capture běží uvnitř kontejneru, který vidí pouze loopback rozhraní — spusťte Capture přímo na hostiteli, abyste viděli skutečnou síťovou aktivitu.",
|
||||
"temp": "Teplota"
|
||||
}
|
||||
},
|
||||
@@ -846,9 +825,9 @@
|
||||
"description": "Aktivita serveru, varování a chyby se zobrazí zde, jak Checkmate běží."
|
||||
},
|
||||
"tabs": {
|
||||
"diagnostics": "Diagnostika",
|
||||
"logs": "Záznamy",
|
||||
"queue": "Fronta úloh"
|
||||
"diagnostics": "",
|
||||
"logs": "",
|
||||
"queue": ""
|
||||
},
|
||||
"logLevelSelect": {
|
||||
"label": "Úroveň logování"
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "ID chatu",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Konfigurace Pushover",
|
||||
"description": "Zadejte token aplikace a uživatelský klíč Pushover.",
|
||||
"optionAppToken": "Token aplikace",
|
||||
"placeholderAppToken": "Zadejte token aplikace",
|
||||
"optionUserKey": "Uživatelský klíč",
|
||||
"placeholderUserKey": "Zadejte uživatelský klíč"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Konfigurace Twilio",
|
||||
"description": "Zadejte přihlašovací údaje účtu Twilio pro odesílání SMS.",
|
||||
"optionAccountSid": "SID účtu",
|
||||
"placeholderAccountSid": "Zadejte SID účtu",
|
||||
"optionAuthToken": "Autentizační token",
|
||||
"placeholderAuthToken": "Zadejte autentizační token",
|
||||
"optionFromNumber": "Z čísla",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "Na číslo",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1255,12 +1214,11 @@
|
||||
"description": "Přidejte ukázkové monitory pro demonstrační účely."
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "Reset systému",
|
||||
"description": "Odstranit všechny monitory ze systému.",
|
||||
"title": "Obnovení systému",
|
||||
"description": "Odeberte všechny monitory z vašeho systému.",
|
||||
"dialog": {
|
||||
"title": "Odebrat všechny monitory?",
|
||||
"description": "Tuto akci nelze vrátit zpět.",
|
||||
"paragraph": "Smaže se také veškerá historie kontrol, incidenty, naplánovaná údržbová okna a stavové stránky, které tyto monitory používaly. Pokud si chcete data zachovat, nejprve monitory exportujte."
|
||||
"description": "Tuto akci nelze vrátit zpět."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "Všechny systémy jsou nedostupné",
|
||||
"allUp": "Všechny systémy fungují",
|
||||
"degraded": "Některé systémy mají problémy",
|
||||
"unknown": "Nelze určit stav systému",
|
||||
"allBreached": "Všechny systémy překročily prahové hodnoty",
|
||||
"allMaintenance": "Všechny systémy jsou v údržbě",
|
||||
"allPaused": "Všechny systémy jsou pozastavené",
|
||||
"allInitializing": "Všechny systémy se inicializují",
|
||||
"breachedAndDown": "Některé systémy překročily prahové hodnoty a jsou nedostupné",
|
||||
"breached": "Některé systémy překročily prahové hodnoty",
|
||||
"maintenanceAndDown": "Některé systémy jsou nedostupné během údržby",
|
||||
"maintenance": "Některé systémy jsou v údržbě",
|
||||
"partiallyPaused": "Některé systémy jsou pozastavené",
|
||||
"initializing": "Některé systémy se inicializují",
|
||||
"noMonitors": "Nebyly nakonfigurovány žádné monitory"
|
||||
"unknown": "Nelze určit stav systému"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "Willkommen bei Checkmate!",
|
||||
"subtitle": "Registrieren Sie sich, um loszulegen",
|
||||
"submit": "Registrieren",
|
||||
"setupNotice": "Diese Seite ist nur während der Ersteinrichtung verfügbar. Sobald ein Super-Admin registriert ist, können weitere Benutzer in den Team-Einstellungen eingeladen werden."
|
||||
"submit": "Registrieren"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Passwort vergessen?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "Mitglied einladen",
|
||||
"openSite": "Seite öffnen",
|
||||
"pause": "Pausieren",
|
||||
"resume": "Fortsetzen",
|
||||
"selectAll": "Alle Monitore auswählen",
|
||||
"selectMonitor": "{{name}} auswählen",
|
||||
"bulkSelected_one": "{{count}} Monitor ausgewählt",
|
||||
"bulkSelected_other": "{{count}} Monitore ausgewählt"
|
||||
"resume": "Fortsetzen"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Aktiv seit",
|
||||
"certificateExpiry": "Zertifikatablauf",
|
||||
"lastCheck": "Letzter Check",
|
||||
"lastResponseTime": "Letzte Antwortzeit",
|
||||
"serviceIsDown": "Dienst ist nicht erreichbar"
|
||||
"lastResponseTime": "Letzte Antwortzeit"
|
||||
},
|
||||
"status": {
|
||||
"down": "ausgefallen",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "Infrastruktur",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "{{count}} Monitor pausiert",
|
||||
"paused_other": "{{count}} Monitore pausiert",
|
||||
"resumed_one": "{{count}} Monitor fortgesetzt",
|
||||
"resumed_other": "{{count}} Monitore fortgesetzt",
|
||||
"alreadyPaused_one": "Der ausgewählte Monitor ist bereits pausiert",
|
||||
"alreadyPaused_other": "Alle ausgewählten Monitore sind bereits pausiert",
|
||||
"alreadyRunning_one": "Der ausgewählte Monitor wird bereits ausgeführt",
|
||||
"alreadyRunning_other": "Alle ausgewählten Monitore werden bereits ausgeführt"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket-URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Capture-Endpunkt",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "Allgemeine Einstellungen",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "Geben Sie die IP-Adresse oder den Hostnamen ein, der angepingt werden soll (z. B. 192.168.1.100 oder example.com), und fügen Sie einen eindeutigen Anzeigenamen hinzu, der auf dem Dashboard angezeigt wird.",
|
||||
"port": "Geben Sie die URL oder IP des Servers, die Portnummer und einen eindeutigen Anzeigenamen ein, der auf dem Dashboard angezeigt wird.",
|
||||
"docker": "Geben Sie die Docker-ID Ihres Containers ein. Docker-IDs müssen die vollständige 64-Zeichen-ID sein. Sie können docker inspect <short_id> ausführen, um die vollständige Container-ID abzurufen.",
|
||||
"game": "IP Adresse oder Hostnamen und Port für Ping eingeben (z.B. 192.168.1.100 oder beispiel.de) und Spieletyp auswählen. Einige Spiele erfordern das Aktivieren von Server-Abfragen in ihrer Konfigurationsdatei — sieh in der Dokumentation deines Spielservers nach. In der <gamedigLink>Liste unterstützter Spiele</gamedigLink> findest du Protokolldetails.",
|
||||
"game": "IP Adresse oder Hostnamen und Port für Ping eingeben (z.B. 192.168.1.100 oder beispiel.de) und Spieletyp auswählen.",
|
||||
"pagespeed": "Verfolgen Sie die Ladezeit, Core Web Vitals und Optimierungswerte Ihrer Website.",
|
||||
"grpc": "Geben Sie den Hostnamen und Port des gRPC-Servers ein, optional einen Dienstnamen für den Health-Check, und fügen Sie einen Anzeigenamen hinzu.",
|
||||
"websocket": "Geben Sie die zu überwachende WebSocket-URL ein (z. B. wss://example.com/socket) und fügen Sie einen Anzeigenamen hinzu.",
|
||||
"hardware": "Überwachen Sie CPU-, Speicher-, Festplattenauslastung und Temperatur Ihrer Infrastruktur. Die URL des Capture-Endpunkts muss den vollständigen Pfad /api/v1/metrics enthalten (z. B. http://192.168.1.10:5001/api/v1/metrics). Fügen Sie für das Autorisierungsgeheimnis nur den Token-String ein — geben Sie 'Bearer' nicht mit an."
|
||||
"hardware": "Überwachen Sie CPU-, Arbeitsspeicher-, Festplattenauslastung und Temperatur Ihrer Infrastruktur."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "Arbeitsspeicherauslastung",
|
||||
"netBytesRecv": "{{name}} - Empfangene Bytes",
|
||||
"netBytesSent": "{{name}} - Gesendete Bytes",
|
||||
"netNoTraffic": "Capture meldet null Datenverkehr auf allen Schnittstellen. Das ist normal, wenn Capture in einem Container läuft, der nur Loopback-Schnittstellen sieht — führe Capture direkt auf dem Host aus, um echten Netzwerkverkehr zu sehen.",
|
||||
"temp": "Temp."
|
||||
}
|
||||
},
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "Chat-ID",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Pushover-Konfiguration",
|
||||
"description": "Geben Sie Ihr Anwendungstoken und Ihren Benutzerschlüssel von Pushover ein.",
|
||||
"optionAppToken": "Anwendungstoken",
|
||||
"placeholderAppToken": "Anwendungstoken eingeben",
|
||||
"optionUserKey": "Benutzerschlüssel",
|
||||
"placeholderUserKey": "Benutzerschlüssel eingeben"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Twilio-Konfiguration",
|
||||
"description": "Geben Sie Ihre Twilio-Kontoanmeldeinformationen ein, um SMS zu senden.",
|
||||
"optionAccountSid": "Account-SID",
|
||||
"placeholderAccountSid": "Account-SID eingeben",
|
||||
"optionAuthToken": "Auth-Token",
|
||||
"placeholderAuthToken": "Auth-Token eingeben",
|
||||
"optionFromNumber": "Absendernummer",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "Empfängernummer",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1256,11 +1215,10 @@
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "System zurücksetzen",
|
||||
"description": "Alle Monitore aus dem System entfernen.",
|
||||
"description": "Alle Monitore aus Ihrem System entfernen.",
|
||||
"dialog": {
|
||||
"title": "Alle Monitore entfernen?",
|
||||
"description": "Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"paragraph": "Dabei werden auch der gesamte Prüfverlauf, alle Vorfälle, geplante Wartungsfenster sowie Statusseiten entfernt, die diese Monitore verwenden. Exportiere die Monitore zuerst, wenn du eine Sicherung der Daten behalten möchtest."
|
||||
"description": "Dies kann nicht rückgängig gemacht werden."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "Alle Systeme sind ausgefallen",
|
||||
"allUp": "Alle Systeme funktionsfähig",
|
||||
"degraded": "Einige Systeme haben Probleme",
|
||||
"unknown": "Systemstatus kann nicht ermittelt werden",
|
||||
"allBreached": "Alle Systeme haben Schwellenwerte überschritten",
|
||||
"allMaintenance": "Alle Systeme befinden sich in Wartung",
|
||||
"allPaused": "Alle Systeme sind pausiert",
|
||||
"allInitializing": "Alle Systeme werden initialisiert",
|
||||
"breachedAndDown": "Einige Systeme haben Schwellenwerte überschritten und sind ausgefallen",
|
||||
"breached": "Einige Systeme haben Schwellenwerte überschritten",
|
||||
"maintenanceAndDown": "Einige Systeme sind während der Wartung ausgefallen",
|
||||
"maintenance": "Einige Systeme befinden sich in Wartung",
|
||||
"partiallyPaused": "Einige Systeme sind pausiert",
|
||||
"initializing": "Einige Systeme werden initialisiert",
|
||||
"noMonitors": "Es wurden keine Monitore konfiguriert"
|
||||
"unknown": "Systemstatus kann nicht ermittelt werden"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -70,9 +70,7 @@
|
||||
},
|
||||
"labels": {
|
||||
"active": "Active",
|
||||
"running": "Running",
|
||||
"paused": "Paused",
|
||||
"idle": "Idle",
|
||||
"paused": "paused",
|
||||
"na": "N/A",
|
||||
"resolved": "Resolved",
|
||||
"responseTime": "Response time"
|
||||
@@ -423,21 +421,7 @@
|
||||
"inviteMember": "Invite member",
|
||||
"openSite": "Open site",
|
||||
"pause": "Pause",
|
||||
"resume": "Resume",
|
||||
"bulkSelected_one": "{{count}} monitor selected",
|
||||
"bulkSelected_other": "{{count}} monitors selected",
|
||||
"selectAll": "Select all monitors",
|
||||
"selectMonitor": "Select {{name}}"
|
||||
},
|
||||
"bulkPause": {
|
||||
"alreadyPaused_one": "The selected monitor is already paused",
|
||||
"alreadyPaused_other": "All selected monitors are already paused",
|
||||
"alreadyRunning_one": "The selected monitor is already running",
|
||||
"alreadyRunning_other": "All selected monitors are already running",
|
||||
"paused_one": "{{count}} monitor paused",
|
||||
"paused_other": "{{count}} monitors paused",
|
||||
"resumed_one": "{{count}} monitor resumed",
|
||||
"resumed_other": "{{count}} monitors resumed"
|
||||
"resume": "Resume"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Active for",
|
||||
@@ -550,10 +534,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Capture endpoint",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "General settings",
|
||||
@@ -562,11 +542,11 @@
|
||||
"ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.",
|
||||
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.",
|
||||
"docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
|
||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type. Some games require enabling server queries in their config file — check your game server's documentation. See the <gamedigLink>supported games list</gamedigLink> for protocol details.",
|
||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
|
||||
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
||||
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
|
||||
"websocket": "Enter the WebSocket URL to monitor (e.g., wss://example.com/socket) and add a display name.",
|
||||
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure. The Capture endpoint URL must include the full path /api/v1/metrics (e.g. http://192.168.1.10:5001/api/v1/metrics). For the authorization secret, paste only the token string — don't include 'Bearer'."
|
||||
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -789,7 +769,6 @@
|
||||
"memory": "Memory usage",
|
||||
"netBytesRecv": "{{name}} - Bytes Received",
|
||||
"netBytesSent": "{{name}} - Bytes Sent",
|
||||
"netNoTraffic": "Capture is reporting zero traffic on every interface. This is normal when Capture runs inside a container that can only see loopback interfaces — run Capture directly on the host to see real network activity.",
|
||||
"temp": "Temp"
|
||||
}
|
||||
},
|
||||
@@ -903,7 +882,6 @@
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"state": "State",
|
||||
"timestamp": "Timestamp",
|
||||
"level": "Level",
|
||||
"service": "Service",
|
||||
@@ -1219,10 +1197,6 @@
|
||||
"label": "Email address - Used for authentication",
|
||||
"placeholder": "you@example.com"
|
||||
},
|
||||
"displayName": {
|
||||
"label": "Sender name (optional) - Friendly name shown in the From field",
|
||||
"placeholder": "Checkmate notifications"
|
||||
},
|
||||
"user": {
|
||||
"label": "Email user - Username for authentication, overrides email address if specified",
|
||||
"placeholder": "Leave empty if not required"
|
||||
@@ -1266,8 +1240,7 @@
|
||||
"description": "Remove all monitors from your system.",
|
||||
"dialog": {
|
||||
"title": "Remove all monitors?",
|
||||
"description": "This cannot be undone.",
|
||||
"paragraph": "All check history, incidents, scheduled maintenance windows, and any status pages that referenced these monitors will be removed too. Export your monitors first to keep a backup of their data."
|
||||
"description": "This cannot be undone."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1301,20 +1274,6 @@
|
||||
"chartTypeHeatmap": "Heatmap",
|
||||
"chartTypeHistogram": "Histogram",
|
||||
"noData": "No data available",
|
||||
"status": {
|
||||
"up": "Operational",
|
||||
"down": "Down",
|
||||
"breached": "Threshold exceeded",
|
||||
"maintenance": "Maintenance",
|
||||
"paused": "Paused",
|
||||
"initializing": "Initializing"
|
||||
},
|
||||
"chart": {
|
||||
"avg": "avg {{value}}ms",
|
||||
"max": "max {{value}}ms",
|
||||
"downTooltip": "Down",
|
||||
"heatmapAria": "Response time heatmap"
|
||||
},
|
||||
"infrastructure": {
|
||||
"title": "Infrastructure",
|
||||
"cpu": "CPU",
|
||||
@@ -1331,8 +1290,6 @@
|
||||
}
|
||||
},
|
||||
"statusBar": {
|
||||
"monitoringSummary": "Monitoring {{count}} service · Updates every 10 seconds",
|
||||
"monitoringSummary_other": "Monitoring {{count}} services · Updates every 10 seconds",
|
||||
"allUp": "All systems operational",
|
||||
"allBreached": "All systems have exceeded thresholds",
|
||||
"allMaintenance": "All systems are under maintenance",
|
||||
@@ -1364,13 +1321,6 @@
|
||||
"addMonitor": "Add a monitor to get started"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Powered by"
|
||||
},
|
||||
"editorial": {
|
||||
"reportTitle": "Service Report",
|
||||
"allUp": "All systems are operating normally."
|
||||
},
|
||||
"form": {
|
||||
"access": {
|
||||
"title": "Access",
|
||||
@@ -1428,35 +1378,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "Page theme",
|
||||
"description": "Choose the visual style for your public status page.",
|
||||
"label": "Theme",
|
||||
"options": {
|
||||
"refined": {
|
||||
"name": "Refined",
|
||||
"description": "Clean, professional, close to the classic Checkmate look."
|
||||
},
|
||||
"modern": {
|
||||
"name": "Modern",
|
||||
"description": "SaaS-grade with a pulsing status dot and softly fading cards."
|
||||
},
|
||||
"bold": {
|
||||
"name": "Bold",
|
||||
"description": "Large type and confident spacing. Great for brand-forward pages."
|
||||
},
|
||||
"editorial": {
|
||||
"name": "Editorial",
|
||||
"description": "Serif headings and hairline dividers. Distinctive and calm."
|
||||
}
|
||||
}
|
||||
},
|
||||
"themeMode": {
|
||||
"label": "Theme mode",
|
||||
"auto": "Auto (follow visitor's device)",
|
||||
"light": "Light only",
|
||||
"dark": "Dark only"
|
||||
},
|
||||
"features": {
|
||||
"title": "Features",
|
||||
"description": "Configure what information is displayed on your status page.",
|
||||
|
||||
+87
-154
@@ -13,7 +13,7 @@
|
||||
"content": "Advertencia: Aún no ha agregado una clave de API de Google PageSpeed. Visite <settingsLink>Configuración</settingsLink> para agregar una. Sin ella, el monitor de PageSpeed no funcionará."
|
||||
}
|
||||
},
|
||||
"appName": "Checkmate",
|
||||
"appName": "",
|
||||
"breadcrumbs": {
|
||||
"details": "Detalles",
|
||||
"home": "Inicio"
|
||||
@@ -35,11 +35,11 @@
|
||||
"sendInvite": "Enviar invitación",
|
||||
"test": "Probar",
|
||||
"testNotifications": "Probar notificaciones",
|
||||
"toggleTheme": "Cambiar tema claro / oscuro",
|
||||
"toggleTheme": "",
|
||||
"flushQueue": "Vaciar cola",
|
||||
"notFound": "Ir al panel principal",
|
||||
"resetPassword": "Restablecer contraseña",
|
||||
"reset": "Restablecer",
|
||||
"resetPassword": "",
|
||||
"reset": "",
|
||||
"clear": "Limpiar",
|
||||
"addDemo": "Agregar monitores de demostración",
|
||||
"removeMonitors": "Eliminar monitores",
|
||||
@@ -47,14 +47,7 @@
|
||||
"exportToJSON": "Exportar a JSON",
|
||||
"importFromJSON": "Importar desde JSON",
|
||||
"clearFilters": "Limpiar filtros",
|
||||
"removeUser": "Eliminar usuario",
|
||||
"testNotification": "Probar notificación",
|
||||
"invite": "Invitar",
|
||||
"copyLink": "Copiar enlace",
|
||||
"regenerateToken": "Regenerar token",
|
||||
"copyToken": "Copiar token",
|
||||
"exportLogs": "Exportar registros",
|
||||
"refresh": "Actualizar"
|
||||
"removeUser": "Eliminar usuario"
|
||||
},
|
||||
"charts": {
|
||||
"labels": {
|
||||
@@ -93,12 +86,12 @@
|
||||
"name": {
|
||||
"option": {
|
||||
"firstName": {
|
||||
"label": "Nombre",
|
||||
"placeholder": "Introduce tu nombre"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
},
|
||||
"lastName": {
|
||||
"label": "Apellidos",
|
||||
"placeholder": "Introduce tus apellidos"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -106,7 +99,7 @@
|
||||
"option": {
|
||||
"email": {
|
||||
"label": "Correo electrónico",
|
||||
"placeholder": "tu@ejemplo.com"
|
||||
"placeholder": "you@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,14 +107,14 @@
|
||||
"table": {
|
||||
"empty": "No hay nada aquí",
|
||||
"headers": {
|
||||
"actions": "Acciones",
|
||||
"actions": "",
|
||||
"dateTime": "Fecha y hora",
|
||||
"message": "Mensaje",
|
||||
"monitor": "Monitor",
|
||||
"monitorId": "ID de monitor",
|
||||
"name": "Nombre",
|
||||
"status": "Estado",
|
||||
"type": "Tipo",
|
||||
"type": "",
|
||||
"url": "URL",
|
||||
"interval": "Intervalo",
|
||||
"active": "Activo",
|
||||
@@ -132,17 +125,17 @@
|
||||
"components": {
|
||||
"imageUpload": {
|
||||
"clickToUpload": "Haga clic para subir",
|
||||
"dragAndDrop": "o arrastra y suelta",
|
||||
"supportedFormats": "Formatos admitidos",
|
||||
"maxSize": "Tamaño máximo",
|
||||
"dragAndDrop": "",
|
||||
"supportedFormats": "",
|
||||
"maxSize": "",
|
||||
"orDragAndDrop": "o arrastre y suelte",
|
||||
"errors": {
|
||||
"invalidFileSize": "¡El archivo es demasiado grande!",
|
||||
"invalidFileFormat": "¡Formato de archivo no admitido!"
|
||||
"invalidFileSize": "",
|
||||
"invalidFileFormat": ""
|
||||
}
|
||||
},
|
||||
"headerStatusPageControls": {
|
||||
"publicLink": "Enlace público"
|
||||
"publicLink": ""
|
||||
},
|
||||
"headerTimeRange": {
|
||||
"labels": {
|
||||
@@ -162,26 +155,26 @@
|
||||
"serverUnreachable": "No se puede conectar al servidor",
|
||||
"retry": "Reintentar",
|
||||
"retrying": "Reintentando...",
|
||||
"reconnected": "Conexión restablecida"
|
||||
"reconnected": ""
|
||||
},
|
||||
"sidebar": {
|
||||
"menu": {
|
||||
"uptime": "Disponibilidad",
|
||||
"pagespeed": "Pagespeed",
|
||||
"infrastructure": "Infraestructura",
|
||||
"notifications": "Notificaciones",
|
||||
"uptime": "",
|
||||
"pagespeed": "",
|
||||
"infrastructure": "",
|
||||
"notifications": "",
|
||||
"checks": "Verificaciones",
|
||||
"incidents": "Incidentes",
|
||||
"statusPages": "Páginas de estado",
|
||||
"maintenance": "Mantenimiento",
|
||||
"logs": "Registros",
|
||||
"settings": "Configuración"
|
||||
"incidents": "",
|
||||
"statusPages": "",
|
||||
"maintenance": "",
|
||||
"logs": "",
|
||||
"settings": ""
|
||||
},
|
||||
"bottomMenu": {
|
||||
"support": "Soporte",
|
||||
"discussions": "Discusiones",
|
||||
"docs": "Documentación",
|
||||
"changelog": "Registro de cambios"
|
||||
"support": "",
|
||||
"discussions": "",
|
||||
"docs": "",
|
||||
"changelog": ""
|
||||
},
|
||||
"accountMenu": {
|
||||
"profile": "Perfil",
|
||||
@@ -193,13 +186,13 @@
|
||||
"description": "Vea las últimas versiones y ayude a crecer la comunidad en GitHub"
|
||||
},
|
||||
"authFooter": {
|
||||
"navControls": "Controles",
|
||||
"logOut": "Cerrar sesión",
|
||||
"navControls": "",
|
||||
"logOut": "",
|
||||
"roles": {
|
||||
"superAdmin": "Superadministrador",
|
||||
"admin": "Administrador",
|
||||
"user": "Usuario",
|
||||
"demoUser": "Usuario demo"
|
||||
"demoUser": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -266,15 +259,15 @@
|
||||
"team": {
|
||||
"filter": {
|
||||
"placeholder": "Filtrar por rol",
|
||||
"all": "Todos los roles",
|
||||
"all": "",
|
||||
"admin": "Administrador",
|
||||
"member": "Miembro"
|
||||
"member": ""
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"email": "Correo electrónico",
|
||||
"role": "Rol",
|
||||
"created": "Creado el"
|
||||
"created": ""
|
||||
}
|
||||
},
|
||||
"addMember": {
|
||||
@@ -313,35 +306,35 @@
|
||||
"common": {
|
||||
"passwordRules": {
|
||||
"length": {
|
||||
"beginning": "Debe tener al menos",
|
||||
"highlighted": "8 caracteres"
|
||||
"beginning": "",
|
||||
"highlighted": ""
|
||||
},
|
||||
"lowercase": {
|
||||
"beginning": "Debe contener al menos",
|
||||
"highlighted": "una letra minúscula"
|
||||
"beginning": "",
|
||||
"highlighted": ""
|
||||
},
|
||||
"match": {
|
||||
"beginning": "Las contraseñas",
|
||||
"highlighted": "deben coincidir"
|
||||
"beginning": "",
|
||||
"highlighted": ""
|
||||
},
|
||||
"number": {
|
||||
"beginning": "Debe contener al menos",
|
||||
"highlighted": "un número"
|
||||
"beginning": "",
|
||||
"highlighted": ""
|
||||
},
|
||||
"special": {
|
||||
"beginning": "Debe contener al menos",
|
||||
"highlighted": "un carácter especial (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)"
|
||||
"beginning": "",
|
||||
"highlighted": ""
|
||||
},
|
||||
"uppercase": {
|
||||
"beginning": "Debe contener al menos",
|
||||
"highlighted": "una letra mayúscula"
|
||||
"beginning": "",
|
||||
"highlighted": ""
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"option": {
|
||||
"email": {
|
||||
"label": "Correo electrónico",
|
||||
"placeholder": "yo@ejemplo.com"
|
||||
"placeholder": "me@example.com"
|
||||
},
|
||||
"password": {
|
||||
"label": "Contraseña",
|
||||
@@ -371,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "¡Bienvenido de nuevo a Checkmate!",
|
||||
"subtitle": "Regístrese para comenzar",
|
||||
"submit": "Registrarse",
|
||||
"setupNotice": "Esta página solo está disponible durante la configuración inicial. Una vez que se haya registrado un super administrador, se podrá invitar a usuarios adicionales desde la configuración del equipo."
|
||||
"submit": "Registrarse"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "¿Olvidó su contraseña?",
|
||||
@@ -428,18 +420,13 @@
|
||||
"inviteMember": "Invitar miembro",
|
||||
"openSite": "Abrir sitio",
|
||||
"pause": "Pausar",
|
||||
"resume": "Reanudar",
|
||||
"selectAll": "Seleccionar todos los monitores",
|
||||
"selectMonitor": "Seleccionar {{name}}",
|
||||
"bulkSelected_one": "{{count}} monitor seleccionado",
|
||||
"bulkSelected_other": "{{count}} monitores seleccionados"
|
||||
"resume": "Reanudar"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Activo desde hace",
|
||||
"certificateExpiry": "Vencimiento del certificado",
|
||||
"lastCheck": "Última verificación",
|
||||
"lastResponseTime": "Último tiempo de respuesta",
|
||||
"serviceIsDown": "El servicio está caído"
|
||||
"lastResponseTime": "Último tiempo de respuesta"
|
||||
},
|
||||
"status": {
|
||||
"down": "caído",
|
||||
@@ -460,16 +447,6 @@
|
||||
"optionHardware": "Infraestructura",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "{{count}} monitor pausado",
|
||||
"paused_other": "{{count}} monitores pausados",
|
||||
"resumed_one": "{{count}} monitor reanudado",
|
||||
"resumed_other": "{{count}} monitores reanudados",
|
||||
"alreadyPaused_one": "El monitor seleccionado ya está pausado",
|
||||
"alreadyPaused_other": "Todos los monitores seleccionados ya están pausados",
|
||||
"alreadyRunning_one": "El monitor seleccionado ya se está ejecutando",
|
||||
"alreadyRunning_other": "Todos los monitores seleccionados ya se están ejecutando"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -490,9 +467,9 @@
|
||||
},
|
||||
"matchMethod": {
|
||||
"label": "Método de coincidencia",
|
||||
"equal": "Igual",
|
||||
"include": "Contiene",
|
||||
"regex": "Regex"
|
||||
"equal": "",
|
||||
"include": "",
|
||||
"regex": ""
|
||||
}
|
||||
},
|
||||
"title": "Configuración avanzada"
|
||||
@@ -504,15 +481,15 @@
|
||||
"label": "Frecuencia de verificación",
|
||||
"value": {
|
||||
"fifteenMinutes": "15 minutos",
|
||||
"fifteenSeconds": "15 segundos",
|
||||
"fifteenSeconds": "",
|
||||
"fiveMinutes": "5 minutos",
|
||||
"fourMinutes": "4 minutos",
|
||||
"oneMinute": "1 minuto",
|
||||
"fourMinutes": "",
|
||||
"oneMinute": "",
|
||||
"tenMinutes": "10 minutos",
|
||||
"thirtyMinutes": "30 minutos",
|
||||
"thirtySeconds": "30 segundos",
|
||||
"threeMinutes": "3 minutos",
|
||||
"twoMinutes": "2 minutos"
|
||||
"thirtySeconds": "",
|
||||
"threeMinutes": "",
|
||||
"twoMinutes": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -541,8 +518,8 @@
|
||||
"placeholder": "https://www.google.com"
|
||||
},
|
||||
"game": {
|
||||
"label": "Elige el juego",
|
||||
"placeholder": "Selecciona un juego"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
},
|
||||
"port": {
|
||||
"label": "Puerto a monitorear",
|
||||
@@ -555,23 +532,19 @@
|
||||
"wsUrl": {
|
||||
"label": "URL de WebSocket",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Endpoint de Capture",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "Configuración general",
|
||||
"description": {
|
||||
"http": "Ingrese la URL o IP a monitorear (p. ej., https://example.com/ o 192.168.1.100) y agregue un nombre para mostrar que aparecerá en el panel.",
|
||||
"ping": "Introduce la dirección IP o el nombre de host para hacer ping (p. ej., 192.168.1.100 o ejemplo.com) y un nombre descriptivo que aparecerá en el panel.",
|
||||
"port": "Introduce la URL o IP del servidor, el número de puerto y un nombre descriptivo que aparecerá en el panel.",
|
||||
"docker": "Introduce el nombre o ID del contenedor de Docker. Puedes usar el nombre del contenedor (p. ej., my-app) o su ID.",
|
||||
"game": "Introduce la dirección IP o el nombre de host y el número de puerto para hacer ping (p. ej., 192.168.1.100 o ejemplo.com) y un nombre descriptivo que aparecerá en el panel. Algunos juegos requieren habilitar consultas del servidor en su archivo de configuración — consulta la documentación de tu servidor. Consulta la <gamedigLink>lista de juegos compatibles</gamedigLink> para ver los detalles del protocolo.",
|
||||
"ping": "",
|
||||
"port": "",
|
||||
"docker": "",
|
||||
"game": "",
|
||||
"pagespeed": "Rastree el rendimiento de carga de página, Core Web Vitals y puntuaciones de optimización de su sitio web.",
|
||||
"grpc": "Ingrese el nombre de host y puerto del servidor gRPC, opcionalmente especifique un nombre de servicio para la verificación de salud, y agregue un nombre para mostrar.",
|
||||
"websocket": "Ingrese la URL de WebSocket a monitorear (p. ej., wss://example.com/socket) y agregue un nombre para mostrar.",
|
||||
"hardware": "Supervisa el uso de CPU, memoria, disco y temperatura de tu infraestructura. La URL del endpoint de Capture debe incluir la ruta completa /api/v1/metrics (p. ej., http://192.168.1.10:5001/api/v1/metrics). Para el secreto de autorización, pega solo la cadena del token — no incluyas 'Bearer'."
|
||||
"hardware": "Monitoree el uso de CPU, memoria, disco y temperatura de su infraestructura."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -772,7 +745,7 @@
|
||||
"headers": {
|
||||
"endTime": "Hora de fin",
|
||||
"resolutionType": "Tipo de resolución",
|
||||
"startTime": "Hora de inicio",
|
||||
"startTime": "",
|
||||
"statusCode": "Código de estado"
|
||||
},
|
||||
"resolvedIncidents": "Incidentes resueltos",
|
||||
@@ -794,7 +767,6 @@
|
||||
"memory": "Uso de memoria",
|
||||
"netBytesRecv": "{{name}} - Bytes recibidos",
|
||||
"netBytesSent": "{{name}} - Bytes enviados",
|
||||
"netNoTraffic": "Capture no informa tráfico en ninguna interfaz. Esto es normal cuando Capture se ejecuta dentro de un contenedor que solo ve interfaces de loopback — ejecuta Capture directamente en el host para ver actividad de red real.",
|
||||
"temp": "Temp"
|
||||
}
|
||||
},
|
||||
@@ -816,10 +788,10 @@
|
||||
}
|
||||
},
|
||||
"statBoxes": {
|
||||
"avgCpuTemperature": "Temperatura media de la CPU",
|
||||
"cpuFrequency": "Frecuencia de la CPU",
|
||||
"cpuLogical": "CPU (lógicas)",
|
||||
"cpuPhysical": "CPU (físicas)",
|
||||
"avgCpuTemperature": "",
|
||||
"cpuFrequency": "",
|
||||
"cpuLogical": "",
|
||||
"cpuPhysical": "",
|
||||
"disk": "Disco",
|
||||
"memory": "Memoria",
|
||||
"os": "SO"
|
||||
@@ -833,7 +805,7 @@
|
||||
},
|
||||
"tabs": {
|
||||
"labels": {
|
||||
"network": "Red",
|
||||
"network": "",
|
||||
"overview": "Resumen"
|
||||
}
|
||||
},
|
||||
@@ -853,9 +825,9 @@
|
||||
"description": "La actividad del servidor, advertencias y errores aparecerán aquí mientras Checkmate se ejecuta."
|
||||
},
|
||||
"tabs": {
|
||||
"diagnostics": "Diagnóstico",
|
||||
"logs": "Registros",
|
||||
"queue": "Cola de trabajos"
|
||||
"diagnostics": "",
|
||||
"logs": "",
|
||||
"queue": ""
|
||||
},
|
||||
"logLevelSelect": {
|
||||
"label": "Nivel de registro"
|
||||
@@ -893,7 +865,7 @@
|
||||
},
|
||||
"sections": {
|
||||
"runtimeStats": {
|
||||
"title": "Estadísticas de tiempo de ejecución",
|
||||
"title": "Estadísticas de ejecución",
|
||||
"description": "Instantánea del proceso Node.js y la máquina anfitriona. Se actualiza cada 5 segundos."
|
||||
},
|
||||
"memoryCpu": {
|
||||
@@ -936,8 +908,8 @@
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"nextWindow": "Próxima ventana",
|
||||
"repeat": "Repetición"
|
||||
"nextWindow": "",
|
||||
"repeat": ""
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1044,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "ID de chat",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Configuración de Pushover",
|
||||
"description": "Introduce tu token de aplicación y la clave de usuario de Pushover.",
|
||||
"optionAppToken": "Token de aplicación",
|
||||
"placeholderAppToken": "Introduce el token de aplicación",
|
||||
"optionUserKey": "Clave de usuario",
|
||||
"placeholderUserKey": "Introduce la clave de usuario"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Configuración de Twilio",
|
||||
"description": "Introduce las credenciales de tu cuenta de Twilio para enviar SMS.",
|
||||
"optionAccountSid": "SID de la cuenta",
|
||||
"placeholderAccountSid": "Introduce el SID de la cuenta",
|
||||
"optionAuthToken": "Token de autenticación",
|
||||
"placeholderAuthToken": "Introduce el token de autenticación",
|
||||
"optionFromNumber": "Número de origen",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "Número de destino",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1208,7 +1160,7 @@
|
||||
"email": {
|
||||
"title": "Configuración de correo electrónico",
|
||||
"description": "Configure los ajustes de correo electrónico de su sistema. Se usa para enviar notificaciones y alertas.",
|
||||
"descriptionTransport": "Esto crea un transporte SMTP para NodeMailer",
|
||||
"descriptionTransport": "",
|
||||
"descriptionTransportLink": "Ver especificaciones aquí",
|
||||
"option": {
|
||||
"host": {
|
||||
@@ -1262,12 +1214,11 @@
|
||||
"description": "Agregue monitores de ejemplo con fines demostrativos."
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "Reiniciar sistema",
|
||||
"description": "Eliminar todos los monitores del sistema.",
|
||||
"title": "Reinicio del sistema",
|
||||
"description": "Elimine todos los monitores de su sistema.",
|
||||
"dialog": {
|
||||
"title": "¿Eliminar todos los monitores?",
|
||||
"description": "Esta acción no se puede deshacer.",
|
||||
"paragraph": "También se eliminarán todo el historial de comprobaciones, los incidentes, las ventanas de mantenimiento programadas y las páginas de estado que usaban estos monitores. Exporta los monitores primero si quieres conservar una copia de seguridad de sus datos."
|
||||
"description": "Esto no se puede deshacer."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1320,18 +1271,7 @@
|
||||
"allDown": "Todos los sistemas están caídos",
|
||||
"allUp": "Todos los sistemas operativos",
|
||||
"degraded": "Algunos sistemas están experimentando problemas",
|
||||
"unknown": "No se puede determinar el estado del sistema",
|
||||
"allBreached": "Todos los sistemas han superado los umbrales",
|
||||
"allMaintenance": "Todos los sistemas están en mantenimiento",
|
||||
"allPaused": "Todos los sistemas están en pausa",
|
||||
"allInitializing": "Todos los sistemas se están inicializando",
|
||||
"breachedAndDown": "Algunos sistemas han superado los umbrales y están caídos",
|
||||
"breached": "Algunos sistemas han superado los umbrales",
|
||||
"maintenanceAndDown": "Algunos sistemas están caídos durante el mantenimiento",
|
||||
"maintenance": "Algunos sistemas están en mantenimiento",
|
||||
"partiallyPaused": "Algunos sistemas están en pausa",
|
||||
"initializing": "Algunos sistemas se están inicializando",
|
||||
"noMonitors": "No se ha configurado ningún monitor"
|
||||
"unknown": "No se puede determinar el estado del sistema"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
@@ -1427,13 +1367,6 @@
|
||||
"header": {
|
||||
"title": "Páginas de estado",
|
||||
"description": "Publica páginas públicas que muestren a tus clientes y partes interesadas la disponibilidad en tiempo real y el historial de incidentes."
|
||||
},
|
||||
"publicPage": {
|
||||
"maintenance": {
|
||||
"upcoming": "Mantenimiento programado",
|
||||
"inProgress": "Mantenimiento en curso",
|
||||
"completed": "Mantenimiento completado"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uptime": {
|
||||
|
||||
+29
-82
@@ -13,7 +13,7 @@
|
||||
"content": "Varoitus: Et ole vielä lisännyt Google PageSpeed API -avainta. Siirry <settingsLink>Asetuksiin</settingsLink> lisätäksesi sellainen. Ilman sitä PageSpeed-monitorointi ei toimi."
|
||||
}
|
||||
},
|
||||
"appName": "Checkmate",
|
||||
"appName": "",
|
||||
"breadcrumbs": {
|
||||
"details": "Tiedot",
|
||||
"home": "Etusivu"
|
||||
@@ -35,10 +35,10 @@
|
||||
"sendInvite": "Lähetä kutsu",
|
||||
"test": "Testaa",
|
||||
"testNotifications": "Testaa ilmoitukset",
|
||||
"toggleTheme": "Vaihda vaalea / tumma",
|
||||
"toggleTheme": "",
|
||||
"flushQueue": "Tyhjennä jono",
|
||||
"notFound": "Siirry pääsivulle",
|
||||
"resetPassword": "Nollaa salasana",
|
||||
"resetPassword": "",
|
||||
"reset": "Palauta",
|
||||
"clear": "Tyhjennä",
|
||||
"addDemo": "Lisää demomonitorit",
|
||||
@@ -53,8 +53,8 @@
|
||||
"labels": {
|
||||
"averageResponseTime": "Keskimääräinen vasteaika",
|
||||
"downtime": "Käyttökatko",
|
||||
"high": "Korkea",
|
||||
"low": "Matala",
|
||||
"high": "",
|
||||
"low": "",
|
||||
"uptime": "Käytettävyys"
|
||||
},
|
||||
"histogram": {
|
||||
@@ -86,12 +86,12 @@
|
||||
"name": {
|
||||
"option": {
|
||||
"firstName": {
|
||||
"label": "Etunimi",
|
||||
"placeholder": "Anna etunimesi"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
},
|
||||
"lastName": {
|
||||
"label": "Sukunimi",
|
||||
"placeholder": "Anna sukunimesi"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -107,14 +107,14 @@
|
||||
"table": {
|
||||
"empty": "Täällä ei ole mitään",
|
||||
"headers": {
|
||||
"actions": "Toiminnot",
|
||||
"actions": "",
|
||||
"dateTime": "Päivämäärä ja aika",
|
||||
"message": "Viesti",
|
||||
"monitor": "Monitori",
|
||||
"monitorId": "Monitorin tunnus",
|
||||
"name": "Nimi",
|
||||
"status": "Tila",
|
||||
"type": "Tyyppi",
|
||||
"type": "",
|
||||
"url": "Osoite",
|
||||
"interval": "Aikaväli",
|
||||
"active": "Aktiivinen",
|
||||
@@ -131,7 +131,7 @@
|
||||
"orDragAndDrop": "tai vedä ja pudota",
|
||||
"errors": {
|
||||
"invalidFileSize": "Tiedoston koko on liian suuri!",
|
||||
"invalidFileFormat": "Tiedostomuotoa ei tueta!"
|
||||
"invalidFileFormat": ""
|
||||
}
|
||||
},
|
||||
"headerStatusPageControls": {
|
||||
@@ -155,19 +155,19 @@
|
||||
"serverUnreachable": "Palvelimeen ei saada yhteyttä",
|
||||
"retry": "Yritä uudelleen",
|
||||
"retrying": "Yritetään uudelleen...",
|
||||
"reconnected": "Yhteys palautettu"
|
||||
"reconnected": ""
|
||||
},
|
||||
"sidebar": {
|
||||
"menu": {
|
||||
"uptime": "Käytettävyys",
|
||||
"pagespeed": "Pagespeed",
|
||||
"uptime": "",
|
||||
"pagespeed": "",
|
||||
"infrastructure": "Infrastruktuuri",
|
||||
"notifications": "Ilmoitukset",
|
||||
"notifications": "",
|
||||
"checks": "Tarkistukset",
|
||||
"incidents": "Häiriöt",
|
||||
"incidents": "",
|
||||
"statusPages": "Tilasivut",
|
||||
"maintenance": "Huolto",
|
||||
"logs": "Lokit",
|
||||
"logs": "",
|
||||
"settings": "Asetukset"
|
||||
},
|
||||
"bottomMenu": {
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "Tervetuloa takaisin Checkmateen!",
|
||||
"subtitle": "Rekisteröidy aloittaaksesi",
|
||||
"submit": "Rekisteröidy",
|
||||
"setupNotice": "Tämä sivu on käytettävissä vain alkuasennuksen aikana. Kun super-admin on rekisteröity, lisäkäyttäjiä voi kutsua tiimiasetuksista."
|
||||
"submit": "Rekisteröidy"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Unohditko salasanasi?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "Kutsu jäsen",
|
||||
"openSite": "Avaa sivusto",
|
||||
"pause": "Keskeytä",
|
||||
"resume": "Jatka",
|
||||
"selectAll": "Valitse kaikki näytöt",
|
||||
"selectMonitor": "Valitse {{name}}",
|
||||
"bulkSelected_one": "{{count}} näyttö valittu",
|
||||
"bulkSelected_other": "{{count}} näyttöä valittu"
|
||||
"resume": "Jatka"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Aktiivinen",
|
||||
"certificateExpiry": "Varmenteen vanheneminen",
|
||||
"lastCheck": "Viimeisin tarkistus",
|
||||
"lastResponseTime": "Viimeisin vasteaika",
|
||||
"serviceIsDown": "Palvelu ei ole käytettävissä"
|
||||
"lastResponseTime": "Viimeisin vasteaika"
|
||||
},
|
||||
"status": {
|
||||
"down": "alhaalla",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "Infrastruktuuri",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "{{count}} näyttö keskeytetty",
|
||||
"paused_other": "{{count}} näyttöä keskeytetty",
|
||||
"resumed_one": "{{count}} näyttö jatkettu",
|
||||
"resumed_other": "{{count}} näyttöä jatkettu",
|
||||
"alreadyPaused_one": "Valittu näyttö on jo keskeytetty",
|
||||
"alreadyPaused_other": "Kaikki valitut näytöt on jo keskeytetty",
|
||||
"alreadyRunning_one": "Valittu näyttö on jo käynnissä",
|
||||
"alreadyRunning_other": "Kaikki valitut näytöt ovat jo käynnissä"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Capture-päätepiste",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "Yleiset asetukset",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "",
|
||||
"port": "",
|
||||
"docker": "",
|
||||
"game": "Anna IP-osoite tai isäntänimi ja portin numero pingausta varten (esim. 192.168.1.100 tai example.com) ja valitse pelityyppi. Jotkin pelit vaativat palvelinkyselyiden käyttöönoton asetustiedostossaan — tarkista pelipalvelimesi dokumentaatio. Katso protokollatiedot <gamedigLink>tuettujen pelien luettelosta</gamedigLink>.",
|
||||
"game": "",
|
||||
"pagespeed": "Seuraa sivun latausnopeutta, Core Web Vitals -arvoja ja optimointipisteitä verkkosivustollasi.",
|
||||
"grpc": "Syötä gRPC-palvelimen isäntänimi ja portti, määritä halutessasi palvelun nimi terveystarkistusta varten ja lisää näyttönimi.",
|
||||
"websocket": "Syötä monitoroitava WebSocket URL (esim. wss://example.com/socket) ja lisää näyttönimi.",
|
||||
"hardware": "Seuraa infrastruktuurisi suorittimen, muistin, levyn käyttöä ja lämpötilaa. Capture-päätepisteen URL-osoitteen on sisällettävä koko polku /api/v1/metrics (esim. http://192.168.1.10:5001/api/v1/metrics). Liitä valtuutusavaimeen vain tokenin merkkijono — älä sisällytä sanaa 'Bearer'."
|
||||
"hardware": "Monitoroi infrastruktuurisi CPU:n, muistin ja levyn käyttöä sekä lämpötilaa."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "Muistin käyttö",
|
||||
"netBytesRecv": "{{name}} - tavua vastaanotettu",
|
||||
"netBytesSent": "{{name}} - tavua lähetetty",
|
||||
"netNoTraffic": "Capture ilmoittaa nollaliikennettä kaikilla rajapinnoilla. Tämä on normaalia, kun Capture käynnistetään säiliössä, joka näkee vain loopback-rajapinnat — suorita Capture suoraan isännällä nähdäksesi todellisen verkkoaktiviteetin.",
|
||||
"temp": "Lämpö"
|
||||
}
|
||||
},
|
||||
@@ -846,9 +825,9 @@
|
||||
"description": "Palvelimen toiminta, varoitukset ja virheet näkyvät täällä Checkmaten toimiessa."
|
||||
},
|
||||
"tabs": {
|
||||
"diagnostics": "Diagnostiikka",
|
||||
"logs": "Lokit",
|
||||
"queue": "Työjono"
|
||||
"diagnostics": "",
|
||||
"logs": "",
|
||||
"queue": ""
|
||||
},
|
||||
"logLevelSelect": {
|
||||
"label": "Lokitaso"
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "Chat-tunnus",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Pushover-määritys",
|
||||
"description": "Anna Pushover-sovellustunnus ja käyttäjäavain.",
|
||||
"optionAppToken": "Sovellustunnus",
|
||||
"placeholderAppToken": "Anna sovellustunnus",
|
||||
"optionUserKey": "Käyttäjäavain",
|
||||
"placeholderUserKey": "Anna käyttäjäavain"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Twilio-määritys",
|
||||
"description": "Anna Twilio-tilin tunnistetiedot tekstiviestien lähettämistä varten.",
|
||||
"optionAccountSid": "Tilin SID",
|
||||
"placeholderAccountSid": "Anna tilin SID",
|
||||
"optionAuthToken": "Todennustunnus",
|
||||
"placeholderAuthToken": "Anna todennustunnus",
|
||||
"optionFromNumber": "Lähettäjän numero",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "Vastaanottajan numero",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1255,12 +1214,11 @@
|
||||
"description": "Lisää esimerkkimonitoreja esittelytarkoituksiin."
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "Järjestelmän palautus",
|
||||
"title": "Järjestelmän nollaus",
|
||||
"description": "Poista kaikki monitorit järjestelmästäsi.",
|
||||
"dialog": {
|
||||
"title": "Poistetaanko kaikki monitorit?",
|
||||
"description": "Tätä toimintoa ei voi kumota.",
|
||||
"paragraph": "Mukana poistuvat myös kaikki tarkistushistoria, häiriöt, ajoitetut huoltoikkunat sekä tilasivut, jotka käyttivät näitä monitoreja. Vie monitorit ensin, jos haluat säilyttää varmuuskopion niiden tiedoista."
|
||||
"description": "Tätä ei voi kumota."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "Kaikki järjestelmät ovat alhaalla",
|
||||
"allUp": "Kaikki järjestelmät toiminnassa",
|
||||
"degraded": "Joissakin järjestelmissä on ongelmia",
|
||||
"unknown": "Järjestelmän tilaa ei voida määrittää",
|
||||
"allBreached": "Kaikki järjestelmät ovat ylittäneet kynnysarvot",
|
||||
"allMaintenance": "Kaikki järjestelmät ovat huollossa",
|
||||
"allPaused": "Kaikki järjestelmät on keskeytetty",
|
||||
"allInitializing": "Kaikki järjestelmät alustuvat",
|
||||
"breachedAndDown": "Jotkin järjestelmät ovat ylittäneet kynnysarvot ja ovat alhaalla",
|
||||
"breached": "Jotkin järjestelmät ovat ylittäneet kynnysarvot",
|
||||
"maintenanceAndDown": "Jotkin järjestelmät ovat alhaalla huollon aikana",
|
||||
"maintenance": "Jotkin järjestelmät ovat huollossa",
|
||||
"partiallyPaused": "Jotkin järjestelmät on keskeytetty",
|
||||
"initializing": "Jotkin järjestelmät alustuvat",
|
||||
"noMonitors": "Yhtään monitoria ei ole määritetty"
|
||||
"unknown": "Järjestelmän tilaa ei voida määrittää"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "Bienvenue sur Checkmate !",
|
||||
"subtitle": "Inscrivez-vous pour commencer",
|
||||
"submit": "S'inscrire",
|
||||
"setupNotice": "Cette page n'est disponible que lors de la configuration initiale. Une fois qu'un super administrateur est enregistré, des utilisateurs supplémentaires peuvent être invités depuis les paramètres de l'équipe."
|
||||
"submit": "S'inscrire"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Mot de passe oublié ?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "Inviter un membre",
|
||||
"openSite": "Ouvrir le site",
|
||||
"pause": "Pause",
|
||||
"resume": "Reprendre",
|
||||
"selectAll": "Sélectionner tous les moniteurs",
|
||||
"selectMonitor": "Sélectionner {{name}}",
|
||||
"bulkSelected_one": "{{count}} moniteur sélectionné",
|
||||
"bulkSelected_other": "{{count}} moniteurs sélectionnés"
|
||||
"resume": "Reprendre"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Actif depuis",
|
||||
"certificateExpiry": "Expiration du certificat",
|
||||
"lastCheck": "Dernière vérification",
|
||||
"lastResponseTime": "Dernier temps de réponse",
|
||||
"serviceIsDown": "Le service est indisponible"
|
||||
"lastResponseTime": "Dernier temps de réponse"
|
||||
},
|
||||
"status": {
|
||||
"down": "hors ligne",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "Infrastructure",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "{{count}} moniteur en pause",
|
||||
"paused_other": "{{count}} moniteurs en pause",
|
||||
"resumed_one": "{{count}} moniteur repris",
|
||||
"resumed_other": "{{count}} moniteurs repris",
|
||||
"alreadyPaused_one": "Le moniteur sélectionné est déjà en pause",
|
||||
"alreadyPaused_other": "Tous les moniteurs sélectionnés sont déjà en pause",
|
||||
"alreadyRunning_one": "Le moniteur sélectionné est déjà en cours d'exécution",
|
||||
"alreadyRunning_other": "Tous les moniteurs sélectionnés sont déjà en cours d'exécution"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "URL WebSocket",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Point de terminaison Capture",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "Paramètres généraux",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "Entrez l'adresse IP ou le nom d'hôte à tester (par exemple, 192.168.1.100 ou exemple.fr) et ajoutez un nom familier qui apparaîtra sur le tableau de bord.",
|
||||
"port": "Entrez l'URL ou l'adresse IP du serveur, le numéro de port et un nom d'affichage familier qui apparaîtra sur le tableau de bord.",
|
||||
"docker": "Entrer l'ID Docker du container. Les identifiants Docker doivent être les 64 caractères de l'ID Docker. Vous pouvez utiliser la commande docker inspect <short_id> pour avoir l'ID complet.",
|
||||
"game": "Saisissez l'adresse IP ou le nom d'hôte et le numéro de port à interroger (par ex. 192.168.1.100 ou example.com) et choisissez le type de jeu. Certains jeux nécessitent l'activation des requêtes serveur dans leur fichier de configuration — consultez la documentation de votre serveur de jeu. Voir la <gamedigLink>liste des jeux pris en charge</gamedigLink> pour les détails du protocole.",
|
||||
"game": "",
|
||||
"pagespeed": "Suivez les performances de chargement, les Core Web Vitals et les scores d'optimisation de votre site web.",
|
||||
"grpc": "Saisissez le nom d'hôte et le port du serveur gRPC, spécifiez éventuellement un nom de service pour le bilan de santé, et ajoutez un nom d'affichage.",
|
||||
"websocket": "Saisissez l'URL WebSocket à surveiller (ex. wss://example.com/socket) et ajoutez un nom d'affichage.",
|
||||
"hardware": "Surveillez l'utilisation du processeur, de la mémoire, du disque et la température de votre infrastructure. L'URL du point de terminaison Capture doit inclure le chemin complet /api/v1/metrics (par ex. http://192.168.1.10:5001/api/v1/metrics). Pour le secret d'autorisation, collez uniquement la chaîne du jeton — n'incluez pas 'Bearer'."
|
||||
"hardware": "Surveillez l'utilisation du CPU, de la mémoire, du disque et la température de votre infrastructure."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "Utilisation mémoire",
|
||||
"netBytesRecv": "{{name}} - Octets reçus",
|
||||
"netBytesSent": "{{name}} - Octets envoyés",
|
||||
"netNoTraffic": "Capture ne signale aucun trafic sur toutes les interfaces. C'est normal lorsque Capture s'exécute dans un conteneur qui ne voit que les interfaces de loopback — exécutez Capture directement sur l'hôte pour voir l'activité réseau réelle.",
|
||||
"temp": "Temp."
|
||||
}
|
||||
},
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "ID du chat",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Configuration Pushover",
|
||||
"description": "Entrez votre jeton d'application et votre clé utilisateur Pushover.",
|
||||
"optionAppToken": "Jeton d'application",
|
||||
"placeholderAppToken": "Entrez le jeton d'application",
|
||||
"optionUserKey": "Clé utilisateur",
|
||||
"placeholderUserKey": "Entrez la clé utilisateur"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Configuration Twilio",
|
||||
"description": "Entrez les identifiants de votre compte Twilio pour envoyer des SMS.",
|
||||
"optionAccountSid": "SID du compte",
|
||||
"placeholderAccountSid": "Entrez le SID du compte",
|
||||
"optionAuthToken": "Jeton d'authentification",
|
||||
"placeholderAuthToken": "Entrez le jeton d'authentification",
|
||||
"optionFromNumber": "Numéro d'origine",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "Numéro de destination",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1255,12 +1214,11 @@
|
||||
"description": "Ajoutez des moniteurs d'exemple à des fins de démonstration."
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "Réinitialiser le système",
|
||||
"description": "Supprimer tous les moniteurs de votre système.",
|
||||
"title": "Réinitialisation du système",
|
||||
"description": "Supprimez tous les moniteurs de votre système.",
|
||||
"dialog": {
|
||||
"title": "Supprimer tous les moniteurs ?",
|
||||
"description": "Cette action est irréversible.",
|
||||
"paragraph": "L'historique des vérifications, les incidents, les fenêtres de maintenance planifiées et les pages de statut qui utilisaient ces moniteurs seront également supprimés. Exportez d'abord les moniteurs si vous souhaitez conserver une sauvegarde de leurs données."
|
||||
"description": "Cette action est irréversible."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "Tous les systèmes sont hors service",
|
||||
"allUp": "Tous les systèmes sont opérationnels",
|
||||
"degraded": "Certains systèmes rencontrent des problèmes",
|
||||
"unknown": "Impossible de déterminer le statut du système",
|
||||
"allBreached": "Tous les systèmes ont dépassé les seuils",
|
||||
"allMaintenance": "Tous les systèmes sont en maintenance",
|
||||
"allPaused": "Tous les systèmes sont en pause",
|
||||
"allInitializing": "Tous les systèmes sont en cours d'initialisation",
|
||||
"breachedAndDown": "Certains systèmes ont dépassé les seuils et sont hors ligne",
|
||||
"breached": "Certains systèmes ont dépassé les seuils",
|
||||
"maintenanceAndDown": "Certains systèmes sont hors ligne pendant la maintenance",
|
||||
"maintenance": "Certains systèmes sont en maintenance",
|
||||
"partiallyPaused": "Certains systèmes sont en pause",
|
||||
"initializing": "Certains systèmes sont en cours d'initialisation",
|
||||
"noMonitors": "Aucun moniteur n'a été configuré"
|
||||
"unknown": "Impossible de déterminer le statut du système"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "Checkmateへようこそ!",
|
||||
"subtitle": "サインアップして始めましょう",
|
||||
"submit": "登録",
|
||||
"setupNotice": "このページは初期セットアップ中のみ利用できます。スーパー管理者が登録されると、追加のユーザーはチーム設定から招待できます。"
|
||||
"submit": "登録"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "パスワードをお忘れですか?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "メンバーを招待",
|
||||
"openSite": "サイトを開く",
|
||||
"pause": "一時停止",
|
||||
"resume": "再開",
|
||||
"selectAll": "すべてのモニターを選択",
|
||||
"selectMonitor": "{{name}} を選択",
|
||||
"bulkSelected_one": "{{count}} 個のモニターが選択されました",
|
||||
"bulkSelected_other": "{{count}} 個のモニターが選択されました"
|
||||
"resume": "再開"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "稼働期間",
|
||||
"certificateExpiry": "証明書の有効期限",
|
||||
"lastCheck": "最終チェック",
|
||||
"lastResponseTime": "最終応答時間",
|
||||
"serviceIsDown": "サービスがダウンしています"
|
||||
"lastResponseTime": "最終応答時間"
|
||||
},
|
||||
"status": {
|
||||
"down": "ダウン",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "インフラストラクチャ",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "{{count}} 個のモニターを一時停止しました",
|
||||
"paused_other": "{{count}} 個のモニターを一時停止しました",
|
||||
"resumed_one": "{{count}} 個のモニターを再開しました",
|
||||
"resumed_other": "{{count}} 個のモニターを再開しました",
|
||||
"alreadyPaused_one": "選択したモニターはすでに一時停止されています",
|
||||
"alreadyPaused_other": "選択したすべてのモニターはすでに一時停止されています",
|
||||
"alreadyRunning_one": "選択したモニターはすでに実行中です",
|
||||
"alreadyRunning_other": "選択したすべてのモニターはすでに実行中です"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Capture エンドポイント",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "一般設定",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "pingするIPアドレスまたはホスト名を入力(例: 192.168.1.100 または example.com)し、ダッシュボードに表示される明確な表示名を追加。",
|
||||
"port": "サーバーのURLまたはIP、ポート番号、ダッシュボードに表示される明確な表示名を入力。",
|
||||
"docker": "コンテナのDocker IDを入力。Docker IDは64文字のフルDocker IDである必要があります。docker inspect <short_id>を実行してフルコンテナIDを取得できます。",
|
||||
"game": "Ping を実行する IP アドレスまたはホスト名とポート番号(例: 192.168.1.100 や example.com)を入力し、ゲームタイプを選択してください。一部のゲームでは設定ファイルでサーバークエリを有効にする必要があります — ゲームサーバーのドキュメントを確認してください。プロトコルの詳細は<gamedigLink>サポートされているゲームの一覧</gamedigLink>をご覧ください。",
|
||||
"game": "",
|
||||
"pagespeed": "Webサイトのページ読み込みパフォーマンス、Core Web Vitals、最適化スコアを追跡します。",
|
||||
"grpc": "gRPCサーバーのホスト名とポートを入力し、必要に応じてヘルスチェック用のサービス名を指定し、表示名を追加してください。",
|
||||
"websocket": "監視するWebSocket URLを入力し(例: wss://example.com/socket)、表示名を追加してください。",
|
||||
"hardware": "インフラの CPU、メモリ、ディスク使用率、温度を監視します。Capture エンドポイント URL には完全なパス /api/v1/metrics を含める必要があります (例: http://192.168.1.10:5001/api/v1/metrics)。認証シークレットにはトークン文字列のみを貼り付けてください — 'Bearer' は含めないでください。"
|
||||
"hardware": "インフラストラクチャのCPU、メモリ、ディスク使用量、温度を監視します。"
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "メモリ使用率",
|
||||
"netBytesRecv": "{{name}} - 受信バイト数",
|
||||
"netBytesSent": "{{name}} - 送信バイト数",
|
||||
"netNoTraffic": "Capture はすべてのインターフェイスでゼロのトラフィックを報告しています。これは Capture がループバックインターフェイスのみを認識できるコンテナ内で実行されている場合に正常です — 実際のネットワークアクティビティを表示するには、ホスト上で Capture を直接実行してください。",
|
||||
"temp": "温度"
|
||||
}
|
||||
},
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "チャットID",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Pushover の設定",
|
||||
"description": "Pushover のアプリケーショントークンとユーザーキーを入力してください。",
|
||||
"optionAppToken": "アプリトークン",
|
||||
"placeholderAppToken": "アプリトークンを入力",
|
||||
"optionUserKey": "ユーザーキー",
|
||||
"placeholderUserKey": "ユーザーキーを入力"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Twilio の設定",
|
||||
"description": "SMS を送信するための Twilio アカウント認証情報を入力してください。",
|
||||
"optionAccountSid": "アカウント SID",
|
||||
"placeholderAccountSid": "アカウント SID を入力",
|
||||
"optionAuthToken": "認証トークン",
|
||||
"placeholderAuthToken": "認証トークンを入力",
|
||||
"optionFromNumber": "送信元番号",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "送信先番号",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1259,8 +1218,7 @@
|
||||
"description": "システムからすべてのモニターを削除します。",
|
||||
"dialog": {
|
||||
"title": "すべてのモニターを削除しますか?",
|
||||
"description": "この操作は元に戻せません。",
|
||||
"paragraph": "これらのモニターに関連するチェック履歴、インシデント、予定されたメンテナンスウィンドウ、ステータスページもすべて削除されます。データのバックアップを残したい場合は、先にモニターをエクスポートしてください。"
|
||||
"description": "この操作は元に戻せません。"
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "すべてのシステムがダウンしています",
|
||||
"allUp": "すべてのシステムが正常に稼働しています",
|
||||
"degraded": "一部のシステムに問題が発生しています",
|
||||
"unknown": "システムステータスを判定できません",
|
||||
"allBreached": "すべてのシステムがしきい値を超えました",
|
||||
"allMaintenance": "すべてのシステムがメンテナンス中です",
|
||||
"allPaused": "すべてのシステムが一時停止中です",
|
||||
"allInitializing": "すべてのシステムが初期化中です",
|
||||
"breachedAndDown": "一部のシステムがしきい値を超えてダウンしています",
|
||||
"breached": "一部のシステムがしきい値を超えています",
|
||||
"maintenanceAndDown": "一部のシステムがメンテナンス中にダウンしています",
|
||||
"maintenance": "一部のシステムがメンテナンス中です",
|
||||
"partiallyPaused": "一部のシステムが一時停止中です",
|
||||
"initializing": "一部のシステムが初期化中です",
|
||||
"noMonitors": "モニターが設定されていません"
|
||||
"unknown": "システムステータスを判定できません"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "Bem-vindo ao Checkmate!",
|
||||
"subtitle": "Cadastre-se para começar",
|
||||
"submit": "Cadastrar",
|
||||
"setupNotice": "Esta página está disponível apenas durante a configuração inicial. Após o registro de um super administrador, usuários adicionais podem ser convidados a partir das configurações da equipe."
|
||||
"submit": "Cadastrar"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Esqueceu sua senha?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "Convidar membro",
|
||||
"openSite": "Abrir site",
|
||||
"pause": "Pausar",
|
||||
"resume": "Retomar",
|
||||
"selectAll": "Selecionar todos os monitores",
|
||||
"selectMonitor": "Selecionar {{name}}",
|
||||
"bulkSelected_one": "{{count}} monitor selecionado",
|
||||
"bulkSelected_other": "{{count}} monitores selecionados"
|
||||
"resume": "Retomar"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Ativo há",
|
||||
"certificateExpiry": "Validade do certificado",
|
||||
"lastCheck": "Última verificação",
|
||||
"lastResponseTime": "Último tempo de resposta",
|
||||
"serviceIsDown": "O serviço está fora do ar"
|
||||
"lastResponseTime": "Último tempo de resposta"
|
||||
},
|
||||
"status": {
|
||||
"down": "fora do ar",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "Infraestrutura",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "{{count}} monitor pausado",
|
||||
"paused_other": "{{count}} monitores pausados",
|
||||
"resumed_one": "{{count}} monitor retomado",
|
||||
"resumed_other": "{{count}} monitores retomados",
|
||||
"alreadyPaused_one": "O monitor selecionado já está pausado",
|
||||
"alreadyPaused_other": "Todos os monitores selecionados já estão pausados",
|
||||
"alreadyRunning_one": "O monitor selecionado já está em execução",
|
||||
"alreadyRunning_other": "Todos os monitores selecionados já estão em execução"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "URL do WebSocket",
|
||||
"placeholder": "wss://exemplo.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Endpoint do Capture",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "Configurações gerais",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "Insira o endereço IP ou nome de domínio para ping (ex. 192.168.1.100 ou exemplo.com.br) e adicione uma descrição que aparecerá na dashboard.",
|
||||
"port": "Insira a URL ou o IP do servidor, aporta e uma descrição que aparecerá na dashboard.",
|
||||
"docker": "Insira o Docker Id do seu container. Docker Ids devem ser todos os 64 caracteres. Você pode executar docker inspect <short_id> para descobrir o Id completo.",
|
||||
"game": "Digite o endereço IP ou nome de host e o número da porta para pingar (por exemplo, 192.168.1.100 ou example.com) e escolha o tipo de jogo. Alguns jogos exigem habilitar consultas ao servidor no arquivo de configuração — consulte a documentação do seu servidor de jogo. Veja a <gamedigLink>lista de jogos suportados</gamedigLink> para detalhes do protocolo.",
|
||||
"game": "",
|
||||
"pagespeed": "Acompanhe o desempenho de carregamento da página, Core Web Vitals e pontuações de otimização do seu site.",
|
||||
"grpc": "Digite o hostname e a porta do servidor gRPC, opcionalmente especifique um nome de serviço para a verificação de saúde e adicione um nome de exibição.",
|
||||
"websocket": "Digite a URL do WebSocket para monitorar (ex.: wss://exemplo.com/socket) e adicione um nome de exibição.",
|
||||
"hardware": "Monitore o uso de CPU, memória, disco e temperatura da sua infraestrutura. A URL do endpoint do Capture deve incluir o caminho completo /api/v1/metrics (por exemplo, http://192.168.1.10:5001/api/v1/metrics). Para a chave de autorização, cole apenas a string do token — não inclua 'Bearer'."
|
||||
"hardware": "Monitore o uso de CPU, memória, disco e temperatura da sua infraestrutura."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "Uso de memória",
|
||||
"netBytesRecv": "{{name}} - Bytes recebidos",
|
||||
"netBytesSent": "{{name}} - Bytes enviados",
|
||||
"netNoTraffic": "Capture está reportando zero tráfego em todas as interfaces. Isso é normal quando Capture é executado dentro de um container que só consegue ver interfaces de loopback — execute Capture diretamente no host para ver a atividade real da rede.",
|
||||
"temp": "Temp"
|
||||
}
|
||||
},
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "ID do chat",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Configuração do Pushover",
|
||||
"description": "Insira o token de aplicação e a chave de usuário do Pushover.",
|
||||
"optionAppToken": "Token de aplicação",
|
||||
"placeholderAppToken": "Insira o token de aplicação",
|
||||
"optionUserKey": "Chave de usuário",
|
||||
"placeholderUserKey": "Insira a chave de usuário"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Configuração do Twilio",
|
||||
"description": "Insira as credenciais da conta Twilio para enviar SMS.",
|
||||
"optionAccountSid": "SID da conta",
|
||||
"placeholderAccountSid": "Insira o SID da conta",
|
||||
"optionAuthToken": "Token de autenticação",
|
||||
"placeholderAuthToken": "Insira o token de autenticação",
|
||||
"optionFromNumber": "Número de origem",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "Número de destino",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1255,12 +1214,11 @@
|
||||
"description": "Adicione monitores de exemplo para fins de demonstração."
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "Redefinir sistema",
|
||||
"description": "Remover todos os monitores do seu sistema.",
|
||||
"title": "Redefinição do sistema",
|
||||
"description": "Remova todos os monitores do seu sistema.",
|
||||
"dialog": {
|
||||
"title": "Remover todos os monitores?",
|
||||
"description": "Esta ação não pode ser desfeita.",
|
||||
"paragraph": "Todo o histórico de verificações, incidentes, janelas de manutenção agendadas e páginas de status que usavam esses monitores também serão removidos. Exporte os monitores antes se quiser manter uma cópia de segurança dos dados."
|
||||
"description": "Isso não pode ser desfeito."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "Todos os sistemas estão fora do ar",
|
||||
"allUp": "Todos os sistemas operacionais",
|
||||
"degraded": "Alguns sistemas estão com problemas",
|
||||
"unknown": "Não foi possível determinar o status do sistema",
|
||||
"allBreached": "Todos os sistemas excederam os limites",
|
||||
"allMaintenance": "Todos os sistemas estão em manutenção",
|
||||
"allPaused": "Todos os sistemas estão pausados",
|
||||
"allInitializing": "Todos os sistemas estão inicializando",
|
||||
"breachedAndDown": "Alguns sistemas excederam os limites e estão fora do ar",
|
||||
"breached": "Alguns sistemas excederam os limites",
|
||||
"maintenanceAndDown": "Alguns sistemas estão fora do ar durante a manutenção",
|
||||
"maintenance": "Alguns sistemas estão em manutenção",
|
||||
"partiallyPaused": "Alguns sistemas estão pausados",
|
||||
"initializing": "Alguns sistemas estão inicializando",
|
||||
"noMonitors": "Nenhum monitor foi configurado"
|
||||
"unknown": "Não foi possível determinar o status do sistema"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "С возвращением в Checkmate!",
|
||||
"subtitle": "Зарегистрируйтесь, чтобы начать",
|
||||
"submit": "Регистрация",
|
||||
"setupNotice": "Эта страница доступна только во время первоначальной настройки. После регистрации суперадминистратора дополнительных пользователей можно пригласить из настроек команды."
|
||||
"submit": "Регистрация"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Забыли пароль?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "Пригласить участника",
|
||||
"openSite": "Открыть сайт",
|
||||
"pause": "Пауза",
|
||||
"resume": "Продолжить",
|
||||
"selectAll": "Выбрать все мониторы",
|
||||
"selectMonitor": "Выбрать {{name}}",
|
||||
"bulkSelected_one": "Выбран {{count}} монитор",
|
||||
"bulkSelected_other": "Выбрано {{count}} мониторов"
|
||||
"resume": "Продолжить"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Активен",
|
||||
"certificateExpiry": "Истечение сертификата",
|
||||
"lastCheck": "Последняя проверка",
|
||||
"lastResponseTime": "Последнее время ответа",
|
||||
"serviceIsDown": "Сервис недоступен"
|
||||
"lastResponseTime": "Последнее время ответа"
|
||||
},
|
||||
"status": {
|
||||
"down": "недоступен",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "Инфраструктура",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "{{count}} монитор приостановлен",
|
||||
"paused_other": "{{count}} мониторов приостановлено",
|
||||
"resumed_one": "{{count}} монитор возобновлен",
|
||||
"resumed_other": "{{count}} мониторов возобновлено",
|
||||
"alreadyPaused_one": "Выбранный монитор уже приостановлен",
|
||||
"alreadyPaused_other": "Все выбранные мониторы уже приостановлены",
|
||||
"alreadyRunning_one": "Выбранный монитор уже запущен",
|
||||
"alreadyRunning_other": "Все выбранные мониторы уже запущены"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Эндпоинт Capture",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "Основные настройки",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "Ведите IP-адрес или имя хоста для пинга (например, 192.168.1.100 или example.com) и добавьте понятное имя, которое будет отображаться на панели инструментов.",
|
||||
"port": "Введите URL-адрес или IP-адрес сервера, номер порта и понятное имя, которое будет отображаться на панели инструментов.",
|
||||
"docker": "Введите Docker ID вашего контейнера. Docker ID должен быть полным 64-символьным Docker ID. Вы можете запустить docker inspect <short_id>, чтобы получить полный ID контейнера.",
|
||||
"game": "Введите IP-адрес или имя хоста и номер порта для пинга (например, 192.168.1.100 или example.com) и выберите тип игры. Некоторым играм требуется включить серверные запросы в файле конфигурации — проверьте документацию вашего игрового сервера. См. <gamedigLink>список поддерживаемых игр</gamedigLink> для деталей протокола.",
|
||||
"game": "Введите IP-адрес или имя хоста и номер порта для пинга (например, 192.168.1.100 или example.com) и выберите тип игры.",
|
||||
"pagespeed": "Отслеживайте производительность загрузки, Core Web Vitals и показатели оптимизации для Вашего сайта.",
|
||||
"grpc": "Введите имя хоста и порт gRPC-сервера, при необходимости укажите имя сервиса для проверки состояния и добавьте отображаемое имя.",
|
||||
"websocket": "Введите WebSocket URL для мониторинга (напр., wss://example.com/socket) и добавьте отображаемое имя.",
|
||||
"hardware": "Отслеживайте использование ЦП, памяти, диска и температуру вашей инфраструктуры. URL эндпоинта Capture должен включать полный путь /api/v1/metrics (например, http://192.168.1.10:5001/api/v1/metrics). В секрете авторизации укажите только строку токена — не включайте 'Bearer'."
|
||||
"hardware": "Мониторинг CPU, памяти, дисков и температуры Вашей инфраструктуры."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "Использование памяти",
|
||||
"netBytesRecv": "{{name}} - байт получено",
|
||||
"netBytesSent": "{{name}} - байт отправлено",
|
||||
"netNoTraffic": "Capture сообщает о нулевом трафике на всех интерфейсах. Это нормально, когда Capture работает внутри контейнера, который видит только loopback-интерфейсы — запустите Capture непосредственно на хосте, чтобы увидеть реальную сетевую активность.",
|
||||
"temp": "Температура"
|
||||
}
|
||||
},
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "ID чата",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Настройка Pushover",
|
||||
"description": "Введите токен приложения и ключ пользователя Pushover.",
|
||||
"optionAppToken": "Токен приложения",
|
||||
"placeholderAppToken": "Введите токен приложения",
|
||||
"optionUserKey": "Ключ пользователя",
|
||||
"placeholderUserKey": "Введите ключ пользователя"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Настройка Twilio",
|
||||
"description": "Введите учётные данные аккаунта Twilio для отправки SMS.",
|
||||
"optionAccountSid": "SID аккаунта",
|
||||
"placeholderAccountSid": "Введите SID аккаунта",
|
||||
"optionAuthToken": "Токен аутентификации",
|
||||
"placeholderAuthToken": "Введите токен аутентификации",
|
||||
"optionFromNumber": "Номер отправителя",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "Номер получателя",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1256,11 +1215,10 @@
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "Сброс системы",
|
||||
"description": "Удалить все мониторы из вашей системы.",
|
||||
"description": "Удалить все мониторы из Вашей системы.",
|
||||
"dialog": {
|
||||
"title": "Удалить все мониторы?",
|
||||
"description": "Это действие нельзя отменить.",
|
||||
"paragraph": "Также будут удалены вся история проверок, инциденты, запланированные окна обслуживания и страницы состояния, использовавшие эти мониторы. Сначала экспортируйте мониторы, если хотите сохранить резервную копию их данных."
|
||||
"description": "Это действие нельзя отменить."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "Все системы недоступны",
|
||||
"allUp": "Все системы работают",
|
||||
"degraded": "Некоторые системы испытывают проблемы",
|
||||
"unknown": "Не удаётся определить статус системы",
|
||||
"allBreached": "Все системы превысили пороговые значения",
|
||||
"allMaintenance": "Все системы на техническом обслуживании",
|
||||
"allPaused": "Все системы приостановлены",
|
||||
"allInitializing": "Все системы инициализируются",
|
||||
"breachedAndDown": "Некоторые системы превысили пороги и недоступны",
|
||||
"breached": "Некоторые системы превысили пороги",
|
||||
"maintenanceAndDown": "Некоторые системы недоступны во время техобслуживания",
|
||||
"maintenance": "Некоторые системы на техобслуживании",
|
||||
"partiallyPaused": "Некоторые системы приостановлены",
|
||||
"initializing": "Некоторые системы инициализируются",
|
||||
"noMonitors": "Мониторы не настроены"
|
||||
"unknown": "Не удаётся определить статус системы"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "ยินดีต้อนรับกลับสู่ Checkmate!",
|
||||
"subtitle": "สมัครสมาชิกเพื่อเริ่มต้น",
|
||||
"submit": "ลงทะเบียน",
|
||||
"setupNotice": "หน้านี้พร้อมใช้งานเฉพาะระหว่างการตั้งค่าเริ่มต้นเท่านั้น เมื่อลงทะเบียนผู้ดูแลระบบสูงสุดแล้ว สามารถเชิญผู้ใช้เพิ่มเติมได้จากการตั้งค่าทีม"
|
||||
"submit": "ลงทะเบียน"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "ลืมรหัสผ่านใช่ไหม?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "เชิญสมาชิก",
|
||||
"openSite": "เปิดเว็บไซต์",
|
||||
"pause": "หยุดชั่วคราว",
|
||||
"resume": "ดำเนินการต่อ",
|
||||
"selectAll": "เลือกจอภาพทั้งหมด",
|
||||
"selectMonitor": "เลือก {{name}}",
|
||||
"bulkSelected_one": "เลือกแล้ว {{count}} จอภาพ",
|
||||
"bulkSelected_other": "เลือกแล้ว {{count}} จอภาพ"
|
||||
"resume": "ดำเนินการต่อ"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "ทำงานมาแล้ว",
|
||||
"certificateExpiry": "วันหมดอายุใบรับรอง",
|
||||
"lastCheck": "ตรวจสอบล่าสุด",
|
||||
"lastResponseTime": "เวลาตอบสนองล่าสุด",
|
||||
"serviceIsDown": "บริการขัดข้อง"
|
||||
"lastResponseTime": "เวลาตอบสนองล่าสุด"
|
||||
},
|
||||
"status": {
|
||||
"down": "ล่ม",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "โครงสร้างพื้นฐาน",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "หยุดจอภาพชั่วคราว {{count}} จอ",
|
||||
"paused_other": "หยุดจอภาพชั่วคราว {{count}} จอ",
|
||||
"resumed_one": "กลับมาทำงานต่อ {{count}} จอ",
|
||||
"resumed_other": "กลับมาทำงานต่อ {{count}} จอ",
|
||||
"alreadyPaused_one": "จอภาพที่เลือกถูกหยุดชั่วคราวแล้ว",
|
||||
"alreadyPaused_other": "จอภาพที่เลือกทั้งหมดถูกหยุดชั่วคราวแล้ว",
|
||||
"alreadyRunning_one": "จอภาพที่เลือกกำลังทำงานอยู่แล้ว",
|
||||
"alreadyRunning_other": "จอภาพที่เลือกทั้งหมดกำลังทำงานอยู่แล้ว"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "URL ของ WebSocket",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "ปลายทาง Capture",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "ตั้งค่าทั่วไป",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "กรอกที่อยู่ IP หรือชื่อโฮสต์ที่ต้องการ ping (เช่น 192.168.1.100 หรือ example.com) และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด",
|
||||
"port": "กรอก URL หรือ IP ของเซิร์ฟเวอร์ หมายเลขพอร์ต และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด",
|
||||
"docker": "กรอกชื่อหรือ ID ของคอนเทนเนอร์ Docker คุณสามารถใช้ชื่อคอนเทนเนอร์ (เช่น my-app) หรือ ID ของคอนเทนเนอร์ (ID เต็ม 64 ตัวอักษร หรือ ID สั้น)",
|
||||
"game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม เกมบางเกมต้องเปิดใช้งานการสอบถามเซิร์ฟเวอร์ในไฟล์การตั้งค่า — โปรดดูเอกสารของเซิร์ฟเวอร์เกมของคุณ ดู<gamedigLink>รายการเกมที่รองรับ</gamedigLink>สำหรับรายละเอียดของโปรโตคอล",
|
||||
"game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม",
|
||||
"pagespeed": "ติดตามประสิทธิภาพการโหลดหน้า Core Web Vitals และคะแนนการเพิ่มประสิทธิภาพของเว็บไซต์",
|
||||
"grpc": "กรอกชื่อโฮสต์และพอร์ตของเซิร์ฟเวอร์ gRPC ระบุชื่อบริการสำหรับ health check (ไม่บังคับ) และตั้งชื่อที่แสดง",
|
||||
"websocket": "กรอก URL ของ WebSocket ที่ต้องการตรวจสอบ (เช่น wss://example.com/socket) และตั้งชื่อที่แสดง",
|
||||
"hardware": "ตรวจสอบการใช้ CPU หน่วยความจำ ดิสก์ และอุณหภูมิของโครงสร้างพื้นฐานของคุณ URL ของปลายทาง Capture ต้องรวมเส้นทางแบบเต็ม /api/v1/metrics (ตัวอย่าง: http://192.168.1.10:5001/api/v1/metrics) สำหรับรหัสลับสำหรับยืนยันตัวตน ให้วางเฉพาะสตริงโทเค็น — อย่ารวมคำว่า 'Bearer'"
|
||||
"hardware": "ตรวจสอบการใช้งาน CPU หน่วยความจำ ดิสก์ และอุณหภูมิของโครงสร้างพื้นฐาน"
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "การใช้งานหน่วยความจำ",
|
||||
"netBytesRecv": "{{name}} - ไบต์ที่รับ",
|
||||
"netBytesSent": "{{name}} - ไบต์ที่ส่ง",
|
||||
"netNoTraffic": "Capture รายงานว่าไม่มีการรับส่งข้อมูลบนอินเทอร์เฟซใดๆ เลย ซึ่งเป็นเรื่องปกติเมื่อ Capture ทำงานอยู่ในคอนเทนเนอร์ที่มองเห็นเพียงอินเทอร์เฟซ loopback เท่านั้น — รัน Capture บนโฮสต์โดยตรงเพื่อดูกิจกรรมเครือข่ายจริง",
|
||||
"temp": "อุณหภูมิ"
|
||||
}
|
||||
},
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "Chat ID",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "การตั้งค่า Pushover",
|
||||
"description": "ป้อนโทเค็นแอปและคีย์ผู้ใช้ Pushover ของคุณ",
|
||||
"optionAppToken": "โทเค็นแอป",
|
||||
"placeholderAppToken": "ป้อนโทเค็นแอป",
|
||||
"optionUserKey": "คีย์ผู้ใช้",
|
||||
"placeholderUserKey": "ป้อนคีย์ผู้ใช้"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "การตั้งค่า Twilio",
|
||||
"description": "ป้อนข้อมูลรับรองบัญชี Twilio เพื่อส่ง SMS",
|
||||
"optionAccountSid": "SID บัญชี",
|
||||
"placeholderAccountSid": "ป้อน SID บัญชี",
|
||||
"optionAuthToken": "โทเค็นการรับรอง",
|
||||
"placeholderAuthToken": "ป้อนโทเค็นการรับรอง",
|
||||
"optionFromNumber": "หมายเลขผู้ส่ง",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "หมายเลขผู้รับ",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1256,11 +1215,10 @@
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "รีเซ็ตระบบ",
|
||||
"description": "ลบมอนิเตอร์ทั้งหมดจากระบบของคุณ",
|
||||
"description": "ลบมอนิเตอร์ทั้งหมดออกจากระบบ",
|
||||
"dialog": {
|
||||
"title": "ลบมอนิเตอร์ทั้งหมด?",
|
||||
"description": "การกระทำนี้ไม่สามารถย้อนกลับได้",
|
||||
"paragraph": "ประวัติการตรวจสอบทั้งหมด เหตุการณ์ ช่วงเวลาบำรุงรักษาที่กำหนดไว้ และหน้าสถานะที่ใช้มอนิเตอร์เหล่านี้จะถูกลบไปด้วย ส่งออกมอนิเตอร์ก่อนหากต้องการเก็บสำเนาข้อมูลสำรองไว้"
|
||||
"description": "ไม่สามารถย้อนกลับได้"
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "ระบบทั้งหมดล่ม",
|
||||
"allUp": "ระบบทั้งหมดทำงานปกติ",
|
||||
"degraded": "บางระบบกำลังประสบปัญหา",
|
||||
"unknown": "ไม่สามารถระบุสถานะระบบได้",
|
||||
"allBreached": "ทุกระบบเกินขีดจำกัด",
|
||||
"allMaintenance": "ทุกระบบอยู่ระหว่างการบำรุงรักษา",
|
||||
"allPaused": "ทุกระบบถูกหยุดชั่วคราว",
|
||||
"allInitializing": "ทุกระบบกำลังเริ่มต้น",
|
||||
"breachedAndDown": "บางระบบเกินขีดจำกัดและขัดข้อง",
|
||||
"breached": "บางระบบเกินขีดจำกัด",
|
||||
"maintenanceAndDown": "บางระบบขัดข้องระหว่างการบำรุงรักษา",
|
||||
"maintenance": "บางระบบอยู่ระหว่างการบำรุงรักษา",
|
||||
"partiallyPaused": "บางระบบถูกหยุดชั่วคราว",
|
||||
"initializing": "บางระบบกำลังเริ่มต้น",
|
||||
"noMonitors": "ยังไม่มีการกำหนดค่ามอนิเตอร์"
|
||||
"unknown": "ไม่สามารถระบุสถานะระบบได้"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
+14
-67
@@ -35,7 +35,7 @@
|
||||
"sendInvite": "Davet gönder",
|
||||
"test": "Test",
|
||||
"testNotifications": "Bildirimleri test et",
|
||||
"toggleTheme": "Açık / koyu temayı değiştir",
|
||||
"toggleTheme": "",
|
||||
"flushQueue": "Kuyruğu temizle",
|
||||
"notFound": "Ana panele git",
|
||||
"resetPassword": "Parolayı sıfırla",
|
||||
@@ -86,7 +86,7 @@
|
||||
"name": {
|
||||
"option": {
|
||||
"firstName": {
|
||||
"label": "Ad",
|
||||
"label": "",
|
||||
"placeholder": "Jordan"
|
||||
},
|
||||
"lastName": {
|
||||
@@ -155,13 +155,13 @@
|
||||
"serverUnreachable": "Sunucuya ulaşılamıyor",
|
||||
"retry": "Tekrar dene",
|
||||
"retrying": "Yeniden deneniyor...",
|
||||
"reconnected": "Bağlantı yeniden kuruldu"
|
||||
"reconnected": ""
|
||||
},
|
||||
"sidebar": {
|
||||
"menu": {
|
||||
"uptime": "Çalışma süresi",
|
||||
"uptime": "",
|
||||
"pagespeed": "Sayfa hızı",
|
||||
"infrastructure": "Altyapı",
|
||||
"infrastructure": "",
|
||||
"notifications": "Bildirimler",
|
||||
"checks": "Kontroller",
|
||||
"incidents": "Olaylar",
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "Checkmate'e tekrar hoş geldiniz!",
|
||||
"subtitle": "Başlamak için kaydolun",
|
||||
"submit": "Kaydol",
|
||||
"setupNotice": "Bu sayfa yalnızca ilk kurulum sırasında kullanılabilir. Süper yönetici kaydedildikten sonra, ek kullanıcılar takım ayarlarından davet edilebilir."
|
||||
"submit": "Kaydol"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Parolanızı mı unuttunuz?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "Üye davet et",
|
||||
"openSite": "Siteyi aç",
|
||||
"pause": "Duraklat",
|
||||
"resume": "Devam et",
|
||||
"selectAll": "Tüm monitörleri seç",
|
||||
"selectMonitor": "{{name}}'i seç",
|
||||
"bulkSelected_one": "{{count}} monitör seçildi",
|
||||
"bulkSelected_other": "{{count}} monitör seçildi"
|
||||
"resume": "Devam et"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Aktif süresi",
|
||||
"certificateExpiry": "Sertifika bitiş tarihi",
|
||||
"lastCheck": "Son kontrol",
|
||||
"lastResponseTime": "Son yanıt süresi",
|
||||
"serviceIsDown": "Servis çalışmıyor"
|
||||
"lastResponseTime": "Son yanıt süresi"
|
||||
},
|
||||
"status": {
|
||||
"down": "çevrimdışı",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "Altyapı",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "{{count}} monitör duraklatıldı",
|
||||
"paused_other": "{{count}} monitör duraklatıldı",
|
||||
"resumed_one": "{{count}} monitör sürdürüldü",
|
||||
"resumed_other": "{{count}} monitör sürdürüldü",
|
||||
"alreadyPaused_one": "Seçili monitör zaten duraklatıldı",
|
||||
"alreadyPaused_other": "Seçili tüm monitörler zaten duraklatıldı",
|
||||
"alreadyRunning_one": "Seçili monitör zaten çalışıyor",
|
||||
"alreadyRunning_other": "Seçili tüm monitörler zaten çalışıyor"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Capture uç noktası",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "Genel ayarlar",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "",
|
||||
"port": "",
|
||||
"docker": "",
|
||||
"game": "Ping atılacak IP adresini veya ana bilgisayar adını ve port numarasını girin (ör. 192.168.1.100 veya example.com) ve oyun türünü seçin. Bazı oyunlar yapılandırma dosyasında sunucu sorgularının etkinleştirilmesini gerektirir — oyun sunucunuzun belgelerine bakın. Protokol ayrıntıları için <gamedigLink>desteklenen oyunlar listesine</gamedigLink> bakın.",
|
||||
"game": "",
|
||||
"pagespeed": "Web sitenizin sayfa yükleme performansını, Core Web Vitals metriklerini ve optimizasyon puanlarını takip edin.",
|
||||
"grpc": "gRPC sunucusunun ana bilgisayar adını ve portunu girin, isteğe bağlı olarak sağlık kontrolü için bir servis adı belirtin ve bir görünen ad ekleyin.",
|
||||
"websocket": "İzlenecek WebSocket URL'sini girin (örn. wss://example.com/socket) ve bir görünen ad ekleyin.",
|
||||
"hardware": "Altyapınızın CPU, bellek, disk kullanımını ve sıcaklığını izleyin. Capture uç nokta URL'si tam yolu /api/v1/metrics içermelidir (ör. http://192.168.1.10:5001/api/v1/metrics). Yetkilendirme anahtarına yalnızca token dizesini yapıştırın — 'Bearer' eklemeyin."
|
||||
"hardware": "Altyapınız için CPU, bellek, disk kullanımı ve sıcaklığı izleyin."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "Bellek kullanımı",
|
||||
"netBytesRecv": "{{name}} - Alınan bayt",
|
||||
"netBytesSent": "{{name}} - Gönderilen bayt",
|
||||
"netNoTraffic": "Capture tüm arabirimlerde sıfır trafik raporluyor. Bu, Capture yalnızca loopback arabirimlerini görebilen bir konteyner içinde çalışırken normaldir — gerçek ağ etkinliğini görmek için Capture'ı doğrudan host üzerinde çalıştırın.",
|
||||
"temp": "Sıcaklık"
|
||||
}
|
||||
},
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "Sohbet ID",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Pushover yapılandırması",
|
||||
"description": "Pushover uygulama belirtecinizi ve kullanıcı anahtarınızı girin.",
|
||||
"optionAppToken": "Uygulama belirteci",
|
||||
"placeholderAppToken": "Uygulama belirtecini girin",
|
||||
"optionUserKey": "Kullanıcı anahtarı",
|
||||
"placeholderUserKey": "Kullanıcı anahtarını girin"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Twilio yapılandırması",
|
||||
"description": "SMS göndermek için Twilio hesap kimlik bilgilerinizi girin.",
|
||||
"optionAccountSid": "Hesap SID",
|
||||
"placeholderAccountSid": "Hesap SID girin",
|
||||
"optionAuthToken": "Kimlik doğrulama belirteci",
|
||||
"placeholderAuthToken": "Kimlik doğrulama belirtecini girin",
|
||||
"optionFromNumber": "Gönderen numara",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "Alıcı numara",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1255,12 +1214,11 @@
|
||||
"description": "Gösterim amaçlı örnek monitörler ekleyin."
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "Sistemi sıfırla",
|
||||
"description": "Sistemdeki tüm monitörleri kaldır.",
|
||||
"title": "Sistem sıfırlama",
|
||||
"description": "Sisteminizdeki tüm monitörleri kaldırın.",
|
||||
"dialog": {
|
||||
"title": "Tüm monitörler kaldırılsın mı?",
|
||||
"description": "Bu işlem geri alınamaz.",
|
||||
"paragraph": "Tüm kontrol geçmişi, olaylar, planlanmış bakım pencereleri ve bu monitörleri kullanan durum sayfaları da kaldırılacaktır. Verilerin yedeğini saklamak istiyorsanız, önce monitörleri dışa aktarın."
|
||||
"description": "Bu işlem geri alınamaz."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "Tüm sistemler çevrimdışı",
|
||||
"allUp": "Tüm sistemler çalışıyor",
|
||||
"degraded": "Bazı sistemlerde sorun yaşanıyor",
|
||||
"unknown": "Sistem durumu belirlenemiyor",
|
||||
"allBreached": "Tüm sistemler eşik değerlerini aştı",
|
||||
"allMaintenance": "Tüm sistemler bakımda",
|
||||
"allPaused": "Tüm sistemler duraklatıldı",
|
||||
"allInitializing": "Tüm sistemler başlatılıyor",
|
||||
"breachedAndDown": "Bazı sistemler eşik değerlerini aştı ve çalışmıyor",
|
||||
"breached": "Bazı sistemler eşik değerlerini aştı",
|
||||
"maintenanceAndDown": "Bazı sistemler bakım sırasında çalışmıyor",
|
||||
"maintenance": "Bazı sistemler bakımda",
|
||||
"partiallyPaused": "Bazı sistemler duraklatıldı",
|
||||
"initializing": "Bazı sistemler başlatılıyor",
|
||||
"noMonitors": "Hiç monitör yapılandırılmadı"
|
||||
"unknown": "Sistem durumu belirlenemiyor"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
+50
-103
@@ -13,7 +13,7 @@
|
||||
"content": "Увага: Ви ще не додали ключ API Google PageSpeed. Перейдіть у <settingsLink>Налаштування</settingsLink>, щоб додати його. Без нього моніторинг PageSpeed не працюватиме."
|
||||
}
|
||||
},
|
||||
"appName": "Checkmate",
|
||||
"appName": "",
|
||||
"breadcrumbs": {
|
||||
"details": "Деталі",
|
||||
"home": "Головна"
|
||||
@@ -31,15 +31,15 @@
|
||||
"inviteMember": "Запросити учасника",
|
||||
"pause": "Призупинити",
|
||||
"resume": "Відновити",
|
||||
"save": "Зберегти",
|
||||
"save": "",
|
||||
"sendInvite": "Надіслати запрошення",
|
||||
"test": "Тест",
|
||||
"testNotifications": "Тестові сповіщення",
|
||||
"toggleTheme": "Перемкнути світлу / темну",
|
||||
"toggleTheme": "",
|
||||
"flushQueue": "Очистити чергу",
|
||||
"notFound": "Перейти на головну панель",
|
||||
"resetPassword": "Скинути пароль",
|
||||
"reset": "Скинути",
|
||||
"resetPassword": "",
|
||||
"reset": "",
|
||||
"clear": "Очистити",
|
||||
"addDemo": "Додати демо-монітори",
|
||||
"removeMonitors": "Видалити монітори",
|
||||
@@ -53,8 +53,8 @@
|
||||
"labels": {
|
||||
"averageResponseTime": "Середній час відповіді",
|
||||
"downtime": "Час простою",
|
||||
"high": "Високий",
|
||||
"low": "Низький",
|
||||
"high": "",
|
||||
"low": "",
|
||||
"uptime": "Час роботи"
|
||||
},
|
||||
"histogram": {
|
||||
@@ -86,12 +86,12 @@
|
||||
"name": {
|
||||
"option": {
|
||||
"firstName": {
|
||||
"label": "Ім'я",
|
||||
"placeholder": "Введіть ім'я"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
},
|
||||
"lastName": {
|
||||
"label": "Прізвище",
|
||||
"placeholder": "Введіть прізвище"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -107,14 +107,14 @@
|
||||
"table": {
|
||||
"empty": "Тут нічого немає",
|
||||
"headers": {
|
||||
"actions": "Дії",
|
||||
"actions": "",
|
||||
"dateTime": "Дата і час",
|
||||
"message": "Повідомлення",
|
||||
"monitor": "Монітор",
|
||||
"monitorId": "ID монітора",
|
||||
"name": "Ім'я",
|
||||
"status": "Статус",
|
||||
"type": "Тип",
|
||||
"type": "",
|
||||
"url": "URL",
|
||||
"interval": "Інтервал",
|
||||
"active": "Активний",
|
||||
@@ -125,17 +125,17 @@
|
||||
"components": {
|
||||
"imageUpload": {
|
||||
"clickToUpload": "Натисніть, щоб завантажити",
|
||||
"dragAndDrop": "або перетягніть",
|
||||
"supportedFormats": "Підтримувані формати",
|
||||
"maxSize": "Макс. розмір",
|
||||
"dragAndDrop": "",
|
||||
"supportedFormats": "",
|
||||
"maxSize": "",
|
||||
"orDragAndDrop": "або перетягніть файл",
|
||||
"errors": {
|
||||
"invalidFileSize": "Розмір файлу занадто великий!",
|
||||
"invalidFileFormat": "Непідтримуваний формат файлу!"
|
||||
"invalidFileSize": "",
|
||||
"invalidFileFormat": ""
|
||||
}
|
||||
},
|
||||
"headerStatusPageControls": {
|
||||
"publicLink": "Публічне посилання"
|
||||
"publicLink": ""
|
||||
},
|
||||
"headerTimeRange": {
|
||||
"labels": {
|
||||
@@ -155,26 +155,26 @@
|
||||
"serverUnreachable": "Не вдається з'єднатися з сервером",
|
||||
"retry": "Повторити",
|
||||
"retrying": "Повторна спроба...",
|
||||
"reconnected": "З'єднання відновлено"
|
||||
"reconnected": ""
|
||||
},
|
||||
"sidebar": {
|
||||
"menu": {
|
||||
"uptime": "Доступність",
|
||||
"pagespeed": "Pagespeed",
|
||||
"infrastructure": "Інфраструктура",
|
||||
"notifications": "Сповіщення",
|
||||
"uptime": "",
|
||||
"pagespeed": "",
|
||||
"infrastructure": "",
|
||||
"notifications": "",
|
||||
"checks": "Перевірки",
|
||||
"incidents": "Інциденти",
|
||||
"statusPages": "Сторінки статусу",
|
||||
"maintenance": "Обслуговування",
|
||||
"logs": "Журнали",
|
||||
"settings": "Налаштування"
|
||||
"incidents": "",
|
||||
"statusPages": "",
|
||||
"maintenance": "",
|
||||
"logs": "",
|
||||
"settings": ""
|
||||
},
|
||||
"bottomMenu": {
|
||||
"support": "Підтримка",
|
||||
"discussions": "Обговорення",
|
||||
"docs": "Документація",
|
||||
"changelog": "Журнал змін"
|
||||
"support": "",
|
||||
"discussions": "",
|
||||
"docs": "",
|
||||
"changelog": ""
|
||||
},
|
||||
"accountMenu": {
|
||||
"profile": "Профіль",
|
||||
@@ -186,13 +186,13 @@
|
||||
"description": "Переглядайте останні оновлення та допомагайте розвивати спільноту на GitHub"
|
||||
},
|
||||
"authFooter": {
|
||||
"navControls": "Керування",
|
||||
"logOut": "Вийти",
|
||||
"navControls": "",
|
||||
"logOut": "",
|
||||
"roles": {
|
||||
"superAdmin": "Суперадміністратор",
|
||||
"admin": "Адміністратор",
|
||||
"user": "Користувач",
|
||||
"demoUser": "Демо-користувач"
|
||||
"demoUser": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -259,15 +259,15 @@
|
||||
"team": {
|
||||
"filter": {
|
||||
"placeholder": "Фільтрувати за роллю",
|
||||
"all": "Усі ролі",
|
||||
"all": "",
|
||||
"admin": "Адміністратор",
|
||||
"member": "Учасник"
|
||||
"member": ""
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"email": "Електронна пошта",
|
||||
"role": "Роль",
|
||||
"created": "Створено"
|
||||
"role": "",
|
||||
"created": ""
|
||||
}
|
||||
},
|
||||
"addMember": {
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "Ласкаво просимо до Checkmate!",
|
||||
"subtitle": "Зареєструйтесь, щоб розпочати",
|
||||
"submit": "Зареєструватися",
|
||||
"setupNotice": "Ця сторінка доступна лише під час початкового налаштування. Після реєстрації суперадміністратора додаткових користувачів можна запросити з налаштувань команди."
|
||||
"submit": "Зареєструватися"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Забули пароль?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "Запросити учасника",
|
||||
"openSite": "Відкрити сайт",
|
||||
"pause": "Призупинити",
|
||||
"resume": "Відновити",
|
||||
"selectAll": "Вибрати всі монітори",
|
||||
"selectMonitor": "Вибрати {{name}}",
|
||||
"bulkSelected_one": "Вибрано {{count}} монітор",
|
||||
"bulkSelected_other": "Вибрано {{count}} моніторів"
|
||||
"resume": "Відновити"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Активний протягом",
|
||||
"certificateExpiry": "Закінчення сертифіката",
|
||||
"lastCheck": "Остання перевірка",
|
||||
"lastResponseTime": "Останній час відповіді",
|
||||
"serviceIsDown": "Сервіс недоступний"
|
||||
"lastResponseTime": "Останній час відповіді"
|
||||
},
|
||||
"status": {
|
||||
"down": "недоступний",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "Інфраструктура",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "{{count}} монітор призупинено",
|
||||
"paused_other": "{{count}} моніторів призупинено",
|
||||
"resumed_one": "{{count}} монітор відновлено",
|
||||
"resumed_other": "{{count}} моніторів відновлено",
|
||||
"alreadyPaused_one": "Вибраний монітор уже призупинено",
|
||||
"alreadyPaused_other": "Всі вибрані монітори уже призупинено",
|
||||
"alreadyRunning_one": "Вибраний монітор уже запущено",
|
||||
"alreadyRunning_other": "Всі вибрані монітори уже запущено"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Точка доступу Capture",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "Загальні налаштування",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "",
|
||||
"port": "",
|
||||
"docker": "",
|
||||
"game": "Введіть IP-адресу або ім'я хоста та номер порту для пінгу (наприклад, 192.168.1.100 або example.com) і виберіть тип гри. Деяким іграм потрібно ввімкнути серверні запити у файлі конфігурації — перевірте документацію вашого ігрового сервера. Деталі протоколу див. у <gamedigLink>списку підтримуваних ігор</gamedigLink>.",
|
||||
"game": "",
|
||||
"pagespeed": "Відстежуйте швидкість завантаження сторінки, показники Core Web Vitals та оцінки оптимізації Вашого сайту.",
|
||||
"grpc": "Введіть ім'я хоста та порт gRPC-сервера, за бажанням вкажіть назву сервісу для перевірки стану та додайте відображуване ім'я.",
|
||||
"websocket": "Введіть WebSocket URL для моніторингу (напр., wss://example.com/socket) та додайте відображуване ім'я.",
|
||||
"hardware": "Відстежуйте використання ЦП, пам'яті, диска та температуру вашої інфраструктури. URL точки доступу Capture має містити повний шлях /api/v1/metrics (наприклад, http://192.168.1.10:5001/api/v1/metrics). У секрет авторизації вставте лише рядок токена — не додавайте 'Bearer'."
|
||||
"hardware": "Моніторинг CPU, пам'яті, використання диска та температури Вашої інфраструктури."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "Використання пам'яті",
|
||||
"netBytesRecv": "{{name}} - Отримано байтів",
|
||||
"netBytesSent": "{{name}} - Надіслано байтів",
|
||||
"netNoTraffic": "Capture повідомляє про нульовий трафік на всіх інтерфейсах. Це нормально, коли Capture працює всередині контейнера, який бачить лише loopback-інтерфейси — запустіть Capture безпосередньо на хості, щоб побачити реальну мережеву активність.",
|
||||
"temp": "Темп."
|
||||
}
|
||||
},
|
||||
@@ -847,8 +826,8 @@
|
||||
},
|
||||
"tabs": {
|
||||
"diagnostics": "Діагностика",
|
||||
"logs": "Журнали",
|
||||
"queue": "Черга завдань"
|
||||
"logs": "",
|
||||
"queue": ""
|
||||
},
|
||||
"logLevelSelect": {
|
||||
"label": "Рівень логів"
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "ID чату",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Налаштування Pushover",
|
||||
"description": "Введіть токен застосунку та ключ користувача Pushover.",
|
||||
"optionAppToken": "Токен застосунку",
|
||||
"placeholderAppToken": "Введіть токен застосунку",
|
||||
"optionUserKey": "Ключ користувача",
|
||||
"placeholderUserKey": "Введіть ключ користувача"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Налаштування Twilio",
|
||||
"description": "Введіть облікові дані Twilio для надсилання SMS.",
|
||||
"optionAccountSid": "SID акаунта",
|
||||
"placeholderAccountSid": "Введіть SID акаунта",
|
||||
"optionAuthToken": "Токен автентифікації",
|
||||
"placeholderAuthToken": "Введіть токен автентифікації",
|
||||
"optionFromNumber": "Номер відправника",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "Номер отримувача",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1256,11 +1215,10 @@
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "Скидання системи",
|
||||
"description": "Видалити всі монітори з вашої системи.",
|
||||
"description": "Видалити всі монітори з Вашої системи.",
|
||||
"dialog": {
|
||||
"title": "Видалити всі монітори?",
|
||||
"description": "Цю дію не можна скасувати.",
|
||||
"paragraph": "Також будуть видалені вся історія перевірок, інциденти, заплановані вікна обслуговування та сторінки статусу, які використовували ці монітори. Спочатку експортуйте монітори, якщо хочете зберегти резервну копію їхніх даних."
|
||||
"description": "Це не можна скасувати."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "Усі системи недоступні",
|
||||
"allUp": "Усі системи працюють",
|
||||
"degraded": "Деякі системи мають проблеми",
|
||||
"unknown": "Неможливо визначити стан системи",
|
||||
"allBreached": "Усі системи перевищили порогові значення",
|
||||
"allMaintenance": "Усі системи на технічному обслуговуванні",
|
||||
"allPaused": "Усі системи призупинено",
|
||||
"allInitializing": "Усі системи ініціалізуються",
|
||||
"breachedAndDown": "Деякі системи перевищили пороги та недоступні",
|
||||
"breached": "Деякі системи перевищили пороги",
|
||||
"maintenanceAndDown": "Деякі системи недоступні під час техобслуговування",
|
||||
"maintenance": "Деякі системи на техобслуговуванні",
|
||||
"partiallyPaused": "Деякі системи призупинено",
|
||||
"initializing": "Деякі системи ініціалізуються",
|
||||
"noMonitors": "Не налаштовано жодного монітора"
|
||||
"unknown": "Неможливо визначити стан системи"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
+50
-103
@@ -13,7 +13,7 @@
|
||||
"content": "Cảnh báo: Bạn chưa thêm khóa API Google PageSpeed. Truy cập <settingsLink>Cài đặt</settingsLink> để thêm. Nếu không có khóa này, trình giám sát PageSpeed sẽ không hoạt động."
|
||||
}
|
||||
},
|
||||
"appName": "Checkmate",
|
||||
"appName": "",
|
||||
"breadcrumbs": {
|
||||
"details": "Chi tiết",
|
||||
"home": "Trang chủ"
|
||||
@@ -31,15 +31,15 @@
|
||||
"inviteMember": "Mời thành viên",
|
||||
"pause": "Tạm dừng",
|
||||
"resume": "Tiếp tục",
|
||||
"save": "Lưu",
|
||||
"save": "",
|
||||
"sendInvite": "Gửi lời mời",
|
||||
"test": "Kiểm tra",
|
||||
"testNotifications": "Kiểm tra thông báo",
|
||||
"toggleTheme": "Chuyển sáng / tối",
|
||||
"toggleTheme": "",
|
||||
"flushQueue": "Xóa hàng đợi",
|
||||
"notFound": "Về bảng điều khiển chính",
|
||||
"resetPassword": "Đặt lại mật khẩu",
|
||||
"reset": "Đặt lại",
|
||||
"resetPassword": "",
|
||||
"reset": "",
|
||||
"clear": "Xóa",
|
||||
"addDemo": "Thêm trình giám sát mẫu",
|
||||
"removeMonitors": "Xóa trình giám sát",
|
||||
@@ -53,8 +53,8 @@
|
||||
"labels": {
|
||||
"averageResponseTime": "Thời gian phản hồi trung bình",
|
||||
"downtime": "Thời gian ngừng hoạt động",
|
||||
"high": "Cao",
|
||||
"low": "Thấp",
|
||||
"high": "",
|
||||
"low": "",
|
||||
"uptime": "Thời gian hoạt động"
|
||||
},
|
||||
"histogram": {
|
||||
@@ -86,12 +86,12 @@
|
||||
"name": {
|
||||
"option": {
|
||||
"firstName": {
|
||||
"label": "Tên",
|
||||
"placeholder": "Nhập tên của bạn"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
},
|
||||
"lastName": {
|
||||
"label": "Họ",
|
||||
"placeholder": "Nhập họ của bạn"
|
||||
"label": "",
|
||||
"placeholder": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -107,14 +107,14 @@
|
||||
"table": {
|
||||
"empty": "Không có dữ liệu",
|
||||
"headers": {
|
||||
"actions": "Hành động",
|
||||
"actions": "",
|
||||
"dateTime": "Ngày & giờ",
|
||||
"message": "Tin nhắn",
|
||||
"monitor": "Trình giám sát",
|
||||
"monitorId": "ID trình giám sát",
|
||||
"name": "Tên",
|
||||
"status": "Trạng thái",
|
||||
"type": "Loại",
|
||||
"type": "",
|
||||
"url": "Url",
|
||||
"interval": "Khoảng thời gian",
|
||||
"active": "Đang hoạt động",
|
||||
@@ -125,17 +125,17 @@
|
||||
"components": {
|
||||
"imageUpload": {
|
||||
"clickToUpload": "Nhấn để tải lên",
|
||||
"dragAndDrop": "hoặc kéo và thả",
|
||||
"supportedFormats": "Định dạng hỗ trợ",
|
||||
"maxSize": "Kích thước tối đa",
|
||||
"dragAndDrop": "",
|
||||
"supportedFormats": "",
|
||||
"maxSize": "",
|
||||
"orDragAndDrop": "hoặc kéo và thả",
|
||||
"errors": {
|
||||
"invalidFileSize": "Tệp quá lớn!",
|
||||
"invalidFileFormat": "Định dạng tệp không được hỗ trợ!"
|
||||
"invalidFileSize": "",
|
||||
"invalidFileFormat": ""
|
||||
}
|
||||
},
|
||||
"headerStatusPageControls": {
|
||||
"publicLink": "Liên kết công khai"
|
||||
"publicLink": ""
|
||||
},
|
||||
"headerTimeRange": {
|
||||
"labels": {
|
||||
@@ -155,26 +155,26 @@
|
||||
"serverUnreachable": "Không thể kết nối đến máy chủ",
|
||||
"retry": "Thử lại",
|
||||
"retrying": "Đang thử lại...",
|
||||
"reconnected": "Đã khôi phục kết nối"
|
||||
"reconnected": ""
|
||||
},
|
||||
"sidebar": {
|
||||
"menu": {
|
||||
"uptime": "Thời gian hoạt động",
|
||||
"pagespeed": "Pagespeed",
|
||||
"infrastructure": "Hạ tầng",
|
||||
"notifications": "Thông báo",
|
||||
"uptime": "",
|
||||
"pagespeed": "",
|
||||
"infrastructure": "",
|
||||
"notifications": "",
|
||||
"checks": "Kiểm tra",
|
||||
"incidents": "Sự cố",
|
||||
"statusPages": "Trang trạng thái",
|
||||
"maintenance": "Bảo trì",
|
||||
"logs": "Nhật ký",
|
||||
"settings": "Cài đặt"
|
||||
"incidents": "",
|
||||
"statusPages": "",
|
||||
"maintenance": "",
|
||||
"logs": "",
|
||||
"settings": ""
|
||||
},
|
||||
"bottomMenu": {
|
||||
"support": "Hỗ trợ",
|
||||
"discussions": "Thảo luận",
|
||||
"docs": "Tài liệu",
|
||||
"changelog": "Nhật ký thay đổi"
|
||||
"support": "",
|
||||
"discussions": "",
|
||||
"docs": "",
|
||||
"changelog": ""
|
||||
},
|
||||
"accountMenu": {
|
||||
"profile": "Hồ sơ",
|
||||
@@ -186,13 +186,13 @@
|
||||
"description": "Xem các bản phát hành mới nhất và giúp phát triển cộng đồng trên GitHub"
|
||||
},
|
||||
"authFooter": {
|
||||
"navControls": "Điều khiển",
|
||||
"logOut": "Đăng xuất",
|
||||
"navControls": "",
|
||||
"logOut": "",
|
||||
"roles": {
|
||||
"superAdmin": "Quản trị viên cấp cao",
|
||||
"admin": "Quản trị viên",
|
||||
"user": "Người dùng",
|
||||
"demoUser": "Người dùng demo"
|
||||
"demoUser": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -259,15 +259,15 @@
|
||||
"team": {
|
||||
"filter": {
|
||||
"placeholder": "Lọc theo vai trò",
|
||||
"all": "Tất cả vai trò",
|
||||
"all": "",
|
||||
"admin": "Quản trị viên",
|
||||
"member": "Thành viên"
|
||||
"member": ""
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"email": "Email",
|
||||
"role": "Vai trò",
|
||||
"created": "Đã tạo"
|
||||
"role": "",
|
||||
"created": ""
|
||||
}
|
||||
},
|
||||
"addMember": {
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "Chào mừng bạn trở lại Checkmate!",
|
||||
"subtitle": "Đăng ký để bắt đầu",
|
||||
"submit": "Đăng ký",
|
||||
"setupNotice": "Trang này chỉ khả dụng trong quá trình thiết lập ban đầu. Sau khi siêu quản trị viên được đăng ký, có thể mời thêm người dùng từ cài đặt nhóm."
|
||||
"submit": "Đăng ký"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "Quên mật khẩu?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "Mời thành viên",
|
||||
"openSite": "Mở trang web",
|
||||
"pause": "Tạm dừng",
|
||||
"resume": "Tiếp tục",
|
||||
"selectAll": "Chọn tất cả màn hình",
|
||||
"selectMonitor": "Chọn {{name}}",
|
||||
"bulkSelected_one": "Đã chọn {{count}} màn hình",
|
||||
"bulkSelected_other": "Đã chọn {{count}} màn hình"
|
||||
"resume": "Tiếp tục"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Hoạt động từ",
|
||||
"certificateExpiry": "Hạn chứng chỉ",
|
||||
"lastCheck": "Lần kiểm tra cuối",
|
||||
"lastResponseTime": "Thời gian phản hồi cuối",
|
||||
"serviceIsDown": "Dịch vụ đang ngừng hoạt động"
|
||||
"lastResponseTime": "Thời gian phản hồi cuối"
|
||||
},
|
||||
"status": {
|
||||
"down": "ngừng hoạt động",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "Hạ tầng",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "Đã tạm dừng {{count}} màn hình",
|
||||
"paused_other": "Đã tạm dừng {{count}} màn hình",
|
||||
"resumed_one": "Đã tiếp tục {{count}} màn hình",
|
||||
"resumed_other": "Đã tiếp tục {{count}} màn hình",
|
||||
"alreadyPaused_one": "Màn hình đã chọn đã được tạm dừng",
|
||||
"alreadyPaused_other": "Tất cả các màn hình đã chọn đã được tạm dừng",
|
||||
"alreadyRunning_one": "Màn hình đã chọn đã đang chạy",
|
||||
"alreadyRunning_other": "Tất cả các màn hình đã chọn đã đang chạy"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "URL WebSocket",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Điểm cuối Capture",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "Cài đặt chung",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "",
|
||||
"port": "",
|
||||
"docker": "",
|
||||
"game": "Nhập địa chỉ IP hoặc tên máy chủ và số cổng để ping (ví dụ: 192.168.1.100 hoặc example.com) và chọn loại trò chơi. Một số trò chơi yêu cầu bật truy vấn máy chủ trong tệp cấu hình của chúng — hãy xem tài liệu của máy chủ trò chơi. Xem <gamedigLink>danh sách các trò chơi được hỗ trợ</gamedigLink> để biết chi tiết giao thức.",
|
||||
"game": "",
|
||||
"pagespeed": "Theo dõi hiệu suất tải trang, Core Web Vitals và điểm tối ưu hóa cho trang web của bạn.",
|
||||
"grpc": "Nhập tên máy chủ và cổng của máy chủ gRPC, tùy chọn chỉ định tên dịch vụ cho kiểm tra sức khỏe và thêm tên hiển thị.",
|
||||
"websocket": "Nhập URL WebSocket cần giám sát (vd: wss://example.com/socket) và thêm tên hiển thị.",
|
||||
"hardware": "Giám sát mức sử dụng CPU, bộ nhớ, ổ đĩa và nhiệt độ của hạ tầng. URL điểm cuối Capture phải bao gồm đường dẫn đầy đủ /api/v1/metrics (ví dụ: http://192.168.1.10:5001/api/v1/metrics). Đối với khóa xác thực, chỉ dán chuỗi token — không bao gồm 'Bearer'."
|
||||
"hardware": "Giám sát CPU, bộ nhớ, dung lượng ổ đĩa và nhiệt độ cho hạ tầng của bạn."
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "Sử dụng bộ nhớ",
|
||||
"netBytesRecv": "{{name}} - Byte nhận",
|
||||
"netBytesSent": "{{name}} - Byte gửi",
|
||||
"netNoTraffic": "Capture đang báo cáo lưu lượng bằng không trên mọi giao diện. Điều này là bình thường khi Capture chạy bên trong một container chỉ có thể thấy các giao diện loopback — hãy chạy Capture trực tiếp trên máy chủ để xem hoạt động mạng thực tế.",
|
||||
"temp": "Nhiệt độ"
|
||||
}
|
||||
},
|
||||
@@ -847,8 +826,8 @@
|
||||
},
|
||||
"tabs": {
|
||||
"diagnostics": "Chẩn đoán",
|
||||
"logs": "Nhật ký",
|
||||
"queue": "Hàng đợi tác vụ"
|
||||
"logs": "",
|
||||
"queue": ""
|
||||
},
|
||||
"logLevelSelect": {
|
||||
"label": "Mức nhật ký"
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "ID cuộc trò chuyện",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Cấu hình Pushover",
|
||||
"description": "Nhập mã thông báo ứng dụng và khóa người dùng Pushover của bạn.",
|
||||
"optionAppToken": "Mã thông báo ứng dụng",
|
||||
"placeholderAppToken": "Nhập mã thông báo ứng dụng",
|
||||
"optionUserKey": "Khóa người dùng",
|
||||
"placeholderUserKey": "Nhập khóa người dùng"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Cấu hình Twilio",
|
||||
"description": "Nhập thông tin xác thực tài khoản Twilio để gửi SMS.",
|
||||
"optionAccountSid": "SID tài khoản",
|
||||
"placeholderAccountSid": "Nhập SID tài khoản",
|
||||
"optionAuthToken": "Mã thông báo xác thực",
|
||||
"placeholderAuthToken": "Nhập mã thông báo xác thực",
|
||||
"optionFromNumber": "Số gửi",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "Số nhận",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1256,11 +1215,10 @@
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "Đặt lại hệ thống",
|
||||
"description": "Xóa tất cả các monitor khỏi hệ thống của bạn.",
|
||||
"description": "Xóa tất cả trình giám sát khỏi hệ thống.",
|
||||
"dialog": {
|
||||
"title": "Xóa tất cả trình giám sát?",
|
||||
"description": "Hành động này không thể hoàn tác.",
|
||||
"paragraph": "Toàn bộ lịch sử kiểm tra, sự cố, cửa sổ bảo trì đã lên lịch và các trang trạng thái sử dụng những trình giám sát này cũng sẽ bị xóa. Hãy xuất trình giám sát trước nếu bạn muốn giữ một bản sao lưu dữ liệu."
|
||||
"description": "Không thể hoàn tác."
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "Tất cả hệ thống đều ngừng hoạt động",
|
||||
"allUp": "Tất cả hệ thống đang hoạt động",
|
||||
"degraded": "Một số hệ thống đang gặp sự cố",
|
||||
"unknown": "Không thể xác định trạng thái hệ thống",
|
||||
"allBreached": "Tất cả hệ thống đã vượt ngưỡng",
|
||||
"allMaintenance": "Tất cả hệ thống đang bảo trì",
|
||||
"allPaused": "Tất cả hệ thống đã tạm dừng",
|
||||
"allInitializing": "Tất cả hệ thống đang khởi tạo",
|
||||
"breachedAndDown": "Một số hệ thống đã vượt ngưỡng và đang ngừng hoạt động",
|
||||
"breached": "Một số hệ thống đã vượt ngưỡng",
|
||||
"maintenanceAndDown": "Một số hệ thống ngừng hoạt động trong quá trình bảo trì",
|
||||
"maintenance": "Một số hệ thống đang bảo trì",
|
||||
"partiallyPaused": "Một số hệ thống đã tạm dừng",
|
||||
"initializing": "Một số hệ thống đang khởi tạo",
|
||||
"noMonitors": "Chưa cấu hình màn hình giám sát nào"
|
||||
"unknown": "Không thể xác định trạng thái hệ thống"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "欢迎回到 Checkmate!",
|
||||
"subtitle": "注册以开始使用",
|
||||
"submit": "注册",
|
||||
"setupNotice": "此页面仅在初始设置期间可用。注册超级管理员后,可以从团队设置中邀请更多用户。"
|
||||
"submit": "注册"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "忘记密码了?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "邀请成员",
|
||||
"openSite": "打开站点",
|
||||
"pause": "暂停",
|
||||
"resume": "恢复",
|
||||
"selectAll": "选择所有监视器",
|
||||
"selectMonitor": "选择 {{name}}",
|
||||
"bulkSelected_one": "已选择 {{count}} 个监视器",
|
||||
"bulkSelected_other": "已选择 {{count}} 个监视器"
|
||||
"resume": "恢复"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "已活跃",
|
||||
"certificateExpiry": "证书到期",
|
||||
"lastCheck": "上次检查",
|
||||
"lastResponseTime": "上次响应时间",
|
||||
"serviceIsDown": "服务已停止"
|
||||
"lastResponseTime": "上次响应时间"
|
||||
},
|
||||
"status": {
|
||||
"down": "宕机",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "基础设施",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "已暂停 {{count}} 个监视器",
|
||||
"paused_other": "已暂停 {{count}} 个监视器",
|
||||
"resumed_one": "已恢复 {{count}} 个监视器",
|
||||
"resumed_other": "已恢复 {{count}} 个监视器",
|
||||
"alreadyPaused_one": "所选监视器已暂停",
|
||||
"alreadyPaused_other": "所有选定的监视器均已暂停",
|
||||
"alreadyRunning_one": "所选监视器已经在运行",
|
||||
"alreadyRunning_other": "所有选定的监视器均已在运行"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Capture 端点",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "常规设置",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "输入要 ping 的 IP 地址或主机名(例如.. 192.168.1.100 或 example.com),并添加显示在仪表板上的清晰显示名称。",
|
||||
"port": "输入服务器的 URL 或 IP、端口号和仪表板上显示的清晰显示名称。",
|
||||
"docker": "码输入 Docker 容器名称或 ID。您可以使用容器名称(例如.. my-app)或容器 ID(完整的 64 字符 ID 或短 ID)。",
|
||||
"game": "输入要 ping 的 IP 地址或主机名和端口号(例如.. 192.168.1.100 或 example.com)并选择游戏类型。某些游戏需要在配置文件中启用服务器查询 — 请查阅你的游戏服务器文档。详见<gamedigLink>支持的游戏列表</gamedigLink>了解协议细节。",
|
||||
"game": "输入要 ping 的 IP 地址或主机名和端口号(例如.. 192.168.1.100 或 example.com)并选择游戏类型。",
|
||||
"pagespeed": "跟踪网站的页面加载性能、Core Web Vitals 和优化评分。",
|
||||
"grpc": "输入 gRPC 服务器的主机名和端口,可选指定健康检查的服务名称,并添加显示名称。",
|
||||
"websocket": "输入要监控的 WebSocket URL(例如 wss://example.com/socket),并添加显示名称。",
|
||||
"hardware": "监控基础设施的 CPU、内存、磁盘使用情况和温度。Capture 端点 URL 必须包含完整路径 /api/v1/metrics(例如 http://192.168.1.10:5001/api/v1/metrics)。授权密钥只需粘贴令牌字符串——请勿包含 'Bearer'。"
|
||||
"hardware": "监控基础设施的 CPU、内存、磁盘使用率和温度。"
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "内存使用率",
|
||||
"netBytesRecv": "{{name}} - 接收字节",
|
||||
"netBytesSent": "{{name}} - 发送字节",
|
||||
"netNoTraffic": "Capture 报告所有接口的流量均为零。当 Capture 在只能看到回环接口的容器内运行时,这属于正常情况——请直接在主机上运行 Capture 才能看到真实的网络活动。",
|
||||
"temp": "温度"
|
||||
}
|
||||
},
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "聊天 ID",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Pushover 配置",
|
||||
"description": "输入您的 Pushover 应用令牌和用户密钥。",
|
||||
"optionAppToken": "应用令牌",
|
||||
"placeholderAppToken": "输入应用令牌",
|
||||
"optionUserKey": "用户密钥",
|
||||
"placeholderUserKey": "输入用户密钥"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Twilio 配置",
|
||||
"description": "输入 Twilio 帐户凭据以发送短信。",
|
||||
"optionAccountSid": "帐户 SID",
|
||||
"placeholderAccountSid": "输入帐户 SID",
|
||||
"optionAuthToken": "认证令牌",
|
||||
"placeholderAuthToken": "输入认证令牌",
|
||||
"optionFromNumber": "发送号码",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "接收号码",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1256,11 +1215,10 @@
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "系统重置",
|
||||
"description": "从系统中移除所有监视器。",
|
||||
"description": "从系统中移除所有监控。",
|
||||
"dialog": {
|
||||
"title": "移除所有监控?",
|
||||
"description": "此操作无法撤销。",
|
||||
"paragraph": "所有检查历史、事件、计划维护窗口以及使用这些监控的状态页面也将一并被移除。如需保留数据备份,请先导出监控。"
|
||||
"description": "此操作无法撤销。"
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "所有系统已宕机",
|
||||
"allUp": "所有系统运行正常",
|
||||
"degraded": "部分系统正在出现问题",
|
||||
"unknown": "无法确定系统状态",
|
||||
"allBreached": "所有系统已超过阈值",
|
||||
"allMaintenance": "所有系统正在维护中",
|
||||
"allPaused": "所有系统已暂停",
|
||||
"allInitializing": "所有系统正在初始化",
|
||||
"breachedAndDown": "部分系统超过阈值且已停止",
|
||||
"breached": "部分系统超过阈值",
|
||||
"maintenanceAndDown": "部分系统在维护期间停止",
|
||||
"maintenance": "部分系统正在维护",
|
||||
"partiallyPaused": "部分系统已暂停",
|
||||
"initializing": "部分系统正在初始化",
|
||||
"noMonitors": "尚未配置任何监控器"
|
||||
"unknown": "无法确定系统状态"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -364,8 +364,7 @@
|
||||
"register": {
|
||||
"title": "歡迎回到 Checkmate!",
|
||||
"subtitle": "註冊以開始使用",
|
||||
"submit": "註冊",
|
||||
"setupNotice": "此頁面僅在初始設定期間可用。註冊超級管理員後,可以從團隊設定中邀請更多使用者。"
|
||||
"submit": "註冊"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"title": "忘記密碼了嗎?",
|
||||
@@ -421,18 +420,13 @@
|
||||
"inviteMember": "邀請成員",
|
||||
"openSite": "開啟網站",
|
||||
"pause": "暫停",
|
||||
"resume": "恢復",
|
||||
"selectAll": "選擇所有監視器",
|
||||
"selectMonitor": "選擇 {{name}}",
|
||||
"bulkSelected_one": "已選擇 {{count}} 個監視器",
|
||||
"bulkSelected_other": "已選擇 {{count}} 個監視器"
|
||||
"resume": "恢復"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "已啟用",
|
||||
"certificateExpiry": "憑證到期",
|
||||
"lastCheck": "最後檢查",
|
||||
"lastResponseTime": "最後回應時間",
|
||||
"serviceIsDown": "服務已停止"
|
||||
"lastResponseTime": "最後回應時間"
|
||||
},
|
||||
"status": {
|
||||
"down": "離線",
|
||||
@@ -453,16 +447,6 @@
|
||||
"optionHardware": "基礎架構",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionWebSocket": "WebSocket"
|
||||
},
|
||||
"bulkPause": {
|
||||
"paused_one": "已暫停 {{count}} 個監視器",
|
||||
"paused_other": "已暫停 {{count}} 個監視器",
|
||||
"resumed_one": "已恢復 {{count}} 個監視器",
|
||||
"resumed_other": "已恢復 {{count}} 個監視器",
|
||||
"alreadyPaused_one": "所選監視器已暫停",
|
||||
"alreadyPaused_other": "所有選定的監視器均已暫停",
|
||||
"alreadyRunning_one": "所選監視器已經在運行",
|
||||
"alreadyRunning_other": "所有選定的監視器均已在運行"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -548,10 +532,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"captureUrl": {
|
||||
"label": "Capture 端點",
|
||||
"placeholder": "http://192.168.1.10:5001/api/v1/metrics"
|
||||
}
|
||||
},
|
||||
"title": "一般設定",
|
||||
@@ -560,11 +540,11 @@
|
||||
"ping": "輸入要 Ping 的 IP 位址或主機名稱(例如 192.168.1.100 或 example.com),並新增一個顯示在儀表板上的清晰顯示名稱。",
|
||||
"port": "輸入伺服器的 URL 或 IP、連接埠號碼,以及顯示在儀表板上的清晰顯示名稱。",
|
||||
"docker": "輸入 Docker 容器名稱或 ID。您可以使用容器名稱(例如 my-app)或容器 ID(完整的 64 字元 ID 或短 ID)。",
|
||||
"game": "輸入要 Ping 的 IP 位址或主機名稱及連接埠號碼(例如 192.168.1.100 或 example.com),並選擇遊戲類型。部分遊戲需要在設定檔中啟用伺服器查詢 — 請參閱你的遊戲伺服器文件。詳見<gamedigLink>支援的遊戲清單</gamedigLink>了解協定細節。",
|
||||
"game": "輸入要 Ping 的 IP 位址或主機名稱及連接埠號碼(例如 192.168.1.100 或 example.com),並選擇遊戲類型。",
|
||||
"pagespeed": "追蹤您網站的頁面載入效能、Core Web Vitals 和最佳化分數。",
|
||||
"grpc": "輸入 gRPC 伺服器的主機名稱和連接埠,可選擇指定健康檢查的服務名稱,並新增一個顯示名稱。",
|
||||
"websocket": "輸入要監控的 WebSocket URL(例如 wss://example.com/socket),並新增一個顯示名稱。",
|
||||
"hardware": "監控基礎架構的 CPU、記憶體、磁碟使用情況和溫度。Capture 端點 URL 必須包含完整路徑 /api/v1/metrics(例如 http://192.168.1.10:5001/api/v1/metrics)。授權密鑰僅貼上權杖字串——請勿包含 'Bearer'。"
|
||||
"hardware": "監控您基礎架構的 CPU、記憶體、磁碟使用量和溫度。"
|
||||
}
|
||||
},
|
||||
"ignoreTls": {
|
||||
@@ -787,7 +767,6 @@
|
||||
"memory": "記憶體使用率",
|
||||
"netBytesRecv": "{{name}} - 接收位元組",
|
||||
"netBytesSent": "{{name}} - 發送位元組",
|
||||
"netNoTraffic": "Capture 回報所有介面的流量皆為零。當 Capture 在只能看到回環介面的容器中執行時,這是正常情況——請直接在主機上執行 Capture 才能看到實際的網路活動。",
|
||||
"temp": "溫度"
|
||||
}
|
||||
},
|
||||
@@ -1037,26 +1016,6 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "聊天室 ID",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Pushover 設定",
|
||||
"description": "輸入您的 Pushover 應用程式權杖和使用者金鑰。",
|
||||
"optionAppToken": "應用程式權杖",
|
||||
"placeholderAppToken": "輸入應用程式權杖",
|
||||
"optionUserKey": "使用者金鑰",
|
||||
"placeholderUserKey": "輸入使用者金鑰"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Twilio 設定",
|
||||
"description": "輸入 Twilio 帳戶憑證以傳送簡訊。",
|
||||
"optionAccountSid": "帳戶 SID",
|
||||
"placeholderAccountSid": "輸入帳戶 SID",
|
||||
"optionAuthToken": "驗證權杖",
|
||||
"placeholderAuthToken": "輸入驗證權杖",
|
||||
"optionFromNumber": "發送號碼",
|
||||
"placeholderFromNumber": "+1234567890",
|
||||
"optionToNumber": "接收號碼",
|
||||
"placeholderToNumber": "+1234567890"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -1255,12 +1214,11 @@
|
||||
"description": "新增範例監控以供示範用途。"
|
||||
},
|
||||
"removeMonitors": {
|
||||
"title": "系統重置",
|
||||
"description": "從系統中移除所有監視器。",
|
||||
"title": "系統重設",
|
||||
"description": "從系統中移除所有監控。",
|
||||
"dialog": {
|
||||
"title": "移除所有監控?",
|
||||
"description": "此操作無法復原。",
|
||||
"paragraph": "所有檢查歷史、事件、排定的維護時段以及使用這些監控的狀態頁面也將一併被移除。如需保留資料備份,請先匯出監控。"
|
||||
"description": "此操作無法復原。"
|
||||
}
|
||||
},
|
||||
"exportMonitors": {
|
||||
@@ -1313,18 +1271,7 @@
|
||||
"allDown": "所有系統已停機",
|
||||
"allUp": "所有系統運作正常",
|
||||
"degraded": "部分系統正在發生問題",
|
||||
"unknown": "無法判斷系統狀態",
|
||||
"allBreached": "所有系統已超過閾值",
|
||||
"allMaintenance": "所有系統正在維護中",
|
||||
"allPaused": "所有系統已暫停",
|
||||
"allInitializing": "所有系統正在初始化",
|
||||
"breachedAndDown": "部分系統超過閾值且已停止",
|
||||
"breached": "部分系統超過閾值",
|
||||
"maintenanceAndDown": "部分系統在維護期間停止",
|
||||
"maintenance": "部分系統正在維護",
|
||||
"partiallyPaused": "部分系統已暫停",
|
||||
"initializing": "部分系統正在初始化",
|
||||
"noMonitors": "尚未設定任何監視器"
|
||||
"unknown": "無法判斷系統狀態"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
|
||||
@@ -18,6 +18,8 @@ services:
|
||||
image: mongo:6.0
|
||||
restart: always
|
||||
command: ["mongod", "--quiet", "--bind_ip_all"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
healthcheck:
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
# Coding conventions
|
||||
|
||||
Style and structural rules the codebase follows. Frontend rules are extracted from review feedback on PR series #3591–#3596; backend rules are extracted from the provider/repository/service layering used across `server/src/service/infrastructure/network/*` and the patterns enforced in code review. These are non-negotiable for new code. They exist to keep the codebase greppable, maintainable, and consistent.
|
||||
|
||||
> If you're touching a file in `client/src` the **frontend** rules apply.
|
||||
> If you're touching a file in `server/src` the **backend** rules apply.
|
||||
> Rule 0 applies everywhere.
|
||||
|
||||
---
|
||||
|
||||
## 0. Look at existing implementations and follow the established pattern
|
||||
|
||||
Before writing new code, find one or two files that already do something similar — a peer provider for a new monitor type, a peer page for a new admin screen, a peer repository method for a new query — and follow the same shape. Match imports, helper-function placement, naming, error handling, mock-construction style, and assertion idioms. If you're tempted to do it differently, the burden is on you to justify the divergence.
|
||||
|
||||
**Why:** consistency is more valuable than locally-optimal cleverness. A reader who has internalized the pattern of one peer file should be able to read the new one with no extra mental load. New shapes also make refactors more expensive — when someone wants to change the convention later, every variant has to be visited and reasoned about separately.
|
||||
|
||||
**How to apply:** before writing a new component, page, provider, repository, or test file:
|
||||
|
||||
1. Grep for the closest existing peers (e.g. `WebSocketProvider.ts` if you're writing a new monitor provider).
|
||||
2. Read at least two of them end-to-end.
|
||||
3. Mirror their structure: same constants at the top (`SERVICE_NAME`, `TIMEOUT_MS`), same import order, same DI shape, same try/catch envelope, same test-file location and helper imports.
|
||||
4. If your new code needs a shape no peer has, name it explicitly in the PR description and link to the peer you diverged from. Don't sneak novelty in.
|
||||
|
||||
**Concrete check before opening a PR:** can a reader open a peer file side-by-side with yours and trace each section in the same order? If not, restructure.
|
||||
|
||||
---
|
||||
|
||||
# Part 1 — Frontend (`client/src`)
|
||||
|
||||
## 1. Prefer MUI native (system) props over `sx`
|
||||
|
||||
MUI components accept layout, spacing, color, border, typography and sizing values as top-level props. Use those. Reach for `sx` only when no native prop exists for the value you need (e.g. `textDecoration`, pseudo-selectors, complex selectors, conditional CSS).
|
||||
|
||||
**Why:** native props are easier to read in JSX, easier to grep, easier to refactor, and don't force the reader to mentally parse a nested object literal for things that are conceptually one attribute. A `sx` block also can't be split-line-edited cleanly when most of its keys are simple primitives.
|
||||
|
||||
**How to apply:** any time you write `sx={{ … }}`, ask whether each key has a native equivalent. If yes, hoist it.
|
||||
|
||||
### Canonical mappings
|
||||
|
||||
| `sx` key | Native prop |
|
||||
|---|---|
|
||||
| `color` | `color` |
|
||||
| `backgroundColor` | `bgcolor` |
|
||||
| `borderRadius` | `borderRadius` |
|
||||
| `border`, `borderTop`, … | `border`, `borderTop`, … |
|
||||
| `width`, `height`, `minHeight`, `maxWidth` | same names as native |
|
||||
| `p`, `pt`, `pb`, `px`, `py` | same names as native |
|
||||
| `m`, `mt`, `mb`, `mx`, `my` | same names as native |
|
||||
| `display` | `display` |
|
||||
| `textAlign` | `textAlign` |
|
||||
| `fontWeight`, `fontSize`, `lineHeight` | same names as native |
|
||||
| `gap` (on Stack) | `gap` |
|
||||
|
||||
### Examples
|
||||
|
||||
```tsx
|
||||
// BAD — everything inside sx
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: theme.spacing(LAYOUT.MD),
|
||||
borderRadius: 1,
|
||||
border: `1px solid ${alpha(tone, 0.45)}`,
|
||||
backgroundColor: alpha(tone, 0.08),
|
||||
textAlign: "left",
|
||||
}}
|
||||
/>
|
||||
|
||||
// GOOD — every key that has a native prop is hoisted
|
||||
<Box
|
||||
width="100%"
|
||||
p={theme.spacing(LAYOUT.MD)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
border={`1px solid ${alpha(tone, 0.45)}`}
|
||||
bgcolor={alpha(tone, 0.08)}
|
||||
textAlign="left"
|
||||
/>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// BAD
|
||||
<Typography sx={{ color: theme.palette.text.primary, lineHeight: 1.55, fontSize: 13 }}>
|
||||
|
||||
// GOOD — hoist what you can; drop fontSize:13 because it's the body default
|
||||
<Typography
|
||||
color={theme.palette.text.primary}
|
||||
lineHeight={1.55}
|
||||
>
|
||||
```
|
||||
|
||||
### When `sx` is still correct
|
||||
|
||||
Keep `sx` for things that don't have a native prop:
|
||||
|
||||
```tsx
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/settings"
|
||||
color={theme.palette.primary.main}
|
||||
fontWeight={600}
|
||||
sx={{ textDecoration: "underline" }} // OK — no native prop for textDecoration
|
||||
/>
|
||||
```
|
||||
|
||||
Pseudo-selectors (`"&:hover"`), `transition`, `cursor`, `outline`, `overflow` (when not on Box/Stack), media queries, and conditional spread objects all stay in `sx`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Use the full theme path for colors, never the shorthand string
|
||||
|
||||
Prop forms like `color="text.secondary"`, `color="primary.main"`, `bgcolor="background.paper"` work, but they are **forbidden in this codebase**.
|
||||
|
||||
**Why:** the shorthand is invisible to grep. If a future change needs to find every place that consumes `theme.palette.text.secondary` — to retheme, to swap, to audit — the shorthand strings are missed. That is a maintenance trap. It's worth a few extra characters at every callsite to keep the codebase searchable.
|
||||
|
||||
**How to apply:** always destructure or reference the theme value explicitly.
|
||||
|
||||
```tsx
|
||||
// BAD
|
||||
<Typography color="text.secondary">…</Typography>
|
||||
<Typography color="primary.main">…</Typography>
|
||||
|
||||
// GOOD
|
||||
const theme = useTheme();
|
||||
<Typography color={theme.palette.text.secondary}>…</Typography>
|
||||
<Typography color={theme.palette.primary.main}>…</Typography>
|
||||
```
|
||||
|
||||
This applies to every theme-aware string prop (`color`, `bgcolor`, `borderColor`, etc.), and inside `sx` if you really need it there.
|
||||
|
||||
---
|
||||
|
||||
## 3. No hardcoded numeric or string literals — use tokens
|
||||
|
||||
The theme exposes tokens for every kind of magic value. Use them. Reserve raw numbers for values that are genuinely one-off (and even then, push back on yourself first).
|
||||
|
||||
**Why:** tokens are the single source of truth. A literal `2` in a `mt` prop is invisible — it could mean `LAYOUT.XXS`, a rounding correction, or a thinko. The reader can't tell. With a token, intent is in the name.
|
||||
|
||||
**How to apply:** before writing a literal, check whether one of these tokens covers it. If it nearly does but not exactly, extend the token rather than hardcoding (Alex did exactly this — he added `LAYOUT.XXS = 2` rather than letting `mt: "2px"` stand).
|
||||
|
||||
### Token sources
|
||||
|
||||
- **Spacing / layout** — `client/src/Utils/Theme/constants.ts`
|
||||
- `LAYOUT.XXS = 2` · `LAYOUT.XS = 4` · `LAYOUT.SM = 6` · `LAYOUT.MD = 8` · `LAYOUT.LG = 10` · `LAYOUT.XL = 12` · `LAYOUT.XXL = 16`
|
||||
- `SPACING` for `theme.spacing(...)` multipliers
|
||||
- **Typography sizes** — `client/src/Utils/Theme/Palette.ts` → `typographyLevels`
|
||||
- `xs / s / m / l / xl / xxl` (rem values)
|
||||
- **Hover / interaction** — `HOVER.DARKEN` etc.
|
||||
- **Shape** — `theme.shape.borderRadius` (don't write `borderRadius: 1` or `borderRadius: 4`)
|
||||
- **Colors** — `theme.palette.*` (rule 2 above)
|
||||
|
||||
### Examples
|
||||
|
||||
```tsx
|
||||
// BAD
|
||||
<Box sx={{ mt: "2px" }} />
|
||||
<Stack spacing={theme.spacing(2)} />
|
||||
<Box sx={{ borderRadius: 1 }} />
|
||||
<Typography fontSize={18} />
|
||||
<Typography fontSize={13} /> // 13 is the body default — drop the prop entirely
|
||||
|
||||
// GOOD
|
||||
<Box mt={LAYOUT.XXS} />
|
||||
<Stack spacing={theme.spacing(LAYOUT.XXS)} />
|
||||
<Box borderRadius={theme.shape.borderRadius} />
|
||||
<Typography fontSize={typographyLevels.xl} />
|
||||
<Typography /> // inherits 13px from theme
|
||||
```
|
||||
|
||||
### Token naming gotcha
|
||||
|
||||
`typographyLevels.xl` was historically used for both 18px and 23px callsites. Don't reuse a token for two different purposes — split it (`xl` = 18, `xxl` = 23) and update every callsite. If you find an ambiguous token while editing, fix it the same way.
|
||||
|
||||
---
|
||||
|
||||
## 4. Use `useTheme()` inside components, don't import the theme module
|
||||
|
||||
The `theme` object exported from `@/Utils/Theme/Theme` is the *factory output*, not a live, mode-aware instance. Importing it directly couples a component to the wrong reference and breaks dark-mode reactivity. It also breaks the build at import time in some module-graph configurations (this happened in PR #3596 and required a follow-up commit).
|
||||
|
||||
**Why:** themes can change at runtime (mode toggle), and the hook is the only correct way to read the active theme. Imports also create circular-graph risk because `Theme.ts` itself depends on `Palette.ts`.
|
||||
|
||||
**How to apply:** inside any component or component-helper that needs theme values:
|
||||
|
||||
```tsx
|
||||
// BAD
|
||||
import { theme } from "@/Utils/Theme/Theme";
|
||||
const SectionHeading = ({ children }) => (
|
||||
<Typography color={theme.palette.text.secondary}>{children}</Typography>
|
||||
);
|
||||
|
||||
// GOOD
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
const SectionHeading = ({ children }) => {
|
||||
const theme = useTheme();
|
||||
return <Typography color={theme.palette.text.secondary}>{children}</Typography>;
|
||||
};
|
||||
```
|
||||
|
||||
Module-level utility files that don't render React (pure helpers) should accept `theme` as a parameter rather than importing it.
|
||||
|
||||
---
|
||||
|
||||
## 5. Discriminated unions: pair the runtime list with the type
|
||||
|
||||
When you have a closed set of string literals that's used both as a type and as runtime data (severities, statuses, kinds), declare the runtime tuple first and derive the type from it. Don't declare them as two parallel literals that can drift.
|
||||
|
||||
**Why:** if the list and the type aren't tied to each other, adding a new value to one and forgetting the other compiles fine and breaks at runtime. Deriving the type from the tuple makes them inseparable.
|
||||
|
||||
```tsx
|
||||
// BAD
|
||||
type Severity = "info" | "warning" | "error";
|
||||
|
||||
// GOOD
|
||||
export const Severities = ["info", "warning", "error"] as const;
|
||||
export type Severity = (typeof Severities)[number];
|
||||
```
|
||||
|
||||
Export both when callers need to iterate (e.g. for a Storybook stories matrix or a select dropdown).
|
||||
|
||||
---
|
||||
|
||||
# Part 2 — Backend (`server/src`)
|
||||
|
||||
## 6. Layering: Controller → Service → Repository → Mongoose
|
||||
|
||||
The backend enforces a strict three-layer separation between HTTP handling, business logic, and data access. Every new feature follows this path, no shortcuts.
|
||||
|
||||
```
|
||||
Request → Controller → Service → Repository → MongoDB (Mongoose)
|
||||
```
|
||||
|
||||
**Why:** this is what makes each layer independently testable. Mongoose calls in services force services to mock the database in tests; raw DB queries in controllers couple HTTP shape to schema shape and turn every refactor into a multi-file rewrite.
|
||||
|
||||
**How to apply:** when adding a new feature, the pattern is — **add a repository method** for any new DB query, **call it from a service**, **expose it via a controller**. If a service constructs a `MonitorModel.findOne({...})`, you've broken the rule; the query belongs in the repository.
|
||||
|
||||
- **Controllers** (`server/src/controllers/`) handle HTTP concerns only: parse params, call the service, return via the `responseHandler` middleware. No business logic.
|
||||
- **Services** (`server/src/service/business/`) contain all business logic. They throw `AppError` on validation/auth failures; they do not write directly to MongoDB.
|
||||
- **Repositories** (`server/src/repositories/`) are the sole layer that talks to MongoDB. They expose clean query methods (`findByMonitorId`, `createCheck`, `updateStatusWindowAndChecks`) and return entities — never Mongoose documents — via a `toEntity` private helper.
|
||||
|
||||
---
|
||||
|
||||
## 7. Provider conventions for `service/infrastructure/network/*`
|
||||
|
||||
Every monitor-type provider implements `IStatusProvider<TPayload>` and follows the same skeleton.
|
||||
|
||||
**Why:** the registry in `services.ts` builds providers identically (`new XxxProvider(deps)`) and `NetworkService` invokes them identically (`provider.handle(monitor)`). If your provider's shape diverges, the registry/dispatch becomes asymmetric and `NetworkService` grows special cases.
|
||||
|
||||
**How to apply:** copy the structure of a recent peer (`WebSocketProvider`, `GrpcProvider`, `PortProvider`). The skeleton:
|
||||
|
||||
```ts
|
||||
const SERVICE_NAME = "XxxProvider";
|
||||
const TIMEOUT_MS = 10000;
|
||||
|
||||
type ClientCtor = typeof SomeNodeStdlibClass;
|
||||
|
||||
export class XxxProvider implements IStatusProvider<XxxStatusPayload> {
|
||||
readonly type = "xxx";
|
||||
|
||||
constructor(private Client: ClientCtor) {}
|
||||
|
||||
supports(type: MonitorType): boolean {
|
||||
return type === "xxx";
|
||||
}
|
||||
|
||||
async handle(monitor: Monitor): Promise<MonitorStatusResponse<XxxStatusPayload>> {
|
||||
try {
|
||||
// 1. validate inputs → throw AppError(400) on bad input
|
||||
// 2. set up the client (use this.Client, never imported singleton)
|
||||
// 3. perform the call inside timeRequest(...), with a setTimeout race
|
||||
// in the Promise body to enforce TIMEOUT_MS
|
||||
// 4. return MonitorStatusResponse with payload
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof AppError) throw err;
|
||||
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||
throw new AppError({
|
||||
message: originalMessage || "Error performing xxx check",
|
||||
status: 500,
|
||||
service: SERVICE_NAME,
|
||||
method: "handle",
|
||||
details: { /* monitor-shaped context */ },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hard rules
|
||||
|
||||
- **DI for all external dependencies.** `typeof WebSocket`, `typeof net` — pass the constructor in via the provider's constructor. Never `import { Foo } from "some/library"` and call it inside `handle()`. This is the single thing that makes the provider unit-testable without `jest.unstable_mockModule`.
|
||||
- **`SERVICE_NAME` and `TIMEOUT_MS`** as module-level constants near the top, not magic strings/numbers in the body.
|
||||
- **Use the shared `timeRequest()` helper** from `service/infrastructure/network/utils.ts` to measure latency. Don't roll your own `process.hrtime()`.
|
||||
- **Enforce a hard timeout.** Every peer provider inlines a `setTimeout` race in the Promise body around the underlying client call (see `WebSocketProvider.ts`); sockets, DNS resolvers, and external clients can hang indefinitely without it. There is no shared `withTimeout` helper today — match the inline pattern.
|
||||
- **Outer try/catch** that converts unknown errors to `AppError({ service, method, details })` while letting your own `AppError` pass through.
|
||||
|
||||
---
|
||||
|
||||
## 8. Mongoose schema fields with closed value sets
|
||||
|
||||
If a field's TypeScript type is a closed union (status, type, record type), the Mongoose schema must declare the same set as `enum`.
|
||||
|
||||
**Why:** the validator and the schema are independent gates. A service-layer Zod check can be bypassed (imports, migrations, direct repo writes). A schema `enum` is the last line of defense and surfaces invalid data as a Mongoose validation error rather than silently accepting `"badtype"` and breaking downstream consumers.
|
||||
|
||||
**How to apply:** when you add a string field whose values are constrained by a const tuple in `types/`, reuse the same const for the schema enum:
|
||||
|
||||
```ts
|
||||
// types/monitor.ts (existing)
|
||||
export const MonitorMatchMethods = ["equal", "include", "regex"] as const;
|
||||
|
||||
// db/models/Monitor.ts
|
||||
{
|
||||
matchMethod: {
|
||||
type: String,
|
||||
enum: MonitorMatchMethods,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The same applies to `MonitorTypes`, `MonitorStatuses`, and any new closed-set field.
|
||||
|
||||
---
|
||||
|
||||
## 9. Test conventions for `server/test/unit/providers/network/*`
|
||||
|
||||
Each provider gets a unit test file at `server/test/unit/providers/network/<providerName>.test.ts`. Follow the structure used by `webSocketProvider.test.ts`, `grpcProvider.test.ts`, `portProvider.test.ts`.
|
||||
|
||||
**Why:** consistency lets reviewers diff a new test file against any peer and only see the actual logic differences, not framework/import noise. It also lets the `testStatusProviderContract` helper guarantee every provider satisfies the `IStatusProvider` contract.
|
||||
|
||||
**How to apply:**
|
||||
|
||||
- **Imports**: `from "@jest/globals"` (not the global `jest`), and `.ts` extensions on every relative import.
|
||||
- **Contract test**: call `testStatusProviderContract("XxxProvider", { create, supportedType, unsupportedType, makeMonitor })` before the per-test `describe` block.
|
||||
- **Mock construction**: build the constructor mock as `jest.fn().mockImplementation(() => mock) as unknown as typeof Client` (typed cast, not `as any`).
|
||||
- **`makeMonitor` factory**: takes `Partial<Monitor>` overrides, spreads onto a baseline, returns `as Monitor`.
|
||||
- **Inline `setup()` per test** rather than `beforeEach` shared state. Each test should construct its own mock + provider, so a single test can be read in isolation:
|
||||
|
||||
```ts
|
||||
const setup = () => {
|
||||
const mock = createMockClient();
|
||||
const Ctor = jest.fn().mockImplementation(() => mock) as unknown as typeof Client;
|
||||
const provider = new XxxProvider(Ctor);
|
||||
return { mock, provider };
|
||||
};
|
||||
|
||||
it("does X", async () => {
|
||||
const { mock, provider } = setup();
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
- **Assertion style**: individual `expect(result.field).toBe(...)` lines or `expect(result).toEqual(expect.objectContaining({...}))`. Both are used; pick what reads cleanest for the case.
|
||||
- **Test naming**: lowercase imperative ("returns success when connection opens", "uses the configured timeout", "throws AppError when host is missing").
|
||||
|
||||
---
|
||||
|
||||
## 10. Validation: centralize, don't inline
|
||||
|
||||
Whenever the same string-set or shape constraint appears in two or more validators, lift it into `types/` as a const tuple and `import` it into both validators. Don't inline `z.enum(["A", "AAAA", ...])` in three places.
|
||||
|
||||
**Why:** drift. Three inline copies will eventually disagree — someone adds a new value to one and forgets the other two. The validator silently accepts it on `POST /monitors` and rejects it on `PATCH`, leading to bug reports the author can't reproduce.
|
||||
|
||||
**How to apply:** keep validation source-of-truth in `server/src/types/*.ts` as paired const tuples + derived types (rule 5). The validators in `server/src/validation/*.ts` and the Mongoose schemas in `server/src/db/models/*.ts` import from `types/` and reuse the same tuple.
|
||||
|
||||
---
|
||||
|
||||
## 11. OpenAPI: response schemas live in the validator file
|
||||
|
||||
The OpenAPI `monitorObject` (and friends) are generated from a `monitorResponseSchema` Zod object exported from the validator file, not hand-built in `server/openapi/routes/`. When you add a field to the data model, add it to the response schema in the same edit.
|
||||
|
||||
**Why:** the spec is auto-generated. If the response schema doesn't list a field, SDK consumers don't see it, even though the server returns it. That's a silent contract gap.
|
||||
|
||||
**How to apply:** when you add an entity field, update three files in the same PR:
|
||||
|
||||
1. `types/<entity>.ts` — add the field to the TS interface.
|
||||
2. `db/models/<Entity>.ts` — add the Mongoose schema entry (with `enum` if the value set is closed; rule 8).
|
||||
3. `validation/<entity>Validation.ts` — add the field to the response schema (and create/edit body schemas if it's user-settable).
|
||||
|
||||
The repository's `toEntity` helper also needs to surface the new field; that's part of the change, not a separate one.
|
||||
|
||||
---
|
||||
|
||||
# Pre-PR checklist
|
||||
|
||||
Before opening a PR, grep your diff for the patterns below. Each item describes the **anti-pattern** to find and fix; if your diff has none, that line passes. Frontend section applies to `client/src`; backend to `server/src`.
|
||||
|
||||
## Universal
|
||||
- [ ] I can name the peer file I used as a template, and my file's structure mirrors it (rule 0).
|
||||
- [ ] `npm run build` and `npm run format-check` are clean in both `client/` and `server/`.
|
||||
|
||||
## Frontend (`client/src`)
|
||||
- [ ] No `sx={{ … }}` block containing keys with a native-prop equivalent (rule 1).
|
||||
- [ ] No `color="text.X"`, `bgcolor="background.X"`, or other short-string palette paths (rule 2).
|
||||
- [ ] No raw number or px-string in spacing / margin / padding / fontSize / borderRadius props that should be a token (rule 3).
|
||||
- [ ] No `import { theme } from "@/Utils/Theme/Theme"` inside a component (rule 4).
|
||||
- [ ] No `type X = "a" | "b" | "c"` for a closed set used at runtime (rule 5).
|
||||
|
||||
## Backend (`server/src`)
|
||||
- [ ] No `MonitorModel.findOne` (or similar Mongoose call) outside `repositories/` (rule 6).
|
||||
- [ ] No provider that imports its underlying client at module top instead of receiving it via constructor DI (rule 7).
|
||||
- [ ] No provider missing `SERVICE_NAME` / `TIMEOUT_MS` / `AppError` outer catch / `timeRequest()` / inline timeout (rule 7).
|
||||
- [ ] No closed-set string field on a Mongoose schema without an `enum` (rule 8).
|
||||
- [ ] No provider test that uses `beforeEach` instead of inline `setup()` (rule 9).
|
||||
- [ ] No duplicated `z.enum([...])` literal that should reference a `types/*` const tuple (rule 10).
|
||||
- [ ] No new entity field missing from `monitorResponseSchema` (or peer response schema) for OpenAPI (rule 11).
|
||||
|
||||
A clean PR on these axes is a PR that ships without a normalize follow-up.
|
||||
@@ -24,9 +24,3 @@ CLIENT_HOST=http://localhost:5173
|
||||
# Optional Configuration
|
||||
# ----------------------
|
||||
ORIGIN=localhost
|
||||
|
||||
# Feature Flags
|
||||
# -------------
|
||||
# Set to "false" to disable the selectable theme feature on status pages.
|
||||
# When disabled, all status pages render with the default "refined" theme.
|
||||
STATUS_PAGE_THEMES_ENABLED=true
|
||||
+4530
-10661
File diff suppressed because it is too large.
Load diff
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user