Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f3de9e064 |
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
|
||||
* @ajhollid @mohicody @Br0wnHammer @Owaiseimdad @karenvicent @shanikauwu1
|
||||
@@ -36,7 +36,8 @@
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1461" height="853" alt="image" src="https://github.com/user-attachments/assets/d5ee1a4e-9db9-47cc-ad47-e1ba88a9d796" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
@@ -122,9 +123,9 @@ Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https:
|
||||
|
||||
### Using a Custom CA
|
||||
|
||||
If you need to monitor internal HTTPS endpoints with certificates from private Certificate Authorities (like Smallstep), see our [Custom CA Trust Guide](./docs/custom-ca-trust.md) for Docker configuration options.
|
||||
If you need to monitor internal HTTPS endpoints with certificates from private Certificate Authorities (like Smallstep), see our [Custom CA Trust Guide](https://checkmate.so/docs/settings/custom-ca-trust) for Docker configuration options.
|
||||
|
||||
For more documentation, see the [docs directory](./docs/).
|
||||
For more documentation, see the [Checkmate documentation](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Performance
|
||||
@@ -211,7 +212,7 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||
<a id="contributing"></a>
|
||||
## Contributing
|
||||
|
||||
We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Malena](https://github.com/malenacaroline) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers.
|
||||
We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers.
|
||||
|
||||
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned almost 11K+ stars and attracted 150+ contributors from around the globe.
|
||||
|
||||
|
||||
Generated
-66
@@ -9,12 +9,8 @@
|
||||
"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",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@mui/lab": "7.0.0",
|
||||
@@ -365,59 +361,6 @@
|
||||
"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",
|
||||
@@ -1007,15 +950,6 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/geist": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.3.0.tgz",
|
||||
"integrity": "sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@hapi/hoek": {
|
||||
"version": "9.3.0",
|
||||
"license": "BSD-3-Clause"
|
||||
|
||||
+1
-6
@@ -12,20 +12,15 @@
|
||||
"lint-strict": "eslint . --ext js,jsx,ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"compile": "tsc -b"
|
||||
"format-check": "prettier --check ."
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
},
|
||||
"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",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@mui/lab": "7.0.0",
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import Box, { type BoxProps } from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
export const BaseBox = forwardRef<HTMLDivElement, BoxProps>(({ sx, ...rest }, ref) => {
|
||||
export const BaseBox = ({ sx, ...rest }: BoxProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
{...rest}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
@@ -18,6 +16,4 @@ export const BaseBox = forwardRef<HTMLDivElement, BoxProps>(({ sx, ...rest }, re
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
BaseBox.displayName = "BaseBox";
|
||||
};
|
||||
@@ -8,7 +8,7 @@ interface IconProps {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const Icon = ({ icon: Icon, size = 16, strokeWidth = 1.5, color }: IconProps) => {
|
||||
const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5, color }: IconProps) => {
|
||||
return (
|
||||
<Icon
|
||||
size={size}
|
||||
|
||||
@@ -37,8 +37,8 @@ export const OfflineBanner = ({ visible }: OfflineBannerProps) => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: theme.zIndex.snackbar,
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
color: "#FFFFFF",
|
||||
backgroundColor: theme.palette.error.main,
|
||||
color: theme.palette.error.contrastText,
|
||||
px: theme.spacing(8),
|
||||
py: theme.spacing(4),
|
||||
transition: "top 1s ease-in-out",
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { ChevronLeft, HelpCircle, FileText, Code } from "lucide-react";
|
||||
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
@@ -110,7 +109,7 @@ export const PageHeader = ({
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.55,
|
||||
maxWidth: "108ch",
|
||||
maxWidth: 640,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
@@ -143,7 +142,7 @@ export const PageHeader = ({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 36,
|
||||
height: INPUT_BASE_HEIGHT,
|
||||
height: 32,
|
||||
color: theme.palette.text.secondary,
|
||||
borderRight:
|
||||
idx < linkItems.length - 1
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Box, Stack, useTheme } from "@mui/material";
|
||||
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface SkeletonCardProps {
|
||||
width?: number | string | { xs?: number | string; md?: number | string };
|
||||
@@ -45,7 +44,7 @@ export const SkeletonCard = ({
|
||||
|
||||
const blockStyle = {
|
||||
width: 55,
|
||||
height: INPUT_BASE_HEIGHT,
|
||||
height: 34,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
background: theme.palette.secondary.main,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { darken, lighten } from "@mui/material/styles";
|
||||
import { lighten } from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import type { PaletteKey } from "@/Utils/Theme/Theme";
|
||||
import { BaseBox, TooltipWithInfo } from "@/Components/design-elements";
|
||||
@@ -19,10 +19,10 @@ export const GradientBox = ({ children, palette, sx, interactive }: GradientBox)
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const isLight = theme.palette.mode === "light";
|
||||
const paper = theme.palette.background.paper;
|
||||
const paperStart = isLight ? darken(paper, 0.02) : lighten(paper, 0.08);
|
||||
const paperStart = lighten(paper, isLight ? 0.06 : 0.08);
|
||||
const paperEnd = paper;
|
||||
const bg = palette
|
||||
? `linear-gradient(135deg, ${lighten(theme.palette[palette].main, 0.12)} 0%, ${theme.palette[palette].main} 100%)`
|
||||
? `linear-gradient(135deg, ${theme.palette[palette].light} 0%, ${theme.palette[palette].main} 100%)`
|
||||
: `linear-gradient(135deg, ${paperStart} 0%, ${paperEnd} 100%)`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BaseBox } from "@/Components/design-elements";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { SxProps } from "@mui/material";
|
||||
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
type StatusBoxProps = React.PropsWithChildren<{
|
||||
children: React.ReactNode;
|
||||
@@ -34,8 +34,8 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => {
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
backgroundImage: `
|
||||
linear-gradient(${alpha(theme.palette.divider, 0.4)} 1px, transparent 1px),
|
||||
linear-gradient(90deg, ${alpha(theme.palette.divider, 0.4)} 1px, transparent 1px)
|
||||
linear-gradient(${theme.palette.divider} 1px, transparent 1px),
|
||||
linear-gradient(90deg, ${theme.palette.divider} 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "24px 24px",
|
||||
maskImage:
|
||||
|
||||
@@ -52,7 +52,6 @@ type DataTableProps<T extends { id?: string | number; _id?: string | number }> =
|
||||
emptyViewText?: string;
|
||||
emptyViewPositive?: boolean;
|
||||
getRowSx?: (row: T) => SxProps<Theme>;
|
||||
isRowSelected?: (row: T) => boolean;
|
||||
};
|
||||
|
||||
export function DataTable<
|
||||
@@ -71,7 +70,6 @@ export function DataTable<
|
||||
emptyViewText,
|
||||
emptyViewPositive,
|
||||
getRowSx,
|
||||
isRowSelected,
|
||||
}: DataTableProps<T>) {
|
||||
const theme = useTheme();
|
||||
const [expanded, setExpanded] = useState<(string | number) | null>(null);
|
||||
@@ -81,21 +79,6 @@ 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) {
|
||||
@@ -117,10 +100,7 @@ export function DataTable<
|
||||
keys.push(key);
|
||||
return (
|
||||
<Stack
|
||||
onClick={(e) => {
|
||||
if (hasTextSelectionWithin(e.currentTarget)) return;
|
||||
if (onRowClick) onRowClick(row);
|
||||
}}
|
||||
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
||||
spacing={theme.spacing(LAYOUT.XS)}
|
||||
sx={{
|
||||
borderStyle: "solid",
|
||||
@@ -217,7 +197,7 @@ export function DataTable<
|
||||
"& :is(th)": {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.secondary,
|
||||
fontWeight: 600,
|
||||
fontWeight: 500,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.08em",
|
||||
padding: `${theme.spacing(SPACING.LG)} ${theme.spacing(LAYOUT.MD)}`,
|
||||
@@ -233,6 +213,10 @@ export function DataTable<
|
||||
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"& .MuiTableBody-root .MuiTableRow-root.is-clickable:hover .MuiTableCell-root":
|
||||
{
|
||||
backgroundColor: theme.palette.action.rowHover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TableHead>
|
||||
@@ -253,29 +237,16 @@ export function DataTable<
|
||||
{data.map((row) => {
|
||||
const key = row.id || row._id || Math.random();
|
||||
const isExpanded = expanded === key;
|
||||
const selected = isRowSelected?.(row);
|
||||
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
<TableRow
|
||||
className={isInteractive ? "is-clickable" : undefined}
|
||||
sx={{
|
||||
cursor: isInteractive ? "pointer" : "default",
|
||||
...(isInteractive && {
|
||||
"&:hover .MuiTableCell-root": {
|
||||
backgroundColor: selected
|
||||
? theme.palette.action.selectedHover
|
||||
: theme.palette.action.rowHover,
|
||||
},
|
||||
}),
|
||||
...(selected && {
|
||||
"& .MuiTableCell-root": {
|
||||
backgroundColor: theme.palette.action.selected,
|
||||
},
|
||||
}),
|
||||
...(getRowSx?.(row) as object),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (hasTextSelectionWithin(e.currentTarget)) return;
|
||||
onClick={() => {
|
||||
if (expandableRows) handleExpand(row);
|
||||
else if (onRowClick) onRowClick(row);
|
||||
}}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import MuiTabs from "@mui/material/Tabs";
|
||||
import type { TabsProps } from "@mui/material/Tabs";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
|
||||
interface CustomTabsProps extends TabsProps {}
|
||||
|
||||
export const Tabs = (props: CustomTabsProps) => {
|
||||
@@ -10,7 +8,7 @@ export const Tabs = (props: CustomTabsProps) => {
|
||||
return (
|
||||
<MuiTabs
|
||||
sx={{
|
||||
minHeight: INPUT_BASE_HEIGHT,
|
||||
minHeight: 34,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
"& .MuiTabs-indicator": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
@@ -40,9 +38,9 @@ export const Tab = (props: CustomTabProps) => {
|
||||
iconPosition="start"
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
fontSize: typographyLevels.m,
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
minHeight: INPUT_BASE_HEIGHT,
|
||||
minHeight: 34,
|
||||
padding: theme.spacing(1, 0),
|
||||
paddingBottom: 0,
|
||||
minWidth: "auto",
|
||||
@@ -50,6 +48,7 @@ export const Tab = (props: CustomTabProps) => {
|
||||
color: theme.palette.text.secondary,
|
||||
"&.Mui-selected": {
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
},
|
||||
"&:hover": {
|
||||
color: theme.palette.text.secondary,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import type { ButtonProps } from "@mui/material/Button";
|
||||
import { useTheme, darken } from "@mui/material/styles";
|
||||
import { INPUT_BASE_HEIGHT, HOVER } from "@/Utils/Theme/constants";
|
||||
import { HOVER } from "@/Utils/Theme/constants";
|
||||
|
||||
const PALETTE_COLORS = [
|
||||
"primary",
|
||||
@@ -43,8 +43,7 @@ export const ButtonInput = ({ sx, ...props }: ButtonProps) => {
|
||||
{...props}
|
||||
sx={{
|
||||
textTransform: "none",
|
||||
height: INPUT_BASE_HEIGHT,
|
||||
lineHeight: 1.2,
|
||||
height: 34,
|
||||
fontWeight: 400,
|
||||
borderRadius: 2,
|
||||
whiteSpace: "nowrap",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import { useTheme } from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { FieldLabel } from "@/Components/inputs/FieldLabel";
|
||||
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface ColorPickerProps extends MuiColorInputProps {
|
||||
fieldLabel?: string;
|
||||
@@ -19,7 +18,7 @@ export const ColorInput = ({ fieldLabel, required, ...props }: ColorPickerProps)
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: INPUT_BASE_HEIGHT,
|
||||
height: 34,
|
||||
fontSize: typographyLevels.base,
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { FieldLabel } from "./FieldLabel";
|
||||
import { Calendar } from "lucide-react";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface DatePickerComponentProps extends Omit<DatePickerProps<Dayjs>, "label"> {
|
||||
fieldLabel?: string;
|
||||
@@ -56,7 +55,7 @@ export const DatePickerComponent = ({
|
||||
flexDirection: "row-reverse",
|
||||
},
|
||||
"& input": {
|
||||
minHeight: INPUT_BASE_HEIGHT,
|
||||
minHeight: 34,
|
||||
p: 0,
|
||||
pl: theme.spacing(3),
|
||||
pr: theme.spacing(5),
|
||||
@@ -67,9 +66,7 @@ export const DatePickerComponent = ({
|
||||
},
|
||||
"&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset":
|
||||
{
|
||||
borderColor: error
|
||||
? theme.palette.error.main
|
||||
: theme.palette.text.disabled,
|
||||
borderColor: error ? theme.palette.error.main : theme.palette.divider,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ import Stack from "@mui/material/Stack";
|
||||
import { FieldLabel } from "./FieldLabel";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface SelectInputProps<T> extends Omit<SelectProps<T>, "label"> {
|
||||
fieldLabel?: string;
|
||||
@@ -72,7 +71,7 @@ const SelectInputInner = <T,>(
|
||||
/>
|
||||
)}
|
||||
sx={{
|
||||
height: `${INPUT_BASE_HEIGHT}px`,
|
||||
height: "34px",
|
||||
"& .MuiSelect-select": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -84,6 +83,10 @@ const SelectInputInner = <T,>(
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
...props.sx,
|
||||
}}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { FieldLabel } from "./FieldLabel";
|
||||
import { INPUT_BASE_HEIGHT, LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface TextInputProps extends Omit<TextFieldProps, "label"> {
|
||||
fieldLabel?: string;
|
||||
@@ -22,10 +22,16 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
|
||||
width: "100%",
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: props.multiline ? "auto" : INPUT_BASE_HEIGHT,
|
||||
height: props.multiline ? "auto" : 34,
|
||||
fontSize: typographyLevels.base,
|
||||
overflow: "hidden",
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
"& .MuiFormHelperText-root": {
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
@@ -33,7 +39,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function T
|
||||
},
|
||||
"& input:-webkit-autofill, & input:-webkit-autofill:hover, & input:-webkit-autofill:focus, & input:-webkit-autofill:active":
|
||||
{
|
||||
WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.paper} inset !important`,
|
||||
WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.default} inset !important`,
|
||||
WebkitTextFillColor: `${theme.palette.text.primary} !important`,
|
||||
caretColor: theme.palette.text.primary,
|
||||
transition: "background-color 5000s ease-in-out 0s",
|
||||
|
||||
@@ -7,7 +7,6 @@ import Typography from "@mui/material/Typography";
|
||||
import { FieldLabel } from "./FieldLabel";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
|
||||
|
||||
interface TimePickerComponentProps extends Omit<MobileTimePickerProps<Dayjs>, "label"> {
|
||||
fieldLabel?: string;
|
||||
@@ -34,7 +33,7 @@ export const TimePickerComponent = ({
|
||||
sx: {
|
||||
width: "fit-content",
|
||||
"& input": {
|
||||
minHeight: INPUT_BASE_HEIGHT,
|
||||
minHeight: 34,
|
||||
p: 0,
|
||||
px: theme.spacing(5),
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { ToggleButtonProps } from "@mui/material/ToggleButton";
|
||||
import type { ToggleButtonGroupProps } from "@mui/material/ToggleButtonGroup";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import { INPUT_BASE_HEIGHT } from "@/Utils/Theme/constants";
|
||||
|
||||
export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
|
||||
const theme = useTheme();
|
||||
@@ -21,7 +20,7 @@ export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
|
||||
sx={{
|
||||
px: 8,
|
||||
textTransform: "none",
|
||||
height: INPUT_BASE_HEIGHT,
|
||||
height: 34,
|
||||
fontWeight: 400,
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
@@ -35,6 +34,7 @@ export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: selectedBg,
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 500,
|
||||
"&:hover": {
|
||||
backgroundColor: selectedHoverBg,
|
||||
},
|
||||
|
||||
@@ -60,22 +60,20 @@ export const HeaderMonitorControls = ({
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
{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"
|
||||
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"
|
||||
|
||||
@@ -35,7 +35,7 @@ export const MonitorListPage = ({
|
||||
headerKey,
|
||||
page,
|
||||
actionLink,
|
||||
controller,
|
||||
controller: c,
|
||||
bulkActions,
|
||||
showTypeFilter,
|
||||
summaryProps,
|
||||
@@ -48,14 +48,14 @@ export const MonitorListPage = ({
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const isAdmin = useIsAdmin();
|
||||
const isLoading = controller.isLoading || Boolean(extraLoading);
|
||||
const isLoading = c.isLoading || Boolean(extraLoading);
|
||||
|
||||
return (
|
||||
<MonitorBasePageWithStates
|
||||
headerKey={headerKey}
|
||||
loading={isLoading}
|
||||
error={controller.error || extraError}
|
||||
totalCount={controller.effectiveTotalCount}
|
||||
error={c.error || extraError}
|
||||
totalCount={c.effectiveTotalCount}
|
||||
page={page}
|
||||
actionLink={actionLink}
|
||||
priorityFallback={priorityFallback}
|
||||
@@ -66,7 +66,7 @@ export const MonitorListPage = ({
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<HeaderMonitorsSummary
|
||||
summary={controller.summary}
|
||||
summary={c.summary}
|
||||
{...summaryProps}
|
||||
/>
|
||||
<Stack
|
||||
@@ -78,27 +78,27 @@ export const MonitorListPage = ({
|
||||
{...(showTypeFilter === false
|
||||
? { showTypes: false }
|
||||
: {
|
||||
selectedTypes: controller.selectedTypes,
|
||||
setSelectedTypes: controller.setSelectedTypes,
|
||||
selectedTypes: c.selectedTypes,
|
||||
setSelectedTypes: c.setSelectedTypes,
|
||||
})}
|
||||
selectedStatus={controller.selectedStatus}
|
||||
setSelectedStatus={controller.setSelectedStatus}
|
||||
selectedState={controller.selectedState}
|
||||
setSelectedState={controller.setSelectedState}
|
||||
tagOptions={controller.tags ?? []}
|
||||
selectedTags={controller.selectedTags}
|
||||
setSelectedTags={controller.setSelectedTags}
|
||||
onClearFilters={controller.handleClearFilters}
|
||||
selectedStatus={c.selectedStatus}
|
||||
setSelectedStatus={c.setSelectedStatus}
|
||||
selectedState={c.selectedState}
|
||||
setSelectedState={c.setSelectedState}
|
||||
tagOptions={c.tags ?? []}
|
||||
selectedTags={c.selectedTags}
|
||||
setSelectedTags={c.setSelectedTags}
|
||||
onClearFilters={c.handleClearFilters}
|
||||
/>
|
||||
<TextField
|
||||
placeholder={t("pages.uptime.filters.search.placeholder")}
|
||||
value={controller.search}
|
||||
value={c.search}
|
||||
onChange={(event) => {
|
||||
controller.setSearch(event.target.value);
|
||||
c.setSearch(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
{controller.selectedTags.length > 0 && (
|
||||
{c.selectedTags.length > 0 && (
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
alignItems={isSmall ? "flex-start" : "center"}
|
||||
@@ -108,8 +108,8 @@ export const MonitorListPage = ({
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{t("pages.uptime.filters.activeTags")}
|
||||
</Typography>
|
||||
{controller.selectedTags.map((tagId) => {
|
||||
const tag = controller.tags?.find((t) => t.id === tagId);
|
||||
{c.selectedTags.map((tagId) => {
|
||||
const tag = c.tags?.find((t) => t.id === tagId);
|
||||
if (!tag) return null;
|
||||
return (
|
||||
<ColoredLabel
|
||||
@@ -123,20 +123,20 @@ export const MonitorListPage = ({
|
||||
)}
|
||||
{bulkActions && !isLoading && (
|
||||
<BulkActionsBar
|
||||
selectedCount={controller.selectedRows.length}
|
||||
onCancel={controller.handleCancelSelection}
|
||||
selectedCount={c.selectedRows.length}
|
||||
onCancel={c.handleCancelSelection}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Play size={16} />}
|
||||
onClick={controller.handleBulkResume}
|
||||
onClick={c.handleBulkResume}
|
||||
>
|
||||
{t("common.buttons.resume")}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<Pause size={16} />}
|
||||
onClick={controller.handleBulkPause}
|
||||
onClick={c.handleBulkPause}
|
||||
>
|
||||
{t("common.buttons.pause")}
|
||||
</Button>
|
||||
@@ -144,12 +144,12 @@ export const MonitorListPage = ({
|
||||
)}
|
||||
{children}
|
||||
<Dialog
|
||||
open={controller.isDialogOpen}
|
||||
open={c.isDialogOpen}
|
||||
title={t("common.dialogs.delete.title")}
|
||||
content={t("common.dialogs.delete.description")}
|
||||
onConfirm={controller.handleConfirmDelete}
|
||||
onCancel={controller.handleCancelDelete}
|
||||
loading={controller.isDeleting}
|
||||
onConfirm={c.handleConfirmDelete}
|
||||
onCancel={c.handleCancelDelete}
|
||||
loading={c.isDeleting}
|
||||
/>
|
||||
</MonitorBasePageWithStates>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Icon } from "@/Components/design-elements";
|
||||
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Globe,
|
||||
Gauge,
|
||||
Link,
|
||||
@@ -27,20 +26,15 @@ export const getMenu = (t: Function) => {
|
||||
{
|
||||
group: t("components.sidebar.menu.groups.monitoring"),
|
||||
items: [
|
||||
{
|
||||
name: t("components.sidebar.menu.dashboard"),
|
||||
path: "dashboard",
|
||||
icon: <Icon icon={LayoutDashboard} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.uptime"),
|
||||
path: "uptime",
|
||||
icon: <Icon icon={Globe} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.docker"),
|
||||
path: "docker",
|
||||
icon: <Icon icon={Container} />,
|
||||
name: t("components.sidebar.menu.pagespeed"),
|
||||
path: "pagespeed",
|
||||
icon: <Icon icon={Gauge} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.infrastructure"),
|
||||
@@ -48,9 +42,9 @@ export const getMenu = (t: Function) => {
|
||||
icon: <Icon icon={Link} />,
|
||||
},
|
||||
{
|
||||
name: t("components.sidebar.menu.pagespeed"),
|
||||
path: "pagespeed",
|
||||
icon: <Icon icon={Gauge} />,
|
||||
name: t("components.sidebar.menu.docker"),
|
||||
path: "docker",
|
||||
icon: <Icon icon={Container} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -25,10 +25,11 @@ export const NavItem = ({
|
||||
const iconStroke = selected
|
||||
? theme.palette.sidebar.accent
|
||||
: theme.palette.text.secondary;
|
||||
|
||||
const buttonBgColor = selected ? theme.palette.action.selected : "transparent";
|
||||
const buttonBgHoverColor = selected
|
||||
? theme.palette.action.selectedHover
|
||||
: theme.palette.action.rowHover;
|
||||
? theme.palette.action.selected
|
||||
: theme.palette.action.hover;
|
||||
const fontWeight = selected ? 600 : 400;
|
||||
return (
|
||||
<Tooltip
|
||||
|
||||
@@ -73,11 +73,11 @@ export const Sidebar = () => {
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
borderRight={`1px solid ${theme.palette.divider}`}
|
||||
width={width}
|
||||
bgcolor={theme.palette.background.paper}
|
||||
sx={{
|
||||
touchAction: "none",
|
||||
transition: transition,
|
||||
zIndex: 1000,
|
||||
backdropFilter: "blur(8px)",
|
||||
}}
|
||||
>
|
||||
<List
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
|
||||
import type { CardId } from "@/Pages/Dashboard/cards";
|
||||
|
||||
type ThemeMode = "light" | "dark";
|
||||
type ChartType = "histogram" | "line";
|
||||
@@ -36,7 +35,6 @@ interface UIState {
|
||||
language: string;
|
||||
starPromptOpen: boolean;
|
||||
chartType: ChartType;
|
||||
dashboardCards: CardId[] | null;
|
||||
}
|
||||
|
||||
const initialMode: ThemeMode = window?.matchMedia?.("(prefers-color-scheme: dark)")
|
||||
@@ -76,7 +74,6 @@ const initialState: UIState = {
|
||||
language: "en",
|
||||
starPromptOpen: true,
|
||||
chartType: "histogram",
|
||||
dashboardCards: null,
|
||||
};
|
||||
|
||||
const uiSlice = createSlice({
|
||||
@@ -120,9 +117,6 @@ const uiSlice = createSlice({
|
||||
setChartType: (state, action: PayloadAction<ChartType>) => {
|
||||
state.chartType = action.payload;
|
||||
},
|
||||
setDashboardCards: (state, action: PayloadAction<CardId[] | null>) => {
|
||||
state.dashboardCards = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -139,5 +133,4 @@ export const {
|
||||
setLanguage,
|
||||
setStarPromptOpen,
|
||||
setChartType,
|
||||
setDashboardCards,
|
||||
} = uiSlice.actions;
|
||||
@@ -6,7 +6,7 @@ import { useState } from "react";
|
||||
import { useToast } from "@/Hooks/UseToast";
|
||||
import { logger } from "@/Utils/logger";
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
msg: string;
|
||||
data: T;
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
import { toast, type ToastOptions } from "react-toastify";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useToast = () => {
|
||||
return useMemo(() => {
|
||||
const showToast = (message: string, options?: ToastOptions) => {
|
||||
toast.dismiss();
|
||||
const baseStyle: React.CSSProperties = {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
};
|
||||
toast(message, {
|
||||
...options,
|
||||
style: { ...baseStyle, ...(options?.style || {}) },
|
||||
});
|
||||
const showToast = (message: string, options?: ToastOptions) => {
|
||||
toast.dismiss();
|
||||
const baseStyle: React.CSSProperties = {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
};
|
||||
toast(message, {
|
||||
...options,
|
||||
style: { ...baseStyle, ...(options?.style || {}) },
|
||||
});
|
||||
};
|
||||
|
||||
const toastSuccess = (msg: string, opts?: ToastOptions) =>
|
||||
showToast(msg, { ...opts, type: "success" });
|
||||
const toastError = (msg: string, opts?: ToastOptions) =>
|
||||
showToast(msg, { ...opts, type: "error" });
|
||||
const toastInfo = (msg: string, opts?: ToastOptions) =>
|
||||
showToast(msg, { ...opts, type: "info" });
|
||||
const toastWarning = (msg: string, opts?: ToastOptions) =>
|
||||
showToast(msg, { ...opts, type: "warning" });
|
||||
const toastSuccess = (msg: string, opts?: ToastOptions) =>
|
||||
showToast(msg, { ...opts, type: "success" });
|
||||
const toastError = (msg: string, opts?: ToastOptions) =>
|
||||
showToast(msg, { ...opts, type: "error" });
|
||||
const toastInfo = (msg: string, opts?: ToastOptions) =>
|
||||
showToast(msg, { ...opts, type: "info" });
|
||||
const toastWarning = (msg: string, opts?: ToastOptions) =>
|
||||
showToast(msg, { ...opts, type: "warning" });
|
||||
|
||||
return { showToast, toastSuccess, toastError, toastInfo, toastWarning };
|
||||
}, []);
|
||||
return { showToast, toastSuccess, toastError, toastInfo, toastWarning };
|
||||
};
|
||||
@@ -77,7 +77,6 @@ export const getMonitorDefaults = (
|
||||
...base,
|
||||
type: "docker",
|
||||
url: data?.url || "",
|
||||
dockerLogsEnabled: data?.dockerLogsEnabled ?? false,
|
||||
};
|
||||
break;
|
||||
case "game":
|
||||
|
||||
@@ -59,12 +59,7 @@ export const HeaderTeamControls = ({
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={
|
||||
<Icon
|
||||
icon={Mail}
|
||||
size={16}
|
||||
/>
|
||||
}
|
||||
startIcon={<Icon icon={Mail} />}
|
||||
onClick={onInviteClick}
|
||||
>
|
||||
{t("common.buttons.inviteMember")}
|
||||
@@ -74,12 +69,7 @@ export const HeaderTeamControls = ({
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={
|
||||
<Icon
|
||||
icon={UserPlus}
|
||||
size={16}
|
||||
/>
|
||||
}
|
||||
startIcon={<Icon icon={UserPlus} />}
|
||||
onClick={onAddMemberClick}
|
||||
>
|
||||
{t("common.buttons.addMember")}
|
||||
|
||||
@@ -118,8 +118,8 @@ const getGeneralSettingsConfig = (
|
||||
showDnsRecordType: false,
|
||||
},
|
||||
docker: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.dockerHost.label"),
|
||||
urlPlaceholder: t("pages.createMonitor.form.general.option.dockerHost.placeholder"),
|
||||
urlLabel: t("pages.createMonitor.form.general.option.container.label"),
|
||||
urlPlaceholder: t("pages.createMonitor.form.general.option.container.placeholder"),
|
||||
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
|
||||
showUrl: true,
|
||||
showProxy: false,
|
||||
@@ -255,7 +255,6 @@ const CreateMonitorPage = () => {
|
||||
const firstSegment = pathSegments[0];
|
||||
if (firstSegment === "pagespeed") return "pagespeed";
|
||||
if (firstSegment === "infrastructure") return "hardware";
|
||||
if (firstSegment === "docker") return "docker";
|
||||
return "uptime";
|
||||
}, [location.pathname]);
|
||||
|
||||
@@ -265,9 +264,7 @@ const CreateMonitorPage = () => {
|
||||
? "pagespeed"
|
||||
: pageType === "hardware"
|
||||
? "hardware"
|
||||
: pageType === "docker"
|
||||
? "docker"
|
||||
: "http";
|
||||
: "http";
|
||||
|
||||
const { data: existingMonitor, refetch: refetchMonitor } = useGet<Monitor>(
|
||||
isEditMode ? `/monitors/${monitorId}` : null
|
||||
@@ -376,8 +373,6 @@ const CreateMonitorPage = () => {
|
||||
navigate("/pagespeed");
|
||||
} else if (pageType === "hardware") {
|
||||
navigate("/infrastructure");
|
||||
} else if (pageType === "docker") {
|
||||
navigate("/docker");
|
||||
} else {
|
||||
navigate("/uptime");
|
||||
}
|
||||
@@ -828,19 +823,6 @@ const CreateMonitorPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showStep(1) && watchedType === "docker" && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.dockerLogs.title")}
|
||||
subtitle={t("pages.createMonitor.form.dockerLogs.description")}
|
||||
rightContent={
|
||||
<FormSwitchField
|
||||
name="dockerLogsEnabled"
|
||||
label={t("pages.createMonitor.form.dockerLogs.option.enabled.label")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showStep(2) &&
|
||||
(watchedType === "http" ||
|
||||
watchedType === "grpc" ||
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
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 { CardSlotContext } from "./CardSlotContext";
|
||||
|
||||
const SortableCard = ({ card }: { card: CardDefinition }) => {
|
||||
const theme = useTheme();
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
|
||||
useSortable({ id: card.id });
|
||||
const Component = card.component;
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={setNodeRef}
|
||||
minWidth={0}
|
||||
sx={{
|
||||
width: {
|
||||
xs: "100%",
|
||||
md:
|
||||
card.width >= GRID_COLUMNS
|
||||
? "100%"
|
||||
: `calc(${(card.width / GRID_COLUMNS) * 100}% - ${theme.spacing(LAYOUT.MD / 2)})`,
|
||||
},
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<CardSlotContext.Provider
|
||||
value={
|
||||
<Box
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
cursor: "grab",
|
||||
"&:active": { cursor: "grabbing" },
|
||||
touchAction: "none",
|
||||
}}
|
||||
>
|
||||
<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,137 +0,0 @@
|
||||
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, Button } from "@/Components/inputs";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { CardGroups, type CardDefinition, type CardGroup, type CardId } from "../cards";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cards are grouped by sidebar section, not by load cost: you look for a card
|
||||
* where you look for the feature it belongs to. Admin-only cards never reach
|
||||
* `available` for a non-admin, so nobody is offered a card they cannot load.
|
||||
*/
|
||||
export const CardPicker = ({
|
||||
open,
|
||||
available,
|
||||
selected,
|
||||
allSelected,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onSelectAll,
|
||||
onDeselectAll,
|
||||
onReset,
|
||||
onClose,
|
||||
}: CardPickerProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
return CardGroups.map((group: CardGroup) => ({
|
||||
group,
|
||||
cards: available.filter((card) => card.group === group),
|
||||
})).filter((entry) => entry.cards.length > 0);
|
||||
}, [available]);
|
||||
|
||||
const chosen = new Set(selected);
|
||||
const someSelected = selected.length > 0 && !allSelected;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
title={t("pages.dashboard.picker.title")}
|
||||
// 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")}
|
||||
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. Groups flow down the columns and
|
||||
* are kept whole so a group is never split across the gap.
|
||||
*/}
|
||||
<Box
|
||||
pt={theme.spacing(LAYOUT.SM)}
|
||||
sx={{
|
||||
columnCount: { xs: 1, sm: 2 },
|
||||
columnGap: theme.spacing(LAYOUT.XL),
|
||||
}}
|
||||
>
|
||||
{grouped.map(({ group, cards }) => (
|
||||
<Stack
|
||||
key={group}
|
||||
gap={theme.spacing(LAYOUT.XXS)}
|
||||
pb={theme.spacing(LAYOUT.MD)}
|
||||
sx={{ breakInside: "avoid" }}
|
||||
>
|
||||
<Typography
|
||||
variant="eyebrow"
|
||||
color={theme.palette.text.disabled}
|
||||
>
|
||||
{t(`pages.dashboard.picker.groups.${group}`, {
|
||||
defaultValue: group,
|
||||
})}
|
||||
</Typography>
|
||||
{cards.map((card) => (
|
||||
<FormControlLabel
|
||||
key={card.id}
|
||||
label={
|
||||
<Typography color={theme.palette.text.primary}>
|
||||
{t(`pages.dashboard.cards.${card.key}.title`)}
|
||||
</Typography>
|
||||
}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={chosen.has(card.id)}
|
||||
onChange={() =>
|
||||
chosen.has(card.id) ? onRemove(card.id) : onAdd(card.id)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
))}
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,275 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { Tooltip } from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* One thickness for every bar on the dashboard. Plain and segmented bars sit
|
||||
* side by side across cards, so differing weights read as an inconsistency
|
||||
* rather than as emphasis.
|
||||
*/
|
||||
const BAR_HEIGHT = 8;
|
||||
|
||||
export interface BarSegment {
|
||||
key: string;
|
||||
value: number;
|
||||
color: string;
|
||||
/** Tooltip text, since the segments themselves carry no labels. */
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A proportional bar. `value` and `max` share whatever unit the caller uses;
|
||||
* percentages come in as 0-100, not as the API's 0-1 fractions.
|
||||
*/
|
||||
export const CardBar = ({
|
||||
value,
|
||||
max = 100,
|
||||
color,
|
||||
}: {
|
||||
value: number;
|
||||
max?: number;
|
||||
color: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const ratio = max > 0 ? Math.min(Math.max(value / max, 0), 1) : 0;
|
||||
|
||||
return (
|
||||
<Box
|
||||
width="100%"
|
||||
height={BAR_HEIGHT}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
bgcolor={theme.palette.action.hover}
|
||||
sx={{ overflow: "hidden" }}
|
||||
>
|
||||
<Box
|
||||
width={`${ratio * 100}%`}
|
||||
height="100%"
|
||||
bgcolor={color}
|
||||
sx={{ transition: "width 0.3s ease" }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* One bar divided into proportional segments — the shape of a whole split into
|
||||
* parts, where the relative sizes are the point rather than any single value.
|
||||
*
|
||||
* Zero-value segments are dropped so they cannot appear as hairlines, and each
|
||||
* segment carries a tooltip because the bar itself has no room for labels.
|
||||
*/
|
||||
export const CardSegmentedBar = ({ segments }: { segments: BarSegment[] }) => {
|
||||
const theme = useTheme();
|
||||
const visible = segments.filter((segment) => segment.value > 0);
|
||||
const total = visible.reduce((sum, segment) => sum + segment.value, 0);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
width="100%"
|
||||
height={BAR_HEIGHT}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
bgcolor={theme.palette.action.hover}
|
||||
gap="1px"
|
||||
sx={{ overflow: "hidden" }}
|
||||
>
|
||||
{total > 0 &&
|
||||
visible.map((segment) => (
|
||||
<Tooltip
|
||||
key={segment.key}
|
||||
title={segment.label}
|
||||
>
|
||||
<Box
|
||||
width={`${(segment.value / total) * 100}%`}
|
||||
height="100%"
|
||||
bgcolor={segment.color}
|
||||
sx={{ transition: "width 0.3s ease" }}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
/** A clickable list row. Rows link to the entity they describe. */
|
||||
export const CardRow = ({ to, children }: { to?: string; children: ReactNode }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
{...(to ? { component: RouterLink, to } : {})}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
px={theme.spacing(LAYOUT.XS)}
|
||||
py={theme.spacing(LAYOUT.XS)}
|
||||
mx={`-${theme.spacing(LAYOUT.XS)}`}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
color="inherit"
|
||||
sx={{
|
||||
textDecoration: "none",
|
||||
...(to && {
|
||||
"&:hover": { backgroundColor: theme.palette.action.hover },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
/** Name plus optional secondary line, truncating rather than wrapping. */
|
||||
export const CardRowLabel = ({
|
||||
primary,
|
||||
secondary,
|
||||
}: {
|
||||
primary: string;
|
||||
secondary?: string | null;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
minWidth={0}
|
||||
flex={1}
|
||||
>
|
||||
<Typography
|
||||
color={theme.palette.text.primary}
|
||||
noWrap
|
||||
>
|
||||
{primary}
|
||||
</Typography>
|
||||
{secondary && (
|
||||
<Typography
|
||||
color={theme.palette.text.secondary}
|
||||
noWrap
|
||||
>
|
||||
{secondary}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
/** A single large figure with a caption — the shape of the stat cards. */
|
||||
export const CardFigure = ({
|
||||
value,
|
||||
caption,
|
||||
color,
|
||||
}: {
|
||||
value: string;
|
||||
caption?: string;
|
||||
color?: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(LAYOUT.XXS)}>
|
||||
<Typography
|
||||
fontSize={typographyLevels.xxl}
|
||||
fontWeight={300}
|
||||
color={color ?? theme.palette.text.primary}
|
||||
lineHeight={1.1}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
{caption && <Typography color={theme.palette.text.secondary}>{caption}</Typography>}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A label with its value on the right — the shape almost every stat card
|
||||
* repeats. `color` highlights the value alone, leaving the label quiet.
|
||||
*/
|
||||
export const CardStatLine = ({
|
||||
label,
|
||||
value,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
color?: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="baseline"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
>
|
||||
<Typography color={theme.palette.text.secondary}>{label}</Typography>
|
||||
<Typography
|
||||
color={color ?? theme.palette.text.primary}
|
||||
noWrap
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A CardStatLine with a proportional bar beneath it — the ranking and
|
||||
* breakdown row. `max` defaults to a percentage scale; pass the largest value
|
||||
* in the set to make bars relative to each other instead.
|
||||
*
|
||||
* `label` may be a node so callers can put a "N down" flag beside the name.
|
||||
*/
|
||||
export const CardMeterRow = ({
|
||||
label,
|
||||
value,
|
||||
max,
|
||||
color,
|
||||
trailing,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
value: number;
|
||||
max?: number;
|
||||
color: string;
|
||||
trailing?: ReactNode;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex={1}
|
||||
minWidth={0}
|
||||
gap={theme.spacing(LAYOUT.XXS)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
>
|
||||
{typeof label === "string" ? (
|
||||
<Typography
|
||||
color={theme.palette.text.primary}
|
||||
noWrap
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
{trailing}
|
||||
</Stack>
|
||||
<CardBar
|
||||
value={value}
|
||||
max={max}
|
||||
color={color}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export const CardSlotContext = createContext<ReactNode>(null);
|
||||
|
||||
export const useCardSlot = (): ReactNode => useContext(CardSlotContext);
|
||||
@@ -1,219 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { formatTimestamp } from "@/Utils/TimeUtils";
|
||||
import { DashboardCard, CardMessage } from "../DashboardCard";
|
||||
import { CardFigure, CardRow, CardRowLabel, CardStatLine } from "../CardPrimitives";
|
||||
|
||||
import { REFRESH_INTERVAL_MS, SLOW_REFRESH_INTERVAL_MS } from "../../cards";
|
||||
|
||||
import type { QueueMetrics } from "@/Types/Queue";
|
||||
import type { Log } from "@/Types/Log";
|
||||
import type { User } from "@/Types/User";
|
||||
|
||||
const MAX_ERROR_ROWS = 5;
|
||||
|
||||
/** Reads as a single reassurance line while nothing is failing. */
|
||||
export const ChecksOnScheduleCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useGet<QueueMetrics>(
|
||||
"/queue/metrics",
|
||||
{},
|
||||
{ refreshInterval: REFRESH_INTERVAL_MS }
|
||||
);
|
||||
|
||||
const healthy = data?.failingJobs === 0;
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.checksOnSchedule.title")}
|
||||
to="/logs"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
>
|
||||
{data && (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
{healthy ? (
|
||||
<CardMessage
|
||||
text={t("pages.dashboard.cards.checksOnSchedule.healthy", {
|
||||
jobs: data.jobs,
|
||||
workers: data.workers.length,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<CardFigure
|
||||
value={String(data.failingJobs)}
|
||||
caption={t("pages.dashboard.cards.checksOnSchedule.failing")}
|
||||
color={theme.palette.error.main}
|
||||
/>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
{data.jobsWithFailures.slice(0, MAX_ERROR_ROWS).map((job) => (
|
||||
<CardRow key={String(job.monitorId)}>
|
||||
<CardRowLabel
|
||||
primary={job.monitorUrl ?? String(job.monitorId)}
|
||||
secondary={job.failReason}
|
||||
/>
|
||||
<Typography
|
||||
color={theme.palette.error.main}
|
||||
flexShrink={0}
|
||||
>
|
||||
{job.failCount}
|
||||
</Typography>
|
||||
</CardRow>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.checksOnSchedule.scheduled")}
|
||||
value={String(data.jobs)}
|
||||
/>
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.checksOnSchedule.runningNow")}
|
||||
value={String(data.activeJobs)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* /logs is an in-memory ring buffer capped at 1000 entries, wiped on restart,
|
||||
* and it ignores page/rowsPerPage. It rotates fast, so this is deliberately
|
||||
* worded "recent" rather than "all".
|
||||
*/
|
||||
export const RecentErrorsCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useGet<Log[]>(
|
||||
"/logs",
|
||||
{},
|
||||
{ refreshInterval: SLOW_REFRESH_INTERVAL_MS }
|
||||
);
|
||||
|
||||
const { errors, warnings, newest } = useMemo(() => {
|
||||
const relevant = (data ?? []).filter(
|
||||
(entry) => entry.level === "error" || entry.level === "warn"
|
||||
);
|
||||
const sorted = [...relevant].sort(
|
||||
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
);
|
||||
return {
|
||||
errors: relevant.filter((entry) => entry.level === "error").length,
|
||||
warnings: relevant.filter((entry) => entry.level === "warn").length,
|
||||
newest: sorted.slice(0, MAX_ERROR_ROWS),
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.recentErrors.title")}
|
||||
to="/logs"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
action={
|
||||
errors > 0 ? (
|
||||
<Typography color={theme.palette.error.main}>
|
||||
{t("pages.dashboard.cards.recentErrors.count", { count: errors })}
|
||||
</Typography>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{newest.length === 0 ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.recentErrors.empty")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.recentErrors.warnings")}
|
||||
value={String(warnings)}
|
||||
/>
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
{newest.map((entry, index) => (
|
||||
<CardRow key={`${entry.timestamp}-${index}`}>
|
||||
<CardRowLabel
|
||||
primary={entry.message}
|
||||
secondary={[entry.service, entry.method].filter(Boolean).join(" · ")}
|
||||
/>
|
||||
<Typography
|
||||
color={
|
||||
entry.level === "error"
|
||||
? theme.palette.error.main
|
||||
: theme.palette.warning.main
|
||||
}
|
||||
flexShrink={0}
|
||||
>
|
||||
{formatTimestamp(entry.timestamp)}
|
||||
</Typography>
|
||||
</CardRow>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
|
||||
/** Never empty — at minimum the current user exists. */
|
||||
export const TeamCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useGet<User[]>("/auth/users");
|
||||
|
||||
const { total, byRole, unverified } = useMemo(() => {
|
||||
const users = data ?? [];
|
||||
const roles = new Map<string, number>();
|
||||
for (const user of users) {
|
||||
for (const role of user.role ?? []) {
|
||||
roles.set(role, (roles.get(role) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
return {
|
||||
total: users.length,
|
||||
byRole: [...roles.entries()].sort((a, b) => b[1] - a[1]),
|
||||
unverified: users.filter((user) => !user.isVerified).length,
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.team.title")}
|
||||
to="/account/team"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
>
|
||||
{data && (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
<CardFigure
|
||||
value={String(total)}
|
||||
caption={t("pages.dashboard.cards.team.members", { count: total })}
|
||||
/>
|
||||
{byRole.map(([role, count]) => (
|
||||
<CardStatLine
|
||||
key={role}
|
||||
label={t(`common.auth.roles.${role}`, { defaultValue: role })}
|
||||
value={String(count)}
|
||||
/>
|
||||
))}
|
||||
{unverified > 0 && (
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.team.unverified")}
|
||||
value={String(unverified)}
|
||||
color={theme.palette.warning.main}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { DashboardCard, CardMessage } from "../DashboardCard";
|
||||
import { CardMeterRow, CardRow } from "../CardPrimitives";
|
||||
|
||||
export interface BreakdownBucket {
|
||||
/** Stable key and displayed label. */
|
||||
key: string;
|
||||
label: string;
|
||||
total: number;
|
||||
down: number;
|
||||
/** Where the row links to, if anywhere. */
|
||||
to?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* "How the fleet splits by X" — one proportional row per bucket, flagging any
|
||||
* bucket with something down. Monitors by type and Monitors by group are the
|
||||
* same card over different keys, so they share this shell.
|
||||
*/
|
||||
export const BreakdownCard = ({
|
||||
titleKey,
|
||||
emptyKey,
|
||||
to,
|
||||
buckets,
|
||||
isLoading,
|
||||
isStale,
|
||||
error,
|
||||
}: {
|
||||
titleKey: string;
|
||||
emptyKey: string;
|
||||
to?: string;
|
||||
buckets: BreakdownBucket[];
|
||||
isLoading?: boolean;
|
||||
isStale?: boolean;
|
||||
error?: unknown;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const max = Math.max(0, ...buckets.map((bucket) => bucket.total));
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t(titleKey)}
|
||||
to={to}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
isStale={isStale}
|
||||
>
|
||||
{buckets.length === 0 ? (
|
||||
<CardMessage text={t(emptyKey)} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
{buckets.map((bucket) => (
|
||||
<CardRow
|
||||
key={bucket.key}
|
||||
to={bucket.to}
|
||||
>
|
||||
<CardMeterRow
|
||||
label={bucket.label}
|
||||
value={bucket.total}
|
||||
max={max}
|
||||
color={
|
||||
bucket.down > 0 ? theme.palette.error.main : theme.palette.primary.main
|
||||
}
|
||||
trailing={
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
flexShrink={0}
|
||||
>
|
||||
{bucket.down > 0 && (
|
||||
<Typography color={theme.palette.error.main}>
|
||||
{t("pages.dashboard.cards.monitorsByType.down", {
|
||||
count: bucket.down,
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{bucket.total}
|
||||
</Typography>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
</CardRow>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,143 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { DashboardCard, CardMessage } from "../DashboardCard";
|
||||
import { CardBar, CardRow, CardRowLabel } from "../CardPrimitives";
|
||||
import { useMonitors } from "../../useDashboardData";
|
||||
|
||||
import { latestCheck } from "../../checks";
|
||||
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
// <60% fine, 60-85% warm, above that hot.
|
||||
const WARM = 60;
|
||||
const HOT = 85;
|
||||
|
||||
interface ServerUsage {
|
||||
monitor: Monitor;
|
||||
cpu: number | null;
|
||||
memory: number | null;
|
||||
disk: number | null;
|
||||
peak: number;
|
||||
}
|
||||
|
||||
const Metric = ({ label, value }: { label: string; value: number | null }) => {
|
||||
const theme = useTheme();
|
||||
const color =
|
||||
value === null
|
||||
? theme.palette.text.secondary
|
||||
: value > HOT
|
||||
? theme.palette.error.main
|
||||
: value > WARM
|
||||
? theme.palette.warning.main
|
||||
: theme.palette.primary.main;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex={1}
|
||||
minWidth={0}
|
||||
gap={theme.spacing(LAYOUT.XXS)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="baseline"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<Typography color={theme.palette.text.secondary}>{label}</Typography>
|
||||
<Typography color={color}>
|
||||
{/* A down server reports nothing — show a dash, never 0%. */}
|
||||
{value === null ? "—" : `${value.toFixed(1)}%`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<CardBar
|
||||
value={value ?? 0}
|
||||
color={color}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const BusiestServersCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useMonitors();
|
||||
|
||||
const servers = useMemo<ServerUsage[]>(() => {
|
||||
return (data?.monitors ?? [])
|
||||
.filter((monitor) => monitor.type === "hardware")
|
||||
.map((monitor) => {
|
||||
const latest = latestCheck(monitor);
|
||||
// snake_case here, unlike camelCase in HardwareCheckStats, and
|
||||
// these are 0-1 fractions, so scale to percent.
|
||||
const toPercent = (value: number | undefined) =>
|
||||
typeof value === "number" ? value * 100 : null;
|
||||
const cpu = toPercent(latest?.cpu?.usage_percent);
|
||||
const memory = toPercent(latest?.memory?.usage_percent);
|
||||
const disks = (latest?.disk ?? [])
|
||||
.map((entry) => entry.usage_percent)
|
||||
.filter((value): value is number => typeof value === "number");
|
||||
const disk = disks.length > 0 ? Math.max(...disks) * 100 : null;
|
||||
return {
|
||||
monitor,
|
||||
cpu,
|
||||
memory,
|
||||
disk,
|
||||
peak: Math.max(cpu ?? 0, memory ?? 0, disk ?? 0),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.peak - a.peak);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.busiestServers.title")}
|
||||
to="/infrastructure"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
>
|
||||
{servers.length === 0 ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.busiestServers.empty")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
{servers.map(({ monitor, cpu, memory, disk }) => (
|
||||
<CardRow
|
||||
key={monitor.id}
|
||||
to={`/infrastructure/${monitor.id}`}
|
||||
>
|
||||
<Stack
|
||||
flex={1}
|
||||
minWidth={0}
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<CardRowLabel primary={monitor.name} />
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
>
|
||||
<Metric
|
||||
label={t("pages.dashboard.cards.busiestServers.cpu")}
|
||||
value={cpu}
|
||||
/>
|
||||
<Metric
|
||||
label={t("pages.dashboard.cards.busiestServers.memory")}
|
||||
value={memory}
|
||||
/>
|
||||
<Metric
|
||||
label={t("pages.dashboard.cards.busiestServers.disk")}
|
||||
value={disk}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CardRow>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { formatDuration } from "@/Utils/TimeUtils";
|
||||
import { DashboardCard } from "../DashboardCard";
|
||||
import { CardBar, CardStatLine } from "../CardPrimitives";
|
||||
|
||||
import { REFRESH_INTERVAL_MS } from "../../cards";
|
||||
|
||||
import type { Diagnostics } from "@/Types/Diagnostics";
|
||||
|
||||
const BYTES_PER_MB = 1024 * 1024;
|
||||
|
||||
// Banding from CARD-SPECS: warm at 50ms of event-loop delay, hot at 200ms;
|
||||
// heap warm past 80% of the V8 limit.
|
||||
const EVENT_LOOP_WARM_MS = 50;
|
||||
const EVENT_LOOP_HOT_MS = 200;
|
||||
const HEAP_WARM_RATIO = 0.8;
|
||||
|
||||
/**
|
||||
* The one card that is never empty — it reports on Checkmate itself, so it
|
||||
* works on a fresh install with zero monitors. Admin only.
|
||||
*/
|
||||
export const CheckmateServerCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useGet<Diagnostics>(
|
||||
"/diagnostic/system",
|
||||
{},
|
||||
{ refreshInterval: REFRESH_INTERVAL_MS }
|
||||
);
|
||||
|
||||
const usedHeap = data?.v8HeapStats.usedHeapSizeBytes ?? 0;
|
||||
const heapLimit = data?.v8HeapStats.heapSizeLimitBytes ?? 0;
|
||||
const heapRatio = heapLimit > 0 ? usedHeap / heapLimit : 0;
|
||||
const heapColor =
|
||||
heapRatio > HEAP_WARM_RATIO ? theme.palette.warning.main : theme.palette.primary.main;
|
||||
|
||||
const eventLoopMs = data?.eventLoopDelayMs ?? 0;
|
||||
const eventLoopColor =
|
||||
eventLoopMs > EVENT_LOOP_HOT_MS
|
||||
? theme.palette.error.main
|
||||
: eventLoopMs > EVENT_LOOP_WARM_MS
|
||||
? theme.palette.warning.main
|
||||
: theme.palette.text.primary;
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.checkmateServer.title")}
|
||||
to="/logs"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
>
|
||||
{data && (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
<Stack gap={theme.spacing(LAYOUT.XXS)}>
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.checkmateServer.memory")}
|
||||
value={`${Math.round(usedHeap / BYTES_PER_MB)} / ${Math.round(
|
||||
heapLimit / BYTES_PER_MB
|
||||
)} MB`}
|
||||
color={heapColor}
|
||||
/>
|
||||
<CardBar
|
||||
value={heapRatio * 100}
|
||||
color={heapColor}
|
||||
/>
|
||||
</Stack>
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.checkmateServer.eventLoop")}
|
||||
value={`${eventLoopMs.toFixed(1)} ms`}
|
||||
color={eventLoopColor}
|
||||
/>
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.checkmateServer.uptime")}
|
||||
value={formatDuration(data.uptimeMs)}
|
||||
/>
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.checkmateServer.database")}
|
||||
value={t("pages.dashboard.cards.checkmateServer.readsWrites", {
|
||||
reads: data.mongoStats.readsPerSecond.toFixed(1),
|
||||
writes: data.mongoStats.writesPerSecond.toFixed(1),
|
||||
})}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,157 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Dot } from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { getStatusColor, getMonitorPath } from "@/Utils/MonitorUtils";
|
||||
import { formatStatusCode } from "@/Utils/statusCode";
|
||||
import { formatDuration } from "@/Utils/TimeUtils";
|
||||
import { DashboardCard, CardMessage } from "../DashboardCard";
|
||||
import { CardRow } from "../CardPrimitives";
|
||||
import { Sparkline } from "../Sparkline";
|
||||
import { useMonitors } from "../../useDashboardData";
|
||||
|
||||
import { latestCheck } from "../../checks";
|
||||
|
||||
import type { Monitor, MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
const SPARKLINE_WIDTH = 80;
|
||||
|
||||
const STATUS_RANK: Partial<Record<MonitorStatus, number>> = {
|
||||
down: 1,
|
||||
breached: 2,
|
||||
};
|
||||
|
||||
const isProblem = (monitor: Monitor) => STATUS_RANK[monitor.status] !== undefined;
|
||||
|
||||
/**
|
||||
* How long the monitor has been down, from the oldest consecutive failing
|
||||
* check. recentChecks is oldest-first, so the run is walked back from the end.
|
||||
*/
|
||||
const downSince = (monitor: Monitor): string | null => {
|
||||
const checks = monitor.recentChecks ?? [];
|
||||
let oldestFailing: string | null = null;
|
||||
for (let index = checks.length - 1; index >= 0; index -= 1) {
|
||||
if (checks[index].status !== false) {
|
||||
break;
|
||||
}
|
||||
oldestFailing = checks[index].createdAt;
|
||||
}
|
||||
return oldestFailing;
|
||||
};
|
||||
|
||||
export const CurrentlyDownCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useMonitors();
|
||||
|
||||
const problems = useMemo(() => {
|
||||
const monitors = data?.monitors ?? [];
|
||||
return monitors
|
||||
.filter(isProblem)
|
||||
.map((monitor) => ({ monitor, since: downSince(monitor) }))
|
||||
.sort((a, b) => {
|
||||
const rank =
|
||||
(STATUS_RANK[a.monitor.status] ?? 99) - (STATUS_RANK[b.monitor.status] ?? 99);
|
||||
if (rank !== 0) {
|
||||
return rank;
|
||||
}
|
||||
// Tie-break on longest downtime: the older timestamp ranks first.
|
||||
return new Date(a.since ?? 0).getTime() - new Date(b.since ?? 0).getTime();
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.currentlyDown.title")}
|
||||
to="/incidents"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
action={
|
||||
problems.length > 0 ? (
|
||||
<Typography color={theme.palette.error.main}>
|
||||
{t("pages.dashboard.cards.currentlyDown.count", {
|
||||
count: problems.length,
|
||||
})}
|
||||
</Typography>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{problems.length === 0 ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.currentlyDown.empty")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
{problems.map(({ monitor, since }) => (
|
||||
<CardRow
|
||||
key={monitor.id}
|
||||
to={`/${getMonitorPath(monitor.type)}/${monitor.id}`}
|
||||
>
|
||||
<Dot
|
||||
color={getStatusColor(monitor.status, theme)}
|
||||
size="md"
|
||||
/>
|
||||
<Stack
|
||||
flex={1}
|
||||
minWidth={0}
|
||||
gap={theme.spacing(LAYOUT.XXS)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="baseline"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
>
|
||||
<Typography
|
||||
color={theme.palette.text.primary}
|
||||
noWrap
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
{since && (
|
||||
<Typography
|
||||
color={theme.palette.error.main}
|
||||
flexShrink={0}
|
||||
>
|
||||
{formatDuration(Date.now() - new Date(since).getTime())}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
>
|
||||
{/* Not StatusCodeLabel: its Tooltip is focusable, and a
|
||||
focusable element inside the row's link is unreachable
|
||||
by keyboard. */}
|
||||
<Typography
|
||||
color={theme.palette.text.secondary}
|
||||
noWrap
|
||||
>
|
||||
{monitor.type} ·{" "}
|
||||
{formatStatusCode(latestCheck(monitor)?.statusCode, t)}
|
||||
</Typography>
|
||||
<Box
|
||||
width={SPARKLINE_WIDTH}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Sparkline
|
||||
checks={monitor.recentChecks ?? []}
|
||||
color={theme.palette.error.main}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CardRow>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { DashboardCard, CardMessage } from "../DashboardCard";
|
||||
import { CardBar, CardFigure } from "../CardPrimitives";
|
||||
|
||||
import type { ChecksSummary } from "@/Types/Check";
|
||||
|
||||
// Day only. `dateRange=week` returns figures identical to `day`, so a range
|
||||
// selector here would silently lie.
|
||||
const SUMMARY_URL = "/checks/team/summary?dateRange=day";
|
||||
|
||||
export const FailedChecksCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useGet<ChecksSummary>(SUMMARY_URL);
|
||||
|
||||
const total = data?.totalChecks ?? 0;
|
||||
const failed = data?.downChecks ?? 0;
|
||||
const percentage = total > 0 ? (failed / total) * 100 : 0;
|
||||
const color = percentage > 10 ? theme.palette.error.main : theme.palette.success.main;
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.failedChecks.title")}
|
||||
to="/checks"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
>
|
||||
{total === 0 ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.failedChecks.empty")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
<CardFigure
|
||||
value={`${percentage.toFixed(1)}%`}
|
||||
caption={t("pages.dashboard.cards.failedChecks.ratio", {
|
||||
failed: failed.toLocaleString(),
|
||||
total: total.toLocaleString(),
|
||||
})}
|
||||
color={color}
|
||||
/>
|
||||
<CardBar
|
||||
value={percentage}
|
||||
color={color}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Dot } from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import { getStatusColor } from "@/Utils/MonitorUtils";
|
||||
import { DashboardCard, CardMessage } from "../DashboardCard";
|
||||
import { CardSegmentedBar, type BarSegment } from "../CardPrimitives";
|
||||
import { useMonitors } from "../../useDashboardData";
|
||||
|
||||
import type { MonitorStatus, MonitorsSummary } from "@/Types/Monitor";
|
||||
|
||||
// Every state the summary reports, ordered worst first so the bar reads
|
||||
// problems-to-healthy left to right and the chips below list what is wrong
|
||||
// before what is merely idle. `up` is handled separately as the headline.
|
||||
const STATES: { key: string; status: MonitorStatus; field: keyof MonitorsSummary }[] = [
|
||||
{ key: "down", status: "down", field: "downMonitors" },
|
||||
{ key: "breached", status: "breached", field: "breachedMonitors" },
|
||||
{ key: "initializing", status: "initializing", field: "initializingMonitors" },
|
||||
{ key: "maintenance", status: "maintenance", field: "maintenanceMonitors" },
|
||||
{ key: "paused", status: "paused", field: "pausedMonitors" },
|
||||
];
|
||||
|
||||
export const MonitorStatusCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useMonitors();
|
||||
const summary = data?.summary ?? null;
|
||||
|
||||
// Only states that actually have monitors are shown. Padding the card with
|
||||
// zeros says nothing, and a healthy fleet should read as calm rather than as
|
||||
// a row of empty counters.
|
||||
const present = summary
|
||||
? STATES.map((state) => ({ ...state, count: summary[state.field] })).filter(
|
||||
(state) => state.count > 0
|
||||
)
|
||||
: [];
|
||||
|
||||
const segments: BarSegment[] = summary
|
||||
? [
|
||||
{
|
||||
key: "up",
|
||||
value: summary.upMonitors,
|
||||
color: getStatusColor("up", theme),
|
||||
label: `${t("pages.common.monitors.status.up")} — ${summary.upMonitors}`,
|
||||
},
|
||||
...present.map((state) => ({
|
||||
key: state.key,
|
||||
value: state.count,
|
||||
color: getStatusColor(state.status, theme),
|
||||
label: `${t(`pages.common.monitors.status.${state.key}`)} — ${state.count}`,
|
||||
})),
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.monitorStatus.title")}
|
||||
to="/uptime"
|
||||
isLoading={isLoading && !summary}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
>
|
||||
{!summary || summary.totalMonitors === 0 ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.monitorStatus.empty")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="baseline"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
<Typography
|
||||
fontSize={typographyLevels.xxl}
|
||||
fontWeight={300}
|
||||
color={theme.palette.text.primary}
|
||||
lineHeight={1.1}
|
||||
>
|
||||
{summary.upMonitors}
|
||||
</Typography>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{t("pages.dashboard.cards.monitorStatus.upOfTotal", {
|
||||
total: summary.totalMonitors,
|
||||
})}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<CardSegmentedBar segments={segments} />
|
||||
</Stack>
|
||||
{present.length > 0 && (
|
||||
<Stack
|
||||
direction="row"
|
||||
flexWrap="wrap"
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
>
|
||||
{present.map((state) => (
|
||||
<Stack
|
||||
key={state.key}
|
||||
component={RouterLink}
|
||||
// Plain /uptime: the monitors list does not read query
|
||||
// params, so a ?status= filter would be silently dropped
|
||||
// and the link would lie about where it goes.
|
||||
to="/uptime"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
px={theme.spacing(LAYOUT.XS)}
|
||||
py={theme.spacing(LAYOUT.XXS)}
|
||||
mx={`-${theme.spacing(LAYOUT.XS)}`}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{
|
||||
textDecoration: "none",
|
||||
"&:hover": { backgroundColor: theme.palette.action.hover },
|
||||
}}
|
||||
>
|
||||
<Dot
|
||||
color={getStatusColor(state.status, theme)}
|
||||
size="md"
|
||||
/>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{t(`pages.common.monitors.status.${state.key}`)}
|
||||
</Typography>
|
||||
<Typography color={theme.palette.text.primary}>
|
||||
{state.count}
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { BreakdownCard, type BreakdownBucket } from "./BreakdownCard";
|
||||
import { useMonitors } from "../../useDashboardData";
|
||||
|
||||
export const MonitorsByGroupCard = () => {
|
||||
const { data, isLoading, isValidating, error } = useMonitors();
|
||||
|
||||
// `group` is a plain string | null on the monitor — there is no groups
|
||||
// endpoint, so the set is whatever monitors actually declare.
|
||||
const buckets = useMemo<BreakdownBucket[]>(() => {
|
||||
const byGroup = new Map<string, BreakdownBucket>();
|
||||
for (const monitor of data?.monitors ?? []) {
|
||||
if (!monitor.group) {
|
||||
continue;
|
||||
}
|
||||
const bucket = byGroup.get(monitor.group) ?? {
|
||||
key: monitor.group,
|
||||
label: monitor.group,
|
||||
total: 0,
|
||||
down: 0,
|
||||
};
|
||||
bucket.total += 1;
|
||||
if (monitor.status === "down" || monitor.status === "breached") {
|
||||
bucket.down += 1;
|
||||
}
|
||||
byGroup.set(monitor.group, bucket);
|
||||
}
|
||||
return [...byGroup.values()].sort((a, b) => b.total - a.total);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<BreakdownCard
|
||||
titleKey="pages.dashboard.cards.monitorsByGroup.title"
|
||||
emptyKey="pages.dashboard.cards.monitorsByGroup.empty"
|
||||
to="/uptime"
|
||||
buckets={buckets}
|
||||
isLoading={isLoading && !data}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { getMonitorPath } from "@/Utils/MonitorUtils";
|
||||
import { BreakdownCard, type BreakdownBucket } from "./BreakdownCard";
|
||||
import { useMonitors } from "../../useDashboardData";
|
||||
|
||||
import type { MonitorType } from "@/Types/Monitor";
|
||||
|
||||
export const MonitorsByTypeCard = () => {
|
||||
const { data, isLoading, isValidating, error } = useMonitors();
|
||||
|
||||
const buckets = useMemo<BreakdownBucket[]>(() => {
|
||||
const byType = new Map<MonitorType, BreakdownBucket>();
|
||||
for (const monitor of data?.monitors ?? []) {
|
||||
const bucket = byType.get(monitor.type) ?? {
|
||||
key: monitor.type,
|
||||
label: monitor.type,
|
||||
total: 0,
|
||||
down: 0,
|
||||
to: `/${getMonitorPath(monitor.type)}`,
|
||||
};
|
||||
bucket.total += 1;
|
||||
if (monitor.status === "down" || monitor.status === "breached") {
|
||||
bucket.down += 1;
|
||||
}
|
||||
byType.set(monitor.type, bucket);
|
||||
}
|
||||
return [...byType.values()].sort((a, b) => b.total - a.total);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<BreakdownCard
|
||||
titleKey="pages.dashboard.cards.monitorsByType.title"
|
||||
emptyKey="pages.dashboard.cards.monitorsByType.empty"
|
||||
to="/uptime"
|
||||
buckets={buckets}
|
||||
isLoading={isLoading && !data}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,175 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { getMonitorPath, getUptimePercentageColor } from "@/Utils/MonitorUtils";
|
||||
import { DashboardCard, CardMessage } from "../DashboardCard";
|
||||
import { CardMeterRow, CardRow, CardRowLabel } from "../CardPrimitives";
|
||||
import { useMonitors } from "../../useDashboardData";
|
||||
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
const TOP_N = 5;
|
||||
// A ranking of one is not a ranking.
|
||||
const MIN_RANKABLE = 2;
|
||||
|
||||
const averageResponseTime = (monitor: Monitor): number | null => {
|
||||
const times = (monitor.recentChecks ?? [])
|
||||
.map((check) => check.responseTime)
|
||||
.filter((value): value is number => typeof value === "number");
|
||||
if (times.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return times.reduce((sum, value) => sum + value, 0) / times.length;
|
||||
};
|
||||
|
||||
/** Header reads "top 5 of 12" so a truncated ranking says so. */
|
||||
const RankingCount = ({ shown, total }: { shown: number; total: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{t("pages.dashboard.cards.ranking.topOf", { shown, total })}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export const SlowestMonitorsCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useMonitors();
|
||||
|
||||
const ranked = useMemo(() => {
|
||||
return (data?.monitors ?? [])
|
||||
.filter((monitor) => monitor.status !== "paused")
|
||||
.map((monitor) => ({ monitor, average: averageResponseTime(monitor) }))
|
||||
.filter(
|
||||
(entry): entry is { monitor: Monitor; average: number } => entry.average !== null
|
||||
)
|
||||
.sort((a, b) => b.average - a.average);
|
||||
}, [data]);
|
||||
|
||||
const slowest = ranked[0]?.average ?? 0;
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.slowestMonitors.title")}
|
||||
to="/uptime"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
action={
|
||||
ranked.length >= MIN_RANKABLE ? (
|
||||
<RankingCount
|
||||
shown={Math.min(TOP_N, ranked.length)}
|
||||
total={ranked.length}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{ranked.length < MIN_RANKABLE ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.ranking.notEnough")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
{ranked.slice(0, TOP_N).map(({ monitor, average }) => (
|
||||
<CardRow
|
||||
key={monitor.id}
|
||||
to={`/${getMonitorPath(monitor.type)}/${monitor.id}`}
|
||||
>
|
||||
<CardMeterRow
|
||||
label={<CardRowLabel primary={monitor.name} />}
|
||||
value={average}
|
||||
max={slowest}
|
||||
color={theme.palette.primary.main}
|
||||
trailing={
|
||||
<Typography
|
||||
color={theme.palette.text.secondary}
|
||||
flexShrink={0}
|
||||
>
|
||||
{Math.round(average)} ms
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</CardRow>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
|
||||
export const LowestUptimeCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useMonitors();
|
||||
|
||||
const ranked = useMemo(() => {
|
||||
return (
|
||||
(data?.monitors ?? [])
|
||||
// Paused monitors keep whatever uptime they had, or zero — a paused
|
||||
// ping monitor reads 0% and would rank beside a genuinely down one.
|
||||
// Neither is a live problem, so neither belongs in a worst-uptime list.
|
||||
.filter((monitor) => monitor.status !== "paused")
|
||||
.filter(
|
||||
(monitor): monitor is Monitor & { uptimePercentage: number } =>
|
||||
typeof monitor.uptimePercentage === "number"
|
||||
)
|
||||
.sort((a, b) => a.uptimePercentage - b.uptimePercentage)
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.lowestUptime.title")}
|
||||
to="/uptime"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
action={
|
||||
ranked.length >= MIN_RANKABLE ? (
|
||||
<RankingCount
|
||||
shown={Math.min(TOP_N, ranked.length)}
|
||||
total={ranked.length}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{ranked.length < MIN_RANKABLE ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.ranking.notEnough")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
{ranked.slice(0, TOP_N).map((monitor) => {
|
||||
// uptimePercentage is a 0-1 fraction, not 0-100.
|
||||
const percentage = monitor.uptimePercentage * 100;
|
||||
const palette = getUptimePercentageColor(monitor.uptimePercentage);
|
||||
return (
|
||||
<CardRow
|
||||
key={monitor.id}
|
||||
to={`/${getMonitorPath(monitor.type)}/${monitor.id}`}
|
||||
>
|
||||
<CardMeterRow
|
||||
label={<CardRowLabel primary={monitor.name} />}
|
||||
// The card ranks worst uptime first, so the bar shows the
|
||||
// shortfall - the worst monitor gets the longest bar.
|
||||
value={100 - percentage}
|
||||
color={theme.palette[palette].main}
|
||||
trailing={
|
||||
<Typography
|
||||
color={theme.palette[palette].main}
|
||||
flexShrink={0}
|
||||
>
|
||||
{percentage.toFixed(1)}%
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</CardRow>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { Dot } from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { formatDuration, formatTimestamp } from "@/Utils/TimeUtils";
|
||||
import { DashboardCard, CardMessage } from "../DashboardCard";
|
||||
import { CardRow, CardRowLabel } from "../CardPrimitives";
|
||||
|
||||
import type { IncidentSummary, IncidentSummaryItem } from "@/Types/Incident";
|
||||
|
||||
const MAX_ROWS = 5;
|
||||
|
||||
/** Open incidents have no end time; closed ones report how long they ran. */
|
||||
const incidentDuration = (incident: IncidentSummaryItem): string | null => {
|
||||
if (!incident.endTime) {
|
||||
return null;
|
||||
}
|
||||
const ms =
|
||||
new Date(incident.endTime).getTime() - new Date(incident.startTime).getTime();
|
||||
return ms > 0 ? formatDuration(ms) : null;
|
||||
};
|
||||
|
||||
export const RecentIncidentsCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useGet<IncidentSummary>(
|
||||
"/incidents/team/summary"
|
||||
);
|
||||
|
||||
const incidents = (data?.latestIncidents ?? []).slice(0, MAX_ROWS);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.recentIncidents.title")}
|
||||
to="/incidents"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
action={
|
||||
data && data.totalActive > 0 ? (
|
||||
<Typography color={theme.palette.error.main}>
|
||||
{t("pages.dashboard.cards.recentIncidents.active", {
|
||||
count: data.totalActive,
|
||||
})}
|
||||
</Typography>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{incidents.length === 0 ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.recentIncidents.empty")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
{incidents.map((incident) => {
|
||||
const isOpen = incident.status === true;
|
||||
const duration = incidentDuration(incident);
|
||||
return (
|
||||
<CardRow
|
||||
key={incident.id}
|
||||
to="/incidents"
|
||||
>
|
||||
<Dot
|
||||
color={isOpen ? theme.palette.error.light : theme.palette.success.light}
|
||||
size="md"
|
||||
/>
|
||||
<CardRowLabel
|
||||
primary={
|
||||
incident.monitorName ??
|
||||
t("pages.dashboard.cards.recentIncidents.unknownMonitor")
|
||||
}
|
||||
secondary={formatTimestamp(incident.startTime)}
|
||||
/>
|
||||
<Typography
|
||||
color={theme.palette.text.secondary}
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
{isOpen
|
||||
? t("pages.dashboard.cards.recentIncidents.ongoing")
|
||||
: (duration ?? "—")}
|
||||
</Typography>
|
||||
{incident.resolutionType === "manual" && (
|
||||
<Typography
|
||||
color={theme.palette.warning.main}
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
{t("pages.dashboard.cards.recentIncidents.manual")}
|
||||
</Typography>
|
||||
)}
|
||||
</CardRow>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,258 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { Dot } from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { formatTimestamp } from "@/Utils/TimeUtils";
|
||||
import { DashboardCard, CardMessage } from "../DashboardCard";
|
||||
import {
|
||||
CardFigure,
|
||||
CardRow,
|
||||
CardRowLabel,
|
||||
CardSegmentedBar,
|
||||
CardStatLine,
|
||||
} from "../CardPrimitives";
|
||||
|
||||
import type { IncidentSummary } from "@/Types/Incident";
|
||||
import type { Notification } from "@/Types/Notification";
|
||||
import type { MaintenanceWindow } from "@/Types/MaintenanceWindow";
|
||||
import type { StatusPage } from "@/Types/StatusPage";
|
||||
|
||||
/** Free when Recent incidents is present — same summary call, shared by SWR. */
|
||||
export const IncidentStatsCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useGet<IncidentSummary>(
|
||||
"/incidents/team/summary"
|
||||
);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.incidentStats.title")}
|
||||
to="/incidents"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
>
|
||||
{data && (
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
{/* Open now leads, paired with the all-time total for context: two
|
||||
open means something different against 12 incidents than 183. */}
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(LAYOUT.XXL)}
|
||||
>
|
||||
<CardFigure
|
||||
value={String(data.totalActive)}
|
||||
caption={t("pages.dashboard.cards.incidentStats.openNow")}
|
||||
color={
|
||||
data.totalActive > 0
|
||||
? theme.palette.error.main
|
||||
: theme.palette.success.main
|
||||
}
|
||||
/>
|
||||
<CardFigure
|
||||
value={data.total.toLocaleString()}
|
||||
caption={t("pages.dashboard.cards.incidentStats.total")}
|
||||
/>
|
||||
</Stack>
|
||||
{/* How much of the fleet heals itself — the automatic/manual split
|
||||
is already computed server-side and said nothing on the card
|
||||
before. A high manual share means people are doing the work. */}
|
||||
{data.totalAutomaticResolutions + data.totalManualResolutions > 0 && (
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<CardSegmentedBar
|
||||
segments={[
|
||||
{
|
||||
key: "automatic",
|
||||
value: data.totalAutomaticResolutions,
|
||||
color: theme.palette.success.main,
|
||||
label: `${t("pages.dashboard.cards.incidentStats.automatic")} — ${data.totalAutomaticResolutions}`,
|
||||
},
|
||||
{
|
||||
key: "manual",
|
||||
value: data.totalManualResolutions,
|
||||
color: theme.palette.warning.main,
|
||||
label: `${t("pages.dashboard.cards.incidentStats.manual")} — ${data.totalManualResolutions}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.incidentStats.resolved")}
|
||||
value={t("pages.dashboard.cards.incidentStats.autoManual", {
|
||||
automatic: data.totalAutomaticResolutions,
|
||||
manual: data.totalManualResolutions,
|
||||
})}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.incidentStats.avgResolution")}
|
||||
value={t("pages.dashboard.cards.incidentStats.hours", {
|
||||
hours: data.avgResolutionTimeHours.toFixed(1),
|
||||
})}
|
||||
/>
|
||||
{data.topMonitor?.monitorName && (
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.incidentStats.mostIncidents")}
|
||||
value={`${data.topMonitor.monitorName} (${data.topMonitor.incidentCount})`}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
|
||||
export const NotificationChannelsCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } =
|
||||
useGet<Notification[]>("/notifications/team");
|
||||
|
||||
// The response also carries accessToken and accountSid. Only `type` is read
|
||||
// here — credential fields must never reach the DOM.
|
||||
const byType = useMemo(() => {
|
||||
const counts = new Map<string, number>();
|
||||
for (const notification of data ?? []) {
|
||||
counts.set(notification.type, (counts.get(notification.type) ?? 0) + 1);
|
||||
}
|
||||
return [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.notificationChannels.title")}
|
||||
to="/notifications"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
>
|
||||
{byType.length === 0 ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.notificationChannels.empty")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
{byType.map(([type, count]) => (
|
||||
<CardStatLine
|
||||
key={type}
|
||||
label={type}
|
||||
value={String(count)}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
|
||||
export const MaintenanceCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } = useGet<{
|
||||
maintenanceWindows: MaintenanceWindow[];
|
||||
maintenanceWindowCount: number;
|
||||
}>("/maintenance-window/team?page=0&rowsPerPage=0");
|
||||
|
||||
// `active` is a user on/off flag, not "running now" — compare the window
|
||||
// client-side. repeat > 0 means recurring, so `end` may sit in the past for
|
||||
// a window that is currently live.
|
||||
const { running, next } = useMemo(() => {
|
||||
const now = Date.now();
|
||||
const windows = (data?.maintenanceWindows ?? []).filter((entry) => entry.active);
|
||||
const live = windows.filter((entry) => {
|
||||
const start = new Date(entry.start).getTime();
|
||||
const end = new Date(entry.end).getTime();
|
||||
return start <= now && (now <= end || entry.repeat > 0);
|
||||
});
|
||||
const upcoming = windows
|
||||
.filter((entry) => new Date(entry.start).getTime() > now)
|
||||
.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
return { running: live.length, next: upcoming[0] ?? null };
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.maintenance.title")}
|
||||
to="/maintenance"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
>
|
||||
{!data || data.maintenanceWindows.length === 0 ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.maintenance.empty")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
<CardFigure
|
||||
value={String(running)}
|
||||
caption={t("pages.dashboard.cards.maintenance.runningNow")}
|
||||
/>
|
||||
{next && (
|
||||
<CardStatLine
|
||||
label={t("pages.dashboard.cards.maintenance.next")}
|
||||
value={formatTimestamp(next.start)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusPagesCard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isValidating, error } =
|
||||
useGet<StatusPage[]>("/status-page/team");
|
||||
|
||||
return (
|
||||
<DashboardCard
|
||||
title={t("pages.dashboard.cards.statusPages.title")}
|
||||
to="/status"
|
||||
isLoading={isLoading && !data}
|
||||
error={error}
|
||||
isStale={isValidating && Boolean(data)}
|
||||
>
|
||||
{!data || data.length === 0 ? (
|
||||
<CardMessage text={t("pages.dashboard.cards.statusPages.empty")} />
|
||||
) : (
|
||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||
{data.map((page) => (
|
||||
<CardRow
|
||||
key={page.id}
|
||||
to={`/status/${page.url}`}
|
||||
>
|
||||
<Dot
|
||||
color={
|
||||
page.isPublished
|
||||
? theme.palette.success.light
|
||||
: theme.palette.text.secondary
|
||||
}
|
||||
size="md"
|
||||
/>
|
||||
<CardRowLabel
|
||||
primary={page.companyName}
|
||||
secondary={t("pages.dashboard.cards.statusPages.monitorCount", {
|
||||
count: page.monitors.length,
|
||||
})}
|
||||
/>
|
||||
<Typography
|
||||
color={theme.palette.text.secondary}
|
||||
flexShrink={0}
|
||||
>
|
||||
{page.isPublished
|
||||
? t("pages.dashboard.cards.statusPages.published")
|
||||
: t("pages.dashboard.cards.statusPages.draft")}
|
||||
</Typography>
|
||||
</CardRow>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,159 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseBox } from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { useCardSlot } from "./CardSlotContext";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// Control height plus the header's vertical padding, so a card with no action
|
||||
// still lines up with one that has a button in its header.
|
||||
const HEADER_MIN_HEIGHT = 44;
|
||||
const SKELETON_ROW_HEIGHT = 20;
|
||||
|
||||
interface DashboardCardProps {
|
||||
title: string;
|
||||
/** Where the title links to — the page this card belongs to. */
|
||||
to?: string;
|
||||
/** Rendered at the right of the header: a count, an average, a filter. */
|
||||
action?: ReactNode;
|
||||
isLoading?: boolean;
|
||||
error?: unknown;
|
||||
/** Dimmed rather than blanked while a refresh is in flight over stale data. */
|
||||
isStale?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DashboardCard = ({
|
||||
title,
|
||||
to,
|
||||
action,
|
||||
isLoading,
|
||||
error,
|
||||
isStale,
|
||||
children,
|
||||
}: DashboardCardProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const slot = useCardSlot();
|
||||
|
||||
return (
|
||||
<BaseBox
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
height="100%"
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
"&:hover .dashboard-card-slot": {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
px={theme.spacing(LAYOUT.MD)}
|
||||
py={theme.spacing(LAYOUT.SM)}
|
||||
borderBottom={`1px solid ${theme.palette.divider}`}
|
||||
minHeight={HEADER_MIN_HEIGHT}
|
||||
>
|
||||
<Typography
|
||||
{...(to ? { component: RouterLink, to } : {})}
|
||||
fontWeight={500}
|
||||
color={theme.palette.text.primary}
|
||||
sx={{
|
||||
textDecoration: "none",
|
||||
...(to && {
|
||||
"&:hover": { color: theme.palette.primary.main },
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
flexShrink={0}
|
||||
position="relative"
|
||||
>
|
||||
{isStale && (
|
||||
<CircularProgress
|
||||
size={16}
|
||||
color="primary"
|
||||
/>
|
||||
)}
|
||||
{action && <Box>{action}</Box>}
|
||||
{slot && (
|
||||
<Box
|
||||
className="dashboard-card-slot"
|
||||
sx={{
|
||||
opacity: 0.3,
|
||||
transition: "opacity 0.15s ease",
|
||||
}}
|
||||
>
|
||||
{slot}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
flex={1}
|
||||
p={theme.spacing(LAYOUT.MD)}
|
||||
sx={{
|
||||
...(isStale && { opacity: 0.5, transition: "opacity 0.2s ease" }),
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<CardSkeleton />
|
||||
) : error ? (
|
||||
<CardMessage text={t("pages.dashboard.card.error")} />
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Box>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
/** Placeholder rows, sized to the row list most cards render. */
|
||||
export const CardSkeleton = ({ rows = 3 }: { rows?: number }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
||||
{Array.from({ length: rows }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
variant="rounded"
|
||||
height={SKELETON_ROW_HEIGHT}
|
||||
width={index === rows - 1 ? "60%" : "100%"}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
/** The quiet one-line state a healthy card collapses to. */
|
||||
export const CardMessage = ({ text, action }: { text: string; action?: ReactNode }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(LAYOUT.SM)}
|
||||
>
|
||||
<Typography color={theme.palette.text.secondary}>{text}</Typography>
|
||||
{action}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { CheckSnapshot } from "@/Types/Check";
|
||||
|
||||
const VIEWBOX_WIDTH = 100;
|
||||
// The svg renders at its viewBox height, so one constant serves both.
|
||||
const VIEWBOX_HEIGHT = 24;
|
||||
// Matches the 1.5 stroke the shared Icon component uses.
|
||||
const STROKE_WIDTH = 1.5;
|
||||
|
||||
/**
|
||||
* Response times as a bare polyline. Deliberately axis-free and label-free —
|
||||
* at this size the shape is the only readable signal.
|
||||
*
|
||||
* `checks` arrives oldest-first, which is already left-to-right reading order.
|
||||
*/
|
||||
export const Sparkline = ({
|
||||
checks,
|
||||
color,
|
||||
}: {
|
||||
checks: CheckSnapshot[];
|
||||
color: string;
|
||||
}) => {
|
||||
const points = useMemo(() => {
|
||||
const values = checks
|
||||
.map((check) => check.responseTime)
|
||||
.filter((value): value is number => typeof value === "number");
|
||||
|
||||
if (values.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const max = Math.max(...values);
|
||||
const min = Math.min(...values);
|
||||
const span = max - min || 1;
|
||||
|
||||
return values
|
||||
.map((value, index) => {
|
||||
const x = (index / (values.length - 1)) * VIEWBOX_WIDTH;
|
||||
const y = VIEWBOX_HEIGHT - ((value - min) / span) * VIEWBOX_HEIGHT;
|
||||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
})
|
||||
.join(" ");
|
||||
}, [checks]);
|
||||
|
||||
if (!points) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="svg"
|
||||
viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`}
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
width="100%"
|
||||
height={VIEWBOX_HEIGHT}
|
||||
display="block"
|
||||
>
|
||||
<polyline
|
||||
points={points}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={STROKE_WIDTH}
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
// The 12-column grid the dashboard lays cards out on.
|
||||
//
|
||||
// Every card is currently half-width, so they pair up down the page and the
|
||||
// grid reads as one system. Full width stays in the type for a card whose
|
||||
// content genuinely needs it. Width is declared per card type in code and is
|
||||
// never a user setting.
|
||||
export const CardWidths = [6, 12] as const;
|
||||
export type CardWidth = (typeof CardWidths)[number];
|
||||
|
||||
export const GRID_COLUMNS = 12;
|
||||
|
||||
// Picker groups mirror the sidebar sections: you look for a card where you
|
||||
// look for the feature it belongs to.
|
||||
export const CardGroups = [
|
||||
"uptime",
|
||||
"infrastructure",
|
||||
"incidents",
|
||||
"notifications",
|
||||
"maintenance",
|
||||
"statusPages",
|
||||
"checks",
|
||||
"logs",
|
||||
"settings",
|
||||
] as const;
|
||||
export type CardGroup = (typeof CardGroups)[number];
|
||||
|
||||
// The Containers card (CARD-SPECS.md #11) is deliberately absent: it needs
|
||||
// `containerSummary` on the check snapshot, which arrives with the Docker
|
||||
// monitoring work. Add it here once that lands on develop.
|
||||
export const CardIds = [
|
||||
"monitorStatus",
|
||||
"currentlyDown",
|
||||
"slowestMonitors",
|
||||
"lowestUptime",
|
||||
"monitorsByType",
|
||||
"monitorsByGroup",
|
||||
"recentIncidents",
|
||||
"incidentStats",
|
||||
"notificationChannels",
|
||||
"maintenance",
|
||||
"statusPages",
|
||||
"failedChecks",
|
||||
"busiestServers",
|
||||
"checkmateServer",
|
||||
"checksOnSchedule",
|
||||
"recentErrors",
|
||||
"team",
|
||||
] as const;
|
||||
export type CardId = (typeof CardIds)[number];
|
||||
|
||||
export interface CardDefinition {
|
||||
id: CardId;
|
||||
/** i18n key fragment under pages.dashboard.cards.<key>. */
|
||||
key: string;
|
||||
group: CardGroup;
|
||||
width: CardWidth;
|
||||
/** Hidden from the page and absent from the picker for non-admins. */
|
||||
adminOnly?: boolean;
|
||||
component: ComponentType;
|
||||
}
|
||||
|
||||
// The cards a fresh dashboard starts with. Chosen so a brand new install still
|
||||
// shows something real: Checkmate server works with zero monitors.
|
||||
// Ordered so the declared widths pair into full rows: a card left alone in a
|
||||
// row is stretched to fill it, so 8- and 4-wide cards are kept adjacent.
|
||||
export const DEFAULT_CARD_IDS: readonly CardId[] = [
|
||||
"monitorStatus",
|
||||
"currentlyDown",
|
||||
"failedChecks",
|
||||
"monitorsByType",
|
||||
"recentIncidents",
|
||||
"checkmateServer",
|
||||
];
|
||||
|
||||
export const DASHBOARD_CARDS_STORAGE_KEY = "checkmate-dashboard-cards";
|
||||
|
||||
/**
|
||||
* Poll rates, in ms. Monitor and server figures move continuously; the log ring
|
||||
* buffer rotates fast but is expensive to re-read, so it is polled slowly.
|
||||
*/
|
||||
export const REFRESH_INTERVAL_MS = 30000;
|
||||
export const SLOW_REFRESH_INTERVAL_MS = 60000;
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
/**
|
||||
* `recentChecks` is oldest-first — the server appends each snapshot with
|
||||
* `$push … $slice: -N` — so the newest retained check is the last element.
|
||||
*/
|
||||
export const latestCheck = (monitor: Monitor) =>
|
||||
monitor.recentChecks?.[monitor.recentChecks.length - 1];
|
||||
@@ -1,86 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { LayoutGrid } from "lucide-react";
|
||||
|
||||
import { BasePage, EmptyState, Icon } from "@/Components/design-elements";
|
||||
import { Button } from "@/Components/inputs";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { CardGrid } from "./Components/CardGrid";
|
||||
import { CardPicker } from "./Components/CardPicker";
|
||||
import { MonitorsContext, useMonitorsRequest } from "./useDashboardData";
|
||||
import { useCardSelection } from "./useCardSelection";
|
||||
|
||||
const Dashboard = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const monitorsRequest = useMonitorsRequest();
|
||||
const {
|
||||
cards,
|
||||
available,
|
||||
selected,
|
||||
addCard,
|
||||
removeCard,
|
||||
reorderCards,
|
||||
resetCards,
|
||||
selectAll,
|
||||
deselectAll,
|
||||
allSelected,
|
||||
} = useCardSelection();
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<MonitorsContext.Provider value={monitorsRequest}>
|
||||
<BasePage headerKey="dashboard">
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => setIsPickerOpen(true)}
|
||||
startIcon={
|
||||
<Icon
|
||||
icon={LayoutGrid}
|
||||
size={16}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t("pages.dashboard.picker.trigger")}
|
||||
</Button>
|
||||
</Stack>
|
||||
{cards.length === 0 ? (
|
||||
<EmptyState
|
||||
title={t("pages.dashboard.empty.title")}
|
||||
description={t("pages.dashboard.empty.description")}
|
||||
actionText={t("pages.dashboard.picker.trigger")}
|
||||
onAction={() => setIsPickerOpen(true)}
|
||||
/>
|
||||
) : (
|
||||
<CardGrid
|
||||
cards={cards}
|
||||
onReorder={reorderCards}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<CardPicker
|
||||
open={isPickerOpen}
|
||||
available={available}
|
||||
selected={selected}
|
||||
allSelected={allSelected}
|
||||
onAdd={addCard}
|
||||
onRemove={removeCard}
|
||||
onSelectAll={selectAll}
|
||||
onDeselectAll={deselectAll}
|
||||
onReset={resetCards}
|
||||
onClose={() => setIsPickerOpen(false)}
|
||||
/>
|
||||
</BasePage>
|
||||
</MonitorsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { CardDefinition } from "./cards";
|
||||
|
||||
export interface PlacedCard {
|
||||
card: CardDefinition;
|
||||
/** Columns rendered — currently always the declared width. */
|
||||
renderedWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs cards into rows of GRID_COLUMNS.
|
||||
*
|
||||
* With a single body width this is a straight pass-through: two halves fill a
|
||||
* row exactly, so no card is ever stretched to close a gap.
|
||||
*
|
||||
* An odd number of cards leaves the last one alone on its row. It keeps its
|
||||
* declared width rather than being promoted to full: widening one card to fill
|
||||
* the row makes it look more important than it is, and an odd count is a
|
||||
* property of the selection, not something the layout should hide.
|
||||
*/
|
||||
export const layoutCards = (cards: CardDefinition[]): PlacedCard[] =>
|
||||
cards.map((card) => ({ card, renderedWidth: card.width }));
|
||||
@@ -1,161 +0,0 @@
|
||||
import { MonitorStatusCard } from "./Components/Cards/MonitorStatusCard";
|
||||
import { CurrentlyDownCard } from "./Components/Cards/CurrentlyDownCard";
|
||||
import { FailedChecksCard } from "./Components/Cards/FailedChecksCard";
|
||||
import { RecentIncidentsCard } from "./Components/Cards/RecentIncidentsCard";
|
||||
import { MonitorsByTypeCard } from "./Components/Cards/MonitorsByTypeCard";
|
||||
import { MonitorsByGroupCard } from "./Components/Cards/MonitorsByGroupCard";
|
||||
import { CheckmateServerCard } from "./Components/Cards/CheckmateServerCard";
|
||||
import { BusiestServersCard } from "./Components/Cards/BusiestServersCard";
|
||||
import { SlowestMonitorsCard, LowestUptimeCard } from "./Components/Cards/RankingCards";
|
||||
import {
|
||||
IncidentStatsCard,
|
||||
NotificationChannelsCard,
|
||||
MaintenanceCard,
|
||||
StatusPagesCard,
|
||||
} from "./Components/Cards/SummaryCards";
|
||||
import {
|
||||
ChecksOnScheduleCard,
|
||||
RecentErrorsCard,
|
||||
TeamCard,
|
||||
} from "./Components/Cards/AdminCards";
|
||||
|
||||
import type { CardDefinition, CardId } from "./cards";
|
||||
|
||||
// Declared width is the minimum number of columns a card needs. The grid may
|
||||
// stretch the last card of a short row; nothing here is a user setting.
|
||||
//
|
||||
// Order is the render order, and the order the picker lists them in.
|
||||
//
|
||||
// Every card is half-width and pairs up down the page. The grid promotes a
|
||||
// lone trailing card to full width so the page does not end on a ragged half.
|
||||
export const CARD_REGISTRY: CardDefinition[] = [
|
||||
{
|
||||
id: "monitorStatus",
|
||||
key: "monitorStatus",
|
||||
group: "uptime",
|
||||
width: 6,
|
||||
component: MonitorStatusCard,
|
||||
},
|
||||
{
|
||||
id: "currentlyDown",
|
||||
key: "currentlyDown",
|
||||
group: "uptime",
|
||||
width: 6,
|
||||
component: CurrentlyDownCard,
|
||||
},
|
||||
{
|
||||
id: "failedChecks",
|
||||
key: "failedChecks",
|
||||
group: "checks",
|
||||
width: 6,
|
||||
component: FailedChecksCard,
|
||||
},
|
||||
{
|
||||
id: "slowestMonitors",
|
||||
key: "slowestMonitors",
|
||||
group: "uptime",
|
||||
width: 6,
|
||||
component: SlowestMonitorsCard,
|
||||
},
|
||||
{
|
||||
id: "lowestUptime",
|
||||
key: "lowestUptime",
|
||||
group: "uptime",
|
||||
width: 6,
|
||||
component: LowestUptimeCard,
|
||||
},
|
||||
{
|
||||
id: "monitorsByType",
|
||||
key: "monitorsByType",
|
||||
group: "uptime",
|
||||
width: 6,
|
||||
component: MonitorsByTypeCard,
|
||||
},
|
||||
{
|
||||
id: "monitorsByGroup",
|
||||
key: "monitorsByGroup",
|
||||
group: "uptime",
|
||||
width: 6,
|
||||
component: MonitorsByGroupCard,
|
||||
},
|
||||
{
|
||||
id: "busiestServers",
|
||||
key: "busiestServers",
|
||||
group: "infrastructure",
|
||||
width: 6,
|
||||
component: BusiestServersCard,
|
||||
},
|
||||
{
|
||||
id: "recentIncidents",
|
||||
key: "recentIncidents",
|
||||
group: "incidents",
|
||||
width: 6,
|
||||
component: RecentIncidentsCard,
|
||||
},
|
||||
{
|
||||
id: "incidentStats",
|
||||
key: "incidentStats",
|
||||
group: "incidents",
|
||||
width: 6,
|
||||
component: IncidentStatsCard,
|
||||
},
|
||||
{
|
||||
id: "notificationChannels",
|
||||
key: "notificationChannels",
|
||||
group: "notifications",
|
||||
width: 6,
|
||||
component: NotificationChannelsCard,
|
||||
},
|
||||
{
|
||||
id: "maintenance",
|
||||
key: "maintenance",
|
||||
group: "maintenance",
|
||||
width: 6,
|
||||
component: MaintenanceCard,
|
||||
},
|
||||
{
|
||||
id: "statusPages",
|
||||
key: "statusPages",
|
||||
group: "statusPages",
|
||||
width: 6,
|
||||
component: StatusPagesCard,
|
||||
},
|
||||
{
|
||||
id: "checkmateServer",
|
||||
key: "checkmateServer",
|
||||
group: "logs",
|
||||
width: 6,
|
||||
adminOnly: true,
|
||||
component: CheckmateServerCard,
|
||||
},
|
||||
{
|
||||
id: "checksOnSchedule",
|
||||
key: "checksOnSchedule",
|
||||
group: "logs",
|
||||
width: 6,
|
||||
adminOnly: true,
|
||||
component: ChecksOnScheduleCard,
|
||||
},
|
||||
{
|
||||
id: "recentErrors",
|
||||
key: "recentErrors",
|
||||
group: "logs",
|
||||
width: 6,
|
||||
adminOnly: true,
|
||||
component: RecentErrorsCard,
|
||||
},
|
||||
{
|
||||
id: "team",
|
||||
key: "team",
|
||||
group: "settings",
|
||||
width: 6,
|
||||
adminOnly: true,
|
||||
component: TeamCard,
|
||||
},
|
||||
];
|
||||
|
||||
const BY_ID = new Map<CardId, CardDefinition>(
|
||||
CARD_REGISTRY.map((card) => [card.id, card])
|
||||
);
|
||||
|
||||
export const getCard = (id: CardId): CardDefinition | undefined => BY_ID.get(id);
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
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);
|
||||
|
||||
export const useCardSelection = () => {
|
||||
const dispatch = useDispatch();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
// 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]
|
||||
);
|
||||
|
||||
const isVisible = useCallback(
|
||||
(card: CardDefinition) => !card.adminOnly || isAdmin,
|
||||
[isAdmin]
|
||||
);
|
||||
|
||||
const cards = useMemo(
|
||||
() =>
|
||||
selected
|
||||
.map(getCard)
|
||||
.filter((card): card is CardDefinition => card !== undefined)
|
||||
.filter(isVisible),
|
||||
[selected, isVisible]
|
||||
);
|
||||
|
||||
const available = useMemo(() => CARD_REGISTRY.filter(isVisible), [isVisible]);
|
||||
|
||||
const addCard = useCallback(
|
||||
(id: CardId) => {
|
||||
if (!selected.includes(id)) dispatch(setDashboardCards([...selected, id]));
|
||||
},
|
||||
[dispatch, selected]
|
||||
);
|
||||
|
||||
const removeCard = useCallback(
|
||||
(id: CardId) => {
|
||||
dispatch(setDashboardCards(selected.filter((cardId) => cardId !== id)));
|
||||
},
|
||||
[dispatch, selected]
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
/**
|
||||
* Reorders cards based on DnD source/dest indices into the visible `cards` array.
|
||||
* Hidden admin cards keep their stored positions unchanged.
|
||||
*/
|
||||
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
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
[dispatch, cards, selected]
|
||||
);
|
||||
|
||||
const allSelected = available.every((card) => selected.includes(card.id));
|
||||
|
||||
return {
|
||||
cards,
|
||||
available,
|
||||
selected,
|
||||
addCard,
|
||||
removeCard,
|
||||
reorderCards,
|
||||
resetCards,
|
||||
selectAll,
|
||||
deselectAll,
|
||||
allSelected,
|
||||
};
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import { MonitorTypes, type MonitorsWithChecksResponse } from "@/Types/Monitor";
|
||||
import { REFRESH_INTERVAL_MS } from "./cards";
|
||||
|
||||
// One request feeds every monitor-derived card, read from context so adding a
|
||||
// card costs no extra call. No rowsPerPage: the dashboard summarises the whole
|
||||
// fleet, and the repository treats its absence as "no limit".
|
||||
const buildMonitorsUrl = () => {
|
||||
const params = new URLSearchParams();
|
||||
MonitorTypes.forEach((type) => params.append("type", type));
|
||||
return `/monitors/team/with-checks?${params.toString()}`;
|
||||
};
|
||||
|
||||
export const useMonitorsRequest = () =>
|
||||
useGet<MonitorsWithChecksResponse>(
|
||||
buildMonitorsUrl(),
|
||||
{},
|
||||
{ refreshInterval: REFRESH_INTERVAL_MS, keepPreviousData: true }
|
||||
);
|
||||
|
||||
export type MonitorsRequest = ReturnType<typeof useMonitorsRequest>;
|
||||
|
||||
export const MonitorsContext = createContext<MonitorsRequest | null>(null);
|
||||
|
||||
export const useMonitors = (): MonitorsRequest => {
|
||||
const context = useContext(MonitorsContext);
|
||||
if (!context) {
|
||||
throw new Error("useMonitors must be used inside the dashboard's MonitorsContext");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Dot } from "@/Components/design-elements";
|
||||
|
||||
// Hooks
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Utils
|
||||
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||
import { MS_PER_SECOND, MS_PER_MINUTE } from "@/Utils/TimeUtils";
|
||||
|
||||
interface HeaderLogsProps {
|
||||
lines: number;
|
||||
loading: boolean;
|
||||
lastUpdatedAt: number | null;
|
||||
}
|
||||
|
||||
export const HeaderLogs = ({ lines, loading, lastUpdatedAt }: HeaderLogsProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (lastUpdatedAt === null) return;
|
||||
setNow(Date.now());
|
||||
const id = window.setInterval(() => setNow(Date.now()), MS_PER_SECOND);
|
||||
return () => window.clearInterval(id);
|
||||
}, [lastUpdatedAt]);
|
||||
|
||||
const renderStatus = () => {
|
||||
if (loading) return t("pages.docker.container.logs.loading");
|
||||
if (lastUpdatedAt === null) return null;
|
||||
const ageMs = Math.max(0, now - lastUpdatedAt);
|
||||
return ageMs < MS_PER_MINUTE
|
||||
? t("pages.docker.container.logs.updatedSecondsAgo", {
|
||||
count: Math.floor(ageMs / MS_PER_SECOND),
|
||||
})
|
||||
: t("pages.docker.container.logs.updatedMinutesAgo", {
|
||||
count: Math.floor(ageMs / MS_PER_MINUTE),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={LAYOUT.MD}
|
||||
>
|
||||
<Typography>
|
||||
{t("pages.docker.container.logs.recentLines", { count: lines })}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={SPACING.SM}
|
||||
>
|
||||
{lastUpdatedAt !== null && !loading && (
|
||||
<Dot
|
||||
color={theme.palette.success.main}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
<Typography color={theme.palette.text.secondary}>{renderStatus()}</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import { Fragment } from "react";
|
||||
|
||||
// Hooks
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Utils
|
||||
import type { DockerLogRow } from "@/Utils/MonitorUtils";
|
||||
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
import type { RootState } from "@/store";
|
||||
|
||||
export const RowLog = ({ row }: { row: DockerLogRow }) => {
|
||||
const theme = useTheme();
|
||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Grid
|
||||
container
|
||||
spacing={{ xs: 0, md: LAYOUT.MD }}
|
||||
py={{ xs: LAYOUT.LG, md: 0 }}
|
||||
>
|
||||
<Grid size={{ xs: 12, md: "auto" }}>
|
||||
<Typography
|
||||
fontFamily="monospace"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{formatDateWithTz(row.ts, "MMM D HH:mm:ss", uiTimezone)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: "grow" }}>
|
||||
<Typography
|
||||
fontFamily={"monospace"}
|
||||
whiteSpace={{ xs: "pre-wrap", md: "nowrap" }}
|
||||
sx={{
|
||||
overflowWrap: { xs: "anywhere", md: "normal" },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component={"span"}
|
||||
color={
|
||||
row.stream === "stderr"
|
||||
? theme.palette.error.main
|
||||
: theme.palette.success.main
|
||||
}
|
||||
>
|
||||
{row.stream}
|
||||
</Box>
|
||||
: {row.text}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{row.gapBefore ? (
|
||||
<Stack
|
||||
width="100%"
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
py={SPACING.MD}
|
||||
>
|
||||
<Typography pr={SPACING.MD}>
|
||||
{t("pages.docker.container.logs.skipped")}
|
||||
</Typography>
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
</Stack>
|
||||
) : (
|
||||
<Divider sx={{ display: { xs: "block", md: "none" } }} />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -10,7 +10,6 @@ import type { DockerContainerInfo } from "@/Types/Check";
|
||||
|
||||
// Utils
|
||||
import { formatDuration } from "@/Utils/TimeUtils";
|
||||
import { getDockerPalette } from "@/Utils/MonitorUtils";
|
||||
|
||||
export const DockerContainerStatusBoxes = ({
|
||||
container,
|
||||
@@ -25,7 +24,6 @@ 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"
|
||||
@@ -35,7 +33,6 @@ export const DockerContainerStatusBoxes = ({
|
||||
<StatBox
|
||||
title={t("common.labels.state")}
|
||||
subtitle={state}
|
||||
palette={palette}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("common.labels.uptime")}
|
||||
|
||||
@@ -1,252 +1,3 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { BaseBox, EmptyState } from "@/Components/design-elements";
|
||||
import { Button } from "@/Components/inputs";
|
||||
import { HeaderLogs } from "@/Pages/Docker/ContainerDetails/Components/HeaderLogs";
|
||||
import { RowLog } from "@/Pages/Docker/ContainerDetails/Components/RowLog";
|
||||
|
||||
// Types
|
||||
import type { DockerContainerLogsResponse } from "@/Types/Monitor";
|
||||
import type { DockerLog } from "@/Types/Check";
|
||||
import type { ApiResponse } from "@/Hooks/UseApi";
|
||||
|
||||
// Hooks
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useCallback,
|
||||
} from "react";
|
||||
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";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { flattenDockerLogs } from "@/Utils/MonitorUtils";
|
||||
|
||||
interface TabLogsProps {
|
||||
monitorId?: string;
|
||||
containerName?: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const SCROLL_PIN_THRESHOLD_PX = 8;
|
||||
const TAIL_POLL_MS = 10_000;
|
||||
|
||||
const isScrolledToBottom = (el: HTMLElement) =>
|
||||
el.scrollHeight - el.scrollTop - el.clientHeight <= SCROLL_PIN_THRESHOLD_PX;
|
||||
|
||||
const fetchLogs = async (url: string, params: { before?: string; after?: string }) => {
|
||||
const res = await get<ApiResponse<DockerContainerLogsResponse>>(url, { params });
|
||||
return res.data.data;
|
||||
};
|
||||
|
||||
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[]>([]);
|
||||
const [olderExhausted, setOlderExhausted] = useState(false);
|
||||
const [loadingInitial, setLoadingInitial] = useState(false);
|
||||
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||
const [lastUpdatedAt, setLastUpdatedAt] = useState<number | null>(null);
|
||||
|
||||
const logsUrl =
|
||||
isAdmin && enabled && monitorId && containerName
|
||||
? `/monitors/docker/details/${encodeURIComponent(monitorId)}/containers/${encodeURIComponent(containerName)}/logs`
|
||||
: null;
|
||||
|
||||
const newestCursor = logs[0]?.checkedAt;
|
||||
const oldestCursor = logs[logs.length - 1]?.checkedAt;
|
||||
|
||||
// Initial page. Resets everything when the container changes.
|
||||
useEffect(() => {
|
||||
setLogs([]);
|
||||
setOlderExhausted(false);
|
||||
if (!logsUrl) return;
|
||||
let cancelled = false;
|
||||
setLoadingInitial(true);
|
||||
fetchLogs(logsUrl, {})
|
||||
.then((page) => {
|
||||
if (cancelled) return;
|
||||
setLogs(page.logs);
|
||||
setOlderExhausted(page.nextCursor === null);
|
||||
setLastUpdatedAt(Date.now());
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return;
|
||||
toastError(t("pages.docker.container.logs.loadError"));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoadingInitial(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [logsUrl, t, toastError]);
|
||||
|
||||
const newestCursorRef = useRef(newestCursor);
|
||||
newestCursorRef.current = newestCursor;
|
||||
|
||||
useEffect(() => {
|
||||
if (!logsUrl) return;
|
||||
let inFlight = false;
|
||||
const tick = async () => {
|
||||
if (inFlight || document.hidden) return;
|
||||
inFlight = true;
|
||||
try {
|
||||
const cursor = newestCursorRef.current;
|
||||
const page = await fetchLogs(logsUrl, cursor ? { after: cursor } : {});
|
||||
if (page.logs.length > 0) setLogs((prev) => [...page.logs, ...prev]);
|
||||
setLastUpdatedAt(Date.now());
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
inFlight = false;
|
||||
}
|
||||
};
|
||||
const id = window.setInterval(tick, TAIL_POLL_MS);
|
||||
document.addEventListener("visibilitychange", tick);
|
||||
return () => {
|
||||
window.clearInterval(id);
|
||||
document.removeEventListener("visibilitychange", tick);
|
||||
};
|
||||
}, [logsUrl]);
|
||||
|
||||
const rows = useMemo(() => flattenDockerLogs(logs), [logs]);
|
||||
|
||||
const logBoxRef = useRef<HTMLDivElement>(null);
|
||||
const pinnedToBottom = useRef(true);
|
||||
const prependAnchor = useRef<{ scrollHeight: number; scrollTop: number } | null>(null);
|
||||
|
||||
// Always pin to bottom when container/monitor changes
|
||||
useLayoutEffect(() => {
|
||||
pinnedToBottom.current = true;
|
||||
}, [monitorId, containerName]);
|
||||
|
||||
// Rows changed: restore position after a prepend, otherwise honour the pin
|
||||
useLayoutEffect(() => {
|
||||
const el = logBoxRef.current;
|
||||
if (!el) return;
|
||||
const anchor = prependAnchor.current;
|
||||
if (anchor) {
|
||||
prependAnchor.current = null;
|
||||
el.scrollTop = el.scrollHeight - anchor.scrollHeight + anchor.scrollTop;
|
||||
return;
|
||||
}
|
||||
if (pinnedToBottom.current) el.scrollTop = el.scrollHeight;
|
||||
}, [rows]);
|
||||
|
||||
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
|
||||
pinnedToBottom.current = isScrolledToBottom(event.currentTarget);
|
||||
}, []);
|
||||
|
||||
const loadOlder = useCallback(async () => {
|
||||
if (!logsUrl || !oldestCursor || loadingOlder) return;
|
||||
setLoadingOlder(true);
|
||||
try {
|
||||
const page = await fetchLogs(logsUrl, { before: oldestCursor });
|
||||
const el = logBoxRef.current;
|
||||
if (el)
|
||||
prependAnchor.current = {
|
||||
scrollHeight: el.scrollHeight,
|
||||
scrollTop: el.scrollTop,
|
||||
};
|
||||
setLogs((prev) => [...prev, ...page.logs]);
|
||||
setOlderExhausted(page.nextCursor === null);
|
||||
} catch {
|
||||
toastError(t("pages.docker.container.logs.loadOlderError"));
|
||||
} finally {
|
||||
setLoadingOlder(false);
|
||||
}
|
||||
}, [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
|
||||
title={t("pages.docker.container.logs.disabled.title")}
|
||||
description={t("pages.docker.container.logs.disabled.description")}
|
||||
actionText={t("pages.docker.container.logs.disabled.action")}
|
||||
actionTo={`/docker/configure/${monitorId}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (lastUpdatedAt !== null && rows.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
title={t("pages.docker.container.logs.empty.title")}
|
||||
description={t("pages.docker.container.logs.empty.description")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={LAYOUT.MD}>
|
||||
<HeaderLogs
|
||||
lines={rows.length}
|
||||
loading={loadingInitial}
|
||||
lastUpdatedAt={lastUpdatedAt}
|
||||
/>
|
||||
<BaseBox
|
||||
ref={logBoxRef}
|
||||
onScroll={handleScroll}
|
||||
minHeight={240}
|
||||
maxHeight="50vh"
|
||||
overflow="auto"
|
||||
padding={LAYOUT.MD}
|
||||
sx={{ overflowAnchor: "none" }}
|
||||
>
|
||||
{rows.length > 0 && (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
pb={LAYOUT.MD}
|
||||
>
|
||||
{olderExhausted ? (
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{t("pages.docker.container.logs.noOlder")}
|
||||
</Typography>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={loadingOlder}
|
||||
onClick={loadOlder}
|
||||
>
|
||||
{loadingOlder
|
||||
? t("pages.docker.container.logs.loadingOlder")
|
||||
: t("pages.docker.container.logs.loadOlder")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{rows.map((row) => {
|
||||
return (
|
||||
<RowLog
|
||||
key={row.key}
|
||||
row={row}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</BaseBox>
|
||||
</Stack>
|
||||
);
|
||||
export const TabLogs = () => {
|
||||
return "This is the logs tab";
|
||||
};
|
||||
@@ -21,7 +21,7 @@ const TAB_MAP = {
|
||||
logs: 1,
|
||||
} as const;
|
||||
|
||||
const DockerDetailsPage = ({ open = "logs" }: DockerContainerDetailsProps) => {
|
||||
const DockerDetailsPage = ({ open = "overview" }: DockerContainerDetailsProps) => {
|
||||
const [activeTab, setActiveTab] = useState<number>(TAB_MAP[open]);
|
||||
const [dateRange, setDateRange] = useState<DateRange>("recent");
|
||||
const { monitorId, containerName } = useParams<{
|
||||
@@ -70,13 +70,7 @@ const DockerDetailsPage = ({ open = "logs" }: DockerContainerDetailsProps) => {
|
||||
<Tab label={t("pages.docker.container.tabs.logs")} />
|
||||
</Tabs>
|
||||
{activeTab === 0 && <TabOverview stats={stats} />}
|
||||
{activeTab === 1 && (
|
||||
<TabLogs
|
||||
monitorId={monitorId}
|
||||
containerName={containerName}
|
||||
enabled={monitor.dockerLogsEnabled ?? false}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 1 && <TabLogs />}
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -327,7 +327,11 @@ export const DockerMonitorsTable = ({
|
||||
onRowClick={(row) => {
|
||||
navigate(`/docker/host/${row.id}`);
|
||||
}}
|
||||
isRowSelected={(row) => isRowSelected(row.id)}
|
||||
getRowSx={(row) => ({
|
||||
backgroundColor: isRowSelected(row.id)
|
||||
? theme.palette.action.selected
|
||||
: "inherit",
|
||||
})}
|
||||
/>
|
||||
<Pagination
|
||||
component="div"
|
||||
|
||||
@@ -30,7 +30,6 @@ const SummaryItem = ({ icon, label, value }: SummaryItemProps) => {
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(2)}
|
||||
sx={{ "& svg": { color: theme.palette.text.secondary } }}
|
||||
>
|
||||
{icon}
|
||||
<Typography variant="body1">{label}</Typography>
|
||||
@@ -144,10 +143,7 @@ const SummaryIncidentItem = ({ incident }: { incident: IncidentSummaryItem }) =>
|
||||
gap: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={Globe}
|
||||
color={theme.palette.text.secondary}
|
||||
/>
|
||||
<Icon icon={Globe} />
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={500}
|
||||
|
||||
@@ -331,7 +331,11 @@ export const InfraMonitorsTable = ({
|
||||
onRowClick={(row) => {
|
||||
navigate(`/infrastructure/${row.id}`);
|
||||
}}
|
||||
isRowSelected={(row) => isRowSelected(row.id)}
|
||||
getRowSx={(row) => ({
|
||||
backgroundColor: isRowSelected(row.id)
|
||||
? theme.palette.action.selected
|
||||
: "inherit",
|
||||
})}
|
||||
/>
|
||||
<Pagination
|
||||
component="div"
|
||||
|
||||
@@ -126,7 +126,6 @@ const NotificationsCreatePage = () => {
|
||||
{watchedType !== "matrix" &&
|
||||
watchedType !== "telegram" &&
|
||||
watchedType !== "pushover" &&
|
||||
watchedType !== "signalgrid" &&
|
||||
watchedType !== "twilio" &&
|
||||
watchedType !== "ntfy" && (
|
||||
<ConfigBox
|
||||
@@ -202,31 +201,6 @@ 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")}
|
||||
|
||||
@@ -25,12 +25,7 @@ export const HeaderConfigStatusControls = ({
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
startIcon={
|
||||
<Icon
|
||||
icon={Trash}
|
||||
size={16}
|
||||
/>
|
||||
}
|
||||
startIcon={<Icon icon={Trash} />}
|
||||
onClick={onDelete}
|
||||
>
|
||||
{translate.t("common.buttons.delete")}
|
||||
|
||||
@@ -77,12 +77,7 @@ export const HeaderStatusPageControls = ({
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={
|
||||
<Icon
|
||||
icon={Settings}
|
||||
size={16}
|
||||
/>
|
||||
}
|
||||
startIcon={<Icon icon={Settings} />}
|
||||
onClick={() => navigate(`/status/configure/${statusPage.url}`)}
|
||||
>
|
||||
{t("common.buttons.configure")}
|
||||
|
||||
@@ -211,7 +211,6 @@ export const BaseStatusPage = ({
|
||||
>
|
||||
<Icon
|
||||
icon={TriangleAlert}
|
||||
size={20}
|
||||
color={theme.palette.warning.main}
|
||||
/>
|
||||
<Typography>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const SANS_STACK =
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||
'-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif';
|
||||
|
||||
export const BOLD_SANS_STACK = `ui-sans-serif, ${SANS_STACK}`;
|
||||
|
||||
|
||||
@@ -350,7 +350,11 @@ export const MonitorTable = ({
|
||||
onRowClick={(row) => {
|
||||
navigate(`/uptime/${row.id}`);
|
||||
}}
|
||||
isRowSelected={(row) => isRowSelected(row.id)}
|
||||
getRowSx={(row) => ({
|
||||
backgroundColor: isRowSelected(row.id)
|
||||
? theme.palette.action.selected
|
||||
: "inherit",
|
||||
})}
|
||||
/>
|
||||
<Pagination
|
||||
component="div"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsT
|
||||
import { useMonitorListController } from "@/Hooks/useMonitorListController";
|
||||
|
||||
const UptimeMonitorsPage = () => {
|
||||
const monitorListController = useMonitorListController({
|
||||
const c = useMonitorListController({
|
||||
types: "selectable",
|
||||
checksLimit: 25,
|
||||
refreshInterval: 5000,
|
||||
@@ -17,25 +17,25 @@ const UptimeMonitorsPage = () => {
|
||||
headerKey="uptime"
|
||||
page="uptime"
|
||||
actionLink="/uptime/create"
|
||||
controller={monitorListController}
|
||||
controller={c}
|
||||
bulkActions
|
||||
>
|
||||
<MonitorTable
|
||||
monitors={monitorListController.monitors || []}
|
||||
tags={monitorListController.tags || []}
|
||||
refetch={monitorListController.refetch}
|
||||
setSelectedMonitor={monitorListController.setSelectedMonitor}
|
||||
count={monitorListController.count || 0}
|
||||
page={monitorListController.page}
|
||||
setPage={monitorListController.setPage}
|
||||
rowsPerPage={monitorListController.rowsPerPage}
|
||||
sortField={monitorListController.sortField}
|
||||
setSortField={monitorListController.setSortField}
|
||||
sortOrder={monitorListController.sortOrder}
|
||||
setSortOrder={monitorListController.setSortOrder}
|
||||
setRowsPerPage={monitorListController.handleSetRowsPerPage}
|
||||
selectedRows={monitorListController.selectedRows}
|
||||
onSelectionChange={monitorListController.setSelectedRows}
|
||||
monitors={c.monitors || []}
|
||||
tags={c.tags || []}
|
||||
refetch={c.refetch}
|
||||
setSelectedMonitor={c.setSelectedMonitor}
|
||||
count={c.count || 0}
|
||||
page={c.page}
|
||||
setPage={c.setPage}
|
||||
rowsPerPage={c.rowsPerPage}
|
||||
sortField={c.sortField}
|
||||
setSortField={c.setSortField}
|
||||
sortOrder={c.sortOrder}
|
||||
setSortOrder={c.setSortOrder}
|
||||
setRowsPerPage={c.handleSetRowsPerPage}
|
||||
selectedRows={c.selectedRows}
|
||||
onSelectionChange={c.setSelectedRows}
|
||||
/>
|
||||
</MonitorListPage>
|
||||
);
|
||||
|
||||
@@ -8,9 +8,6 @@ import AuthRegister from "@/Pages/Auth/Register";
|
||||
import AuthForgotPassword from "@/Pages/Auth/Recovery";
|
||||
import AuthSetNewPassword from "@/Pages/Auth/SetNewPassword";
|
||||
|
||||
// Dashboard
|
||||
import Dashboard from "@/Pages/Dashboard";
|
||||
|
||||
// Uptime
|
||||
import Uptime from "@/Pages/Uptime/Monitors";
|
||||
import UptimeDetails from "@/Pages/Uptime/Details";
|
||||
@@ -93,11 +90,7 @@ const Routes = () => {
|
||||
>
|
||||
<Route
|
||||
path="/"
|
||||
element={<Navigate to="/dashboard" />}
|
||||
/>
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={<Dashboard />}
|
||||
element={<Navigate to="/uptime" />}
|
||||
/>
|
||||
<Route
|
||||
path="/uptime"
|
||||
|
||||
@@ -161,31 +161,6 @@ export interface DockerContainerMount {
|
||||
rw: boolean;
|
||||
}
|
||||
|
||||
export const DockerLogStreams = ["stdout", "stderr"] as const;
|
||||
export type DockerLogStream = (typeof DockerLogStreams)[number];
|
||||
|
||||
export interface DockerLogLine {
|
||||
ts: string;
|
||||
stream: DockerLogStream;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface DockerLog {
|
||||
id: string;
|
||||
metadata: {
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
containerId: string;
|
||||
containerName: string;
|
||||
};
|
||||
lines: DockerLogLine[];
|
||||
gap: boolean;
|
||||
checkedAt: string;
|
||||
expiry: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DockerContainerInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
DailyCheckBucket,
|
||||
DockerContainerInfo,
|
||||
DockerContainerSummary,
|
||||
DockerLog,
|
||||
} from "@/Types/Check";
|
||||
import type { PageSpeedGroupedCheck } from "@/Types/Check";
|
||||
import type { GeoContinent } from "@/Types/GeoCheck";
|
||||
@@ -159,7 +158,6 @@ export interface Monitor {
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
geoCheckInterval?: number;
|
||||
dockerLogsEnabled?: boolean;
|
||||
dnsServer?: string;
|
||||
dnsRecordType?: DnsRecordType;
|
||||
recentChecks: CheckSnapshot[];
|
||||
@@ -324,10 +322,6 @@ export interface DockerContainerResponse {
|
||||
stats: DockerContainerStats;
|
||||
}
|
||||
|
||||
export interface DockerContainerLogsResponse {
|
||||
logs: DockerLog[];
|
||||
nextCursor: string | null;
|
||||
}
|
||||
export interface Game {
|
||||
name: string;
|
||||
options?: {
|
||||
|
||||
@@ -9,7 +9,6 @@ export const NotificationChannels = [
|
||||
"teams",
|
||||
"telegram",
|
||||
"pushover",
|
||||
"signalgrid",
|
||||
"twilio",
|
||||
"ntfy",
|
||||
] as const;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import type { MonitorStatus, MonitorType } from "@/Types/Monitor";
|
||||
import type { PaletteKey } from "@/Utils/Theme/Theme";
|
||||
import type { ValueType } from "@/Components/design-elements/StatusLabel";
|
||||
import {
|
||||
DockerLogStreams,
|
||||
type DockerContainerMount,
|
||||
type DockerContainerPort,
|
||||
type DockerContainerState,
|
||||
type DockerLog,
|
||||
type DockerLogLine,
|
||||
import type {
|
||||
DockerContainerMount,
|
||||
DockerContainerPort,
|
||||
DockerContainerState,
|
||||
} from "@/Types/Check";
|
||||
|
||||
export const getMonitorPath = (type: MonitorType): string => {
|
||||
@@ -40,14 +37,6 @@ 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",
|
||||
@@ -155,35 +144,3 @@ export const getDockerMountLabel = (mount: DockerContainerMount) => {
|
||||
if (mount.source) return mount.source;
|
||||
return mount.type;
|
||||
};
|
||||
|
||||
export const DockerLogStreamFilters = ["all", ...DockerLogStreams] as const;
|
||||
export type DockerLogStreamFilter = (typeof DockerLogStreamFilters)[number];
|
||||
|
||||
export interface DockerLogRow extends DockerLogLine {
|
||||
key: string;
|
||||
gapBefore: boolean; // This signifies lines skipped
|
||||
}
|
||||
|
||||
export const flattenDockerLogs = (logs: DockerLog[]): DockerLogRow[] => {
|
||||
const rows: DockerLogRow[] = [];
|
||||
for (const log of [...logs].reverse()) {
|
||||
log.lines.forEach((line: DockerLogLine, idx: number) => {
|
||||
rows.push({ ...line, key: `${log.id}:${idx}`, gapBefore: idx === 0 && log.gap });
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
};
|
||||
|
||||
export const filterDockerLogRows = (
|
||||
rows: DockerLogRow[],
|
||||
stream: DockerLogStreamFilter,
|
||||
query: string
|
||||
): DockerLogRow[] => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
return rows.filter((row) => {
|
||||
return (
|
||||
(stream === "all" || row.stream === stream) &&
|
||||
(normalizedQuery === "" || row.text.toLowerCase().includes(normalizedQuery))
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -18,20 +18,6 @@ export const colors = {
|
||||
gray700: "#313131",
|
||||
gray900: "#151518",
|
||||
gray850: "#1c1c21",
|
||||
ink900: "#101917",
|
||||
ink600: "#55635C",
|
||||
ink400: "#7C8983",
|
||||
surface50: "#F6F8F7",
|
||||
rule200: "#C7D2CC",
|
||||
successMain: "#0D6049",
|
||||
errorMain: "#A02B24",
|
||||
warningMain: "#8C5806",
|
||||
inkDark900: "#ECEFED",
|
||||
inkDark600: "#A3ADA8",
|
||||
ruleDark: "#3A4642",
|
||||
successDark: "#4BC49E",
|
||||
errorDark: "#E8837A",
|
||||
warningDark: "#E0A458",
|
||||
brandGreen: "#13715B",
|
||||
brandGreenLight: "#4DAF94",
|
||||
green200: "#ECF7F2",
|
||||
@@ -57,31 +43,10 @@ export const lightPalette = {
|
||||
action: {
|
||||
rowHover: alpha(colors.gray900, HOVER.ROW),
|
||||
controlHover: alpha(colors.gray900, HOVER.CONTROL),
|
||||
selected: alpha(colors.brandGreen, 0.06),
|
||||
selectedHover: alpha(colors.brandGreen, 0.1),
|
||||
},
|
||||
secondary: {
|
||||
main: colors.gray200,
|
||||
},
|
||||
background: {
|
||||
default: colors.surface50,
|
||||
paper: "#FFFFFF",
|
||||
},
|
||||
text: {
|
||||
primary: colors.ink900,
|
||||
secondary: colors.ink600,
|
||||
disabled: colors.ink400,
|
||||
},
|
||||
divider: colors.rule200,
|
||||
success: {
|
||||
main: colors.successMain,
|
||||
},
|
||||
error: {
|
||||
main: colors.errorMain,
|
||||
},
|
||||
warning: {
|
||||
main: colors.warningMain,
|
||||
},
|
||||
sidebar: {
|
||||
accent: colors.brandGreen,
|
||||
},
|
||||
@@ -104,13 +69,10 @@ export const lightPalette = {
|
||||
export const darkPalette = {
|
||||
primary: {
|
||||
main: colors.brandGreen,
|
||||
light: colors.brandGreenLight,
|
||||
},
|
||||
action: {
|
||||
rowHover: alpha("#FFFFFF", HOVER.ROW),
|
||||
controlHover: alpha("#FFFFFF", HOVER.CONTROL),
|
||||
selected: alpha(colors.brandGreenLight, 0.07),
|
||||
selectedHover: alpha(colors.brandGreenLight, 0.11),
|
||||
},
|
||||
secondary: {
|
||||
main: colors.gray700,
|
||||
@@ -119,21 +81,6 @@ export const darkPalette = {
|
||||
default: colors.gray900,
|
||||
paper: colors.gray850,
|
||||
},
|
||||
text: {
|
||||
primary: colors.inkDark900,
|
||||
secondary: colors.inkDark600,
|
||||
disabled: colors.ink400,
|
||||
},
|
||||
divider: colors.ruleDark,
|
||||
success: {
|
||||
main: colors.successDark,
|
||||
},
|
||||
error: {
|
||||
main: colors.errorDark,
|
||||
},
|
||||
warning: {
|
||||
main: colors.warningDark,
|
||||
},
|
||||
sidebar: {
|
||||
accent: colors.brandGreenLight,
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ declare module "@mui/material/styles" {
|
||||
interface TypeAction {
|
||||
rowHover: string;
|
||||
controlHover: string;
|
||||
selectedHover: string;
|
||||
}
|
||||
interface Palette {
|
||||
sidebar: { accent: string };
|
||||
@@ -58,7 +57,7 @@ export type PaletteKey = {
|
||||
[K in keyof Theme["palette"]]: Theme["palette"][K] extends { main: any } ? K : never;
|
||||
}[keyof Theme["palette"]];
|
||||
|
||||
const fontFamilyPrimary = '"Geist Variable", system-ui, sans-serif';
|
||||
const fontFamilyPrimary = "system-ui, sans-serif";
|
||||
const fontFamilyMonospace =
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace';
|
||||
const shadow =
|
||||
@@ -86,13 +85,11 @@ export const theme = (mode: string, palette: any) =>
|
||||
fontSize: typographyLevels.base,
|
||||
h1: {
|
||||
fontSize: typographyLevels.xxl,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.015em",
|
||||
fontWeight: 500,
|
||||
},
|
||||
h2: {
|
||||
fontSize: typographyLevels.l,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "-0.01em",
|
||||
fontWeight: 400,
|
||||
},
|
||||
body1: {
|
||||
fontSize: typographyLevels.m,
|
||||
@@ -134,11 +131,7 @@ export const theme = (mode: string, palette: any) =>
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `1px solid ${
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.primary.light
|
||||
: theme.palette.primary.main
|
||||
}`,
|
||||
outline: `1px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: 2,
|
||||
},
|
||||
}),
|
||||
@@ -192,38 +185,18 @@ export const theme = (mode: string, palette: any) =>
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
input: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
height: "100%",
|
||||
},
|
||||
root: ({ theme }) => ({
|
||||
"& .MuiSelect-select": {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
minHeight: "unset",
|
||||
},
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.divider,
|
||||
transition: "border-color 0.15s ease",
|
||||
},
|
||||
"&:hover:not(.Mui-focused):not(.Mui-disabled) .MuiOutlinedInput-notchedOutline":
|
||||
{
|
||||
borderColor: theme.palette.text.disabled,
|
||||
},
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor:
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.primary.light
|
||||
: theme.palette.primary.main,
|
||||
borderWidth: 2,
|
||||
? "rgba(255, 255, 255, 0.23)"
|
||||
: theme.palette.text.primary,
|
||||
},
|
||||
"&.Mui-error .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.error.main,
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: palette.primary.main,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -18,8 +18,6 @@ export const LAYOUT = {
|
||||
XXL: 16,
|
||||
} as const;
|
||||
|
||||
export const INPUT_BASE_HEIGHT = 32; // Height for inputs/controls
|
||||
|
||||
export const HOVER = {
|
||||
DARKEN: 0.06, // This is a coefficient for darkening function
|
||||
ROW: 0.025, // Overlay alpha for hoverable rows and cards
|
||||
|
||||
@@ -111,8 +111,7 @@ const portSchema = baseSchema.extend({
|
||||
// Docker monitor schema
|
||||
const dockerSchema = baseSchema.extend({
|
||||
type: z.literal("docker"),
|
||||
url: z.string().min(1, "Docker host URL is required"),
|
||||
dockerLogsEnabled: z.boolean().register(monitorStepRegistry, { step: 1 }),
|
||||
url: z.string().min(1, "Container ID is required"),
|
||||
});
|
||||
|
||||
// Game server monitor schema
|
||||
|
||||
@@ -70,12 +70,6 @@ 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"),
|
||||
@@ -101,7 +95,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
teamsSchema,
|
||||
telegramSchema,
|
||||
pushoverSchema,
|
||||
signalgridSchema,
|
||||
twilioSchema,
|
||||
ntfySchema,
|
||||
]);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
+6
-191
@@ -206,7 +206,6 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"menu": {
|
||||
"dashboard": "Dashboard",
|
||||
"uptime": "Uptime",
|
||||
"pagespeed": "Pagespeed",
|
||||
"infrastructure": "Infrastructure",
|
||||
@@ -259,147 +258,6 @@
|
||||
}
|
||||
},
|
||||
"pages": {
|
||||
"dashboard": {
|
||||
"header": {
|
||||
"title": "Dashboard",
|
||||
"description": "See every monitor type in one place and spot what needs attention."
|
||||
},
|
||||
"card": {
|
||||
"error": "Could not load this card."
|
||||
},
|
||||
"cards": {
|
||||
"monitorStatus": {
|
||||
"title": "Monitor status",
|
||||
"empty": "No monitors yet.",
|
||||
"upOfTotal": "of {{total}} up"
|
||||
},
|
||||
"currentlyDown": {
|
||||
"title": "Currently down",
|
||||
"empty": "All monitors are up. No incidents open.",
|
||||
"count": "{{count}} needing attention"
|
||||
},
|
||||
"monitorsByType": {
|
||||
"title": "Monitors by type",
|
||||
"empty": "No monitors yet.",
|
||||
"down": "{{count}} down"
|
||||
},
|
||||
"recentIncidents": {
|
||||
"title": "Recent incidents",
|
||||
"empty": "No incidents — all systems healthy.",
|
||||
"active": "{{count}} open",
|
||||
"ongoing": "Ongoing",
|
||||
"manual": "Manual",
|
||||
"unknownMonitor": "Unknown monitor"
|
||||
},
|
||||
"failedChecks": {
|
||||
"title": "Failed checks",
|
||||
"empty": "No checks yet.",
|
||||
"ratio": "{{failed}} of {{total}} in the last day"
|
||||
},
|
||||
"checkmateServer": {
|
||||
"title": "Checkmate server",
|
||||
"memory": "Memory",
|
||||
"eventLoop": "Event loop delay",
|
||||
"uptime": "Uptime",
|
||||
"database": "Database",
|
||||
"readsWrites": "{{reads}} reads/s · {{writes}} writes/s"
|
||||
},
|
||||
"ranking": {
|
||||
"topOf": "Top {{shown}} of {{total}}",
|
||||
"notEnough": "Not enough monitors to rank yet."
|
||||
},
|
||||
"slowestMonitors": {
|
||||
"title": "Slowest monitors"
|
||||
},
|
||||
"lowestUptime": {
|
||||
"title": "Lowest uptime"
|
||||
},
|
||||
"monitorsByGroup": {
|
||||
"title": "Monitors by group",
|
||||
"empty": "No groups yet. Groups are set per monitor."
|
||||
},
|
||||
"busiestServers": {
|
||||
"title": "Busiest servers",
|
||||
"empty": "No infrastructure monitors yet.",
|
||||
"cpu": "CPU",
|
||||
"memory": "Memory",
|
||||
"disk": "Disk"
|
||||
},
|
||||
"incidentStats": {
|
||||
"title": "Incident stats",
|
||||
"openNow": "Open now",
|
||||
"avgResolution": "Average time to resolve",
|
||||
"hours": "{{hours}} h",
|
||||
"mostIncidents": "Most incidents",
|
||||
"total": "total",
|
||||
"resolved": "Resolved",
|
||||
"automatic": "Automatic",
|
||||
"manual": "Manual",
|
||||
"autoManual": "{{automatic}} automatic · {{manual}} manual"
|
||||
},
|
||||
"notificationChannels": {
|
||||
"title": "Notification channels",
|
||||
"empty": "No channels yet."
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Maintenance windows",
|
||||
"empty": "No maintenance scheduled.",
|
||||
"runningNow": "Running now",
|
||||
"next": "Next window"
|
||||
},
|
||||
"statusPages": {
|
||||
"title": "Status pages",
|
||||
"empty": "No status pages yet.",
|
||||
"monitorCount": "{{count}} monitors",
|
||||
"published": "Published",
|
||||
"draft": "Draft"
|
||||
},
|
||||
"checksOnSchedule": {
|
||||
"title": "Check queue",
|
||||
"healthy": "All {{jobs}} jobs on schedule across {{workers}} worker(s).",
|
||||
"failing": "Failing jobs",
|
||||
"scheduled": "Scheduled jobs",
|
||||
"runningNow": "Running now"
|
||||
},
|
||||
"recentErrors": {
|
||||
"title": "Recent errors",
|
||||
"empty": "No errors logged.",
|
||||
"count": "{{count}} errors",
|
||||
"warnings": "Warnings"
|
||||
},
|
||||
"team": {
|
||||
"title": "Team members",
|
||||
"members": "{{count}} members",
|
||||
"unverified": "Unverified"
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"trigger": "Edit cards",
|
||||
"title": "Dashboard cards",
|
||||
"selectAll": "Select all",
|
||||
"done": "Done",
|
||||
"reset": "Reset to defaults",
|
||||
"groups": {
|
||||
"uptime": "Uptime",
|
||||
"infrastructure": "Infrastructure",
|
||||
"incidents": "Incidents",
|
||||
"notifications": "Notifications",
|
||||
"maintenance": "Maintenance",
|
||||
"statusPages": "Status pages",
|
||||
"checks": "Checks",
|
||||
"logs": "System",
|
||||
"settings": "Settings"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
"title": "No cards on your dashboard",
|
||||
"description": "Choose the cards you want to see. Your selection is remembered in this browser."
|
||||
},
|
||||
"reorder": {
|
||||
"moveUp": "Move up",
|
||||
"moveDown": "Move down"
|
||||
}
|
||||
},
|
||||
"proxies": {
|
||||
"header": {
|
||||
"title": "Proxies",
|
||||
@@ -774,9 +632,9 @@
|
||||
},
|
||||
"general": {
|
||||
"option": {
|
||||
"dockerHost": {
|
||||
"label": "Docker host",
|
||||
"placeholder": "unix:///var/run/docker.sock"
|
||||
"container": {
|
||||
"label": "Container name/ID",
|
||||
"placeholder": "my-app or abcd1234"
|
||||
},
|
||||
"host": {
|
||||
"label": "Host",
|
||||
@@ -858,7 +716,7 @@
|
||||
"http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard. Set an optional proxy server to connect through.",
|
||||
"ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.",
|
||||
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.",
|
||||
"docker": "Enter the Docker socket to monitor, as unix:///var/run/docker.sock or an absolute path such as /var/run/docker.sock. Every container on the host is monitored.",
|
||||
"docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
|
||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type. Some games require enabling server queries in their config file — check your game server's documentation. See the <gamedigLink>supported games list</gamedigLink> for protocol details.",
|
||||
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
||||
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
|
||||
@@ -898,7 +756,7 @@
|
||||
},
|
||||
"type": {
|
||||
"description": "Select the type of check to perform",
|
||||
"optionDockerDescription": "Monitor a Docker host and every container running on it.",
|
||||
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
||||
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
||||
@@ -956,15 +814,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dockerLogs": {
|
||||
"title": "Container logs",
|
||||
"description": "Collect the most recent log lines from every container on this host on each check. Logs are kept for 7 days. Leave this off for hosts with chatty containers to save database space.",
|
||||
"option": {
|
||||
"enabled": {
|
||||
"label": "Collect container logs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"title": "Monitor history",
|
||||
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.",
|
||||
@@ -1107,31 +956,6 @@
|
||||
"tabs": {
|
||||
"overview": "Overview",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"logs": {
|
||||
"recentLines": "{{count}} recent lines",
|
||||
"loading": "Loading...",
|
||||
"updatedSecondsAgo": "Updated {{count}}s ago",
|
||||
"updatedMinutesAgo": "Updated {{count}}m ago",
|
||||
"skipped": "Some log entries were skipped",
|
||||
"loadOlder": "Load older logs",
|
||||
"loadingOlder": "Loading...",
|
||||
"noOlder": "Beginning of retained logs",
|
||||
"loadError": "Could not load logs",
|
||||
"loadOlderError": "Could not load older logs",
|
||||
"empty": {
|
||||
"title": "No logs collected yet",
|
||||
"description": "Lines will appear here after the next check. Only output written since collection was turned on is stored."
|
||||
},
|
||||
"disabled": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
@@ -1393,7 +1217,7 @@
|
||||
},
|
||||
"startTime": {
|
||||
"title": "Start time",
|
||||
"description": "Set the start time and duration for your maintenance window. All values are in your local time zone.",
|
||||
"description": "Set the start time and duration for your maintenance window. All values are in UTC",
|
||||
"option": {
|
||||
"duration": {
|
||||
"label": "Duration"
|
||||
@@ -1475,7 +1299,6 @@
|
||||
"teams": "Teams",
|
||||
"telegram": "Telegram",
|
||||
"pushover": "Pushover",
|
||||
"signalgrid": "Signalgrid",
|
||||
"twilio": "Twilio",
|
||||
"ntfy": "ntfy"
|
||||
}
|
||||
@@ -1496,14 +1319,6 @@
|
||||
"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.",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.jsx";
|
||||
// Self-hosted so the app keeps its typeface on air-gapped installs
|
||||
import "@fontsource-variable/geist";
|
||||
import "./index.css";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
@@ -24,8 +24,6 @@ services:
|
||||
checkmate-demo:
|
||||
image: ghcr.io/bluewave-labs/checkmate:master
|
||||
container_name: checkmate-demo
|
||||
group_add:
|
||||
- "989"
|
||||
restart: always
|
||||
env_file:
|
||||
- server-prod.env
|
||||
@@ -70,8 +68,6 @@ services:
|
||||
checkmate-staging:
|
||||
image: ghcr.io/bluewave-labs/checkmate:develop
|
||||
container_name: checkmate-staging
|
||||
group_add:
|
||||
- "989"
|
||||
restart: always
|
||||
env_file:
|
||||
- server-staging.env
|
||||
|
||||
@@ -37,12 +37,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>تطبيق مفتوح المصدر لمراقبة الجاهزية والبنية التحتية</strong></p>
|
||||
<p align="center"><strong>تطبيق مفتوح المصدر لمراقبة الجاهزية والبنية التحتية</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -87,13 +88,15 @@
|
||||
|
||||
اطّلع على تعليمات التثبيت في [بوابة وثائق Checkmate](https://checkmate.so/docs).
|
||||
|
||||
تتوفر أيضًا خيارات تثبيت بنقرة واحدة مثل [Repocloud](https://repocloud.io/details/Checkmate)، و[Pikapods](https://www.pikapods.com/)، و[Coolify](https://coolify.io/)، و[Elestio](https://elest.io/open-source/checkmate)، و[K8s](../../charts/helm/checkmate/INSTALLATION.md)، و[Sive Host](https://sive.host)، أو [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
بدلاً من ذلك، يمكنك استخدام [Coolify](https://coolify.io/)، أو [Elestio](https://elest.io/open-source/checkmate)، أو [K8s](../../charts/helm/checkmate/INSTALLATION.md)، أو [Sive Host](https://sive.host) (جنوب أفريقيا)، أو [Cloudzy](https://cloudzy.com/marketplace/checkmate)، أو [Pikapods](https://www.pikapods.com/) لتشغيل نسخة من Checkmate بسرعة. إن أردت مراقبة البنية التحتية لخوادمك، ستحتاج إلى [وكيل Capture](https://github.com/bluewave-labs/capture). يحتوي مستودع Capture أيضًا على تعليمات التثبيت.
|
||||
|
||||
### استخدام شهادة CA مخصصة
|
||||
|
||||
إذا كنت بحاجة لمراقبة نقاط نهاية HTTPS داخلية بشهادات صادرة عن سلطات شهادات خاصة (مثل Smallstep)، راجع [دليل الثقة بشهادات CA المخصصة](../custom-ca-trust.md) لخيارات إعداد Docker.
|
||||
إذا كنت بحاجة لمراقبة نقاط نهاية HTTPS داخلية بشهادات صادرة عن سلطات شهادات خاصة (مثل Smallstep)، راجع [دليل الثقة بشهادات CA المخصصة](https://checkmate.so/docs/settings/custom-ca-trust) لخيارات إعداد Docker.
|
||||
|
||||
للمزيد من الوثائق، راجع [مجلد docs](../).
|
||||
للمزيد من الوثائق، راجع [وثائق Checkmate](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## الأداء
|
||||
@@ -180,9 +183,9 @@
|
||||
<a id="contributing"></a>
|
||||
## المساهمة
|
||||
|
||||
نحن [Alex](http://github.com/ajhollid) (قائد الفريق)، و[Gorkem](http://github.com/gorkem-bwl/)، و[Aryaman](https://github.com/Br0wnHammer)، و[Mert](https://github.com/mertssmnoglu)، و[Karen](https://github.com/karenvicent) نساعد الأفراد والشركات على مراقبة بنيتهم التحتية وخوادمهم.
|
||||
نحن [Alex](http://github.com/ajhollid) (قائد الفريق)، و[Gorkem](http://github.com/gorkem-bwl/)، و[Aryaman](https://github.com/Br0wnHammer)، و[Mert](https://github.com/mertssmnoglu) نساعد الأفراد والشركات على مراقبة بنيتهم التحتية وخوادمهم.
|
||||
|
||||
نفخر ببناء علاقات قوية مع المساهمين على جميع المستويات. وعلى الرغم من حداثة المشروع، فقد حصل Checkmate بالفعل على أكثر من 7000 نجمة، وجذب أكثر من 90 مساهمًا من جميع أنحاء العالم.
|
||||
نفخر ببناء علاقات قوية مع المساهمين على جميع المستويات. وعلى الرغم من حداثة المشروع، فقد حصل Checkmate بالفعل على ما يقارب 11 ألف نجمة، وجذب أكثر من 150 مساهمًا من جميع أنحاء العالم.
|
||||
|
||||
تم تنجيم مستودعنا من قِبل موظفين في **Google، Microsoft، Intel، Cisco، Tencent، Electronic Arts، ByteDance، JP Morgan Chase، Deloitte، Accenture، Foxconn، Broadcom، China Telecom، Barclays، Capgemini، Wipro، Cloudflare، Dassault Systèmes، وNEC** — فلا تتردد، انضم إلينا، وساهم، وتعلّم معنا!
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Una aplicació de codi obert per a la monitorització de disponibilitat i infraestructura</strong></p>
|
||||
<p align="center"><strong>Una aplicació de codi obert per a la monitorització de disponibilitat i infraestructura</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -85,13 +86,15 @@ Pots trobar les instruccions d'ús [aquí](https://checkmate.so/docs).
|
||||
|
||||
Consulta les instruccions d'instal·lació al [portal de documentació de Checkmate](https://checkmate.so/docs).
|
||||
|
||||
També hi ha opcions d'instal·lació amb un sol clic com ara [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) o [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
Alternativament, també pots utilitzar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sud-àfrica), [Cloudzy](https://cloudzy.com/marketplace/checkmate) o [Pikapods](https://www.pikapods.com/) per posar en marxa ràpidament una instància de Checkmate. Si vols monitoritzar la infraestructura de servidors, necessitaràs l'[agent Capture](https://github.com/bluewave-labs/capture). El repositori de Capture també conté les instruccions d'instal·lació.
|
||||
|
||||
### Utilitzar una CA personalitzada
|
||||
|
||||
Si necessites monitoritzar punts HTTPS interns amb certificats d'autoritats certificadores privades (com Smallstep), consulta la nostra [Guia de confiança per a CA personalitzades](../custom-ca-trust.md) per a les opcions de configuració de Docker.
|
||||
Si necessites monitoritzar punts HTTPS interns amb certificats d'autoritats certificadores privades (com Smallstep), consulta la nostra [Guia de confiança per a CA personalitzades](https://checkmate.so/docs/settings/custom-ca-trust) per a les opcions de configuració de Docker.
|
||||
|
||||
Per a més documentació, consulta el [directori docs](../).
|
||||
Per a més documentació, consulta la [documentació de Checkmate](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Rendiment
|
||||
@@ -178,9 +181,9 @@ No dubtis a preguntar o compartir les teves idees — ens encantarà tenir notí
|
||||
<a id="contributing"></a>
|
||||
## Contribuir
|
||||
|
||||
Som l'[Alex](http://github.com/ajhollid) (líder d'equip), en [Gorkem](http://github.com/gorkem-bwl/), l'[Aryaman](https://github.com/Br0wnHammer), en [Mert](https://github.com/mertssmnoglu) i la [Karen](https://github.com/karenvicent), i ajudem persones i empreses a monitoritzar la seva infraestructura i els seus servidors.
|
||||
Som l'[Alex](http://github.com/ajhollid) (líder d'equip), en [Gorkem](http://github.com/gorkem-bwl/), l'[Aryaman](https://github.com/Br0wnHammer) i en [Mert](https://github.com/mertssmnoglu), i ajudem persones i empreses a monitoritzar la seva infraestructura i els seus servidors.
|
||||
|
||||
Ens enorgullim de construir vincles forts amb els col·laboradors a tots els nivells. Tot i ser un projecte jove, Checkmate ja ha aconseguit més de 7.000 estrelles i ha atret més de 90 col·laboradors d'arreu del món.
|
||||
Ens enorgullim de construir vincles forts amb els col·laboradors a tots els nivells. Tot i ser un projecte jove, Checkmate ja ha aconseguit gairebé 11.000 estrelles i ha atret més de 150 col·laboradors d'arreu del món.
|
||||
|
||||
El nostre repositori té estrelles d'empleats de **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes i NEC** — no t'estiguis de res: uneix-te, contribueix i aprèn amb nosaltres!
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Open source aplikace pro monitorování dostupnosti a infrastruktury</strong></p>
|
||||
<p align="center"><strong>Open source aplikace pro monitorování dostupnosti a infrastruktury</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -85,13 +86,15 @@ Pokyny k použití najdete [zde](https://checkmate.so/docs).
|
||||
|
||||
Pokyny k instalaci najdete v [dokumentačním portálu Checkmate](https://checkmate.so/docs).
|
||||
|
||||
K dispozici jsou také možnosti instalace jedním kliknutím, jako [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) nebo [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
Případně můžete použít [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Jižní Afrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) nebo [Pikapods](https://www.pikapods.com/) pro rychlé spuštění instance Checkmate. Pokud chcete monitorovat infrastrukturu serverů, budete potřebovat [agenta Capture](https://github.com/bluewave-labs/capture). Repozitář Capture obsahuje také pokyny k instalaci.
|
||||
|
||||
### Použití vlastní CA
|
||||
|
||||
Pokud potřebujete monitorovat interní HTTPS endpointy s certifikáty od soukromých certifikačních autorit (jako je Smallstep), přečtěte si naši [příručku pro důvěřování vlastním CA](../custom-ca-trust.md) pro možnosti konfigurace Dockeru.
|
||||
Pokud potřebujete monitorovat interní HTTPS endpointy s certifikáty od soukromých certifikačních autorit (jako je Smallstep), přečtěte si naši [příručku pro důvěřování vlastním CA](https://checkmate.so/docs/settings/custom-ca-trust) pro možnosti konfigurace Dockeru.
|
||||
|
||||
Další dokumentaci najdete v [adresáři docs](../).
|
||||
Další dokumentaci najdete v [dokumentaci Checkmate](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Výkon
|
||||
@@ -178,9 +181,9 @@ Neváhejte se ptát nebo sdílet své nápady — moc rádi vás slyšíme!
|
||||
<a id="contributing"></a>
|
||||
## Přispívání
|
||||
|
||||
Jsme [Alex](http://github.com/ajhollid) (vedoucí týmu), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) a [Karen](https://github.com/karenvicent) a pomáháme jednotlivcům a firmám monitorovat jejich infrastrukturu a servery.
|
||||
Jsme [Alex](http://github.com/ajhollid) (vedoucí týmu), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) a [Mert](https://github.com/mertssmnoglu) a pomáháme jednotlivcům a firmám monitorovat jejich infrastrukturu a servery.
|
||||
|
||||
Jsme hrdí na to, že budujeme silné vztahy s přispěvateli na všech úrovních. Přestože jsme mladý projekt, Checkmate už získal přes 7000 hvězdiček a přilákal více než 90 přispěvatelů z celého světa.
|
||||
Jsme hrdí na to, že budujeme silné vztahy s přispěvateli na všech úrovních. Přestože jsme mladý projekt, Checkmate už získal téměř 11 tisíc hvězdiček a přilákal více než 150 přispěvatelů z celého světa.
|
||||
|
||||
Náš repozitář mají označený zaměstnanci společností **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes a NEC** — takže neváhejte, zapojte se, přispějte a učte se s námi!
|
||||
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Eine Open-Source-Anwendung zur Überwachung von Verfügbarkeit und Infrastruktur</strong></p>
|
||||
<p align="center"><strong>Eine Open-Source-Anwendung zur Überwachung von Verfügbarkeit und Infrastruktur</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -82,13 +83,15 @@ Die Nutzungsanleitung findest du [hier](https://checkmate.so/docs).
|
||||
|
||||
Die Installationsanleitung findest du im [Dokumentationsportal von Checkmate](https://checkmate.so/docs).
|
||||
|
||||
Es gibt auch 1-Klick-Installationsoptionen wie [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) oder [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
Alternativ kannst du [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Südafrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) oder [Pikapods](https://www.pikapods.com/) nutzen, um schnell eine Checkmate-Instanz hochzuziehen. Für das Monitoring deiner Server-Infrastruktur brauchst du den [Capture-Agenten](https://github.com/bluewave-labs/capture); dessen Repository enthält ebenfalls eine Installationsanleitung.
|
||||
|
||||
### Eigene CA verwenden
|
||||
|
||||
Wenn du interne HTTPS-Endpunkte mit Zertifikaten privater Zertifizierungsstellen (z. B. Smallstep) überwachen möchtest, schau in unseren [Leitfaden für eigene CAs](../custom-ca-trust.md) — dort stehen die passenden Docker-Konfigurationen.
|
||||
Wenn du interne HTTPS-Endpunkte mit Zertifikaten privater Zertifizierungsstellen (z. B. Smallstep) überwachen möchtest, schau in unseren [Leitfaden für eigene CAs](https://checkmate.so/docs/settings/custom-ca-trust) — dort stehen die passenden Docker-Konfigurationen.
|
||||
|
||||
Weitere Dokumentation liegt im [docs-Verzeichnis](../).
|
||||
Weitere Dokumentation findest du in der [Checkmate-Dokumentation](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Leistung
|
||||
@@ -175,9 +178,9 @@ Frag einfach drauflos oder teile deine Ideen — wir freuen uns auf dein Feedbac
|
||||
<a id="contributing"></a>
|
||||
## Mitwirken
|
||||
|
||||
Wir sind [Alex](http://github.com/ajhollid) (Team-Lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) und [Karen](https://github.com/karenvicent) und helfen Einzelpersonen und Unternehmen dabei, ihre Infrastruktur und Server zu überwachen.
|
||||
Wir sind [Alex](http://github.com/ajhollid) (Team-Lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) und [Mert](https://github.com/mertssmnoglu) und helfen Einzelpersonen und Unternehmen dabei, ihre Infrastruktur und Server zu überwachen.
|
||||
|
||||
Wir sind stolz darauf, auf jeder Ebene starke Beziehungen zu unseren Beitragenden zu pflegen. Obwohl Checkmate ein junges Projekt ist, hat es bereits über 7.000 Sterne und mehr als 90 Beitragende aus aller Welt angezogen.
|
||||
Wir sind stolz darauf, auf jeder Ebene starke Beziehungen zu unseren Beitragenden zu pflegen. Obwohl Checkmate ein junges Projekt ist, hat es bereits fast 11.000 Sterne und mehr als 150 Beitragende aus aller Welt angezogen.
|
||||
|
||||
Mitarbeiterinnen und Mitarbeiter von **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes und NEC** haben unser Repository bereits gesternt — also keine falsche Zurückhaltung: mitmachen, beitragen und mit uns lernen!
|
||||
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Una aplicación de código abierto para monitorización de disponibilidad e infraestructura</strong></p>
|
||||
<p align="center"><strong>Una aplicación de código abierto para monitorización de disponibilidad e infraestructura</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -82,13 +83,15 @@ Las instrucciones de uso están disponibles [aquí](https://checkmate.so/docs).
|
||||
|
||||
Consulta las instrucciones de instalación en el [portal de documentación de Checkmate](https://checkmate.so/docs).
|
||||
|
||||
También existen opciones de instalación con un clic como [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) o [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
También puedes utilizar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sudáfrica), [Cloudzy](https://cloudzy.com/marketplace/checkmate) o [Pikapods](https://www.pikapods.com/) para levantar rápidamente una instancia de Checkmate. Si quieres monitorizar la infraestructura de tus servidores, necesitarás el [agente Capture](https://github.com/bluewave-labs/capture). El repositorio de Capture también incluye las instrucciones de instalación.
|
||||
|
||||
### Uso de una CA personalizada
|
||||
|
||||
Si necesitas monitorizar endpoints HTTPS internos con certificados emitidos por autoridades certificadoras privadas (como Smallstep), consulta nuestra [guía de confianza para CAs personalizadas](../custom-ca-trust.md) con las opciones de configuración para Docker.
|
||||
Si necesitas monitorizar endpoints HTTPS internos con certificados emitidos por autoridades certificadoras privadas (como Smallstep), consulta nuestra [guía de confianza para CAs personalizadas](https://checkmate.so/docs/settings/custom-ca-trust) con las opciones de configuración para Docker.
|
||||
|
||||
Para más documentación, consulta el [directorio docs](../).
|
||||
Para más documentación, consulta la [documentación de Checkmate](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Rendimiento
|
||||
@@ -175,9 +178,9 @@ No dudes en preguntar o compartir tus ideas — ¡nos encantará saber de ti!
|
||||
<a id="contributing"></a>
|
||||
## Contribuir
|
||||
|
||||
Somos [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) y [Karen](https://github.com/karenvicent), y ayudamos a personas y empresas a monitorizar su infraestructura y sus servidores.
|
||||
Somos [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) y [Mert](https://github.com/mertssmnoglu), y ayudamos a personas y empresas a monitorizar su infraestructura y sus servidores.
|
||||
|
||||
Nos enorgullecemos de construir relaciones sólidas con la comunidad de contribuidores de cualquier nivel. A pesar de ser un proyecto joven, Checkmate ya ha conseguido más de 7000 estrellas y reúne a 90+ contribuidores de todo el mundo.
|
||||
Nos enorgullecemos de construir relaciones sólidas con la comunidad de contribuidores de cualquier nivel. A pesar de ser un proyecto joven, Checkmate ya ha conseguido casi 11.000 estrellas y reúne a 150+ contribuidores de todo el mundo.
|
||||
|
||||
Nuestro repositorio cuenta con estrellas de empleados de **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes y NEC** — así que no te cortes: únete, contribuye y aprende con nosotros.
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Avoimen lähdekoodin sovellus saatavuuden ja infrastruktuurin valvontaan</strong></p>
|
||||
<p align="center"><strong>Avoimen lähdekoodin sovellus saatavuuden ja infrastruktuurin valvontaan</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -85,13 +86,15 @@ Käyttöohjeet löytyvät [täältä](https://checkmate.so/docs).
|
||||
|
||||
Asennusohjeet löytyvät [Checkmaten dokumentaatioportaalista](https://checkmate.so/docs).
|
||||
|
||||
Tarjolla on myös yhden klikkauksen asennusvaihtoehtoja, kuten [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) tai [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
Vaihtoehtoisesti voit käyttää [Coolifya](https://coolify.io/), [Elestiota](https://elest.io/open-source/checkmate), [K8s:ää](../../charts/helm/checkmate/INSTALLATION.md), [Sive Hostia](https://sive.host) (Etelä-Afrikka), [Cloudzya](https://cloudzy.com/marketplace/checkmate) tai [Pikapodsia](https://www.pikapods.com/) Checkmate-instanssin nopeaan käynnistämiseen. Jos haluat valvoa palvelininfrastruktuuriasi, tarvitset [Capture-agentin](https://github.com/bluewave-labs/capture). Capturen repositorio sisältää myös sen asennusohjeet.
|
||||
|
||||
### Mukautetun CA:n käyttö
|
||||
|
||||
Jos haluat valvoa sisäisiä HTTPS-päätepisteitä yksityisten varmenneauktoriteettien (kuten Smallstepin) varmenteilla, lue [mukautetun CA:n luottamusopas](../custom-ca-trust.md) Dockerin konfigurointivaihtoehdoista.
|
||||
Jos haluat valvoa sisäisiä HTTPS-päätepisteitä yksityisten varmenneauktoriteettien (kuten Smallstepin) varmenteilla, lue [mukautetun CA:n luottamusopas](https://checkmate.so/docs/settings/custom-ca-trust) Dockerin konfigurointivaihtoehdoista.
|
||||
|
||||
Lisää dokumentaatiota löydät [docs-hakemistosta](../).
|
||||
Lisää dokumentaatiota löydät [Checkmaten dokumentaatiosta](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Suorituskyky
|
||||
@@ -178,9 +181,9 @@ Kysy rohkeasti tai jaa ideoitasi — kuulemme sinusta mielellämme!
|
||||
<a id="contributing"></a>
|
||||
## Osallistuminen
|
||||
|
||||
Olemme [Alex](http://github.com/ajhollid) (tiiminvetäjä), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) ja [Karen](https://github.com/karenvicent) — autamme yksityishenkilöitä ja yrityksiä valvomaan infrastruktuuriaan ja palvelimiaan.
|
||||
Olemme [Alex](http://github.com/ajhollid) (tiiminvetäjä), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) ja [Mert](https://github.com/mertssmnoglu) — autamme yksityishenkilöitä ja yrityksiä valvomaan infrastruktuuriaan ja palvelimiaan.
|
||||
|
||||
Olemme ylpeitä siitä, että rakennamme vahvoja suhteita osallistujiin kaikilla tasoilla. Vaikka projekti on nuori, Checkmate on jo saanut yli 7000 tähteä ja yli 90 osallistujaa eri puolilta maailmaa.
|
||||
Olemme ylpeitä siitä, että rakennamme vahvoja suhteita osallistujiin kaikilla tasoilla. Vaikka projekti on nuori, Checkmate on jo saanut lähes 11 000 tähteä ja yli 150 osallistujaa eri puolilta maailmaa.
|
||||
|
||||
Repoamme tähdittävät työntekijät yhtiöistä **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes ja NEC** — älä siis epäröi, hyppää mukaan, osallistu ja opi kanssamme!
|
||||
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Une application open source de surveillance de la disponibilité et de l'infrastructure</strong></p>
|
||||
<p align="center"><strong>Une application open source de surveillance de la disponibilité et de l'infrastructure</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -82,13 +83,15 @@ Les instructions d'utilisation sont disponibles [ici](https://checkmate.so/docs)
|
||||
|
||||
Consultez les instructions d'installation dans le [portail de documentation Checkmate](https://checkmate.so/docs).
|
||||
|
||||
Il existe également des options d'installation en un clic comme [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) ou [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
Vous pouvez également utiliser [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Afrique du Sud), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ou [Pikapods](https://www.pikapods.com/) pour démarrer rapidement une instance Checkmate. Pour surveiller votre infrastructure serveur, vous aurez besoin de l'[agent Capture](https://github.com/bluewave-labs/capture). Le dépôt Capture contient également les instructions d'installation.
|
||||
|
||||
### Utiliser une autorité de certification personnalisée
|
||||
|
||||
Si vous devez surveiller des points d'accès HTTPS internes avec des certificats issus d'autorités de certification privées (comme Smallstep), consultez notre [guide de confiance d'autorité de certification personnalisée](../custom-ca-trust.md) pour les options de configuration Docker.
|
||||
Si vous devez surveiller des points d'accès HTTPS internes avec des certificats issus d'autorités de certification privées (comme Smallstep), consultez notre [guide de confiance d'autorité de certification personnalisée](https://checkmate.so/docs/settings/custom-ca-trust) pour les options de configuration Docker.
|
||||
|
||||
Pour plus de documentation, consultez le [répertoire docs](../).
|
||||
Pour plus de documentation, consultez la [documentation Checkmate](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Performance
|
||||
@@ -175,9 +178,9 @@ N'hésitez pas à poser vos questions ou partager vos idées — nous adorerions
|
||||
<a id="contributing"></a>
|
||||
## Contribuer
|
||||
|
||||
Nous sommes [Alex](http://github.com/ajhollid) (chef d'équipe), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) et [Karen](https://github.com/karenvicent), et nous aidons les particuliers et les entreprises à surveiller leur infrastructure et leurs serveurs.
|
||||
Nous sommes [Alex](http://github.com/ajhollid) (chef d'équipe), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) et [Mert](https://github.com/mertssmnoglu), et nous aidons les particuliers et les entreprises à surveiller leur infrastructure et leurs serveurs.
|
||||
|
||||
Nous sommes fiers de construire des liens solides avec les contributeurs à tous les niveaux. Bien que jeune, Checkmate a déjà obtenu plus de 7 000 étoiles et attiré plus de 90 contributeurs du monde entier.
|
||||
Nous sommes fiers de construire des liens solides avec les contributeurs à tous les niveaux. Bien que jeune, Checkmate a déjà obtenu près de 11 000 étoiles et attiré plus de 150 contributeurs du monde entier.
|
||||
|
||||
Notre dépôt a été suivi par des employés de **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes et NEC** — alors n'hésitez pas, rejoignez-nous, contribuez et apprenez avec nous !
|
||||
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Un'applicazione open source per il monitoraggio di uptime e infrastruttura</strong></p>
|
||||
<p align="center"><strong>Un'applicazione open source per il monitoraggio di uptime e infrastruttura</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -80,15 +81,17 @@ Le istruzioni per l'uso sono disponibili [qui](https://checkmate.so/docs).
|
||||
<a id="installation"></a>
|
||||
## Installazione
|
||||
|
||||
Consulta le istruzioni di installazione nel [portale della documentazione di Checkmate](https://checkmate.so/docs).
|
||||
Consulta le istruzioni di installazione nel [portale della documentazione di Checkmate](https://checkmate.so/docs).
|
||||
|
||||
Sono disponibili anche opzioni di installazione con un clic come [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) o [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
In alternativa, puoi usare [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sudafrica), [Cloudzy](https://cloudzy.com/marketplace/checkmate) o [Pikapods](https://www.pikapods.com/) per creare rapidamente un'istanza di Checkmate. Se desideri monitorare l'infrastruttura dei tuoi server, ti servirà l'[agente Capture](https://github.com/bluewave-labs/capture). Anche il repository di Capture contiene le istruzioni di installazione.
|
||||
|
||||
### Usare una CA personalizzata
|
||||
|
||||
Se hai bisogno di monitorare endpoint HTTPS interni con certificati emessi da Autorità di Certificazione (CA) private (come Smallstep), consulta la nostra [Guida all'attendibilità delle CA personalizzate](../custom-ca-trust.md) per le opzioni di configurazione di Docker.
|
||||
Se hai bisogno di monitorare endpoint HTTPS interni con certificati emessi da Autorità di Certificazione (CA) private (come Smallstep), consulta la nostra [Guida all'attendibilità delle CA personalizzate](https://checkmate.so/docs/settings/custom-ca-trust) per le opzioni di configurazione di Docker.
|
||||
|
||||
Per ulteriore documentazione, consulta la [cartella docs](../).
|
||||
Per ulteriore documentazione, consulta la [documentazione di Checkmate](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Prestazioni
|
||||
@@ -175,9 +178,9 @@ Non esitare a fare domande o a condividere le tue idee: ci farebbe piacere senti
|
||||
<a id="contributing"></a>
|
||||
## Come contribuire
|
||||
|
||||
Siamo [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) e [Karen](https://github.com/karenvicent), e aiutiamo privati e aziende a monitorare la propria infrastruttura e i propri server.
|
||||
Siamo [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) e [Mert](https://github.com/mertssmnoglu), e aiutiamo privati e aziende a monitorare la propria infrastruttura e i propri server.
|
||||
|
||||
Siamo orgogliosi di costruire legami solidi con i contributori a ogni livello. Pur essendo un progetto giovane, Checkmate ha già raccolto oltre 7000 stelle e attirato più di 90 contributori da tutto il mondo.
|
||||
Siamo orgogliosi di costruire legami solidi con i contributori a ogni livello. Pur essendo un progetto giovane, Checkmate ha già raccolto quasi 11.000 stelle e attirato più di 150 contributori da tutto il mondo.
|
||||
|
||||
Il nostro repository ha ricevuto una stella da dipendenti di **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes e NEC**, quindi non trattenerti: unisciti a noi, contribuisci e impara con noi!
|
||||
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>オープンソースの稼働状況とインフラ監視アプリケーション</strong></p>
|
||||
<p align="center"><strong>オープンソースの稼働状況とインフラ監視アプリケーション</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -82,13 +83,15 @@ Checkmate は 1000 以上のアクティブなモニターでストレステス
|
||||
|
||||
インストール手順は [Checkmate ドキュメントポータル](https://checkmate.so/docs) を参照してください。
|
||||
|
||||
[Repocloud](https://repocloud.io/details/Checkmate)、[Pikapods](https://www.pikapods.com/)、[Coolify](https://coolify.io/)、[Elestio](https://elest.io/open-source/checkmate)、[K8s](../../charts/helm/checkmate/INSTALLATION.md)、[Sive Host](https://sive.host)、[Cloudzy](https://cloudzy.com/marketplace/checkmate) などのワンクリックインストールオプションもあります。
|
||||
|
||||
代わりに、[Coolify](https://coolify.io/)、[Elestio](https://elest.io/open-source/checkmate)、[K8s](../../charts/helm/checkmate/INSTALLATION.md)、[Sive Host](https://sive.host)(南アフリカ)、[Cloudzy](https://cloudzy.com/marketplace/checkmate)、[Pikapods](https://www.pikapods.com/) を使って Checkmate インスタンスをすばやく立ち上げることもできます。サーバーインフラを監視したい場合は、[Capture エージェント](https://github.com/bluewave-labs/capture) が必要です。Capture リポジトリにもインストール手順が記載されています。
|
||||
|
||||
### カスタム CA の利用
|
||||
|
||||
プライベート認証局(Smallstep など)が発行した証明書で内部 HTTPS エンドポイントを監視する必要がある場合は、Docker 設定のオプションを記載した [カスタム CA 信頼ガイド](../custom-ca-trust.md) をご覧ください。
|
||||
プライベート認証局(Smallstep など)が発行した証明書で内部 HTTPS エンドポイントを監視する必要がある場合は、Docker 設定のオプションを記載した [カスタム CA 信頼ガイド](https://checkmate.so/docs/settings/custom-ca-trust) をご覧ください。
|
||||
|
||||
その他のドキュメントは [docs ディレクトリ](../) を参照してください。
|
||||
その他のドキュメントは [Checkmate ドキュメント](https://checkmate.so/docs) を参照してください。
|
||||
|
||||
<a id="performance"></a>
|
||||
## パフォーマンス
|
||||
@@ -175,9 +178,9 @@ Checkmate は 1000 以上のアクティブなモニターでストレステス
|
||||
<a id="contributing"></a>
|
||||
## コントリビューション
|
||||
|
||||
私たちは [Alex](http://github.com/ajhollid)(チームリード)、[Gorkem](http://github.com/gorkem-bwl/)、[Aryaman](https://github.com/Br0wnHammer)、[Mert](https://github.com/mertssmnoglu)、[Karen](https://github.com/karenvicent) で、個人や企業がインフラとサーバーを監視するお手伝いをしています。
|
||||
私たちは [Alex](http://github.com/ajhollid)(チームリード)、[Gorkem](http://github.com/gorkem-bwl/)、[Aryaman](https://github.com/Br0wnHammer)、[Mert](https://github.com/mertssmnoglu) で、個人や企業がインフラとサーバーを監視するお手伝いをしています。
|
||||
|
||||
あらゆるレベルのコントリビューターとの強いつながりを築くことを誇りにしています。若いプロジェクトでありながら、Checkmate はすでに 7000 を超えるスターと、世界中から 90 名以上のコントリビューターを集めています。
|
||||
あらゆるレベルのコントリビューターとの強いつながりを築くことを誇りにしています。若いプロジェクトでありながら、Checkmate はすでに 11,000 近くのスターと、世界中から 150 名以上のコントリビューターを集めています。
|
||||
|
||||
このリポジトリには **Google、Microsoft、Intel、Cisco、Tencent、Electronic Arts、ByteDance、JP Morgan Chase、Deloitte、Accenture、Foxconn、Broadcom、China Telecom、Barclays、Capgemini、Wipro、Cloudflare、Dassault Systèmes、NEC** の社員からスターが付いています — ぜひ気軽に参加し、貢献し、私たちと一緒に学びましょう!
|
||||
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Uma aplicação de código aberto para monitoramento de disponibilidade e infraestrutura</strong></p>
|
||||
<p align="center"><strong>Uma aplicação de código aberto para monitoramento de disponibilidade e infraestrutura</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -82,13 +83,15 @@ As instruções de uso podem ser encontradas [aqui](https://checkmate.so/docs).
|
||||
|
||||
Veja as instruções de instalação no [portal de documentação do Checkmate](https://checkmate.so/docs).
|
||||
|
||||
Também há opções de instalação com 1 clique, como [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) ou [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
Alternativamente, você pode usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (África do Sul), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ou [Pikapods](https://www.pikapods.com/) para subir rapidamente uma instância do Checkmate. Se quiser monitorar sua infraestrutura de servidores, você precisará do [agente Capture](https://github.com/bluewave-labs/capture). O repositório do Capture também contém as instruções de instalação.
|
||||
|
||||
### Usando uma CA personalizada
|
||||
|
||||
Se você precisa monitorar endpoints HTTPS internos com certificados emitidos por autoridades certificadoras privadas (como Smallstep), veja nosso [Guia de confiança em CA personalizada](../custom-ca-trust.md) para opções de configuração do Docker.
|
||||
Se você precisa monitorar endpoints HTTPS internos com certificados emitidos por autoridades certificadoras privadas (como Smallstep), veja nosso [Guia de confiança em CA personalizada](https://checkmate.so/docs/settings/custom-ca-trust) para opções de configuração do Docker.
|
||||
|
||||
Para mais documentação, veja o [diretório docs](../).
|
||||
Para mais documentação, veja a [documentação do Checkmate](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Desempenho
|
||||
@@ -175,9 +178,9 @@ Sinta-se à vontade para perguntar ou compartilhar suas ideias — adoramos ouvi
|
||||
<a id="contributing"></a>
|
||||
## Contribuindo
|
||||
|
||||
Somos o [Alex](http://github.com/ajhollid) (líder de equipe), o [Gorkem](http://github.com/gorkem-bwl/), o [Aryaman](https://github.com/Br0wnHammer), o [Mert](https://github.com/mertssmnoglu) e a [Karen](https://github.com/karenvicent) — ajudamos indivíduos e empresas a monitorar suas infraestruturas e servidores.
|
||||
Somos o [Alex](http://github.com/ajhollid) (líder de equipe), o [Gorkem](http://github.com/gorkem-bwl/), o [Aryaman](https://github.com/Br0wnHammer) e o [Mert](https://github.com/mertssmnoglu) — ajudamos indivíduos e empresas a monitorar suas infraestruturas e servidores.
|
||||
|
||||
Nos orgulhamos de construir conexões fortes com contribuidores de todos os níveis. Apesar de ser um projeto jovem, o Checkmate já conquistou mais de 7000 estrelas e atraiu mais de 90 contribuidores no mundo todo.
|
||||
Nos orgulhamos de construir conexões fortes com contribuidores de todos os níveis. Apesar de ser um projeto jovem, o Checkmate já conquistou quase 11 mil estrelas e atraiu mais de 150 contribuidores no mundo todo.
|
||||
|
||||
Nosso repositório recebeu estrelas de funcionários do **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes e NEC** — então não se segure: entre, contribua e aprenda com a gente!
|
||||
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Open source приложение для мониторинга доступности и инфраструктуры</strong></p>
|
||||
<p align="center"><strong>Open source приложение для мониторинга доступности и инфраструктуры</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -82,13 +83,15 @@ Checkmate прошёл нагрузочное тестирование с 1000+
|
||||
|
||||
Инструкции по установке смотрите на [портале документации Checkmate](https://checkmate.so/docs).
|
||||
|
||||
Также доступны варианты установки в один клик, такие как [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) или [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
В качестве альтернативы вы можете использовать [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (ЮАР), [Cloudzy](https://cloudzy.com/marketplace/checkmate) или [Pikapods](https://www.pikapods.com/), чтобы быстро поднять инстанс Checkmate. Если вы хотите мониторить серверную инфраструктуру, потребуется агент [Capture](https://github.com/bluewave-labs/capture). В репозитории Capture также есть инструкции по установке.
|
||||
|
||||
### Использование собственного УЦ
|
||||
|
||||
Если нужно мониторить внутренние HTTPS-эндпоинты с сертификатами от приватных УЦ (например, Smallstep), смотрите наше [руководство по доверию собственным УЦ](../custom-ca-trust.md) с параметрами конфигурации Docker.
|
||||
Если нужно мониторить внутренние HTTPS-эндпоинты с сертификатами от приватных УЦ (например, Smallstep), смотрите наше [руководство по доверию собственным УЦ](https://checkmate.so/docs/settings/custom-ca-trust) с параметрами конфигурации Docker.
|
||||
|
||||
Дополнительная документация — в [каталоге docs](../).
|
||||
Дополнительная документация — в [документации Checkmate](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Производительность
|
||||
@@ -175,9 +178,9 @@ Checkmate прошёл нагрузочное тестирование с 1000+
|
||||
<a id="contributing"></a>
|
||||
## Участие
|
||||
|
||||
Мы — [Alex](http://github.com/ajhollid) (тимлид), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) и [Karen](https://github.com/karenvicent) — помогаем частным лицам и компаниям мониторить их инфраструктуру и серверы.
|
||||
Мы — [Alex](http://github.com/ajhollid) (тимлид), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) и [Mert](https://github.com/mertssmnoglu) — помогаем частным лицам и компаниям мониторить их инфраструктуру и серверы.
|
||||
|
||||
Мы гордимся тем, что выстраиваем крепкие связи с контрибьюторами на всех уровнях. Несмотря на то что проект молодой, Checkmate уже получил более 7000 звёзд и более 90 контрибьюторов со всего мира.
|
||||
Мы гордимся тем, что выстраиваем крепкие связи с контрибьюторами на всех уровнях. Несмотря на то что проект молодой, Checkmate уже получил почти 11 тысяч звёзд и более 150 контрибьюторов со всего мира.
|
||||
|
||||
Наш репозиторий помечен сотрудниками **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes и NEC** — так что не стесняйтесь, присоединяйтесь, вносите вклад и учитесь вместе с нами!
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>แอปพลิเคชันโอเพนซอร์สสำหรับตรวจสอบสถานะการทำงานและโครงสร้างพื้นฐาน</strong></p>
|
||||
<p align="center"><strong>แอปพลิเคชันโอเพนซอร์สสำหรับตรวจสอบสถานะการทำงานและโครงสร้างพื้นฐาน</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -85,13 +86,15 @@ Checkmate ผ่านการทดสอบโหลดสำหรับม
|
||||
|
||||
ดูวิธีการติดตั้งใน [พอร์ทัลเอกสารของ Checkmate](https://checkmate.so/docs)
|
||||
|
||||
นอกจากนี้ยังมีตัวเลือกการติดตั้งแบบคลิกเดียว เช่น [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) หรือ [Cloudzy](https://cloudzy.com/marketplace/checkmate)
|
||||
|
||||
อีกทางเลือกหนึ่ง คุณสามารถใช้ [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (แอฟริกาใต้), [Cloudzy](https://cloudzy.com/marketplace/checkmate) หรือ [Pikapods](https://www.pikapods.com/) เพื่อเปิดอินสแตนซ์ Checkmate ได้อย่างรวดเร็ว หากต้องการมอนิเตอร์โครงสร้างพื้นฐานเซิร์ฟเวอร์ คุณจะต้องใช้ [เอเจนต์ Capture](https://github.com/bluewave-labs/capture) รีโพ Capture ยังมีวิธีการติดตั้งด้วย
|
||||
|
||||
### การใช้ Custom CA
|
||||
|
||||
หากคุณต้องการมอนิเตอร์ HTTPS เอนด์พอยต์ภายในที่ใช้ใบรับรองจาก CA ส่วนตัว (เช่น Smallstep) โปรดดู [คู่มือการเชื่อถือ CA แบบกำหนดเอง](../custom-ca-trust.md) สำหรับตัวเลือกการตั้งค่า Docker
|
||||
หากคุณต้องการมอนิเตอร์ HTTPS เอนด์พอยต์ภายในที่ใช้ใบรับรองจาก CA ส่วนตัว (เช่น Smallstep) โปรดดู [คู่มือการเชื่อถือ CA แบบกำหนดเอง](https://checkmate.so/docs/settings/custom-ca-trust) สำหรับตัวเลือกการตั้งค่า Docker
|
||||
|
||||
สำหรับเอกสารเพิ่มเติม ดูที่ [โฟลเดอร์ docs](../)
|
||||
สำหรับเอกสารเพิ่มเติม ดูที่ [เอกสารของ Checkmate](https://checkmate.so/docs)
|
||||
|
||||
<a id="performance"></a>
|
||||
## ประสิทธิภาพ
|
||||
@@ -178,9 +181,9 @@ Checkmate ผ่านการทดสอบโหลดสำหรับม
|
||||
<a id="contributing"></a>
|
||||
## การมีส่วนร่วม
|
||||
|
||||
พวกเราคือ [Alex](http://github.com/ajhollid) (หัวหน้าทีม), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) และ [Karen](https://github.com/karenvicent) ที่ช่วยให้บุคคลและธุรกิจสามารถมอนิเตอร์โครงสร้างพื้นฐานและเซิร์ฟเวอร์ได้
|
||||
พวกเราคือ [Alex](http://github.com/ajhollid) (หัวหน้าทีม), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) และ [Mert](https://github.com/mertssmnoglu) ที่ช่วยให้บุคคลและธุรกิจสามารถมอนิเตอร์โครงสร้างพื้นฐานและเซิร์ฟเวอร์ได้
|
||||
|
||||
เราภูมิใจกับการสร้างความสัมพันธ์ที่แข็งแกร่งกับผู้ร่วมพัฒนาในทุกระดับ แม้จะเป็นโครงการที่ยังใหม่ แต่ Checkmate ได้รับมากกว่า 7000 ดาว และมีผู้ร่วมพัฒนามากกว่า 90 คนจากทั่วโลก
|
||||
เราภูมิใจกับการสร้างความสัมพันธ์ที่แข็งแกร่งกับผู้ร่วมพัฒนาในทุกระดับ แม้จะเป็นโครงการที่ยังใหม่ แต่ Checkmate ได้รับเกือบ 11,000 ดาว และมีผู้ร่วมพัฒนามากกว่า 150 คนจากทั่วโลก
|
||||
|
||||
รีโพของเราได้รับดาวจากพนักงานของ **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes และ NEC** — เพราะฉะนั้นอย่าลังเล ร่วมพัฒนา เรียนรู้ และเติบโตไปกับเรา!
|
||||
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Açık kaynaklı çalışma süresi ve altyapı izleme uygulaması</strong></p>
|
||||
<p align="center"><strong>Açık kaynaklı çalışma süresi ve altyapı izleme uygulaması</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -82,13 +83,15 @@ Kullanım talimatlarını [burada](https://checkmate.so/docs) bulabilirsiniz.
|
||||
|
||||
Kurulum talimatları için [Checkmate dokümantasyon portalına](https://checkmate.so/docs) bakın.
|
||||
|
||||
Ayrıca [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) veya [Cloudzy](https://cloudzy.com/marketplace/checkmate) gibi tek tıkla kurulum seçenekleri de vardır.
|
||||
|
||||
Alternatif olarak hızlıca bir Checkmate örneği çalıştırmak için [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Güney Afrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) veya [Pikapods](https://www.pikapods.com/)'u kullanabilirsiniz. Sunucu altyapınızı izlemek isterseniz [Capture ajanına](https://github.com/bluewave-labs/capture) ihtiyacınız olacak. Capture deposu da kurulum talimatlarını içerir.
|
||||
|
||||
### Özel CA Kullanımı
|
||||
|
||||
Özel sertifika otoritelerinden (örneğin Smallstep) gelen sertifikalarla iç HTTPS uç noktalarını izlemeniz gerekiyorsa, Docker yapılandırma seçenekleri için [Özel CA Güven Kılavuzumuza](../custom-ca-trust.md) bakın.
|
||||
Özel sertifika otoritelerinden (örneğin Smallstep) gelen sertifikalarla iç HTTPS uç noktalarını izlemeniz gerekiyorsa, Docker yapılandırma seçenekleri için [Özel CA Güven Kılavuzumuza](https://checkmate.so/docs/settings/custom-ca-trust) bakın.
|
||||
|
||||
Daha fazla dokümantasyon için [docs dizinine](../) göz atın.
|
||||
Daha fazla dokümantasyon için [Checkmate dokümantasyonuna](https://checkmate.so/docs) göz atın.
|
||||
|
||||
<a id="performance"></a>
|
||||
## Performans
|
||||
@@ -175,9 +178,9 @@ Soru sormaktan ya da fikirlerinizi paylaşmaktan çekinmeyin — sizi dinlemekte
|
||||
<a id="contributing"></a>
|
||||
## Katkıda bulunma
|
||||
|
||||
Biz; [Alex](http://github.com/ajhollid) (takım lideri), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) ve [Karen](https://github.com/karenvicent) olarak bireylerin ve işletmelerin altyapı ile sunucularını izlemelerine yardımcı oluyoruz.
|
||||
Biz; [Alex](http://github.com/ajhollid) (takım lideri), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) ve [Mert](https://github.com/mertssmnoglu) olarak bireylerin ve işletmelerin altyapı ile sunucularını izlemelerine yardımcı oluyoruz.
|
||||
|
||||
Her seviyedeki katkıda bulunanlarla güçlü bağlar kurmaktan gurur duyuyoruz. Genç bir proje olmasına rağmen Checkmate şimdiden 7000'den fazla yıldız ve dünya genelinde 90'dan fazla katkıda bulunan kazanmıştır.
|
||||
Her seviyedeki katkıda bulunanlarla güçlü bağlar kurmaktan gurur duyuyoruz. Genç bir proje olmasına rağmen Checkmate şimdiden 11 bine yakın yıldız ve dünya genelinde 150'den fazla katkıda bulunan kazanmıştır.
|
||||
|
||||
Reponuz **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes ve NEC** çalışanları tarafından yıldızlanmıştır — siz de çekinmeyin, katılın, katkıda bulunun ve bizimle birlikte öğrenin!
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Open source застосунок для моніторингу доступності та інфраструктури</strong></p>
|
||||
<p align="center"><strong>Open source застосунок для моніторингу доступності та інфраструктури</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -85,13 +86,15 @@ Checkmate пройшов навантажувальне тестування з
|
||||
|
||||
Інструкції зі встановлення дивіться у [порталі документації Checkmate](https://checkmate.so/docs).
|
||||
|
||||
Також доступні варіанти встановлення в один клік, як-от [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) або [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
Альтернативно, для швидкого розгортання Checkmate можна скористатися [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Південна Африка), [Cloudzy](https://cloudzy.com/marketplace/checkmate) або [Pikapods](https://www.pikapods.com/). Якщо хочете моніторити серверну інфраструктуру, знадобиться агент [Capture](https://github.com/bluewave-labs/capture). У репозиторії Capture також є інструкції зі встановлення.
|
||||
|
||||
### Використання власного УЦ
|
||||
|
||||
Якщо потрібно моніторити внутрішні HTTPS-ендпоїнти з сертифікатами від приватних центрів сертифікації (як-от Smallstep), див. наш [посібник з довіри власному УЦ](../custom-ca-trust.md) для опцій конфігурації Docker.
|
||||
Якщо потрібно моніторити внутрішні HTTPS-ендпоїнти з сертифікатами від приватних центрів сертифікації (як-от Smallstep), див. наш [посібник з довіри власному УЦ](https://checkmate.so/docs/settings/custom-ca-trust) для опцій конфігурації Docker.
|
||||
|
||||
Додаткову документацію шукайте у [каталозі docs](../).
|
||||
Додаткову документацію шукайте в [документації Checkmate](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Продуктивність
|
||||
@@ -178,9 +181,9 @@ Checkmate пройшов навантажувальне тестування з
|
||||
<a id="contributing"></a>
|
||||
## Внески
|
||||
|
||||
Ми — [Alex](http://github.com/ajhollid) (тімлід), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) і [Karen](https://github.com/karenvicent) — допомагаємо приватним особам і компаніям моніторити їх інфраструктуру та сервери.
|
||||
Ми — [Alex](http://github.com/ajhollid) (тімлід), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) і [Mert](https://github.com/mertssmnoglu) — допомагаємо приватним особам і компаніям моніторити їх інфраструктуру та сервери.
|
||||
|
||||
Ми пишаємося тим, що будуємо міцні зв'язки з контриб'юторами на всіх рівнях. Попри молодий вік, Checkmate уже зібрав понад 7000 зірок і понад 90 контриб'юторів з усього світу.
|
||||
Ми пишаємося тим, що будуємо міцні зв'язки з контриб'юторами на всіх рівнях. Попри молодий вік, Checkmate уже зібрав майже 11 тисяч зірок і понад 150 контриб'юторів з усього світу.
|
||||
|
||||
Наш репозиторій позначений співробітниками **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes і NEC** — тож не соромтеся, приєднуйтесь, робіть внесок і вчіться разом з нами!
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>Ứng dụng mã nguồn mở giám sát tình trạng hoạt động và hạ tầng</strong></p>
|
||||
<p align="center"><strong>Ứng dụng mã nguồn mở giám sát tình trạng hoạt động và hạ tầng</strong>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
|
||||
<img width="1703" height="1041" alt="image" src="https://github.com/user-attachments/assets/0f4dcf38-9b42-4b84-8633-ff34778df1a8" />
|
||||
</p>
|
||||
<br>
|
||||
|
||||
|
||||
@@ -85,13 +86,15 @@ Có thể tìm thấy hướng dẫn sử dụng [tại đây](https://checkmate
|
||||
|
||||
Xem hướng dẫn cài đặt trong [cổng tài liệu của Checkmate](https://checkmate.so/docs).
|
||||
|
||||
Ngoài ra còn có các tùy chọn cài đặt chỉ với 1 cú nhấp như [Repocloud](https://repocloud.io/details/Checkmate), [Pikapods](https://www.pikapods.com/), [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) hoặc [Cloudzy](https://cloudzy.com/marketplace/checkmate).
|
||||
|
||||
Hoặc bạn có thể dùng [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Nam Phi), [Cloudzy](https://cloudzy.com/marketplace/checkmate) hoặc [Pikapods](https://www.pikapods.com/) để dựng nhanh một instance Checkmate. Nếu muốn giám sát hạ tầng máy chủ, bạn sẽ cần [agent Capture](https://github.com/bluewave-labs/capture). Kho Capture cũng chứa hướng dẫn cài đặt.
|
||||
|
||||
### Sử dụng CA tuỳ chỉnh
|
||||
|
||||
Nếu bạn cần giám sát các endpoint HTTPS nội bộ với chứng chỉ từ các tổ chức phát hành riêng (ví dụ Smallstep), hãy xem [Hướng dẫn tin cậy CA tuỳ chỉnh](../custom-ca-trust.md) với các tuỳ chọn cấu hình Docker.
|
||||
Nếu bạn cần giám sát các endpoint HTTPS nội bộ với chứng chỉ từ các tổ chức phát hành riêng (ví dụ Smallstep), hãy xem [Hướng dẫn tin cậy CA tuỳ chỉnh](https://checkmate.so/docs/settings/custom-ca-trust) với các tuỳ chọn cấu hình Docker.
|
||||
|
||||
Để biết thêm tài liệu, xem [thư mục docs](../).
|
||||
Để biết thêm tài liệu, xem [tài liệu của Checkmate](https://checkmate.so/docs).
|
||||
|
||||
<a id="performance"></a>
|
||||
## Hiệu năng
|
||||
@@ -178,9 +181,9 @@ Nếu bạn có câu hỏi, góp ý hoặc bình luận, có một vài tuỳ ch
|
||||
<a id="contributing"></a>
|
||||
## Đóng góp
|
||||
|
||||
Chúng tôi là [Alex](http://github.com/ajhollid) (trưởng nhóm), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) và [Karen](https://github.com/karenvicent), hỗ trợ cá nhân và doanh nghiệp giám sát hạ tầng và máy chủ.
|
||||
Chúng tôi là [Alex](http://github.com/ajhollid) (trưởng nhóm), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer) và [Mert](https://github.com/mertssmnoglu), hỗ trợ cá nhân và doanh nghiệp giám sát hạ tầng và máy chủ.
|
||||
|
||||
Chúng tôi tự hào xây dựng những mối liên kết bền chặt với người đóng góp ở mọi cấp độ. Dù còn trẻ, Checkmate đã có hơn 7000 sao và thu hút hơn 90 người đóng góp trên khắp thế giới.
|
||||
Chúng tôi tự hào xây dựng những mối liên kết bền chặt với người đóng góp ở mọi cấp độ. Dù còn trẻ, Checkmate đã có gần 11.000 sao và thu hút hơn 150 người đóng góp trên khắp thế giới.
|
||||
|
||||
Kho của chúng tôi đã được nhân viên của **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes và NEC** đánh sao — đừng ngần ngại, hãy tham gia, đóng góp và học hỏi cùng chúng tôi!
|
||||
|
||||
|
||||
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user