Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0e5a334bb | ||
|
|
22232489a8 | ||
|
|
04277a6eb4 | ||
|
|
a8dc867e78 | ||
|
|
533783ba3e | ||
|
|
fd7b3e5f20 | ||
|
|
ae4b851620 | ||
|
|
e83f97a05a | ||
|
|
070682af74 | ||
|
|
8609e4bca0 | ||
|
|
2661d7a384 | ||
|
|
5b9d554f46 | ||
|
|
7fb8bab4be | ||
|
|
75d29124b6 | ||
|
|
1f0ea6cedc | ||
|
|
07a38b9c2c | ||
|
|
67806c94b1 | ||
|
|
2eeb088b61 | ||
|
|
9c796edbde | ||
|
|
84a1b994a1 | ||
|
|
db4689207a | ||
|
|
af465766c8 | ||
|
|
c13f26a4c3 |
No files matched your search
Generated
+58
-22
@@ -9,6 +9,9 @@
|
||||
"version": "3.11.0",
|
||||
"dependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource-variable/geist": "^5.3.0",
|
||||
@@ -93,7 +96,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
@@ -363,6 +365,59 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/babel-plugin": {
|
||||
"version": "11.13.5",
|
||||
"license": "MIT",
|
||||
@@ -409,7 +464,6 @@
|
||||
"node_modules/@emotion/react": {
|
||||
"version": "11.14.0",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -447,7 +501,6 @@
|
||||
"node_modules/@emotion/styled": {
|
||||
"version": "11.14.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -1236,7 +1289,6 @@
|
||||
"node_modules/@mui/material": {
|
||||
"version": "7.3.7",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/core-downloads-tracker": "^7.3.7",
|
||||
@@ -1341,7 +1393,6 @@
|
||||
"node_modules/@mui/system": {
|
||||
"version": "7.3.7",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/private-theming": "^7.3.7",
|
||||
@@ -2386,7 +2437,6 @@
|
||||
"version": "24.5.2",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.12.0"
|
||||
}
|
||||
@@ -2402,7 +2452,6 @@
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.27",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -2493,7 +2542,6 @@
|
||||
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
@@ -2774,7 +2822,6 @@
|
||||
"version": "8.15.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3131,7 +3178,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.42",
|
||||
"caniuse-lite": "^1.0.30001800",
|
||||
@@ -3575,8 +3621,7 @@
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
@@ -3975,7 +4020,6 @@
|
||||
"version": "8.57.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -4788,7 +4832,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -5600,7 +5643,6 @@
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
|
||||
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-rewind": "^0.5.2",
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
@@ -6382,7 +6424,6 @@
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -6394,7 +6435,6 @@
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.71.1",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -6598,8 +6638,7 @@
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-persist": {
|
||||
"version": "6.0.0",
|
||||
@@ -6749,7 +6788,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
|
||||
"integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.9"
|
||||
},
|
||||
@@ -7591,7 +7629,6 @@
|
||||
"version": "5.9.3",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7729,7 +7766,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
|
||||
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource-variable/geist": "^5.3.0",
|
||||
|
||||
@@ -81,6 +81,21 @@ export function DataTable<
|
||||
};
|
||||
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
|
||||
const hasTextSelectionWithin = (element: Element) => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.isCollapsed || !selection.toString().trim()) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
selection.anchorNode !== null &&
|
||||
element.contains(
|
||||
selection.anchorNode.nodeType === Node.TEXT_NODE
|
||||
? selection.anchorNode.parentNode
|
||||
: selection.anchorNode
|
||||
)
|
||||
);
|
||||
};
|
||||
const isInteractive = Boolean(onRowClick) || expandableRows;
|
||||
|
||||
if (data.length === 0 || headers.length === 0) {
|
||||
@@ -102,7 +117,10 @@ export function DataTable<
|
||||
keys.push(key);
|
||||
return (
|
||||
<Stack
|
||||
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
||||
onClick={(e) => {
|
||||
if (hasTextSelectionWithin(e.currentTarget)) return;
|
||||
if (onRowClick) onRowClick(row);
|
||||
}}
|
||||
spacing={theme.spacing(LAYOUT.XS)}
|
||||
sx={{
|
||||
borderStyle: "solid",
|
||||
@@ -256,7 +274,8 @@ export function DataTable<
|
||||
}),
|
||||
...(getRowSx?.(row) as object),
|
||||
}}
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
if (hasTextSelectionWithin(e.currentTarget)) return;
|
||||
if (expandableRows) handleExpand(row);
|
||||
else if (onRowClick) onRowClick(row);
|
||||
}}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Button } from "@/Components/inputs";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SxProps } from "@mui/material/styles";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export const DialogInput = ({
|
||||
@@ -26,8 +25,6 @@ export const DialogInput = ({
|
||||
maxWidth,
|
||||
fullWidth = false,
|
||||
additionalButtons,
|
||||
cancelSx,
|
||||
onEscape,
|
||||
}: {
|
||||
open: boolean;
|
||||
title?: string;
|
||||
@@ -42,14 +39,6 @@ export const DialogInput = ({
|
||||
maxWidth?: DialogProps["maxWidth"];
|
||||
fullWidth?: boolean;
|
||||
additionalButtons?: ReactNode;
|
||||
/** Styles the cancel button — e.g. an auto margin to push it left. */
|
||||
cancelSx?: SxProps;
|
||||
/**
|
||||
* Called when Escape is pressed. Defaults to `onCancel`, which is right when
|
||||
* cancel simply dismisses — but not when it does something else, so a dialog
|
||||
* whose cancel slot carries another action passes its own dismiss here.
|
||||
*/
|
||||
onEscape?(): void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@@ -59,12 +48,7 @@ export const DialogInput = ({
|
||||
maxWidth={maxWidth}
|
||||
fullWidth={fullWidth}
|
||||
onClose={(_event, reason) => {
|
||||
if (reason === "backdropClick") return;
|
||||
if (onEscape) {
|
||||
onEscape();
|
||||
return;
|
||||
}
|
||||
if (onCancel) onCancel(undefined);
|
||||
if (reason !== "backdropClick" && onCancel) onCancel(undefined);
|
||||
}}
|
||||
>
|
||||
{title && (
|
||||
@@ -110,7 +94,6 @@ export const DialogInput = ({
|
||||
loading={loading}
|
||||
variant="outlined"
|
||||
onClick={onCancel}
|
||||
sx={cancelSx}
|
||||
>
|
||||
{cancelText ?? t("common.buttons.cancel")}
|
||||
</Button>
|
||||
|
||||
@@ -60,20 +60,22 @@ export const HeaderMonitorControls = ({
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isPosting}
|
||||
startIcon={<Icon icon={Mail} />}
|
||||
onClick={async () => {
|
||||
await post(`/notifications/test/all`, { monitorId: monitor.id });
|
||||
}}
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{t("common.buttons.testNotifications")}
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isPosting}
|
||||
startIcon={<Icon icon={Mail} />}
|
||||
onClick={async () => {
|
||||
await post(`/notifications/test/all`, { monitorId: monitor.id });
|
||||
}}
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{t("common.buttons.testNotifications")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
|
||||
import type { CardId } from "@/Pages/Dashboard/cards";
|
||||
|
||||
type ThemeMode = "light" | "dark";
|
||||
type ChartType = "histogram" | "line";
|
||||
@@ -35,6 +36,7 @@ interface UIState {
|
||||
language: string;
|
||||
starPromptOpen: boolean;
|
||||
chartType: ChartType;
|
||||
dashboardCards: CardId[] | null;
|
||||
}
|
||||
|
||||
const initialMode: ThemeMode = window?.matchMedia?.("(prefers-color-scheme: dark)")
|
||||
@@ -74,6 +76,7 @@ const initialState: UIState = {
|
||||
language: "en",
|
||||
starPromptOpen: true,
|
||||
chartType: "histogram",
|
||||
dashboardCards: null,
|
||||
};
|
||||
|
||||
const uiSlice = createSlice({
|
||||
@@ -117,6 +120,9 @@ const uiSlice = createSlice({
|
||||
setChartType: (state, action: PayloadAction<ChartType>) => {
|
||||
state.chartType = action.payload;
|
||||
},
|
||||
setDashboardCards: (state, action: PayloadAction<CardId[] | null>) => {
|
||||
state.dashboardCards = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -133,4 +139,5 @@ export const {
|
||||
setLanguage,
|
||||
setStarPromptOpen,
|
||||
setChartType,
|
||||
setDashboardCards,
|
||||
} = uiSlice.actions;
|
||||
@@ -1,78 +1,123 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useMemo } from "react";
|
||||
import { GripVertical } from "lucide-react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import type { DragEndEvent } from "@dnd-kit/core";
|
||||
import { SortableContext, useSortable, rectSortingStrategy } from "@dnd-kit/sortable";
|
||||
|
||||
import { Icon } from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { GRID_COLUMNS, type CardDefinition, type CardId } from "../cards";
|
||||
import { layoutCards } from "../layout";
|
||||
import { useCardTransitions } from "../useCardTransitions";
|
||||
import { CardReorderControls } from "./CardReorderControls";
|
||||
import { CardSlotContext } from "./CardSlotContext";
|
||||
|
||||
export const CardGrid = ({
|
||||
cards,
|
||||
onMove,
|
||||
}: {
|
||||
cards: CardDefinition[];
|
||||
onMove: (id: CardId, delta: -1 | 1) => void;
|
||||
}) => {
|
||||
const SortableCard = ({ card }: { card: CardDefinition }) => {
|
||||
const theme = useTheme();
|
||||
const placed = useMemo(() => layoutCards(cards), [cards]);
|
||||
const { registerCard, capturePositions } = useCardTransitions();
|
||||
|
||||
// Positions are measured before the state change so the cards can be
|
||||
// animated from where they were to where they land.
|
||||
const handleMove = (id: CardId, delta: -1 | 1) => {
|
||||
capturePositions(id);
|
||||
onMove(id, delta);
|
||||
};
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
|
||||
useSortable({ id: card.id });
|
||||
const Component = card.component;
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="grid"
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
ref={setNodeRef}
|
||||
minWidth={0}
|
||||
sx={{
|
||||
gridTemplateColumns: {
|
||||
xs: "1fr",
|
||||
md: `repeat(${GRID_COLUMNS}, 1fr)`,
|
||||
width: {
|
||||
xs: "100%",
|
||||
md:
|
||||
card.width >= GRID_COLUMNS
|
||||
? "100%"
|
||||
: `calc(${(card.width / GRID_COLUMNS) * 100}% - ${theme.spacing(LAYOUT.MD / 2)})`,
|
||||
},
|
||||
alignItems: "stretch",
|
||||
transform: transform
|
||||
? `translate3d(${Math.round(transform.x)}px, ${Math.round(transform.y)}px, 0)`
|
||||
: undefined,
|
||||
transition,
|
||||
opacity: isDragging ? 0.4 : 1,
|
||||
zIndex: isDragging ? 1 : "auto",
|
||||
}}
|
||||
>
|
||||
{placed.map(({ card, renderedWidth }, index) => {
|
||||
const Component = card.component;
|
||||
return (
|
||||
<CardSlotContext.Provider
|
||||
value={
|
||||
<Box
|
||||
key={card.id}
|
||||
ref={(node: HTMLElement | null) => registerCard(card.id, node)}
|
||||
minWidth={0}
|
||||
// Positioned so the moving card can be lifted above its
|
||||
// neighbours with a z-index while it travels.
|
||||
position="relative"
|
||||
gridColumn={{ xs: "auto", md: `span ${renderedWidth}` }}
|
||||
// Hints the compositor before a transform is applied, so the
|
||||
// first frame of a move does not stutter.
|
||||
sx={{ willChange: "transform" }}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
cursor: "grab",
|
||||
"&:active": { cursor: "grabbing" },
|
||||
touchAction: "none",
|
||||
}}
|
||||
>
|
||||
{/*
|
||||
* The controls go through context rather than a prop so the 17
|
||||
* card components stay unaware of their own position.
|
||||
*/}
|
||||
<CardSlotContext.Provider
|
||||
value={
|
||||
<CardReorderControls
|
||||
canMoveUp={index > 0}
|
||||
canMoveDown={index < placed.length - 1}
|
||||
onMoveUp={() => handleMove(card.id, -1)}
|
||||
onMoveDown={() => handleMove(card.id, 1)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Component />
|
||||
</CardSlotContext.Provider>
|
||||
<Icon
|
||||
icon={GripVertical}
|
||||
size={16}
|
||||
color={theme.palette.text.secondary}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
}
|
||||
>
|
||||
<Component />
|
||||
</CardSlotContext.Provider>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardGrid = ({
|
||||
cards,
|
||||
onReorder,
|
||||
}: {
|
||||
cards: CardDefinition[];
|
||||
onReorder: (sourceIndex: number, destIndex: number) => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 8 },
|
||||
})
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
const sourceIndex = cards.findIndex((c) => c.id === active.id);
|
||||
const destIndex = cards.findIndex((c) => c.id === over.id);
|
||||
if (sourceIndex !== -1 && destIndex !== -1) onReorder(sourceIndex, destIndex);
|
||||
};
|
||||
|
||||
const cardIds = cards.map((c) => c.id);
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={cardIds}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
flexWrap="wrap"
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
alignItems="stretch"
|
||||
>
|
||||
{cards.map((card) => (
|
||||
<SortableCard
|
||||
key={card.id}
|
||||
card={card}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Dialog, Checkbox } from "@/Components/inputs";
|
||||
import { Dialog, Checkbox, Button } from "@/Components/inputs";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { CardGroups, type CardDefinition, type CardGroup, type CardId } from "../cards";
|
||||
|
||||
@@ -14,8 +14,11 @@ interface CardPickerProps {
|
||||
open: boolean;
|
||||
available: CardDefinition[];
|
||||
selected: CardId[];
|
||||
allSelected: boolean;
|
||||
onAdd: (id: CardId) => void;
|
||||
onRemove: (id: CardId) => void;
|
||||
onSelectAll: () => void;
|
||||
onDeselectAll: () => void;
|
||||
onReset: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
@@ -29,8 +32,11 @@ export const CardPicker = ({
|
||||
open,
|
||||
available,
|
||||
selected,
|
||||
allSelected,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onSelectAll,
|
||||
onDeselectAll,
|
||||
onReset,
|
||||
onClose,
|
||||
}: CardPickerProps) => {
|
||||
@@ -45,6 +51,7 @@ export const CardPicker = ({
|
||||
}, [available]);
|
||||
|
||||
const chosen = new Set(selected);
|
||||
const someSelected = selected.length > 0 && !allSelected;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -52,27 +59,35 @@ export const CardPicker = ({
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
title={t("pages.dashboard.picker.title")}
|
||||
// The dialog lays its actions out as cancel → additional → confirm, all
|
||||
// right-aligned. Reset takes the cancel slot with an auto right margin
|
||||
// so it sits alone on the left, away from Done — a destructive action
|
||||
// Reset sits alone on the left, away from Done — a destructive action
|
||||
// should not be adjacent to the one people reach for by reflex.
|
||||
onCancel={onReset}
|
||||
cancelText={t("pages.dashboard.picker.reset")}
|
||||
cancelSx={{ marginRight: "auto" }}
|
||||
// Escape must dismiss, not reset. The dialog sends Escape to onCancel by
|
||||
// default, and this one's cancel slot resets the selection — so without
|
||||
// this, Escape would wipe the user's cards instead of closing.
|
||||
onEscape={onClose}
|
||||
// Changes apply immediately, so Done only dismisses; it is the primary
|
||||
// action here and carries the usual contained styling.
|
||||
onConfirm={onClose}
|
||||
confirmText={t("pages.dashboard.picker.done")}
|
||||
additionalButtons={
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={allSelected ? onDeselectAll : onSelectAll}
|
||||
startIcon={
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
indeterminate={someSelected}
|
||||
size="small"
|
||||
sx={{ p: 0 }}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t("pages.dashboard.picker.selectAll")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{/*
|
||||
* Two columns: seventeen cards in one list makes the dialog scroll,
|
||||
* which hides most of the catalogue behind a scrollbar. Groups flow
|
||||
* down the columns and are kept whole, so a group is never split
|
||||
* across the gap.
|
||||
* which hides most of the catalogue. Groups flow down the columns and
|
||||
* are kept whole so a group is never split across the gap.
|
||||
*/}
|
||||
<Box
|
||||
pt={theme.spacing(LAYOUT.SM)}
|
||||
@@ -92,11 +107,6 @@ export const CardPicker = ({
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.disabled}
|
||||
>
|
||||
{/*
|
||||
* The picker has its own group labels rather than reusing the
|
||||
* sidebar's: the Logs page hosts logs, queue and diagnostics
|
||||
* tabs, so "Logs" would misdescribe the three cards under it.
|
||||
*/}
|
||||
{t(`pages.dashboard.picker.groups.${group}`, {
|
||||
defaultValue: group,
|
||||
})}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronUp, ChevronDown } from "lucide-react";
|
||||
|
||||
import { Icon, Tooltip } from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
// Snug around the 16px icon without inflating the card header.
|
||||
const CONTROL_SIZE = 24;
|
||||
const ICON_SIZE = 16;
|
||||
|
||||
/**
|
||||
* Move-up / move-down controls, revealed on hover over the card they belong to.
|
||||
* Kept out of DashboardCard so the 17 card components stay unaware of their own
|
||||
* position — the grid owns ordering.
|
||||
*/
|
||||
export const CardReorderControls = ({
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
canMoveUp,
|
||||
canMoveDown,
|
||||
}: {
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
canMoveUp: boolean;
|
||||
canMoveDown: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const buttonSx = {
|
||||
width: CONTROL_SIZE,
|
||||
height: CONTROL_SIZE,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
color: theme.palette.text.secondary,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
"&.Mui-disabled": { color: theme.palette.text.disabled },
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(LAYOUT.XXS)}
|
||||
p={theme.spacing(LAYOUT.XXS)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
bgcolor={theme.palette.background.paper}
|
||||
border={`1px solid ${theme.palette.divider}`}
|
||||
>
|
||||
<Tooltip title={t("pages.dashboard.reorder.moveUp")}>
|
||||
{/* span keeps the tooltip working while the button is disabled */}
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={!canMoveUp}
|
||||
onClick={onMoveUp}
|
||||
aria-label={t("pages.dashboard.reorder.moveUp")}
|
||||
sx={buttonSx}
|
||||
>
|
||||
<Icon
|
||||
icon={ChevronUp}
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("pages.dashboard.reorder.moveDown")}>
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={!canMoveDown}
|
||||
onClick={onMoveDown}
|
||||
aria-label={t("pages.dashboard.reorder.moveDown")}
|
||||
sx={buttonSx}
|
||||
>
|
||||
<Icon
|
||||
icon={ChevronDown}
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -51,12 +51,8 @@ export const DashboardCard = ({
|
||||
height="100%"
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
"&:hover .dashboard-card-slot, &:has(:focus-visible) .dashboard-card-slot": {
|
||||
"&:hover .dashboard-card-slot": {
|
||||
opacity: 1,
|
||||
pointerEvents: "auto",
|
||||
},
|
||||
"&:hover .dashboard-card-action, &:has(:focus-visible) .dashboard-card-action": {
|
||||
opacity: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -96,22 +92,12 @@ export const DashboardCard = ({
|
||||
color="primary"
|
||||
/>
|
||||
)}
|
||||
{action && (
|
||||
<Box
|
||||
className="dashboard-card-action"
|
||||
sx={{ transition: "opacity 0.15s ease" }}
|
||||
>
|
||||
{action}
|
||||
</Box>
|
||||
)}
|
||||
{action && <Box>{action}</Box>}
|
||||
{slot && (
|
||||
<Box
|
||||
className="dashboard-card-slot"
|
||||
position="absolute"
|
||||
right={0}
|
||||
sx={{
|
||||
opacity: 0,
|
||||
pointerEvents: "none",
|
||||
opacity: 0.3,
|
||||
transition: "opacity 0.15s ease",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -16,8 +16,18 @@ const Dashboard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const monitorsRequest = useMonitorsRequest();
|
||||
const { cards, available, selected, addCard, removeCard, moveCard, resetCards } =
|
||||
useCardSelection();
|
||||
const {
|
||||
cards,
|
||||
available,
|
||||
selected,
|
||||
addCard,
|
||||
removeCard,
|
||||
reorderCards,
|
||||
resetCards,
|
||||
selectAll,
|
||||
deselectAll,
|
||||
allSelected,
|
||||
} = useCardSelection();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -52,7 +62,7 @@ const Dashboard = () => {
|
||||
) : (
|
||||
<CardGrid
|
||||
cards={cards}
|
||||
onMove={moveCard}
|
||||
onReorder={reorderCards}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -60,8 +70,11 @@ const Dashboard = () => {
|
||||
open={isPickerOpen}
|
||||
available={available}
|
||||
selected={selected}
|
||||
allSelected={allSelected}
|
||||
onAdd={addCard}
|
||||
onRemove={removeCard}
|
||||
onSelectAll={selectAll}
|
||||
onDeselectAll={deselectAll}
|
||||
onReset={resetCards}
|
||||
onClose={() => setIsPickerOpen(false)}
|
||||
/>
|
||||
|
||||
@@ -1,79 +1,33 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
|
||||
import { logger } from "@/Utils/logger";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import {
|
||||
CardIds,
|
||||
DASHBOARD_CARDS_STORAGE_KEY,
|
||||
DEFAULT_CARD_IDS,
|
||||
type CardId,
|
||||
} from "./cards";
|
||||
import { setDashboardCards } from "@/Features/UI/uiSlice";
|
||||
import { CardIds, DEFAULT_CARD_IDS, type CardId } from "./cards";
|
||||
import { CARD_REGISTRY, getCard } from "./registry";
|
||||
|
||||
import type { CardDefinition } from "./cards";
|
||||
import type { RootState } from "@/store";
|
||||
|
||||
const isCardId = (value: unknown): value is CardId =>
|
||||
typeof value === "string" && (CardIds as readonly string[]).includes(value);
|
||||
|
||||
/** Stored ids are validated: a card removed from a release must not break the page. */
|
||||
const readStored = (): CardId[] | null => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(DASHBOARD_CARDS_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
// An empty array is a real choice and must not fall back to the defaults;
|
||||
// only an absent or malformed key returns null. Unknown ids are dropped
|
||||
// so a card removed in a later release cannot break the page.
|
||||
return parsed.filter(isCardId);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Could not read dashboard card selection",
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Which cards are on the page, persisted per browser. Admin-only cards are
|
||||
* filtered out entirely for non-admins — hidden, not disabled.
|
||||
*/
|
||||
export const useCardSelection = () => {
|
||||
const dispatch = useDispatch();
|
||||
const isAdmin = useIsAdmin();
|
||||
const [selected, setSelected] = useState<CardId[]>(
|
||||
() => readStored() ?? [...DEFAULT_CARD_IDS]
|
||||
|
||||
// null means the user has never customised; fall back to defaults.
|
||||
const stored = useSelector((state: RootState) => state.ui.dashboardCards);
|
||||
const selected: CardId[] = useMemo(
|
||||
() => (stored !== null ? stored.filter(isCardId) : [...DEFAULT_CARD_IDS]),
|
||||
[stored]
|
||||
);
|
||||
|
||||
// Skip the write on mount: if the read failed and fell back to the defaults,
|
||||
// writing would overwrite the user's real selection permanently.
|
||||
const hasMounted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasMounted.current) {
|
||||
hasMounted.current = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.localStorage.setItem(DASHBOARD_CARDS_STORAGE_KEY, JSON.stringify(selected));
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Could not persist dashboard card selection",
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const isVisible = useCallback(
|
||||
(card: CardDefinition) => !card.adminOnly || isAdmin,
|
||||
[isAdmin]
|
||||
);
|
||||
|
||||
// Selection order is the render order.
|
||||
const cards = useMemo(
|
||||
() =>
|
||||
selected
|
||||
@@ -85,55 +39,71 @@ export const useCardSelection = () => {
|
||||
|
||||
const available = useMemo(() => CARD_REGISTRY.filter(isVisible), [isVisible]);
|
||||
|
||||
const addCard = useCallback((id: CardId) => {
|
||||
setSelected((current) => (current.includes(id) ? current : [...current, id]));
|
||||
}, []);
|
||||
const addCard = useCallback(
|
||||
(id: CardId) => {
|
||||
if (!selected.includes(id)) dispatch(setDashboardCards([...selected, id]));
|
||||
},
|
||||
[dispatch, selected]
|
||||
);
|
||||
|
||||
const removeCard = useCallback((id: CardId) => {
|
||||
setSelected((current) => current.filter((cardId) => cardId !== id));
|
||||
}, []);
|
||||
const removeCard = useCallback(
|
||||
(id: CardId) => {
|
||||
dispatch(setDashboardCards(selected.filter((cardId) => cardId !== id)));
|
||||
},
|
||||
[dispatch, selected]
|
||||
);
|
||||
|
||||
const resetCards = useCallback(() => setSelected([...DEFAULT_CARD_IDS]), []);
|
||||
const resetCards = useCallback(() => dispatch(setDashboardCards(null)), [dispatch]);
|
||||
|
||||
const selectAll = useCallback(
|
||||
() =>
|
||||
dispatch(
|
||||
setDashboardCards(
|
||||
available.map((card) => card.id).filter((id) => !selected.includes(id)).length >
|
||||
0
|
||||
? [...new Set([...selected, ...available.map((card) => card.id)])]
|
||||
: selected
|
||||
)
|
||||
),
|
||||
[dispatch, available, selected]
|
||||
);
|
||||
|
||||
const deselectAll = useCallback(() => dispatch(setDashboardCards([])), [dispatch]);
|
||||
|
||||
/**
|
||||
* Swaps a card with its neighbour; `delta` is -1 for up, +1 for down.
|
||||
* Resolved against the visible order, so moving past a card hidden from a
|
||||
* non-admin does not appear to do nothing.
|
||||
* Reorders cards based on DnD source/dest indices into the visible `cards` array.
|
||||
* Hidden admin cards keep their stored positions unchanged.
|
||||
*/
|
||||
const moveCard = useCallback(
|
||||
(id: CardId, delta: -1 | 1) => {
|
||||
setSelected((current) => {
|
||||
const visible = current.filter((cardId) => {
|
||||
const card = getCard(cardId);
|
||||
return card !== undefined && isVisible(card);
|
||||
});
|
||||
const from = visible.indexOf(id);
|
||||
const to = from + delta;
|
||||
if (from === -1 || to < 0 || to >= visible.length) {
|
||||
return current;
|
||||
}
|
||||
// Swap in the visible order, then write that order back over the
|
||||
// stored positions the visible cards occupy, leaving hidden cards
|
||||
// exactly where they were.
|
||||
const reordered = [...visible];
|
||||
[reordered[from], reordered[to]] = [reordered[to], reordered[from]];
|
||||
|
||||
let next = 0;
|
||||
return current.map((cardId) =>
|
||||
visible.includes(cardId) ? reordered[next++] : cardId
|
||||
);
|
||||
});
|
||||
const reorderCards = useCallback(
|
||||
(sourceIndex: number, destIndex: number) => {
|
||||
const visibleIds = cards.map((c) => c.id);
|
||||
const reordered = [...visibleIds];
|
||||
const [moved] = reordered.splice(sourceIndex, 1);
|
||||
reordered.splice(destIndex, 0, moved);
|
||||
let visibleIdx = 0;
|
||||
dispatch(
|
||||
setDashboardCards(
|
||||
selected.map((cardId) =>
|
||||
visibleIds.includes(cardId) ? reordered[visibleIdx++] : cardId
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
[isVisible]
|
||||
[dispatch, cards, selected]
|
||||
);
|
||||
|
||||
const allSelected = available.every((card) => selected.includes(card.id));
|
||||
|
||||
return {
|
||||
cards,
|
||||
available,
|
||||
selected,
|
||||
addCard,
|
||||
removeCard,
|
||||
moveCard,
|
||||
reorderCards,
|
||||
resetCards,
|
||||
selectAll,
|
||||
deselectAll,
|
||||
allSelected,
|
||||
};
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
||||
|
||||
import type { CardId } from "./cards";
|
||||
|
||||
/** Long enough to follow a card across a row, short enough not to feel slow. */
|
||||
const DURATION_MS = 260;
|
||||
/** Decelerating: quick to leave, gentle to settle. */
|
||||
const EASING = "cubic-bezier(0.2, 0.8, 0.2, 1)";
|
||||
|
||||
export const useCardTransitions = () => {
|
||||
const nodes = useRef(new Map<CardId, HTMLElement>());
|
||||
const positions = useRef(new Map<CardId, DOMRect>());
|
||||
/** Set only for the render that follows a reorder, so nothing animates on data refresh. */
|
||||
const isAnimating = useRef(false);
|
||||
/** The card the user moved, lifted above the rest while it travels. */
|
||||
const movedCard = useRef<CardId | null>(null);
|
||||
/** Pending frame and settle handles, cancelled on unmount and on a new move. */
|
||||
const frames = useRef<number[]>([]);
|
||||
const timers = useRef<number[]>([]);
|
||||
|
||||
const cancelPending = useCallback(() => {
|
||||
frames.current.forEach(cancelAnimationFrame);
|
||||
timers.current.forEach(clearTimeout);
|
||||
frames.current = [];
|
||||
timers.current = [];
|
||||
}, []);
|
||||
|
||||
useEffect(() => cancelPending, [cancelPending]);
|
||||
|
||||
const registerCard = useCallback((id: CardId, node: HTMLElement | null) => {
|
||||
if (node) {
|
||||
nodes.current.set(id, node);
|
||||
} else {
|
||||
nodes.current.delete(id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Call immediately before the state change that reorders the cards.
|
||||
* `movedId` is the card the user acted on; it rides above the others so the
|
||||
* swap reads as one card moving rather than two sliding through each other.
|
||||
*/
|
||||
const capturePositions = useCallback(
|
||||
(movedId: CardId) => {
|
||||
// A move while a previous one is still settling would otherwise have
|
||||
// its inversion wiped by the earlier timeout, and the card would jump.
|
||||
cancelPending();
|
||||
positions.current.clear();
|
||||
for (const [id, node] of nodes.current) {
|
||||
positions.current.set(id, node.getBoundingClientRect());
|
||||
}
|
||||
movedCard.current = movedId;
|
||||
isAnimating.current = true;
|
||||
},
|
||||
[cancelPending]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!isAnimating.current) {
|
||||
return;
|
||||
}
|
||||
isAnimating.current = false;
|
||||
|
||||
const reducedMotion = window.matchMedia?.(
|
||||
"(prefers-reduced-motion: reduce)"
|
||||
)?.matches;
|
||||
if (reducedMotion) {
|
||||
positions.current.clear();
|
||||
movedCard.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [id, node] of nodes.current) {
|
||||
const previous = positions.current.get(id);
|
||||
if (!previous) {
|
||||
continue;
|
||||
}
|
||||
const current = node.getBoundingClientRect();
|
||||
const deltaX = previous.left - current.left;
|
||||
const deltaY = previous.top - current.top;
|
||||
if (deltaX === 0 && deltaY === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Invert: jump back to the old position with no transition...
|
||||
node.style.transition = "none";
|
||||
node.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
||||
if (id === movedCard.current) {
|
||||
node.style.zIndex = "1";
|
||||
}
|
||||
|
||||
// ...then play: on the next frame, release to the new one.
|
||||
frames.current.push(
|
||||
requestAnimationFrame(() => {
|
||||
node.style.transition = `transform ${DURATION_MS}ms ${EASING}`;
|
||||
node.style.transform = "";
|
||||
// Clear the inline styles once settled. Left in place they would
|
||||
// animate the card again on any later reflow it did not ask for.
|
||||
timers.current.push(
|
||||
window.setTimeout(() => {
|
||||
node.style.transition = "";
|
||||
node.style.zIndex = "";
|
||||
}, DURATION_MS)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
positions.current.clear();
|
||||
movedCard.current = null;
|
||||
});
|
||||
|
||||
return { registerCard, capturePositions };
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import type { DockerContainerInfo } from "@/Types/Check";
|
||||
|
||||
// Utils
|
||||
import { formatDuration } from "@/Utils/TimeUtils";
|
||||
import { getDockerPalette } from "@/Utils/MonitorUtils";
|
||||
|
||||
export const DockerContainerStatusBoxes = ({
|
||||
container,
|
||||
@@ -24,6 +25,7 @@ export const DockerContainerStatusBoxes = ({
|
||||
const { state, restartCount, health } = container;
|
||||
const startedAt = container.startedAt ?? "0";
|
||||
const runningFor = Date.now() - new Date(startedAt).getTime();
|
||||
const palette = getDockerPalette(state);
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -33,6 +35,7 @@ export const DockerContainerStatusBoxes = ({
|
||||
<StatBox
|
||||
title={t("common.labels.state")}
|
||||
subtitle={state}
|
||||
palette={palette}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("common.labels.uptime")}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useToast } from "@/Hooks/UseToast";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
|
||||
// Utils
|
||||
import { get } from "@/Utils/ApiClient";
|
||||
@@ -50,6 +51,7 @@ export const TabLogs = ({ monitorId, containerName, enabled }: TabLogsProps) =>
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { toastError } = useToast();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
// logs is newest-first, matching the API and what flattenDockerLogs expects.
|
||||
const [logs, setLogs] = useState<DockerLog[]>([]);
|
||||
@@ -59,7 +61,7 @@ export const TabLogs = ({ monitorId, containerName, enabled }: TabLogsProps) =>
|
||||
const [lastUpdatedAt, setLastUpdatedAt] = useState<number | null>(null);
|
||||
|
||||
const logsUrl =
|
||||
enabled && monitorId && containerName
|
||||
isAdmin && enabled && monitorId && containerName
|
||||
? `/monitors/docker/details/${encodeURIComponent(monitorId)}/containers/${encodeURIComponent(containerName)}/logs`
|
||||
: null;
|
||||
|
||||
@@ -168,6 +170,15 @@ export const TabLogs = ({ monitorId, containerName, enabled }: TabLogsProps) =>
|
||||
}
|
||||
}, [logsUrl, oldestCursor, loadingOlder, t, toastError]);
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<EmptyState
|
||||
title={t("pages.docker.container.logs.forbidden.title")}
|
||||
description={t("pages.docker.container.logs.forbidden.description")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
return (
|
||||
<EmptyState
|
||||
|
||||
@@ -126,6 +126,7 @@ const NotificationsCreatePage = () => {
|
||||
{watchedType !== "matrix" &&
|
||||
watchedType !== "telegram" &&
|
||||
watchedType !== "pushover" &&
|
||||
watchedType !== "signalgrid" &&
|
||||
watchedType !== "twilio" &&
|
||||
watchedType !== "ntfy" && (
|
||||
<ConfigBox
|
||||
@@ -201,6 +202,31 @@ const NotificationsCreatePage = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "signalgrid" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.signalgrid.title")}
|
||||
subtitle={t("pages.notifications.form.signalgrid.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<FormTextField
|
||||
name="accessToken"
|
||||
fieldLabel={t("pages.notifications.form.signalgrid.optionClientKey")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.signalgrid.placeholderClientKey"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormTextField
|
||||
name="address"
|
||||
fieldLabel={t("pages.notifications.form.signalgrid.optionChannel")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.signalgrid.placeholderChannel"
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "twilio" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.twilio.title")}
|
||||
|
||||
@@ -93,7 +93,7 @@ const Routes = () => {
|
||||
>
|
||||
<Route
|
||||
path="/"
|
||||
element={<Navigate to="/uptime" />}
|
||||
element={<Navigate to="/dashboard" />}
|
||||
/>
|
||||
<Route
|
||||
path="/dashboard"
|
||||
|
||||
@@ -9,6 +9,7 @@ export const NotificationChannels = [
|
||||
"teams",
|
||||
"telegram",
|
||||
"pushover",
|
||||
"signalgrid",
|
||||
"twilio",
|
||||
"ntfy",
|
||||
] as const;
|
||||
|
||||
@@ -40,6 +40,14 @@ export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
|
||||
return "warning";
|
||||
};
|
||||
|
||||
export const getDockerPalette = (dockerState: DockerContainerState): PaletteKey => {
|
||||
if (dockerState === "created") return "success";
|
||||
if (dockerState === "running") return "success";
|
||||
if (dockerState === "dead") return "error";
|
||||
if (dockerState === "exited") return "error";
|
||||
return "warning";
|
||||
};
|
||||
|
||||
export const getValuePalette = (value: ValueType): PaletteKey => {
|
||||
const paletteMap: Record<ValueType, PaletteKey> = {
|
||||
positive: "success",
|
||||
|
||||
@@ -70,6 +70,12 @@ const pushoverSchema = baseSchema.extend({
|
||||
accessToken: z.string().min(1, "App token is required"),
|
||||
});
|
||||
|
||||
const signalgridSchema = baseSchema.extend({
|
||||
type: z.literal("signalgrid"),
|
||||
address: z.string().min(1, "Channel is required"),
|
||||
accessToken: z.string().min(1, "Client key is required"),
|
||||
});
|
||||
|
||||
const twilioSchema = baseSchema.extend({
|
||||
type: z.literal("twilio"),
|
||||
accountSid: z.string().min(1, "Account SID is required"),
|
||||
@@ -95,6 +101,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
teamsSchema,
|
||||
telegramSchema,
|
||||
pushoverSchema,
|
||||
signalgridSchema,
|
||||
twilioSchema,
|
||||
ntfySchema,
|
||||
]);
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "الصيانة",
|
||||
"logs": "السجلات",
|
||||
"settings": "الإعدادات",
|
||||
"dashboard": "لوحة المعلومات",
|
||||
"groups": {
|
||||
"monitoring": "المراقبة",
|
||||
"alerting": "التنبيهات",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "لوحة المعلومات",
|
||||
"description": "اطّلع على كل أنواع المراقبات في مكان واحد ولاحظ ما يحتاج إلى انتباه."
|
||||
},
|
||||
"card": {
|
||||
"error": "تعذّر تحميل هذه البطاقة."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "حالة المراقبات",
|
||||
"empty": "لا توجد مراقبات بعد.",
|
||||
"upOfTotal": "من {{total}} تعمل"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "متوقفة حاليًا",
|
||||
"empty": "جميع المراقبات تعمل. لا توجد حوادث مفتوحة.",
|
||||
"count": "{{count}} تحتاج إلى انتباه"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "المراقبات حسب النوع",
|
||||
"empty": "لا توجد مراقبات بعد.",
|
||||
"down": "{{count}} متوقفة"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "الحوادث الأخيرة",
|
||||
"empty": "لا توجد حوادث — جميع الأنظمة سليمة.",
|
||||
"active": "{{count}} مفتوحة",
|
||||
"ongoing": "جارية",
|
||||
"manual": "يدوي",
|
||||
"unknownMonitor": "مراقبة غير معروفة"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "الفحوصات الفاشلة",
|
||||
"empty": "لا توجد فحوصات بعد.",
|
||||
"ratio": "{{failed}} من {{total}} في آخر يوم"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "خادم Checkmate",
|
||||
"memory": "الذاكرة",
|
||||
"eventLoop": "تأخير حلقة الأحداث",
|
||||
"uptime": "مدة التشغيل",
|
||||
"database": "قاعدة البيانات",
|
||||
"readsWrites": "{{reads}} قراءة/ث · {{writes}} كتابة/ث"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "أعلى {{shown}} من {{total}}",
|
||||
"notEnough": "لا توجد مراقبات كافية للترتيب."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "أبطأ المراقبات"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "أدنى وقت تشغيل"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "المراقبات حسب المجموعة",
|
||||
"empty": "لا توجد مجموعات بعد. تُحدَّد المجموعات لكل مراقبة."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "أكثر الخوادم انشغالًا",
|
||||
"empty": "لا توجد مراقبات بنية تحتية بعد.",
|
||||
"cpu": "المعالج",
|
||||
"memory": "الذاكرة",
|
||||
"disk": "القرص"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "إحصائيات الحوادث",
|
||||
"openNow": "مفتوحة الآن",
|
||||
"avgResolution": "متوسط وقت الحل",
|
||||
"hours": "{{hours}} س",
|
||||
"mostIncidents": "الأكثر حوادث",
|
||||
"total": "الإجمالي",
|
||||
"resolved": "تم حلها",
|
||||
"automatic": "تلقائي",
|
||||
"manual": "يدوي",
|
||||
"autoManual": "{{automatic}} تلقائي · {{manual}} يدوي"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "قنوات الإشعارات",
|
||||
"empty": "لا توجد قنوات بعد."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "نوافذ الصيانة",
|
||||
"empty": "لا توجد صيانة مجدولة.",
|
||||
"runningNow": "قيد التشغيل",
|
||||
"next": "النافذة التالية"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "صفحات الحالة",
|
||||
"empty": "لا توجد صفحات حالة بعد.",
|
||||
"monitorCount": "{{count}} مراقبة",
|
||||
"published": "منشورة",
|
||||
"draft": "مسودة"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "طابور الفحوصات",
|
||||
"healthy": "جميع المهام البالغ عددها {{jobs}} تسير وفق الجدول على {{workers}} عامل.",
|
||||
"failing": "المهام الفاشلة",
|
||||
"scheduled": "المهام المجدولة",
|
||||
"runningNow": "قيد التشغيل"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "الأخطاء الأخيرة",
|
||||
"empty": "لم تُسجَّل أخطاء.",
|
||||
"count": "{{count}} أخطاء",
|
||||
"warnings": "التحذيرات"
|
||||
},
|
||||
"team": {
|
||||
"title": "أعضاء الفريق",
|
||||
"members": "{{count}} عضو",
|
||||
"unverified": "غير مُوثَّقين"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "تحرير البطاقات",
|
||||
"title": "بطاقات لوحة المعلومات",
|
||||
"done": "تم",
|
||||
"reset": "إعادة التعيين إلى الافتراضي",
|
||||
"groups": {
|
||||
"uptime": "وقت التشغيل",
|
||||
"infrastructure": "البنية التحتية",
|
||||
"incidents": "الحوادث",
|
||||
"notifications": "الإشعارات",
|
||||
"maintenance": "الصيانة",
|
||||
"statusPages": "صفحات الحالة",
|
||||
"checks": "الفحوصات",
|
||||
"logs": "النظام",
|
||||
"settings": "الإعدادات"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "لا توجد بطاقات في لوحة المعلومات",
|
||||
"description": "اختر البطاقات التي تريد رؤيتها. يُحفظ اختيارك في هذا المتصفح."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "نقل لأعلى",
|
||||
"moveDown": "نقل لأسفل"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "يا إلهي! لقد أسقطت السوشي!",
|
||||
"subtitle": "إما أن الرابط غير موجود، أو ليس لديك صلاحية الوصول إليه."
|
||||
|
||||
@@ -190,7 +190,6 @@
|
||||
"maintenance": "Manteniment",
|
||||
"logs": "Registres",
|
||||
"settings": "Configuració",
|
||||
"dashboard": "Tauler",
|
||||
"groups": {
|
||||
"monitoring": "Monitoratge",
|
||||
"alerting": "Alertes",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Tauler",
|
||||
"description": "Vegeu cada tipus de monitor en un sol lloc i detecteu què necessita atenció."
|
||||
},
|
||||
"card": {
|
||||
"error": "No s'ha pogut carregar aquesta targeta."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Estat dels monitors",
|
||||
"empty": "Encara no hi ha monitors.",
|
||||
"upOfTotal": "de {{total}} actius"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Actualment caiguts",
|
||||
"empty": "Tots els monitors estan actius. Cap incident obert.",
|
||||
"count": "{{count}} requereixen atenció"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Monitors per tipus",
|
||||
"empty": "Encara no hi ha monitors.",
|
||||
"down": "{{count}} caiguts"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Incidents recents",
|
||||
"empty": "Cap incident — tots els sistemes sans.",
|
||||
"active": "{{count}} oberts",
|
||||
"ongoing": "En curs",
|
||||
"manual": "Manual",
|
||||
"unknownMonitor": "Monitor desconegut"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Comprovacions fallides",
|
||||
"empty": "Encara no hi ha comprovacions.",
|
||||
"ratio": "{{failed}} de {{total}} el darrer dia"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Servidor de Checkmate",
|
||||
"memory": "Memòria",
|
||||
"eventLoop": "Retard del bucle d'esdeveniments",
|
||||
"uptime": "Temps actiu",
|
||||
"database": "Base de dades",
|
||||
"readsWrites": "{{reads}} lectures/s · {{writes}} escriptures/s"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} de {{total}}",
|
||||
"notEnough": "No hi ha prou monitors per fer una classificació."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Monitors més lents"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Temps en línia més baix"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Monitors per grup",
|
||||
"empty": "Encara no hi ha grups. Els grups es defineixen per monitor."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Servidors més carregats",
|
||||
"empty": "Encara no hi ha monitors d'infraestructura.",
|
||||
"cpu": "CPU",
|
||||
"memory": "Memòria",
|
||||
"disk": "Disc"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Estadístiques d'incidents",
|
||||
"openNow": "Oberts ara",
|
||||
"avgResolution": "Temps mitjà de resolució",
|
||||
"hours": "{{hours}} h",
|
||||
"mostIncidents": "Més incidents",
|
||||
"total": "total",
|
||||
"resolved": "Resolts",
|
||||
"automatic": "Automàtica",
|
||||
"manual": "Manual",
|
||||
"autoManual": "{{automatic}} automàtiques · {{manual}} manuals"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Canals de notificació",
|
||||
"empty": "Encara no hi ha canals."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Finestres de manteniment",
|
||||
"empty": "Cap manteniment programat.",
|
||||
"runningNow": "En execució",
|
||||
"next": "Propera finestra"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Pàgines d'estat",
|
||||
"empty": "Encara no hi ha pàgines d'estat.",
|
||||
"monitorCount": "{{count}} monitors",
|
||||
"published": "Publicada",
|
||||
"draft": "Esborrany"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Cua de comprovacions",
|
||||
"healthy": "Les {{jobs}} tasques van segons el previst en {{workers}} worker(s).",
|
||||
"failing": "Tasques fallides",
|
||||
"scheduled": "Tasques programades",
|
||||
"runningNow": "En execució"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Errors recents",
|
||||
"empty": "Cap error registrat.",
|
||||
"count": "{{count}} errors",
|
||||
"warnings": "Advertiments"
|
||||
},
|
||||
"team": {
|
||||
"title": "Membres de l'equip",
|
||||
"members": "{{count}} membres",
|
||||
"unverified": "Sense verificar"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Edita les targetes",
|
||||
"title": "Targetes del tauler",
|
||||
"done": "Fet",
|
||||
"reset": "Restableix els valors predeterminats",
|
||||
"groups": {
|
||||
"uptime": "Temps en línia",
|
||||
"infrastructure": "Infraestructura",
|
||||
"incidents": "Incidents",
|
||||
"notifications": "Notificacions",
|
||||
"maintenance": "Manteniment",
|
||||
"statusPages": "Pàgines d'estat",
|
||||
"checks": "Comprovacions",
|
||||
"logs": "Sistema",
|
||||
"settings": "Configuració"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "No hi ha targetes al vostre tauler",
|
||||
"description": "Trieu les targetes que voleu veure. La vostra selecció es recorda en aquest navegador."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Mou amunt",
|
||||
"moveDown": "Mou avall"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Oh no! Has deixat caure el teu sushi!",
|
||||
"subtitle": "O bé la URL no existeix, o bé no tens accés."
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "Údržba",
|
||||
"logs": "Záznamy",
|
||||
"settings": "Nastavení",
|
||||
"dashboard": "Přehled",
|
||||
"groups": {
|
||||
"monitoring": "Monitorování",
|
||||
"alerting": "Upozornění",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Přehled",
|
||||
"description": "Zobrazte každý typ monitoru na jednom místě a poznejte, co vyžaduje pozornost."
|
||||
},
|
||||
"card": {
|
||||
"error": "Tuto kartu se nepodařilo načíst."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Stav monitorů",
|
||||
"empty": "Zatím žádné monitory.",
|
||||
"upOfTotal": "z {{total}} běží"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Aktuálně nedostupné",
|
||||
"empty": "Všechny monitory běží. Žádné otevřené incidenty.",
|
||||
"count": "{{count}} vyžaduje pozornost"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Monitory podle typu",
|
||||
"empty": "Zatím žádné monitory.",
|
||||
"down": "{{count}} nedostupných"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Nedávné incidenty",
|
||||
"empty": "Žádné incidenty — všechny systémy v pořádku.",
|
||||
"active": "{{count}} otevřených",
|
||||
"ongoing": "Probíhá",
|
||||
"manual": "Ručně",
|
||||
"unknownMonitor": "Neznámý monitor"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Neúspěšné kontroly",
|
||||
"empty": "Zatím žádné kontroly.",
|
||||
"ratio": "{{failed}} z {{total}} za poslední den"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Server Checkmate",
|
||||
"memory": "Paměť",
|
||||
"eventLoop": "Zpoždění event loop",
|
||||
"uptime": "Doba běhu",
|
||||
"database": "Databáze",
|
||||
"readsWrites": "{{reads}} čtení/s · {{writes}} zápisů/s"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} z {{total}}",
|
||||
"notEnough": "Zatím není dost monitorů pro žebříček."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Nejpomalejší monitory"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Nejnižší dostupnost"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Monitory podle skupiny",
|
||||
"empty": "Zatím žádné skupiny. Skupiny se nastavují u monitoru."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Nejvytíženější servery",
|
||||
"empty": "Zatím žádné monitory infrastruktury.",
|
||||
"cpu": "CPU",
|
||||
"memory": "Paměť",
|
||||
"disk": "Disk"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Statistiky incidentů",
|
||||
"openNow": "Nyní otevřené",
|
||||
"avgResolution": "Průměrná doba vyřešení",
|
||||
"hours": "{{hours}} h",
|
||||
"mostIncidents": "Nejvíce incidentů",
|
||||
"total": "celkem",
|
||||
"resolved": "Vyřešeno",
|
||||
"automatic": "Automaticky",
|
||||
"manual": "Ručně",
|
||||
"autoManual": "{{automatic}} automaticky · {{manual}} ručně"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Kanály oznámení",
|
||||
"empty": "Zatím žádné kanály."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Okna údržby",
|
||||
"empty": "Žádná naplánovaná údržba.",
|
||||
"runningNow": "Právě probíhá",
|
||||
"next": "Další okno"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Stavové stránky",
|
||||
"empty": "Zatím žádné stavové stránky.",
|
||||
"monitorCount": "{{count}} monitorů",
|
||||
"published": "Publikováno",
|
||||
"draft": "Koncept"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Fronta kontrol",
|
||||
"healthy": "Všech {{jobs}} úloh běží podle plánu na {{workers}} workerech.",
|
||||
"failing": "Selhávající úlohy",
|
||||
"scheduled": "Naplánované úlohy",
|
||||
"runningNow": "Právě probíhá"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Nedávné chyby",
|
||||
"empty": "Žádné zaznamenané chyby.",
|
||||
"count": "{{count}} chyb",
|
||||
"warnings": "Varování"
|
||||
},
|
||||
"team": {
|
||||
"title": "Členové týmu",
|
||||
"members": "{{count}} členů",
|
||||
"unverified": "Neověření"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Upravit karty",
|
||||
"title": "Karty přehledu",
|
||||
"done": "Hotovo",
|
||||
"reset": "Obnovit výchozí",
|
||||
"groups": {
|
||||
"uptime": "Dostupnost",
|
||||
"infrastructure": "Infrastruktura",
|
||||
"incidents": "Incidenty",
|
||||
"notifications": "Notifikace",
|
||||
"maintenance": "Údržba",
|
||||
"statusPages": "Stavové stránky",
|
||||
"checks": "Kontroly",
|
||||
"logs": "Systém",
|
||||
"settings": "Nastavení"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Ve vašem přehledu nejsou žádné karty",
|
||||
"description": "Vyberte karty, které chcete zobrazit. Váš výběr se uchová v tomto prohlížeči."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Posunout nahoru",
|
||||
"moveDown": "Posunout dolů"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Ale ne! Upustili jste své sushi!",
|
||||
"subtitle": "Buď tato URL neexistuje, nebo k ní nemáte přístup."
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "Wartung",
|
||||
"logs": "Protokolle",
|
||||
"settings": "Einstellungen",
|
||||
"dashboard": "Dashboard",
|
||||
"groups": {
|
||||
"monitoring": "Überwachung",
|
||||
"alerting": "Benachrichtigungen",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Dashboard",
|
||||
"description": "Sehen Sie jeden Monitortyp an einem Ort und erkennen Sie, was Aufmerksamkeit braucht."
|
||||
},
|
||||
"card": {
|
||||
"error": "Diese Karte konnte nicht geladen werden."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Monitor-Status",
|
||||
"empty": "Noch keine Monitore.",
|
||||
"upOfTotal": "von {{total}} online"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Derzeit offline",
|
||||
"empty": "Alle Monitore sind online. Keine offenen Vorfälle.",
|
||||
"count": "{{count}} benötigen Aufmerksamkeit"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Monitore nach Typ",
|
||||
"empty": "Noch keine Monitore.",
|
||||
"down": "{{count}} offline"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Letzte Vorfälle",
|
||||
"empty": "Keine Vorfälle — alle Systeme gesund.",
|
||||
"active": "{{count}} offen",
|
||||
"ongoing": "Laufend",
|
||||
"manual": "Manuell",
|
||||
"unknownMonitor": "Unbekannter Monitor"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Fehlgeschlagene Prüfungen",
|
||||
"empty": "Noch keine Prüfungen.",
|
||||
"ratio": "{{failed}} von {{total}} am letzten Tag"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Checkmate-Server",
|
||||
"memory": "Arbeitsspeicher",
|
||||
"eventLoop": "Event-Loop-Verzögerung",
|
||||
"uptime": "Laufzeit",
|
||||
"database": "Datenbank",
|
||||
"readsWrites": "{{reads}} Lesevorgänge/s · {{writes}} Schreibvorgänge/s"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} von {{total}}",
|
||||
"notEnough": "Noch nicht genug Monitore für ein Ranking."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Langsamste Monitore"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Niedrigste Uptime"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Monitore nach Gruppe",
|
||||
"empty": "Noch keine Gruppen. Gruppen werden pro Monitor gesetzt."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Ausgelastetste Server",
|
||||
"empty": "Noch keine Infrastruktur-Monitore.",
|
||||
"cpu": "CPU",
|
||||
"memory": "Arbeitsspeicher",
|
||||
"disk": "Festplatte"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Vorfallstatistik",
|
||||
"openNow": "Jetzt offen",
|
||||
"avgResolution": "Durchschnittliche Lösungszeit",
|
||||
"hours": "{{hours}} Std.",
|
||||
"mostIncidents": "Meiste Vorfälle",
|
||||
"total": "gesamt",
|
||||
"resolved": "Gelöst",
|
||||
"automatic": "Automatisch",
|
||||
"manual": "Manuell",
|
||||
"autoManual": "{{automatic}} automatisch · {{manual}} manuell"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Benachrichtigungskanäle",
|
||||
"empty": "Noch keine Kanäle."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Wartungsfenster",
|
||||
"empty": "Keine Wartung geplant.",
|
||||
"runningNow": "Läuft gerade",
|
||||
"next": "Nächstes Fenster"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Statusseiten",
|
||||
"empty": "Noch keine Statusseiten.",
|
||||
"monitorCount": "{{count}} Monitore",
|
||||
"published": "Veröffentlicht",
|
||||
"draft": "Entwurf"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Prüfungs-Warteschlange",
|
||||
"healthy": "Alle {{jobs}} Jobs planmäßig auf {{workers}} Worker(n).",
|
||||
"failing": "Fehlgeschlagene Jobs",
|
||||
"scheduled": "Geplante Jobs",
|
||||
"runningNow": "Läuft gerade"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Letzte Fehler",
|
||||
"empty": "Keine Fehler protokolliert.",
|
||||
"count": "{{count}} Fehler",
|
||||
"warnings": "Warnungen"
|
||||
},
|
||||
"team": {
|
||||
"title": "Teammitglieder",
|
||||
"members": "{{count}} Mitglieder",
|
||||
"unverified": "Nicht verifiziert"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Karten bearbeiten",
|
||||
"title": "Dashboard-Karten",
|
||||
"done": "Fertig",
|
||||
"reset": "Auf Standard zurücksetzen",
|
||||
"groups": {
|
||||
"uptime": "Uptime",
|
||||
"infrastructure": "Infrastruktur",
|
||||
"incidents": "Vorfälle",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"maintenance": "Wartung",
|
||||
"statusPages": "Statusseiten",
|
||||
"checks": "Prüfungen",
|
||||
"logs": "System",
|
||||
"settings": "Einstellungen"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Keine Karten auf Ihrem Dashboard",
|
||||
"description": "Wählen Sie die Karten, die Sie sehen möchten. Ihre Auswahl wird in diesem Browser gespeichert."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Nach oben",
|
||||
"moveDown": "Nach unten"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Oh nein! Sie haben Ihr Sushi fallen lassen!",
|
||||
"subtitle": "Entweder existiert die URL nicht oder Sie haben keinen Zugriff darauf."
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"menu": {
|
||||
"dashboard": "Dashboard",
|
||||
"uptime": "Uptime",
|
||||
"pagespeed": "Pagespeed",
|
||||
"infrastructure": "Infrastructure",
|
||||
@@ -219,7 +220,6 @@
|
||||
"maintenance": "Maintenance",
|
||||
"logs": "Logs",
|
||||
"settings": "Settings",
|
||||
"dashboard": "Dashboard",
|
||||
"groups": {
|
||||
"monitoring": "Monitoring",
|
||||
"alerting": "Alerting",
|
||||
@@ -376,6 +376,7 @@
|
||||
"picker": {
|
||||
"trigger": "Edit cards",
|
||||
"title": "Dashboard cards",
|
||||
"selectAll": "Select all",
|
||||
"done": "Done",
|
||||
"reset": "Reset to defaults",
|
||||
"groups": {
|
||||
@@ -1126,6 +1127,10 @@
|
||||
"title": "Log collection is off for this host",
|
||||
"description": "Turn on container logs in the monitor settings to start collecting.",
|
||||
"action": "Configure monitor"
|
||||
},
|
||||
"forbidden": {
|
||||
"title": "You don't have access to container logs",
|
||||
"description": "Only administrators can view logs. Ask an administrator for access if you need to see them."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1388,7 +1393,7 @@
|
||||
},
|
||||
"startTime": {
|
||||
"title": "Start time",
|
||||
"description": "Set the start time and duration for your maintenance window. All values are in UTC",
|
||||
"description": "Set the start time and duration for your maintenance window. All values are in your local time zone.",
|
||||
"option": {
|
||||
"duration": {
|
||||
"label": "Duration"
|
||||
@@ -1470,6 +1475,7 @@
|
||||
"teams": "Teams",
|
||||
"telegram": "Telegram",
|
||||
"pushover": "Pushover",
|
||||
"signalgrid": "Signalgrid",
|
||||
"twilio": "Twilio",
|
||||
"ntfy": "ntfy"
|
||||
}
|
||||
@@ -1490,6 +1496,14 @@
|
||||
"optionUserKey": "User key",
|
||||
"placeholderUserKey": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
|
||||
},
|
||||
"signalgrid": {
|
||||
"title": "Signalgrid configuration",
|
||||
"description": "Configure Signalgrid to receive push notifications on your devices.",
|
||||
"optionClientKey": "Client key",
|
||||
"placeholderClientKey": "Your Signalgrid client key",
|
||||
"optionChannel": "Channel",
|
||||
"placeholderChannel": "Your Signalgrid channel"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Twilio SMS configuration",
|
||||
"description": "Configure Twilio to send SMS notifications to a phone number.",
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "Mantenimiento",
|
||||
"logs": "Registros",
|
||||
"settings": "Configuración",
|
||||
"dashboard": "Panel",
|
||||
"groups": {
|
||||
"monitoring": "Monitorización",
|
||||
"alerting": "Alertas",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Panel",
|
||||
"description": "Vea cada tipo de monitor en un solo lugar y detecte lo que necesita atención."
|
||||
},
|
||||
"card": {
|
||||
"error": "No se pudo cargar esta tarjeta."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Estado de los monitores",
|
||||
"empty": "Aún no hay monitores.",
|
||||
"upOfTotal": "de {{total}} activos"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Caídos ahora",
|
||||
"empty": "Todos los monitores están activos. Sin incidentes abiertos.",
|
||||
"count": "{{count}} requieren atención"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Monitores por tipo",
|
||||
"empty": "Aún no hay monitores.",
|
||||
"down": "{{count}} caídos"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Incidentes recientes",
|
||||
"empty": "Sin incidentes: todos los sistemas están sanos.",
|
||||
"active": "{{count}} abiertos",
|
||||
"ongoing": "En curso",
|
||||
"manual": "Manual",
|
||||
"unknownMonitor": "Monitor desconocido"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Comprobaciones fallidas",
|
||||
"empty": "Aún no hay comprobaciones.",
|
||||
"ratio": "{{failed}} de {{total}} en el último día"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Servidor de Checkmate",
|
||||
"memory": "Memoria",
|
||||
"eventLoop": "Retardo del bucle de eventos",
|
||||
"uptime": "Tiempo activo",
|
||||
"database": "Base de datos",
|
||||
"readsWrites": "{{reads}} lecturas/s · {{writes}} escrituras/s"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} de {{total}}",
|
||||
"notEnough": "No hay suficientes monitores para clasificar."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Monitores más lentos"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Menor uptime"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Monitores por grupo",
|
||||
"empty": "Aún no hay grupos. Los grupos se definen por monitor."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Servidores más cargados",
|
||||
"empty": "Aún no hay monitores de infraestructura.",
|
||||
"cpu": "CPU",
|
||||
"memory": "Memoria",
|
||||
"disk": "Disco"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Estadísticas de incidentes",
|
||||
"openNow": "Abiertos ahora",
|
||||
"avgResolution": "Tiempo medio de resolución",
|
||||
"hours": "{{hours}} h",
|
||||
"mostIncidents": "Más incidentes",
|
||||
"total": "total",
|
||||
"resolved": "Resueltos",
|
||||
"automatic": "Automática",
|
||||
"manual": "Manual",
|
||||
"autoManual": "{{automatic}} automáticas · {{manual}} manuales"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Canales de notificación",
|
||||
"empty": "Aún no hay canales."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Ventanas de mantenimiento",
|
||||
"empty": "Sin mantenimiento programado.",
|
||||
"runningNow": "En ejecución",
|
||||
"next": "Próxima ventana"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Páginas de estado",
|
||||
"empty": "Aún no hay páginas de estado.",
|
||||
"monitorCount": "{{count}} monitores",
|
||||
"published": "Publicada",
|
||||
"draft": "Borrador"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Cola de comprobaciones",
|
||||
"healthy": "Los {{jobs}} trabajos van según lo previsto en {{workers}} worker(s).",
|
||||
"failing": "Trabajos fallidos",
|
||||
"scheduled": "Trabajos programados",
|
||||
"runningNow": "En ejecución"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Errores recientes",
|
||||
"empty": "No se registraron errores.",
|
||||
"count": "{{count}} errores",
|
||||
"warnings": "Advertencias"
|
||||
},
|
||||
"team": {
|
||||
"title": "Miembros del equipo",
|
||||
"members": "{{count}} miembros",
|
||||
"unverified": "Sin verificar"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Editar tarjetas",
|
||||
"title": "Tarjetas del panel",
|
||||
"done": "Listo",
|
||||
"reset": "Restablecer valores predeterminados",
|
||||
"groups": {
|
||||
"uptime": "Uptime",
|
||||
"infrastructure": "Infraestructura",
|
||||
"incidents": "Incidentes",
|
||||
"notifications": "Notificaciones",
|
||||
"maintenance": "Mantenimiento",
|
||||
"statusPages": "Páginas de estado",
|
||||
"checks": "Comprobaciones",
|
||||
"logs": "Sistema",
|
||||
"settings": "Configuración"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "No hay tarjetas en tu panel",
|
||||
"description": "Elige las tarjetas que quieres ver. Tu selección se recuerda en este navegador."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Mover arriba",
|
||||
"moveDown": "Mover abajo"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "¡Oh no! ¡Se le cayó el sushi!",
|
||||
"subtitle": "La URL no existe o no tiene acceso a ella."
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "Huolto",
|
||||
"logs": "Lokit",
|
||||
"settings": "Asetukset",
|
||||
"dashboard": "Kojelauta",
|
||||
"groups": {
|
||||
"monitoring": "Valvonta",
|
||||
"alerting": "Hälytykset",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Kojelauta",
|
||||
"description": "Näe kaikki monitorityypit yhdessä paikassa ja huomaa huomiota vaativat asiat."
|
||||
},
|
||||
"card": {
|
||||
"error": "Tätä korttia ei voitu ladata."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Monitorien tila",
|
||||
"empty": "Ei vielä monitoreja.",
|
||||
"upOfTotal": "/ {{total}} käynnissä"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Tällä hetkellä alhaalla",
|
||||
"empty": "Kaikki monitorit ovat käynnissä. Ei avoimia tapauksia.",
|
||||
"count": "{{count}} vaatii huomiota"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Monitorit tyypeittäin",
|
||||
"empty": "Ei vielä monitoreja.",
|
||||
"down": "{{count}} alhaalla"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Viimeisimmät tapaukset",
|
||||
"empty": "Ei tapauksia — kaikki järjestelmät kunnossa.",
|
||||
"active": "{{count}} avoinna",
|
||||
"ongoing": "Käynnissä",
|
||||
"manual": "Manuaalinen",
|
||||
"unknownMonitor": "Tuntematon monitori"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Epäonnistuneet tarkistukset",
|
||||
"empty": "Ei vielä tarkistuksia.",
|
||||
"ratio": "{{failed}} / {{total}} viimeisen vuorokauden aikana"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Checkmate-palvelin",
|
||||
"memory": "Muisti",
|
||||
"eventLoop": "Tapahtumasilmukan viive",
|
||||
"uptime": "Käyntiaika",
|
||||
"database": "Tietokanta",
|
||||
"readsWrites": "{{reads}} lukua/s · {{writes}} kirjoitusta/s"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} / {{total}}",
|
||||
"notEnough": "Ei tarpeeksi monitoreja järjestykseen."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Hitaimmat monitorit"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Alhaisin käytettävyys"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Monitorit ryhmittäin",
|
||||
"empty": "Ei vielä ryhmiä. Ryhmät asetetaan monitorikohtaisesti."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Kuormitetuimmat palvelimet",
|
||||
"empty": "Ei vielä infrastruktuurimonitoreja.",
|
||||
"cpu": "Suoritin",
|
||||
"memory": "Muisti",
|
||||
"disk": "Levy"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Tapaustilastot",
|
||||
"openNow": "Avoinna nyt",
|
||||
"avgResolution": "Keskimääräinen ratkaisuaika",
|
||||
"hours": "{{hours}} t",
|
||||
"mostIncidents": "Eniten tapauksia",
|
||||
"total": "yhteensä",
|
||||
"resolved": "Ratkaistu",
|
||||
"automatic": "Automaattinen",
|
||||
"manual": "Manuaalinen",
|
||||
"autoManual": "{{automatic}} automaattista · {{manual}} manuaalista"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Ilmoituskanavat",
|
||||
"empty": "Ei vielä kanavia."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Huoltoikkunat",
|
||||
"empty": "Ei suunniteltua huoltoa.",
|
||||
"runningNow": "Käynnissä nyt",
|
||||
"next": "Seuraava ikkuna"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Tilasivut",
|
||||
"empty": "Ei vielä tilasivuja.",
|
||||
"monitorCount": "{{count}} monitoria",
|
||||
"published": "Julkaistu",
|
||||
"draft": "Luonnos"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Tarkistusjono",
|
||||
"healthy": "Kaikki {{jobs}} työtä aikataulussa {{workers}} työntekijällä.",
|
||||
"failing": "Epäonnistuvat työt",
|
||||
"scheduled": "Ajastetut työt",
|
||||
"runningNow": "Käynnissä nyt"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Viimeisimmät virheet",
|
||||
"empty": "Ei kirjattuja virheitä.",
|
||||
"count": "{{count}} virhettä",
|
||||
"warnings": "Varoitukset"
|
||||
},
|
||||
"team": {
|
||||
"title": "Tiimin jäsenet",
|
||||
"members": "{{count}} jäsentä",
|
||||
"unverified": "Vahvistamattomat"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Muokkaa kortteja",
|
||||
"title": "Kojelaudan kortit",
|
||||
"done": "Valmis",
|
||||
"reset": "Palauta oletukset",
|
||||
"groups": {
|
||||
"uptime": "Käytettävyys",
|
||||
"infrastructure": "Infrastruktuuri",
|
||||
"incidents": "Tapaukset",
|
||||
"notifications": "Ilmoitukset",
|
||||
"maintenance": "Huolto",
|
||||
"statusPages": "Tilasivut",
|
||||
"checks": "Tarkistukset",
|
||||
"logs": "Järjestelmä",
|
||||
"settings": "Asetukset"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Kojelaudallasi ei ole kortteja",
|
||||
"description": "Valitse näytettävät kortit. Valintasi muistetaan tässä selaimessa."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Siirrä ylös",
|
||||
"moveDown": "Siirrä alas"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Voi ei! Sushisi putosi!",
|
||||
"subtitle": "Joko URL-osoitetta ei ole olemassa tai sinulla ei ole pääsyä siihen."
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "Maintenance",
|
||||
"logs": "Journaux",
|
||||
"settings": "Paramètres",
|
||||
"dashboard": "Tableau de bord",
|
||||
"groups": {
|
||||
"monitoring": "Surveillance",
|
||||
"alerting": "Alertes",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Tableau de bord",
|
||||
"description": "Voyez chaque type de moniteur au même endroit et repérez ce qui demande votre attention."
|
||||
},
|
||||
"card": {
|
||||
"error": "Impossible de charger cette carte."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "État des moniteurs",
|
||||
"empty": "Aucun moniteur pour l'instant.",
|
||||
"upOfTotal": "sur {{total}} en ligne"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Actuellement hors ligne",
|
||||
"empty": "Tous les moniteurs sont en ligne. Aucun incident ouvert.",
|
||||
"count": "{{count}} à surveiller"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Moniteurs par type",
|
||||
"empty": "Aucun moniteur pour l'instant.",
|
||||
"down": "{{count}} hors ligne"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Incidents récents",
|
||||
"empty": "Aucun incident — tous les systèmes sont sains.",
|
||||
"active": "{{count}} ouverts",
|
||||
"ongoing": "En cours",
|
||||
"manual": "Manuel",
|
||||
"unknownMonitor": "Moniteur inconnu"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Vérifications échouées",
|
||||
"empty": "Aucune vérification pour l'instant.",
|
||||
"ratio": "{{failed}} sur {{total}} le dernier jour"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Serveur Checkmate",
|
||||
"memory": "Mémoire",
|
||||
"eventLoop": "Latence de la boucle d'événements",
|
||||
"uptime": "Durée de fonctionnement",
|
||||
"database": "Base de données",
|
||||
"readsWrites": "{{reads}} lectures/s · {{writes}} écritures/s"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} sur {{total}}",
|
||||
"notEnough": "Pas assez de moniteurs pour établir un classement."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Moniteurs les plus lents"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Disponibilité la plus faible"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Moniteurs par groupe",
|
||||
"empty": "Aucun groupe pour l'instant. Les groupes se définissent par moniteur."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Serveurs les plus chargés",
|
||||
"empty": "Aucun moniteur d'infrastructure pour l'instant.",
|
||||
"cpu": "Processeur",
|
||||
"memory": "Mémoire",
|
||||
"disk": "Disque"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Statistiques d'incidents",
|
||||
"openNow": "Ouverts maintenant",
|
||||
"avgResolution": "Temps moyen de résolution",
|
||||
"hours": "{{hours}} h",
|
||||
"mostIncidents": "Le plus d'incidents",
|
||||
"total": "total",
|
||||
"resolved": "Résolus",
|
||||
"automatic": "Automatique",
|
||||
"manual": "Manuel",
|
||||
"autoManual": "{{automatic}} automatiques · {{manual}} manuels"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Canaux de notification",
|
||||
"empty": "Aucun canal pour l'instant."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Fenêtres de maintenance",
|
||||
"empty": "Aucune maintenance planifiée.",
|
||||
"runningNow": "En cours",
|
||||
"next": "Prochaine fenêtre"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Pages de statut",
|
||||
"empty": "Aucune page de statut pour l'instant.",
|
||||
"monitorCount": "{{count}} moniteurs",
|
||||
"published": "Publiée",
|
||||
"draft": "Brouillon"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "File de vérifications",
|
||||
"healthy": "Les {{jobs}} tâches sont à l'heure sur {{workers}} worker(s).",
|
||||
"failing": "Tâches en échec",
|
||||
"scheduled": "Tâches planifiées",
|
||||
"runningNow": "En cours"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Erreurs récentes",
|
||||
"empty": "Aucune erreur enregistrée.",
|
||||
"count": "{{count}} erreurs",
|
||||
"warnings": "Avertissements"
|
||||
},
|
||||
"team": {
|
||||
"title": "Membres de l'équipe",
|
||||
"members": "{{count}} membres",
|
||||
"unverified": "Non vérifiés"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Modifier les cartes",
|
||||
"title": "Cartes du tableau de bord",
|
||||
"done": "Terminé",
|
||||
"reset": "Réinitialiser par défaut",
|
||||
"groups": {
|
||||
"uptime": "Disponibilité",
|
||||
"infrastructure": "Infrastructure",
|
||||
"incidents": "Incidents",
|
||||
"notifications": "Notifications",
|
||||
"maintenance": "Maintenance",
|
||||
"statusPages": "Pages de statut",
|
||||
"checks": "Vérifications",
|
||||
"logs": "Système",
|
||||
"settings": "Paramètres"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Aucune carte sur votre tableau de bord",
|
||||
"description": "Choisissez les cartes à afficher. Votre sélection est mémorisée dans ce navigateur."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Monter",
|
||||
"moveDown": "Descendre"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Oh non ! Vous avez fait tomber vos sushis !",
|
||||
"subtitle": "Soit l'URL n'existe pas, soit vous n'y avez pas accès."
|
||||
|
||||
@@ -194,7 +194,6 @@
|
||||
"maintenance": "Manutenzione",
|
||||
"logs": "Log",
|
||||
"settings": "Impostazioni",
|
||||
"dashboard": "Dashboard",
|
||||
"groups": {
|
||||
"monitoring": "Monitoraggio",
|
||||
"alerting": "Avvisi",
|
||||
@@ -234,146 +233,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Dashboard",
|
||||
"description": "Vedi ogni tipo di monitor in un unico posto e individua ciò che richiede attenzione."
|
||||
},
|
||||
"card": {
|
||||
"error": "Impossibile caricare questa scheda."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Stato dei monitor",
|
||||
"empty": "Nessun monitor ancora.",
|
||||
"upOfTotal": "su {{total}} attivi"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Attualmente offline",
|
||||
"empty": "Tutti i monitor sono attivi. Nessun incidente aperto.",
|
||||
"count": "{{count}} richiedono attenzione"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Monitor per tipo",
|
||||
"empty": "Nessun monitor ancora.",
|
||||
"down": "{{count}} offline"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Incidenti recenti",
|
||||
"empty": "Nessun incidente — tutti i sistemi sono sani.",
|
||||
"active": "{{count}} aperti",
|
||||
"ongoing": "In corso",
|
||||
"manual": "Manuale",
|
||||
"unknownMonitor": "Monitor sconosciuto"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Verifiche fallite",
|
||||
"empty": "Nessuna verifica ancora.",
|
||||
"ratio": "{{failed}} su {{total}} nell'ultimo giorno"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Server Checkmate",
|
||||
"memory": "Memoria",
|
||||
"eventLoop": "Ritardo dell'event loop",
|
||||
"uptime": "Tempo di attività",
|
||||
"database": "Database",
|
||||
"readsWrites": "{{reads}} letture/s · {{writes}} scritture/s"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} su {{total}}",
|
||||
"notEnough": "Non ci sono abbastanza monitor per una classifica."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Monitor più lenti"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Uptime più basso"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Monitor per gruppo",
|
||||
"empty": "Nessun gruppo ancora. I gruppi si impostano per monitor."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Server più carichi",
|
||||
"empty": "Nessun monitor infrastruttura ancora.",
|
||||
"cpu": "CPU",
|
||||
"memory": "Memoria",
|
||||
"disk": "Disco"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Statistiche incidenti",
|
||||
"openNow": "Aperti ora",
|
||||
"avgResolution": "Tempo medio di risoluzione",
|
||||
"hours": "{{hours}} h",
|
||||
"mostIncidents": "Più incidenti",
|
||||
"total": "totale",
|
||||
"resolved": "Risolti",
|
||||
"automatic": "Automatica",
|
||||
"manual": "Manuale",
|
||||
"autoManual": "{{automatic}} automatiche · {{manual}} manuali"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Canali di notifica",
|
||||
"empty": "Nessun canale ancora."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Finestre di manutenzione",
|
||||
"empty": "Nessuna manutenzione pianificata.",
|
||||
"runningNow": "In esecuzione",
|
||||
"next": "Prossima finestra"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Pagine di stato",
|
||||
"empty": "Nessuna pagina di stato ancora.",
|
||||
"monitorCount": "{{count}} monitor",
|
||||
"published": "Pubblicata",
|
||||
"draft": "Bozza"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Coda delle verifiche",
|
||||
"healthy": "Tutti i {{jobs}} job sono in orario su {{workers}} worker.",
|
||||
"failing": "Job falliti",
|
||||
"scheduled": "Job pianificati",
|
||||
"runningNow": "In esecuzione"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Errori recenti",
|
||||
"empty": "Nessun errore registrato.",
|
||||
"count": "{{count}} errori",
|
||||
"warnings": "Avvisi"
|
||||
},
|
||||
"team": {
|
||||
"title": "Membri del team",
|
||||
"members": "{{count}} membri",
|
||||
"unverified": "Non verificati"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Modifica schede",
|
||||
"title": "Schede della dashboard",
|
||||
"done": "Fatto",
|
||||
"reset": "Ripristina predefiniti",
|
||||
"groups": {
|
||||
"uptime": "Uptime",
|
||||
"infrastructure": "Infrastruttura",
|
||||
"incidents": "Incidenti",
|
||||
"notifications": "Notifiche",
|
||||
"maintenance": "Manutenzione",
|
||||
"statusPages": "Pagine di stato",
|
||||
"checks": "Verifiche",
|
||||
"logs": "Sistema",
|
||||
"settings": "Impostazioni"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Nessuna scheda nella tua dashboard",
|
||||
"description": "Scegli le schede da mostrare. La tua selezione viene ricordata in questo browser."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Sposta su",
|
||||
"moveDown": "Sposta giù"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Oh no! Hai fatto cadere il tuo sushi!",
|
||||
"subtitle": "L'URL non esiste, oppure non hai accesso."
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "メンテナンス",
|
||||
"logs": "ログ",
|
||||
"settings": "設定",
|
||||
"dashboard": "ダッシュボード",
|
||||
"groups": {
|
||||
"monitoring": "監視",
|
||||
"alerting": "アラート",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "ダッシュボード",
|
||||
"description": "すべての監視タイプを一か所で確認し、対応が必要な箇所を把握できます。"
|
||||
},
|
||||
"card": {
|
||||
"error": "このカードを読み込めませんでした。"
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "監視の状態",
|
||||
"empty": "監視はまだありません。",
|
||||
"upOfTotal": "/ {{total}} が稼働中"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "現在ダウン中",
|
||||
"empty": "すべての監視が稼働中です。未解決のインシデントはありません。",
|
||||
"count": "{{count}} 件が要対応"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "種類別の監視",
|
||||
"empty": "監視はまだありません。",
|
||||
"down": "{{count}} 件ダウン"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "最近のインシデント",
|
||||
"empty": "インシデントなし — すべて正常です。",
|
||||
"active": "{{count}} 件未解決",
|
||||
"ongoing": "進行中",
|
||||
"manual": "手動",
|
||||
"unknownMonitor": "不明な監視"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "失敗したチェック",
|
||||
"empty": "チェックはまだありません。",
|
||||
"ratio": "直近1日で {{total}} 件中 {{failed}} 件"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Checkmate サーバー",
|
||||
"memory": "メモリ",
|
||||
"eventLoop": "イベントループ遅延",
|
||||
"uptime": "稼働時間",
|
||||
"database": "データベース",
|
||||
"readsWrites": "{{reads}} 読み取り/秒 · {{writes}} 書き込み/秒"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "{{total}} 件中の上位 {{shown}} 件",
|
||||
"notEnough": "ランキングに十分な監視がありません。"
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "最も遅い監視"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "稼働率が最も低い監視"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "グループ別の監視",
|
||||
"empty": "グループはまだありません。グループは監視ごとに設定します。"
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "最も負荷の高いサーバー",
|
||||
"empty": "インフラ監視はまだありません。",
|
||||
"cpu": "CPU",
|
||||
"memory": "メモリ",
|
||||
"disk": "ディスク"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "インシデント統計",
|
||||
"openNow": "現在未解決",
|
||||
"avgResolution": "平均解決時間",
|
||||
"hours": "{{hours}} 時間",
|
||||
"mostIncidents": "インシデント最多",
|
||||
"total": "合計",
|
||||
"resolved": "解決済み",
|
||||
"automatic": "自動",
|
||||
"manual": "手動",
|
||||
"autoManual": "自動 {{automatic}} 件 · 手動 {{manual}} 件"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "通知チャンネル",
|
||||
"empty": "チャンネルはまだありません。"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "メンテナンスウィンドウ",
|
||||
"empty": "予定されたメンテナンスはありません。",
|
||||
"runningNow": "実行中",
|
||||
"next": "次のウィンドウ"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "ステータスページ",
|
||||
"empty": "ステータスページはまだありません。",
|
||||
"monitorCount": "{{count}} 件の監視",
|
||||
"published": "公開中",
|
||||
"draft": "下書き"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "チェックキュー",
|
||||
"healthy": "{{workers}} 個のワーカーで {{jobs}} 件のジョブがすべて予定どおりです。",
|
||||
"failing": "失敗中のジョブ",
|
||||
"scheduled": "予定されたジョブ",
|
||||
"runningNow": "実行中"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "最近のエラー",
|
||||
"empty": "エラーは記録されていません。",
|
||||
"count": "{{count}} 件のエラー",
|
||||
"warnings": "警告"
|
||||
},
|
||||
"team": {
|
||||
"title": "チームメンバー",
|
||||
"members": "{{count}} 人のメンバー",
|
||||
"unverified": "未確認"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "カードを編集",
|
||||
"title": "ダッシュボードのカード",
|
||||
"done": "完了",
|
||||
"reset": "既定に戻す",
|
||||
"groups": {
|
||||
"uptime": "稼働監視",
|
||||
"infrastructure": "インフラ",
|
||||
"incidents": "インシデント",
|
||||
"notifications": "通知",
|
||||
"maintenance": "メンテナンス",
|
||||
"statusPages": "ステータスページ",
|
||||
"checks": "チェック",
|
||||
"logs": "システム",
|
||||
"settings": "設定"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "ダッシュボードにカードがありません",
|
||||
"description": "表示したいカードを選択してください。選択内容はこのブラウザーに保存されます。"
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "上へ移動",
|
||||
"moveDown": "下へ移動"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "おっと!お寿司を落としてしまいました!",
|
||||
"subtitle": "URLが存在しないか、アクセス権限がありません。"
|
||||
|
||||
@@ -215,7 +215,6 @@
|
||||
"statusPages": "Strony statusu",
|
||||
"tags": "Tagi",
|
||||
"uptime": "Czas działania",
|
||||
"dashboard": "Pulpit",
|
||||
"groups": {
|
||||
"monitoring": "Monitorowanie",
|
||||
"alerting": "Alerty",
|
||||
@@ -234,146 +233,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Pulpit",
|
||||
"description": "Zobacz każdy typ monitora w jednym miejscu i dostrzeż, co wymaga uwagi."
|
||||
},
|
||||
"card": {
|
||||
"error": "Nie udało się załadować tej karty."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Stan monitorów",
|
||||
"empty": "Brak monitorów.",
|
||||
"upOfTotal": "z {{total}} działa"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Obecnie niedostępne",
|
||||
"empty": "Wszystkie monitory działają. Brak otwartych incydentów.",
|
||||
"count": "{{count}} wymaga uwagi"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Monitory według typu",
|
||||
"empty": "Brak monitorów.",
|
||||
"down": "{{count}} niedostępnych"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Ostatnie incydenty",
|
||||
"empty": "Brak incydentów — wszystkie systemy sprawne.",
|
||||
"active": "{{count}} otwartych",
|
||||
"ongoing": "Trwa",
|
||||
"manual": "Ręcznie",
|
||||
"unknownMonitor": "Nieznany monitor"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Nieudane checki",
|
||||
"empty": "Brak checków.",
|
||||
"ratio": "{{failed}} z {{total}} w ostatnim dniu"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Serwer Checkmate",
|
||||
"memory": "Pamięć",
|
||||
"eventLoop": "Opóźnienie pętli zdarzeń",
|
||||
"uptime": "Czas działania",
|
||||
"database": "Baza danych",
|
||||
"readsWrites": "{{reads}} odczytów/s · {{writes}} zapisów/s"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} z {{total}}",
|
||||
"notEnough": "Za mało monitorów, aby utworzyć ranking."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Najwolniejsze monitory"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Najniższa dostępność"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Monitory według grupy",
|
||||
"empty": "Brak grup. Grupy ustawia się przy monitorze."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Najbardziej obciążone serwery",
|
||||
"empty": "Brak monitorów infrastruktury.",
|
||||
"cpu": "CPU",
|
||||
"memory": "Pamięć",
|
||||
"disk": "Dysk"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Statystyki incydentów",
|
||||
"openNow": "Otwarte teraz",
|
||||
"avgResolution": "Średni czas rozwiązania",
|
||||
"hours": "{{hours}} godz.",
|
||||
"mostIncidents": "Najwięcej incydentów",
|
||||
"total": "łącznie",
|
||||
"resolved": "Rozwiązane",
|
||||
"automatic": "Automatycznie",
|
||||
"manual": "Ręcznie",
|
||||
"autoManual": "{{automatic}} automatycznie · {{manual}} ręcznie"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Kanały powiadomień",
|
||||
"empty": "Brak kanałów."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Przerwy techniczne",
|
||||
"empty": "Brak zaplanowanych przerw.",
|
||||
"runningNow": "Trwa teraz",
|
||||
"next": "Następna przerwa"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Strony statusu",
|
||||
"empty": "Brak stron statusu.",
|
||||
"monitorCount": "{{count}} monitorów",
|
||||
"published": "Opublikowana",
|
||||
"draft": "Wersja robocza"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Kolejka checków",
|
||||
"healthy": "Wszystkie {{jobs}} zadania zgodnie z planem na {{workers}} workerach.",
|
||||
"failing": "Nieudane zadania",
|
||||
"scheduled": "Zaplanowane zadania",
|
||||
"runningNow": "Trwa teraz"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Ostatnie błędy",
|
||||
"empty": "Nie zarejestrowano błędów.",
|
||||
"count": "{{count}} błędów",
|
||||
"warnings": "Ostrzeżenia"
|
||||
},
|
||||
"team": {
|
||||
"title": "Członkowie zespołu",
|
||||
"members": "{{count}} członków",
|
||||
"unverified": "Niezweryfikowani"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Edytuj karty",
|
||||
"title": "Karty pulpitu",
|
||||
"done": "Gotowe",
|
||||
"reset": "Przywróć domyślne",
|
||||
"groups": {
|
||||
"uptime": "Dostępność",
|
||||
"infrastructure": "Infrastruktura",
|
||||
"incidents": "Incydenty",
|
||||
"notifications": "Powiadomienia",
|
||||
"maintenance": "Przerwy techniczne",
|
||||
"statusPages": "Strony statusu",
|
||||
"checks": "Checki",
|
||||
"logs": "System",
|
||||
"settings": "Ustawienia"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Brak kart na pulpicie",
|
||||
"description": "Wybierz karty, które chcesz widzieć. Twój wybór zostanie zapamiętany w tej przeglądarce."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Przenieś w górę",
|
||||
"moveDown": "Przenieś w dół"
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"form": {
|
||||
"currentPassword": {
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "Manutenção",
|
||||
"logs": "Registros",
|
||||
"settings": "Configurações",
|
||||
"dashboard": "Painel",
|
||||
"groups": {
|
||||
"monitoring": "Monitoramento",
|
||||
"alerting": "Alertas",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Painel",
|
||||
"description": "Veja cada tipo de monitor em um só lugar e identifique o que precisa de atenção."
|
||||
},
|
||||
"card": {
|
||||
"error": "Não foi possível carregar este cartão."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Status dos monitores",
|
||||
"empty": "Ainda não há monitores.",
|
||||
"upOfTotal": "de {{total}} no ar"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Fora do ar agora",
|
||||
"empty": "Todos os monitores estão no ar. Nenhum incidente aberto.",
|
||||
"count": "{{count}} precisam de atenção"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Monitores por tipo",
|
||||
"empty": "Ainda não há monitores.",
|
||||
"down": "{{count}} fora do ar"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Incidentes recentes",
|
||||
"empty": "Nenhum incidente — todos os sistemas saudáveis.",
|
||||
"active": "{{count}} abertos",
|
||||
"ongoing": "Em andamento",
|
||||
"manual": "Manual",
|
||||
"unknownMonitor": "Monitor desconhecido"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Verificações com falha",
|
||||
"empty": "Ainda não há verificações.",
|
||||
"ratio": "{{failed}} de {{total}} no último dia"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Servidor Checkmate",
|
||||
"memory": "Memória",
|
||||
"eventLoop": "Atraso do event loop",
|
||||
"uptime": "Tempo no ar",
|
||||
"database": "Banco de dados",
|
||||
"readsWrites": "{{reads}} leituras/s · {{writes}} gravações/s"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} de {{total}}",
|
||||
"notEnough": "Não há monitores suficientes para classificar."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Monitores mais lentos"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Menor uptime"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Monitores por grupo",
|
||||
"empty": "Ainda não há grupos. Grupos são definidos por monitor."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Servidores mais ocupados",
|
||||
"empty": "Ainda não há monitores de infraestrutura.",
|
||||
"cpu": "CPU",
|
||||
"memory": "Memória",
|
||||
"disk": "Disco"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Estatísticas de incidentes",
|
||||
"openNow": "Abertos agora",
|
||||
"avgResolution": "Tempo médio de resolução",
|
||||
"hours": "{{hours}} h",
|
||||
"mostIncidents": "Mais incidentes",
|
||||
"total": "total",
|
||||
"resolved": "Resolvidos",
|
||||
"automatic": "Automática",
|
||||
"manual": "Manual",
|
||||
"autoManual": "{{automatic}} automáticas · {{manual}} manuais"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Canais de notificação",
|
||||
"empty": "Ainda não há canais."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Janelas de manutenção",
|
||||
"empty": "Nenhuma manutenção agendada.",
|
||||
"runningNow": "Em execução",
|
||||
"next": "Próxima janela"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Páginas de status",
|
||||
"empty": "Ainda não há páginas de status.",
|
||||
"monitorCount": "{{count}} monitores",
|
||||
"published": "Publicada",
|
||||
"draft": "Rascunho"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Fila de verificações",
|
||||
"healthy": "Todos os {{jobs}} jobs em dia em {{workers}} worker(s).",
|
||||
"failing": "Jobs com falha",
|
||||
"scheduled": "Jobs agendados",
|
||||
"runningNow": "Em execução"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Erros recentes",
|
||||
"empty": "Nenhum erro registrado.",
|
||||
"count": "{{count}} erros",
|
||||
"warnings": "Avisos"
|
||||
},
|
||||
"team": {
|
||||
"title": "Membros da equipe",
|
||||
"members": "{{count}} membros",
|
||||
"unverified": "Não verificados"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Editar cartões",
|
||||
"title": "Cartões do painel",
|
||||
"done": "Concluído",
|
||||
"reset": "Restaurar padrões",
|
||||
"groups": {
|
||||
"uptime": "Uptime",
|
||||
"infrastructure": "Infraestrutura",
|
||||
"incidents": "Incidentes",
|
||||
"notifications": "Notificações",
|
||||
"maintenance": "Manutenção",
|
||||
"statusPages": "Páginas de status",
|
||||
"checks": "Verificações",
|
||||
"logs": "Sistema",
|
||||
"settings": "Configurações"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Nenhum cartão no seu painel",
|
||||
"description": "Escolha os cartões que deseja ver. Sua seleção é lembrada neste navegador."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Mover para cima",
|
||||
"moveDown": "Mover para baixo"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Oh não! Você derrubou seu sushi!",
|
||||
"subtitle": "A URL não existe ou você não tem acesso a ela."
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "Обслуживание",
|
||||
"logs": "Логи",
|
||||
"settings": "Настройки",
|
||||
"dashboard": "Панель",
|
||||
"groups": {
|
||||
"monitoring": "Мониторинг",
|
||||
"alerting": "Оповещения",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Панель",
|
||||
"description": "Смотрите все типы мониторов в одном месте и замечайте требующее внимания."
|
||||
},
|
||||
"card": {
|
||||
"error": "Не удалось загрузить эту карточку."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Состояние мониторов",
|
||||
"empty": "Мониторов пока нет.",
|
||||
"upOfTotal": "из {{total}} работают"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Сейчас недоступны",
|
||||
"empty": "Все мониторы работают. Открытых инцидентов нет.",
|
||||
"count": "{{count}} требуют внимания"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Мониторы по типу",
|
||||
"empty": "Мониторов пока нет.",
|
||||
"down": "{{count}} недоступны"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Последние инциденты",
|
||||
"empty": "Инцидентов нет — все системы в порядке.",
|
||||
"active": "{{count}} открыто",
|
||||
"ongoing": "В процессе",
|
||||
"manual": "Вручную",
|
||||
"unknownMonitor": "Неизвестный монитор"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Неудачные проверки",
|
||||
"empty": "Проверок пока нет.",
|
||||
"ratio": "{{failed}} из {{total}} за последние сутки"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Сервер Checkmate",
|
||||
"memory": "Память",
|
||||
"eventLoop": "Задержка event loop",
|
||||
"uptime": "Время работы",
|
||||
"database": "База данных",
|
||||
"readsWrites": "{{reads}} чтений/с · {{writes}} записей/с"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Топ {{shown}} из {{total}}",
|
||||
"notEnough": "Недостаточно мониторов для рейтинга."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Самые медленные мониторы"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Наименьшая работоспособность"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Мониторы по группе",
|
||||
"empty": "Групп пока нет. Группы задаются у монитора."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Самые загруженные серверы",
|
||||
"empty": "Инфраструктурных мониторов пока нет.",
|
||||
"cpu": "ЦП",
|
||||
"memory": "Память",
|
||||
"disk": "Диск"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Статистика инцидентов",
|
||||
"openNow": "Открыто сейчас",
|
||||
"avgResolution": "Среднее время решения",
|
||||
"hours": "{{hours}} ч",
|
||||
"mostIncidents": "Больше всего инцидентов",
|
||||
"total": "всего",
|
||||
"resolved": "Решено",
|
||||
"automatic": "Автоматически",
|
||||
"manual": "Вручную",
|
||||
"autoManual": "{{automatic}} автоматически · {{manual}} вручную"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Каналы уведомлений",
|
||||
"empty": "Каналов пока нет."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Окна обслуживания",
|
||||
"empty": "Обслуживание не запланировано.",
|
||||
"runningNow": "Выполняется",
|
||||
"next": "Следующее окно"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Страницы состояния",
|
||||
"empty": "Страниц состояния пока нет.",
|
||||
"monitorCount": "{{count}} мониторов",
|
||||
"published": "Опубликована",
|
||||
"draft": "Черновик"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Очередь проверок",
|
||||
"healthy": "Все {{jobs}} задач идут по расписанию на {{workers}} воркерах.",
|
||||
"failing": "Сбойные задачи",
|
||||
"scheduled": "Запланированные задачи",
|
||||
"runningNow": "Выполняется"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Последние ошибки",
|
||||
"empty": "Ошибок не зафиксировано.",
|
||||
"count": "{{count}} ошибок",
|
||||
"warnings": "Предупреждения"
|
||||
},
|
||||
"team": {
|
||||
"title": "Участники команды",
|
||||
"members": "{{count}} участников",
|
||||
"unverified": "Не подтверждены"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Изменить карточки",
|
||||
"title": "Карточки панели",
|
||||
"done": "Готово",
|
||||
"reset": "Сбросить по умолчанию",
|
||||
"groups": {
|
||||
"uptime": "Работоспособность",
|
||||
"infrastructure": "Инфраструктура",
|
||||
"incidents": "Инциденты",
|
||||
"notifications": "Уведомления",
|
||||
"maintenance": "Обслуживание",
|
||||
"statusPages": "Страницы состояния",
|
||||
"checks": "Проверки",
|
||||
"logs": "Система",
|
||||
"settings": "Настройки"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "На вашей панели нет карточек",
|
||||
"description": "Выберите карточки, которые хотите видеть. Ваш выбор сохраняется в этом браузере."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Переместить вверх",
|
||||
"moveDown": "Переместить вниз"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Ой! Вы уронили суши!",
|
||||
"subtitle": "Либо URL не существует, либо у Вас нет к нему доступа."
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "การบำรุงรักษา",
|
||||
"logs": "บันทึก",
|
||||
"settings": "การตั้งค่า",
|
||||
"dashboard": "แดชบอร์ด",
|
||||
"groups": {
|
||||
"monitoring": "การตรวจสอบ",
|
||||
"alerting": "การแจ้งเตือน",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "แดชบอร์ด",
|
||||
"description": "ดูตัวตรวจสอบทุกประเภทในที่เดียว และสังเกตสิ่งที่ต้องดูแล"
|
||||
},
|
||||
"card": {
|
||||
"error": "ไม่สามารถโหลดการ์ดนี้ได้"
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "สถานะตัวตรวจสอบ",
|
||||
"empty": "ยังไม่มีตัวตรวจสอบ",
|
||||
"upOfTotal": "จาก {{total}} ที่ทำงาน"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "ขณะนี้ล่ม",
|
||||
"empty": "ตัวตรวจสอบทั้งหมดทำงานอยู่ ไม่มีเหตุการณ์ที่เปิดค้าง",
|
||||
"count": "{{count}} รายการต้องดูแล"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "ตัวตรวจสอบตามประเภท",
|
||||
"empty": "ยังไม่มีตัวตรวจสอบ",
|
||||
"down": "ล่ม {{count}} รายการ"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "เหตุการณ์ล่าสุด",
|
||||
"empty": "ไม่มีเหตุการณ์ — ทุกระบบปกติ",
|
||||
"active": "เปิดอยู่ {{count}} รายการ",
|
||||
"ongoing": "กำลังดำเนินอยู่",
|
||||
"manual": "ด้วยตนเอง",
|
||||
"unknownMonitor": "ตัวตรวจสอบที่ไม่รู้จัก"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "การตรวจสอบที่ล้มเหลว",
|
||||
"empty": "ยังไม่มีการตรวจสอบ",
|
||||
"ratio": "{{failed}} จาก {{total}} ในวันที่ผ่านมา"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "เซิร์ฟเวอร์ Checkmate",
|
||||
"memory": "หน่วยความจำ",
|
||||
"eventLoop": "ความหน่วงของ event loop",
|
||||
"uptime": "เวลาทำงาน",
|
||||
"database": "ฐานข้อมูล",
|
||||
"readsWrites": "อ่าน {{reads}}/วิ · เขียน {{writes}}/วิ"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "อันดับต้น {{shown}} จาก {{total}}",
|
||||
"notEnough": "มีตัวตรวจสอบไม่พอสำหรับจัดอันดับ"
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "ตัวตรวจสอบที่ช้าที่สุด"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "uptime ต่ำที่สุด"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "ตัวตรวจสอบตามกลุ่ม",
|
||||
"empty": "ยังไม่มีกลุ่ม กลุ่มถูกกำหนดในแต่ละตัวตรวจสอบ"
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "เซิร์ฟเวอร์ที่ใช้งานหนักที่สุด",
|
||||
"empty": "ยังไม่มีตัวตรวจสอบโครงสร้างพื้นฐาน",
|
||||
"cpu": "CPU",
|
||||
"memory": "หน่วยความจำ",
|
||||
"disk": "ดิสก์"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "สถิติเหตุการณ์",
|
||||
"openNow": "เปิดอยู่ตอนนี้",
|
||||
"avgResolution": "เวลาแก้ไขโดยเฉลี่ย",
|
||||
"hours": "{{hours}} ชม.",
|
||||
"mostIncidents": "เหตุการณ์มากที่สุด",
|
||||
"total": "ทั้งหมด",
|
||||
"resolved": "แก้ไขแล้ว",
|
||||
"automatic": "อัตโนมัติ",
|
||||
"manual": "ด้วยตนเอง",
|
||||
"autoManual": "อัตโนมัติ {{automatic}} · ด้วยตนเอง {{manual}}"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "ช่องทางการแจ้งเตือน",
|
||||
"empty": "ยังไม่มีช่องทาง"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "ช่วงเวลาบำรุงรักษา",
|
||||
"empty": "ไม่มีการบำรุงรักษาที่กำหนดไว้",
|
||||
"runningNow": "กำลังดำเนินการ",
|
||||
"next": "ช่วงถัดไป"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "หน้าสถานะ",
|
||||
"empty": "ยังไม่มีหน้าสถานะ",
|
||||
"monitorCount": "{{count}} ตัวตรวจสอบ",
|
||||
"published": "เผยแพร่แล้ว",
|
||||
"draft": "ฉบับร่าง"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "คิวการตรวจสอบ",
|
||||
"healthy": "งานทั้ง {{jobs}} รายการเป็นไปตามกำหนดบน {{workers}} worker",
|
||||
"failing": "งานที่ล้มเหลว",
|
||||
"scheduled": "งานที่กำหนดเวลาไว้",
|
||||
"runningNow": "กำลังดำเนินการ"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "ข้อผิดพลาดล่าสุด",
|
||||
"empty": "ไม่มีข้อผิดพลาดที่บันทึกไว้",
|
||||
"count": "{{count}} ข้อผิดพลาด",
|
||||
"warnings": "คำเตือน"
|
||||
},
|
||||
"team": {
|
||||
"title": "สมาชิกทีม",
|
||||
"members": "{{count}} สมาชิก",
|
||||
"unverified": "ยังไม่ยืนยัน"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "แก้ไขการ์ด",
|
||||
"title": "การ์ดแดชบอร์ด",
|
||||
"done": "เสร็จสิ้น",
|
||||
"reset": "รีเซ็ตเป็นค่าเริ่มต้น",
|
||||
"groups": {
|
||||
"uptime": "Uptime",
|
||||
"infrastructure": "โครงสร้างพื้นฐาน",
|
||||
"incidents": "เหตุการณ์",
|
||||
"notifications": "การแจ้งเตือน",
|
||||
"maintenance": "การบำรุงรักษา",
|
||||
"statusPages": "หน้าสถานะ",
|
||||
"checks": "การตรวจสอบ",
|
||||
"logs": "ระบบ",
|
||||
"settings": "การตั้งค่า"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "ไม่มีการ์ดบนแดชบอร์ดของคุณ",
|
||||
"description": "เลือกการ์ดที่ต้องการเห็น การเลือกของคุณจะถูกจดจำในเบราว์เซอร์นี้"
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "เลื่อนขึ้น",
|
||||
"moveDown": "เลื่อนลง"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "โอ้ไม่! ซูชิของคุณหล่นแล้ว!",
|
||||
"subtitle": "URL ไม่มีอยู่ หรือคุณไม่มีสิทธิ์เข้าถึง"
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "Bakım",
|
||||
"logs": "Günlükler",
|
||||
"settings": "Ayarlar",
|
||||
"dashboard": "Panel",
|
||||
"groups": {
|
||||
"monitoring": "İzleme",
|
||||
"alerting": "Uyarılar",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Panel",
|
||||
"description": "Her izleyici türünü tek yerde görün ve neyin ilgi gerektirdiğini fark edin."
|
||||
},
|
||||
"card": {
|
||||
"error": "Bu kart yüklenemedi."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "İzleyici durumu",
|
||||
"empty": "Henüz izleyici yok.",
|
||||
"upOfTotal": "/ {{total}} çalışıyor"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Şu anda kapalı",
|
||||
"empty": "Tüm izleyiciler çalışıyor. Açık olay yok.",
|
||||
"count": "{{count}} ilgi bekliyor"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Türe göre izleyiciler",
|
||||
"empty": "Henüz izleyici yok.",
|
||||
"down": "{{count}} kapalı"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Son olaylar",
|
||||
"empty": "Olay yok — tüm sistemler sağlıklı.",
|
||||
"active": "{{count}} açık",
|
||||
"ongoing": "Devam ediyor",
|
||||
"manual": "Manuel",
|
||||
"unknownMonitor": "Bilinmeyen izleyici"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Başarısız kontroller",
|
||||
"empty": "Henüz kontrol yok.",
|
||||
"ratio": "son günde {{total}} kontrolün {{failed}} tanesi"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Checkmate sunucusu",
|
||||
"memory": "Bellek",
|
||||
"eventLoop": "Olay döngüsü gecikmesi",
|
||||
"uptime": "Çalışma süresi",
|
||||
"database": "Veritabanı",
|
||||
"readsWrites": "{{reads}} okuma/sn · {{writes}} yazma/sn"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "{{total}} içinden ilk {{shown}}",
|
||||
"notEnough": "Sıralama için yeterli izleyici yok."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "En yavaş izleyiciler"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "En düşük çalışma süresi"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Gruba göre izleyiciler",
|
||||
"empty": "Henüz grup yok. Gruplar izleyici başına ayarlanır."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "En yoğun sunucular",
|
||||
"empty": "Henüz altyapı izleyicisi yok.",
|
||||
"cpu": "CPU",
|
||||
"memory": "Bellek",
|
||||
"disk": "Disk"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Olay istatistikleri",
|
||||
"openNow": "Şu anda açık",
|
||||
"avgResolution": "Ortalama çözüm süresi",
|
||||
"hours": "{{hours}} sa",
|
||||
"mostIncidents": "En çok olay",
|
||||
"total": "toplam",
|
||||
"resolved": "Çözüldü",
|
||||
"automatic": "Otomatik",
|
||||
"manual": "Manuel",
|
||||
"autoManual": "{{automatic}} otomatik · {{manual}} manuel"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Bildirim kanalları",
|
||||
"empty": "Henüz kanal yok."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Bakım pencereleri",
|
||||
"empty": "Planlanmış bakım yok.",
|
||||
"runningNow": "Şu anda çalışıyor",
|
||||
"next": "Sonraki pencere"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Durum sayfaları",
|
||||
"empty": "Henüz durum sayfası yok.",
|
||||
"monitorCount": "{{count}} izleyici",
|
||||
"published": "Yayında",
|
||||
"draft": "Taslak"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Kontrol kuyruğu",
|
||||
"healthy": "{{workers}} işçide {{jobs}} işin tümü planlandığı gibi.",
|
||||
"failing": "Başarısız işler",
|
||||
"scheduled": "Planlanmış işler",
|
||||
"runningNow": "Şu anda çalışıyor"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Son hatalar",
|
||||
"empty": "Kaydedilmiş hata yok.",
|
||||
"count": "{{count}} hata",
|
||||
"warnings": "Uyarılar"
|
||||
},
|
||||
"team": {
|
||||
"title": "Ekip üyeleri",
|
||||
"members": "{{count}} üye",
|
||||
"unverified": "Doğrulanmamış"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Kartları düzenle",
|
||||
"title": "Panel kartları",
|
||||
"done": "Tamam",
|
||||
"reset": "Varsayılanlara sıfırla",
|
||||
"groups": {
|
||||
"uptime": "Çalışma süresi",
|
||||
"infrastructure": "Altyapı",
|
||||
"incidents": "Olaylar",
|
||||
"notifications": "Bildirimler",
|
||||
"maintenance": "Bakım",
|
||||
"statusPages": "Durum sayfaları",
|
||||
"checks": "Kontroller",
|
||||
"logs": "Sistem",
|
||||
"settings": "Ayarlar"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Panelinizde kart yok",
|
||||
"description": "Görmek istediğiniz kartları seçin. Seçiminiz bu tarayıcıda hatırlanır."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Yukarı taşı",
|
||||
"moveDown": "Aşağı taşı"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Eyvah! Sushini düşürdün!",
|
||||
"subtitle": "URL mevcut değil veya erişim yetkiniz yok."
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "Обслуговування",
|
||||
"logs": "Журнали",
|
||||
"settings": "Налаштування",
|
||||
"dashboard": "Панель",
|
||||
"groups": {
|
||||
"monitoring": "Моніторинг",
|
||||
"alerting": "Сповіщення",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Панель",
|
||||
"description": "Переглядайте всі типи моніторів в одному місці та помічайте те, що потребує уваги."
|
||||
},
|
||||
"card": {
|
||||
"error": "Не вдалося завантажити цю картку."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Стан моніторів",
|
||||
"empty": "Моніторів поки немає.",
|
||||
"upOfTotal": "з {{total}} працюють"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Зараз недоступні",
|
||||
"empty": "Усі монітори працюють. Відкритих інцидентів немає.",
|
||||
"count": "{{count}} потребують уваги"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Монітори за типом",
|
||||
"empty": "Моніторів поки немає.",
|
||||
"down": "{{count}} недоступні"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Останні інциденти",
|
||||
"empty": "Інцидентів немає — усі системи справні.",
|
||||
"active": "{{count}} відкрито",
|
||||
"ongoing": "Триває",
|
||||
"manual": "Вручну",
|
||||
"unknownMonitor": "Невідомий монітор"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Невдалі перевірки",
|
||||
"empty": "Перевірок поки немає.",
|
||||
"ratio": "{{failed}} з {{total}} за останню добу"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Сервер Checkmate",
|
||||
"memory": "Пам'ять",
|
||||
"eventLoop": "Затримка event loop",
|
||||
"uptime": "Час роботи",
|
||||
"database": "База даних",
|
||||
"readsWrites": "{{reads}} читань/с · {{writes}} записів/с"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Топ {{shown}} з {{total}}",
|
||||
"notEnough": "Недостатньо моніторів для рейтингу."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Найповільніші монітори"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Найнижча працездатність"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Монітори за групою",
|
||||
"empty": "Груп поки немає. Групи задаються в моніторі."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Найзавантаженіші сервери",
|
||||
"empty": "Інфраструктурних моніторів поки немає.",
|
||||
"cpu": "ЦП",
|
||||
"memory": "Пам'ять",
|
||||
"disk": "Диск"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Статистика інцидентів",
|
||||
"openNow": "Відкрито зараз",
|
||||
"avgResolution": "Середній час вирішення",
|
||||
"hours": "{{hours}} год",
|
||||
"mostIncidents": "Найбільше інцидентів",
|
||||
"total": "усього",
|
||||
"resolved": "Вирішено",
|
||||
"automatic": "Автоматично",
|
||||
"manual": "Вручну",
|
||||
"autoManual": "{{automatic}} автоматично · {{manual}} вручну"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Канали сповіщень",
|
||||
"empty": "Каналів поки немає."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Вікна обслуговування",
|
||||
"empty": "Обслуговування не заплановано.",
|
||||
"runningNow": "Виконується",
|
||||
"next": "Наступне вікно"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Сторінки статусу",
|
||||
"empty": "Сторінок статусу поки немає.",
|
||||
"monitorCount": "{{count}} моніторів",
|
||||
"published": "Опубліковано",
|
||||
"draft": "Чернетка"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Черга перевірок",
|
||||
"healthy": "Усі {{jobs}} завдань за розкладом на {{workers}} воркерах.",
|
||||
"failing": "Невдалі завдання",
|
||||
"scheduled": "Заплановані завдання",
|
||||
"runningNow": "Виконується"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Останні помилки",
|
||||
"empty": "Помилок не зафіксовано.",
|
||||
"count": "{{count}} помилок",
|
||||
"warnings": "Попередження"
|
||||
},
|
||||
"team": {
|
||||
"title": "Учасники команди",
|
||||
"members": "{{count}} учасників",
|
||||
"unverified": "Не підтверджені"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Редагувати картки",
|
||||
"title": "Картки панелі",
|
||||
"done": "Готово",
|
||||
"reset": "Скинути до типових",
|
||||
"groups": {
|
||||
"uptime": "Працездатність",
|
||||
"infrastructure": "Інфраструктура",
|
||||
"incidents": "Інциденти",
|
||||
"notifications": "Сповіщення",
|
||||
"maintenance": "Обслуговування",
|
||||
"statusPages": "Сторінки статусу",
|
||||
"checks": "Перевірки",
|
||||
"logs": "Система",
|
||||
"settings": "Налаштування"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "На вашій панелі немає карток",
|
||||
"description": "Виберіть картки, які хочете бачити. Ваш вибір зберігається в цьому браузері."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Перемістити вгору",
|
||||
"moveDown": "Перемістити вниз"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Ой ні! Ви впустили суші!",
|
||||
"subtitle": "Або URL не існує, або у Вас немає доступу до нього."
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "Bảo trì",
|
||||
"logs": "Nhật ký",
|
||||
"settings": "Cài đặt",
|
||||
"dashboard": "Bảng điều khiển",
|
||||
"groups": {
|
||||
"monitoring": "Giám sát",
|
||||
"alerting": "Cảnh báo",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Bảng điều khiển",
|
||||
"description": "Xem mọi loại trình giám sát ở cùng một nơi và nhận ra điều cần chú ý."
|
||||
},
|
||||
"card": {
|
||||
"error": "Không thể tải thẻ này."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Trạng thái giám sát",
|
||||
"empty": "Chưa có trình giám sát nào.",
|
||||
"upOfTotal": "trên {{total}} đang hoạt động"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Hiện đang ngừng",
|
||||
"empty": "Tất cả trình giám sát đang hoạt động. Không có sự cố nào đang mở.",
|
||||
"count": "{{count}} cần chú ý"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Trình giám sát theo loại",
|
||||
"empty": "Chưa có trình giám sát nào.",
|
||||
"down": "{{count}} đang ngừng"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Sự cố gần đây",
|
||||
"empty": "Không có sự cố — mọi hệ thống đều ổn.",
|
||||
"active": "{{count}} đang mở",
|
||||
"ongoing": "Đang diễn ra",
|
||||
"manual": "Thủ công",
|
||||
"unknownMonitor": "Trình giám sát không xác định"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Kiểm tra thất bại",
|
||||
"empty": "Chưa có lần kiểm tra nào.",
|
||||
"ratio": "{{failed}} trên {{total}} trong ngày qua"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Máy chủ Checkmate",
|
||||
"memory": "Bộ nhớ",
|
||||
"eventLoop": "Độ trễ vòng lặp sự kiện",
|
||||
"uptime": "Thời gian hoạt động",
|
||||
"database": "Cơ sở dữ liệu",
|
||||
"readsWrites": "{{reads}} lượt đọc/giây · {{writes}} lượt ghi/giây"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} trên {{total}}",
|
||||
"notEnough": "Chưa đủ trình giám sát để xếp hạng."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Trình giám sát chậm nhất"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Uptime thấp nhất"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Trình giám sát theo nhóm",
|
||||
"empty": "Chưa có nhóm nào. Nhóm được đặt cho từng trình giám sát."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Máy chủ bận nhất",
|
||||
"empty": "Chưa có trình giám sát hạ tầng nào.",
|
||||
"cpu": "CPU",
|
||||
"memory": "Bộ nhớ",
|
||||
"disk": "Ổ đĩa"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Thống kê sự cố",
|
||||
"openNow": "Đang mở",
|
||||
"avgResolution": "Thời gian xử lý trung bình",
|
||||
"hours": "{{hours}} giờ",
|
||||
"mostIncidents": "Nhiều sự cố nhất",
|
||||
"total": "tổng cộng",
|
||||
"resolved": "Đã xử lý",
|
||||
"automatic": "Tự động",
|
||||
"manual": "Thủ công",
|
||||
"autoManual": "{{automatic}} tự động · {{manual}} thủ công"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Kênh thông báo",
|
||||
"empty": "Chưa có kênh nào."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Cửa sổ bảo trì",
|
||||
"empty": "Không có bảo trì nào được lên lịch.",
|
||||
"runningNow": "Đang chạy",
|
||||
"next": "Cửa sổ tiếp theo"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Trang trạng thái",
|
||||
"empty": "Chưa có trang trạng thái nào.",
|
||||
"monitorCount": "{{count}} trình giám sát",
|
||||
"published": "Đã xuất bản",
|
||||
"draft": "Bản nháp"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Hàng đợi kiểm tra",
|
||||
"healthy": "Tất cả {{jobs}} tác vụ đúng lịch trên {{workers}} worker.",
|
||||
"failing": "Tác vụ thất bại",
|
||||
"scheduled": "Tác vụ đã lên lịch",
|
||||
"runningNow": "Đang chạy"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Lỗi gần đây",
|
||||
"empty": "Không có lỗi nào được ghi.",
|
||||
"count": "{{count}} lỗi",
|
||||
"warnings": "Cảnh báo"
|
||||
},
|
||||
"team": {
|
||||
"title": "Thành viên nhóm",
|
||||
"members": "{{count}} thành viên",
|
||||
"unverified": "Chưa xác minh"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Chỉnh sửa thẻ",
|
||||
"title": "Thẻ bảng điều khiển",
|
||||
"done": "Xong",
|
||||
"reset": "Đặt lại mặc định",
|
||||
"groups": {
|
||||
"uptime": "Uptime",
|
||||
"infrastructure": "Hạ tầng",
|
||||
"incidents": "Sự cố",
|
||||
"notifications": "Thông báo",
|
||||
"maintenance": "Bảo trì",
|
||||
"statusPages": "Trang trạng thái",
|
||||
"checks": "Kiểm tra",
|
||||
"logs": "Hệ thống",
|
||||
"settings": "Cài đặt"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "Không có thẻ nào trên bảng điều khiển",
|
||||
"description": "Chọn các thẻ bạn muốn xem. Lựa chọn của bạn được ghi nhớ trong trình duyệt này."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Di chuyển lên",
|
||||
"moveDown": "Di chuyển xuống"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Ôi không! Bạn đã làm rơi sushi!",
|
||||
"subtitle": "Either the URL doesn’t exist, or you don’t have access to it."
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "维护",
|
||||
"logs": "日志",
|
||||
"settings": "设置",
|
||||
"dashboard": "仪表板",
|
||||
"groups": {
|
||||
"monitoring": "监控",
|
||||
"alerting": "告警",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "仪表板",
|
||||
"description": "在一个页面查看所有类型的监控器,快速发现需要关注的问题。"
|
||||
},
|
||||
"card": {
|
||||
"error": "无法加载此卡片。"
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "监控状态",
|
||||
"empty": "暂无监控器。",
|
||||
"upOfTotal": "/ {{total}} 正常"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "当前离线",
|
||||
"empty": "所有监控器均正常。没有未处理的事件。",
|
||||
"count": "{{count}} 项需要关注"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "按类型分组的监控器",
|
||||
"empty": "暂无监控器。",
|
||||
"down": "{{count}} 个离线"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "最近事件",
|
||||
"empty": "没有事件 — 所有系统正常。",
|
||||
"active": "{{count}} 个未处理",
|
||||
"ongoing": "进行中",
|
||||
"manual": "手动",
|
||||
"unknownMonitor": "未知监控器"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "失败的检查",
|
||||
"empty": "暂无检查。",
|
||||
"ratio": "最近一天 {{total}} 次中的 {{failed}} 次"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Checkmate 服务器",
|
||||
"memory": "内存",
|
||||
"eventLoop": "事件循环延迟",
|
||||
"uptime": "运行时间",
|
||||
"database": "数据库",
|
||||
"readsWrites": "{{reads}} 读/秒 · {{writes}} 写/秒"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "{{total}} 个中的前 {{shown}} 个",
|
||||
"notEnough": "监控器数量不足,无法排名。"
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "最慢的监控器"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "运行时间最低"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "按分组的监控器",
|
||||
"empty": "暂无分组。分组在每个监控器上设置。"
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "最繁忙的服务器",
|
||||
"empty": "暂无基础设施监控器。",
|
||||
"cpu": "CPU",
|
||||
"memory": "内存",
|
||||
"disk": "磁盘"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "事件统计",
|
||||
"openNow": "当前未处理",
|
||||
"avgResolution": "平均解决时间",
|
||||
"hours": "{{hours}} 小时",
|
||||
"mostIncidents": "事件最多",
|
||||
"total": "总计",
|
||||
"resolved": "已解决",
|
||||
"automatic": "自动",
|
||||
"manual": "手动",
|
||||
"autoManual": "{{automatic}} 个自动 · {{manual}} 个手动"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "通知渠道",
|
||||
"empty": "暂无渠道。"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "维护窗口",
|
||||
"empty": "没有计划的维护。",
|
||||
"runningNow": "正在进行",
|
||||
"next": "下一个窗口"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "状态页面",
|
||||
"empty": "暂无状态页面。",
|
||||
"monitorCount": "{{count}} 个监控器",
|
||||
"published": "已发布",
|
||||
"draft": "草稿"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "检查队列",
|
||||
"healthy": "{{workers}} 个工作进程上的 {{jobs}} 个任务均按计划运行。",
|
||||
"failing": "失败的任务",
|
||||
"scheduled": "计划任务",
|
||||
"runningNow": "正在进行"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "最近错误",
|
||||
"empty": "没有记录到错误。",
|
||||
"count": "{{count}} 个错误",
|
||||
"warnings": "警告"
|
||||
},
|
||||
"team": {
|
||||
"title": "团队成员",
|
||||
"members": "{{count}} 名成员",
|
||||
"unverified": "未验证"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "编辑卡片",
|
||||
"title": "仪表板卡片",
|
||||
"done": "完成",
|
||||
"reset": "恢复默认",
|
||||
"groups": {
|
||||
"uptime": "运行时间",
|
||||
"infrastructure": "基础设施",
|
||||
"incidents": "事件",
|
||||
"notifications": "通知",
|
||||
"maintenance": "维护",
|
||||
"statusPages": "状态页面",
|
||||
"checks": "检查",
|
||||
"logs": "系统",
|
||||
"settings": "设置"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "您的仪表板上没有卡片",
|
||||
"description": "选择您想查看的卡片。您的选择会记录在此浏览器中。"
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "上移",
|
||||
"moveDown": "下移"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "哦不!你的寿司掉了!",
|
||||
"subtitle": "该 URL 不存在,或者您没有访问权限。"
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
"maintenance": "維護",
|
||||
"logs": "日誌",
|
||||
"settings": "設定",
|
||||
"dashboard": "儀表板",
|
||||
"groups": {
|
||||
"monitoring": "監控",
|
||||
"alerting": "警示",
|
||||
@@ -230,146 +229,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "儀表板",
|
||||
"description": "在同一個頁面檢視所有類型的監視器,快速發現需要留意的問題。"
|
||||
},
|
||||
"card": {
|
||||
"error": "無法載入此卡片。"
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "監視狀態",
|
||||
"empty": "尚無監視器。",
|
||||
"upOfTotal": "/ {{total}} 正常"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "目前離線",
|
||||
"empty": "所有監視器均正常。沒有未處理的事件。",
|
||||
"count": "{{count}} 項需要留意"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "依類型分類的監視器",
|
||||
"empty": "尚無監視器。",
|
||||
"down": "{{count}} 個離線"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "最近事件",
|
||||
"empty": "沒有事件 — 所有系統正常。",
|
||||
"active": "{{count}} 個未處理",
|
||||
"ongoing": "進行中",
|
||||
"manual": "手動",
|
||||
"unknownMonitor": "未知監視器"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "失敗的檢查",
|
||||
"empty": "尚無檢查。",
|
||||
"ratio": "最近一天 {{total}} 次中的 {{failed}} 次"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Checkmate 伺服器",
|
||||
"memory": "記憶體",
|
||||
"eventLoop": "事件迴圈延遲",
|
||||
"uptime": "運行時間",
|
||||
"database": "資料庫",
|
||||
"readsWrites": "{{reads}} 讀取/秒 · {{writes}} 寫入/秒"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "{{total}} 個中的前 {{shown}} 個",
|
||||
"notEnough": "監視器數量不足,無法排名。"
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "最慢的監視器"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "運行時間最低"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "依群組分類的監視器",
|
||||
"empty": "尚無群組。群組在每個監視器上設定。"
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "最繁忙的伺服器",
|
||||
"empty": "尚無基礎設施監視器。",
|
||||
"cpu": "CPU",
|
||||
"memory": "記憶體",
|
||||
"disk": "磁碟"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "事件統計",
|
||||
"openNow": "目前未處理",
|
||||
"avgResolution": "平均解決時間",
|
||||
"hours": "{{hours}} 小時",
|
||||
"mostIncidents": "事件最多",
|
||||
"total": "總計",
|
||||
"resolved": "已解決",
|
||||
"automatic": "自動",
|
||||
"manual": "手動",
|
||||
"autoManual": "{{automatic}} 個自動 · {{manual}} 個手動"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "通知管道",
|
||||
"empty": "尚無管道。"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "維護時段",
|
||||
"empty": "沒有排定的維護。",
|
||||
"runningNow": "進行中",
|
||||
"next": "下一個時段"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "狀態頁面",
|
||||
"empty": "尚無狀態頁面。",
|
||||
"monitorCount": "{{count}} 個監視器",
|
||||
"published": "已發布",
|
||||
"draft": "草稿"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "檢查佇列",
|
||||
"healthy": "{{workers}} 個工作處理程序上的 {{jobs}} 個工作均按計畫執行。",
|
||||
"failing": "失敗的工作",
|
||||
"scheduled": "排定的工作",
|
||||
"runningNow": "進行中"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "最近錯誤",
|
||||
"empty": "沒有記錄到錯誤。",
|
||||
"count": "{{count}} 個錯誤",
|
||||
"warnings": "警告"
|
||||
},
|
||||
"team": {
|
||||
"title": "團隊成員",
|
||||
"members": "{{count}} 位成員",
|
||||
"unverified": "未驗證"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "編輯卡片",
|
||||
"title": "儀表板卡片",
|
||||
"done": "完成",
|
||||
"reset": "還原預設值",
|
||||
"groups": {
|
||||
"uptime": "運行時間",
|
||||
"infrastructure": "基礎設施",
|
||||
"incidents": "事件",
|
||||
"notifications": "通知",
|
||||
"maintenance": "維護",
|
||||
"statusPages": "狀態頁面",
|
||||
"checks": "檢查",
|
||||
"logs": "系統",
|
||||
"settings": "設定"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "您的儀表板上沒有卡片",
|
||||
"description": "選擇您想檢視的卡片。您的選擇會記錄在此瀏覽器中。"
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "上移",
|
||||
"moveDown": "下移"
|
||||
}
|
||||
},
|
||||
"notFound": {
|
||||
"title": "哎呀!您的壽司掉了!",
|
||||
"subtitle": "該 URL 不存在,或您沒有存取權限。"
|
||||
|
||||
@@ -65,6 +65,10 @@ const notificationVariantMeta: Record<string, { component: string; example: Reco
|
||||
component: "PushoverNotification",
|
||||
example: { notificationName: "Pushover personal", type: "pushover", address: "u1234567890abcdef", accessToken: "a1234567890abcdef" },
|
||||
},
|
||||
signalgrid: {
|
||||
component: "SignalgridNotification",
|
||||
example: { notificationName: "Signalgrid", type: "signalgrid", address: "your-channel", accessToken: "your-client-key" },
|
||||
},
|
||||
twilio: {
|
||||
component: "TwilioNotification",
|
||||
example: {
|
||||
|
||||
@@ -9,6 +9,6 @@ export const createCheckRoutes = (checkController: ICheckController): Router =>
|
||||
router.get("/team", checkController.getChecksByTeam);
|
||||
router.delete("/team", isAllowed(["admin", "superadmin"]), checkController.deleteChecksByTeamId);
|
||||
router.get("/:monitorId", checkController.getChecksByMonitor);
|
||||
router.delete("/:monitorId", checkController.deleteChecks);
|
||||
router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), checkController.deleteChecks);
|
||||
return router;
|
||||
};
|
||||
@@ -1,15 +1,16 @@
|
||||
import { IMaintenanceWindowController } from "@/api/controllers/maintenanceWindowController.js";
|
||||
import { isAllowed } from "@/api/middleware/isAllowed.js";
|
||||
import { Router } from "express";
|
||||
|
||||
export const createMaintenanceWindowRoutes = (maintenanceWindowController: IMaintenanceWindowController): Router => {
|
||||
const router = Router();
|
||||
router.post("/", maintenanceWindowController.createMaintenanceWindows);
|
||||
router.post("/", isAllowed(["admin", "superadmin"]), maintenanceWindowController.createMaintenanceWindows);
|
||||
router.get("/team/", maintenanceWindowController.getMaintenanceWindowsByTeamId);
|
||||
|
||||
router.get("/monitor/:monitorId", maintenanceWindowController.getMaintenanceWindowsByMonitorId);
|
||||
|
||||
router.get("/:id", maintenanceWindowController.getMaintenanceWindowById);
|
||||
router.patch("/:id", maintenanceWindowController.editMaintenanceWindow);
|
||||
router.delete("/:id", maintenanceWindowController.deleteMaintenanceWindow);
|
||||
router.patch("/:id", isAllowed(["admin", "superadmin"]), maintenanceWindowController.editMaintenanceWindow);
|
||||
router.delete("/:id", isAllowed(["admin", "superadmin"]), maintenanceWindowController.deleteMaintenanceWindow);
|
||||
return router;
|
||||
};
|
||||
@@ -21,7 +21,11 @@ export const createMonitorRoutes = (monitorController: IMonitorController): Rout
|
||||
// Docker routes
|
||||
router.get("/docker/details/:monitorId", monitorController.getDockerDetailsById);
|
||||
router.get("/docker/details/:monitorId/containers/:containerName", monitorController.getDockerContainerByName);
|
||||
router.get("/docker/details/:monitorId/containers/:containerName/logs", monitorController.getDockerContainerLogs);
|
||||
router.get(
|
||||
"/docker/details/:monitorId/containers/:containerName/logs",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
monitorController.getDockerContainerLogs
|
||||
);
|
||||
|
||||
// Geo checks routes
|
||||
router.get("/:monitorId/geo-checks", monitorController.getGeoChecksByMonitorId);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { INotificationController } from "@/api/controllers/notificationController.js";
|
||||
import { isAllowed } from "@/api/middleware/isAllowed.js";
|
||||
import { Router } from "express";
|
||||
|
||||
export const createNotificationRoutes = (notificationController: INotificationController): Router => {
|
||||
const router = Router();
|
||||
router.post("/", notificationController.createNotification);
|
||||
router.post("/test/all", notificationController.testAllNotifications);
|
||||
router.post("/test", notificationController.testNotification);
|
||||
router.post("/", isAllowed(["admin", "superadmin"]), notificationController.createNotification);
|
||||
router.post("/test/all", isAllowed(["admin", "superadmin"]), notificationController.testAllNotifications);
|
||||
router.post("/test", isAllowed(["admin", "superadmin"]), notificationController.testNotification);
|
||||
router.get("/team", notificationController.getNotificationsByTeamId);
|
||||
router.get("/:id", notificationController.getNotificationById);
|
||||
router.delete("/:id", notificationController.deleteNotification);
|
||||
router.patch("/:id", notificationController.editNotification);
|
||||
router.delete("/:id", isAllowed(["admin", "superadmin"]), notificationController.deleteNotification);
|
||||
router.patch("/:id", isAllowed(["admin", "superadmin"]), notificationController.editNotification);
|
||||
return router;
|
||||
};
|
||||
@@ -123,6 +123,13 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [
|
||||
address: z.string().min(1, "User key is required"),
|
||||
accessToken: z.string().min(1, "App token is required"),
|
||||
}),
|
||||
// Signalgrid notification
|
||||
z.object({
|
||||
notificationName: z.string().min(1, "Notification name is required"),
|
||||
type: z.literal("signalgrid"),
|
||||
address: z.string().min(1, "Channel is required"),
|
||||
accessToken: z.string().min(1, "Client key is required"),
|
||||
}),
|
||||
// Twilio SMS notification
|
||||
z.object({
|
||||
notificationName: z.string().min(1, "Notification name is required"),
|
||||
|
||||
@@ -33,6 +33,7 @@ import { MatrixProvider } from "@/domain/notifications/providers/matrix.js";
|
||||
import { TeamsProvider } from "@/domain/notifications/providers/teams.js";
|
||||
import { TelegramProvider } from "@/domain/notifications/providers/telegram.js";
|
||||
import { PushoverProvider } from "@/domain/notifications/providers/pushover.js";
|
||||
import { SignalgridProvider } from "@/domain/notifications/providers/signalgrid.js";
|
||||
import { TwilioProvider } from "@/domain/notifications/providers/twilio.js";
|
||||
import { NtfyProvider } from "@/domain/notifications/providers/ntfy.js";
|
||||
|
||||
@@ -178,6 +179,7 @@ export const buildShared = async ({
|
||||
const teamsProvider = new TeamsProvider(logger);
|
||||
const telegramProvider = new TelegramProvider(logger);
|
||||
const pushoverProvider = new PushoverProvider(logger);
|
||||
const signalgridProvider = new SignalgridProvider(logger);
|
||||
const twilioProvider = new TwilioProvider(logger);
|
||||
const ntfyProvider = new NtfyProvider(logger);
|
||||
|
||||
@@ -192,6 +194,7 @@ export const buildShared = async ({
|
||||
teams: teamsProvider,
|
||||
telegram: telegramProvider,
|
||||
pushover: pushoverProvider,
|
||||
signalgrid: signalgridProvider,
|
||||
twilio: twilioProvider,
|
||||
ntfy: ntfyProvider,
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ const computeRestartsInRange = (aggregate: DockerContainerStatsBucket[]): number
|
||||
|
||||
export interface IMonitorService {
|
||||
// create
|
||||
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<void>;
|
||||
createMonitor(teamId: string, userId: string, body: Partial<Monitor>): Promise<Monitor>;
|
||||
createMonitors(monitors: Array<Monitor>): Promise<Monitor[] | null>;
|
||||
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
|
||||
|
||||
@@ -180,7 +180,7 @@ export class MonitorService implements IMonitorService {
|
||||
this.incidentsRepository = incidentsRepository;
|
||||
}
|
||||
|
||||
createMonitor = async (teamId: string, userId: string, body: Monitor): Promise<void> => {
|
||||
createMonitor = async (teamId: string, userId: string, body: Monitor): Promise<Monitor> => {
|
||||
// proxyId is only needed in custom mode
|
||||
if (body.proxyMode !== "custom") {
|
||||
delete body.proxyId;
|
||||
@@ -191,6 +191,7 @@ export class MonitorService implements IMonitorService {
|
||||
}
|
||||
|
||||
this.scheduler.addJob(monitor.id, monitor);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
createMonitors = async (monitors: Array<Monitor>): Promise<Monitor[] | null> => {
|
||||
|
||||
@@ -37,6 +37,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
|
||||
"teams",
|
||||
"telegram",
|
||||
"pushover",
|
||||
"signalgrid",
|
||||
"twilio",
|
||||
"ntfy",
|
||||
] as NotificationChannel[],
|
||||
|
||||
@@ -9,6 +9,7 @@ export const NotificationChannels = [
|
||||
"teams",
|
||||
"telegram",
|
||||
"pushover",
|
||||
"signalgrid",
|
||||
"twilio",
|
||||
"ntfy",
|
||||
] as const;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
const SERVICE_NAME = "SignalgridProvider";
|
||||
import type { Notification, NotificationMessage, NotificationSeverity } from "@/domain/notifications/notification.type.js";
|
||||
import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js";
|
||||
import { getTestMessage } from "@/domain/notifications/providers/utils.js";
|
||||
import got from "got";
|
||||
|
||||
export class SignalgridProvider extends NotificationProvider {
|
||||
async sendTestAlert(notification: Partial<Notification>): Promise<boolean> {
|
||||
if (!notification.address || !notification.accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await got.post("https://api.signalgrid.co/v1/push", {
|
||||
form: {
|
||||
client_key: notification.accessToken,
|
||||
channel: notification.address,
|
||||
title: "Checkmate Test Notification",
|
||||
body: getTestMessage(),
|
||||
type: "INFO",
|
||||
critical: false,
|
||||
},
|
||||
...this.gotRequestOptions(),
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : "unknown error";
|
||||
const errStack = error instanceof Error ? error.stack : undefined;
|
||||
this.logger.warn({
|
||||
message: "Signalgrid test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
stack: errStack,
|
||||
details: { error: errMsg },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||
if (!notification.address || !notification.accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await got.post("https://api.signalgrid.co/v1/push", {
|
||||
form: {
|
||||
client_key: notification.accessToken,
|
||||
channel: notification.address,
|
||||
title: message.content.title,
|
||||
body: this.buildSignalgridText(message),
|
||||
type: this.mapSeverity(message.severity),
|
||||
critical: false,
|
||||
},
|
||||
...this.gotRequestOptions(),
|
||||
});
|
||||
|
||||
this.logger.info({
|
||||
message: "Signalgrid notification sent",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : "unknown error";
|
||||
const errStack = error instanceof Error ? error.stack : undefined;
|
||||
this.logger.warn({
|
||||
message: "Signalgrid alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendMessage",
|
||||
stack: errStack,
|
||||
details: { error: errMsg },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private mapSeverity(severity: NotificationSeverity): "CRIT" | "WARN" | "INFO" | "SUCCESS" {
|
||||
switch (severity) {
|
||||
case "critical":
|
||||
return "CRIT";
|
||||
case "warning":
|
||||
return "WARN";
|
||||
case "success":
|
||||
return "SUCCESS";
|
||||
default:
|
||||
return "INFO";
|
||||
}
|
||||
}
|
||||
|
||||
private buildSignalgridText(message: NotificationMessage): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(message.content.summary);
|
||||
lines.push("");
|
||||
lines.push("Monitor Details:");
|
||||
lines.push(`• Name: ${message.monitor.name}`);
|
||||
lines.push(`• URL: ${message.monitor.url}`);
|
||||
lines.push(`• Type: ${message.monitor.type}`);
|
||||
lines.push(`• Status: ${message.monitor.status}`);
|
||||
|
||||
if (message.content.details && message.content.details.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Additional Information:");
|
||||
message.content.details.forEach((detail) => lines.push(`• ${detail}`));
|
||||
}
|
||||
|
||||
if (message.content.thresholds && message.content.thresholds.length > 0) {
|
||||
lines.push("");
|
||||
lines.push("Threshold Breaches:");
|
||||
message.content.thresholds.forEach((breach) => {
|
||||
lines.push(`• ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`);
|
||||
});
|
||||
}
|
||||
|
||||
if (message.content.incident) {
|
||||
lines.push("");
|
||||
lines.push(`View Incident: ${message.clientHost}/incidents/${message.monitor.id}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it, jest } from "@jest/globals";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import { isAllowed } from "../../../src/api/middleware/isAllowed.ts";
|
||||
import { AppError } from "../../../src/utils/AppError.ts";
|
||||
|
||||
describe("isAllowed middleware", () => {
|
||||
it("calls next() without error when user has an allowed role", () => {
|
||||
const middleware = isAllowed(["admin", "superadmin"]);
|
||||
const req = {
|
||||
user: {
|
||||
id: "user-1",
|
||||
teamId: "team-1",
|
||||
email: "admin@example.com",
|
||||
role: ["admin"],
|
||||
},
|
||||
} as unknown as Request;
|
||||
const res = {} as Response;
|
||||
const next = jest.fn() as unknown as NextFunction;
|
||||
|
||||
middleware(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("calls next(error) with 403 when user does not have an allowed role", () => {
|
||||
const middleware = isAllowed(["admin", "superadmin"]);
|
||||
const req = {
|
||||
user: {
|
||||
id: "user-2",
|
||||
teamId: "team-1",
|
||||
email: "user@example.com",
|
||||
role: ["user"],
|
||||
},
|
||||
} as unknown as Request;
|
||||
const res = {} as Response;
|
||||
const next = jest.fn() as unknown as NextFunction;
|
||||
|
||||
middleware(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
const error = (next as unknown as jest.Mock).mock.calls[0][0] as AppError;
|
||||
expect(error).toBeInstanceOf(AppError);
|
||||
expect(error.status).toBe(403);
|
||||
expect(error.message).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("calls next(error) with 403 when req.user is undefined", () => {
|
||||
const middleware = isAllowed(["admin", "superadmin"]);
|
||||
const req = {} as Request;
|
||||
const res = {} as Response;
|
||||
const next = jest.fn() as unknown as NextFunction;
|
||||
|
||||
middleware(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
const error = (next as unknown as jest.Mock).mock.calls[0][0] as AppError;
|
||||
expect(error).toBeInstanceOf(AppError);
|
||||
expect(error.status).toBe(403);
|
||||
expect(error.message).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("calls next() when user has multiple roles and at least one matches", () => {
|
||||
const middleware = isAllowed(["admin", "superadmin"]);
|
||||
const req = {
|
||||
user: {
|
||||
id: "user-3",
|
||||
teamId: "team-1",
|
||||
email: "multi@example.com",
|
||||
role: ["user", "admin"],
|
||||
},
|
||||
} as unknown as Request;
|
||||
const res = {} as Response;
|
||||
const next = jest.fn() as unknown as NextFunction;
|
||||
|
||||
middleware(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
|
||||
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
|
||||
import { makeNotification, makeMessage, makeMessageWithThresholds, makeMessageWithIncident } from "../../../helpers/notificationMessage.ts";
|
||||
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
|
||||
|
||||
const mockGotPost = jest.fn().mockResolvedValue({});
|
||||
jest.unstable_mockModule("got", () => ({ default: { post: mockGotPost } }));
|
||||
|
||||
const { SignalgridProvider } = await import("../../../../src/domain/notifications/providers/signalgrid.ts");
|
||||
|
||||
const createProvider = () => {
|
||||
const logger = createMockLogger();
|
||||
return { provider: new SignalgridProvider(logger as any), logger };
|
||||
};
|
||||
|
||||
testNotificationProviderContract("SignalgridProvider", {
|
||||
create: () => {
|
||||
mockGotPost.mockResolvedValue({});
|
||||
return createProvider().provider;
|
||||
},
|
||||
makeNotification: () => makeNotification(),
|
||||
});
|
||||
|
||||
describe("SignalgridProvider", () => {
|
||||
beforeEach(() => mockGotPost.mockReset().mockResolvedValue({}));
|
||||
|
||||
describe("sendTestAlert", () => {
|
||||
it("sends to Signalgrid API and returns true", async () => {
|
||||
expect(await createProvider().provider.sendTestAlert(makeNotification())).toBe(true);
|
||||
expect(mockGotPost).toHaveBeenCalledWith(
|
||||
"https://api.signalgrid.co/v1/push",
|
||||
expect.objectContaining({
|
||||
form: expect.objectContaining({
|
||||
client_key: "token-abc",
|
||||
channel: "https://hooks.example.com/webhook",
|
||||
type: "INFO",
|
||||
critical: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false when address is missing", async () => {
|
||||
expect(await createProvider().provider.sendTestAlert(makeNotification({ address: "" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when accessToken is missing", async () => {
|
||||
expect(await createProvider().provider.sendTestAlert(makeNotification({ accessToken: undefined }))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false and logs on error", async () => {
|
||||
mockGotPost.mockRejectedValue(new Error("fail"));
|
||||
const { provider, logger } = createProvider();
|
||||
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ method: "sendTestAlert", details: { error: "fail" } }));
|
||||
});
|
||||
|
||||
it("handles non-Error thrown values in sendTestAlert", async () => {
|
||||
mockGotPost.mockRejectedValue("string error");
|
||||
const { provider, logger } = createProvider();
|
||||
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined, details: { error: "unknown error" } }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMessage", () => {
|
||||
it("sends message and returns true", async () => {
|
||||
const { provider } = createProvider();
|
||||
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(true);
|
||||
const form = mockGotPost.mock.calls[0][1].form;
|
||||
expect(form.body).toContain("Monitor Details:");
|
||||
expect(form.title).toBeDefined();
|
||||
expect(form.critical).toBe(false);
|
||||
});
|
||||
|
||||
it("maps critical severity to CRIT", async () => {
|
||||
const { provider } = createProvider();
|
||||
const message = makeMessage();
|
||||
message.severity = "critical";
|
||||
await provider.sendMessage(makeNotification() as any, message);
|
||||
expect(mockGotPost.mock.calls[0][1].form.type).toBe("CRIT");
|
||||
});
|
||||
|
||||
it("maps warning severity to WARN", async () => {
|
||||
const { provider } = createProvider();
|
||||
const message = makeMessage();
|
||||
message.severity = "warning";
|
||||
await provider.sendMessage(makeNotification() as any, message);
|
||||
expect(mockGotPost.mock.calls[0][1].form.type).toBe("WARN");
|
||||
});
|
||||
|
||||
it("maps success severity to SUCCESS", async () => {
|
||||
const { provider } = createProvider();
|
||||
const message = makeMessage();
|
||||
message.severity = "success";
|
||||
await provider.sendMessage(makeNotification() as any, message);
|
||||
expect(mockGotPost.mock.calls[0][1].form.type).toBe("SUCCESS");
|
||||
});
|
||||
|
||||
it("maps info severity to INFO", async () => {
|
||||
const { provider } = createProvider();
|
||||
const message = makeMessage();
|
||||
message.severity = "info";
|
||||
await provider.sendMessage(makeNotification() as any, message);
|
||||
expect(mockGotPost.mock.calls[0][1].form.type).toBe("INFO");
|
||||
});
|
||||
|
||||
it("returns false when address is missing", async () => {
|
||||
expect(await createProvider().provider.sendMessage(makeNotification({ address: "" }) as any, makeMessage())).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when accessToken is missing", async () => {
|
||||
expect(await createProvider().provider.sendMessage(makeNotification({ accessToken: undefined }) as any, makeMessage())).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false and logs on error", async () => {
|
||||
mockGotPost.mockRejectedValue(new Error("fail"));
|
||||
const { provider, logger } = createProvider();
|
||||
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ method: "sendMessage" }));
|
||||
});
|
||||
|
||||
it("handles non-Error thrown values in sendMessage", async () => {
|
||||
mockGotPost.mockRejectedValue(42);
|
||||
const { provider, logger } = createProvider();
|
||||
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ details: { error: "unknown error" } }));
|
||||
});
|
||||
|
||||
it("includes thresholds in text", async () => {
|
||||
const { provider } = createProvider();
|
||||
await provider.sendMessage(makeNotification() as any, makeMessageWithThresholds());
|
||||
expect(mockGotPost.mock.calls[0][1].form.body).toContain("CPU");
|
||||
});
|
||||
|
||||
it("includes incident link in text", async () => {
|
||||
const { provider } = createProvider();
|
||||
await provider.sendMessage(makeNotification() as any, makeMessageWithIncident());
|
||||
expect(mockGotPost.mock.calls[0][1].form.body).toContain("View Incident");
|
||||
});
|
||||
|
||||
it("omits optional sections when not present", async () => {
|
||||
const { provider } = createProvider();
|
||||
const msg = makeMessage();
|
||||
msg.content.thresholds = undefined;
|
||||
msg.content.details = undefined;
|
||||
msg.content.incident = undefined;
|
||||
await provider.sendMessage(makeNotification() as any, msg);
|
||||
const text = mockGotPost.mock.calls[0][1].form.body;
|
||||
expect(text).not.toContain("Threshold");
|
||||
expect(text).not.toContain("Additional");
|
||||
expect(text).not.toContain("View Incident");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -122,16 +122,17 @@ const createService = (
|
||||
|
||||
describe("MonitorService", () => {
|
||||
describe("createMonitor", () => {
|
||||
it("creates a monitor and adds a job", async () => {
|
||||
it("creates a monitor, adds a job and returns the created document", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
const monitor = makeMonitor();
|
||||
(monitorsRepository.create as jest.Mock).mockResolvedValue(monitor);
|
||||
const { service, jobQueue } = createService({ monitorsRepository });
|
||||
|
||||
await service.createMonitor(TEAM_ID, USER_ID, monitor as any);
|
||||
const result = await service.createMonitor(TEAM_ID, USER_ID, monitor as any);
|
||||
|
||||
expect(monitorsRepository.create).toHaveBeenCalledWith(monitor, TEAM_ID, USER_ID);
|
||||
expect(jobQueue.addJob).toHaveBeenCalledWith(MONITOR_ID, monitor);
|
||||
expect(result).toBe(monitor);
|
||||
});
|
||||
|
||||
it("strips a stray proxyId when proxyMode is not custom", async () => {
|
||||
|
||||
Reference in new issue
Block a user