Merge branch 'develop' into feat-allow-admin-change-passwords

This commit is contained in:
karenvicent committed 2025-10-02 15:20:32 -04:00
commit c5bbb7aa2c
35 files changed
+702 -158

No files matched your search

@@ -1,16 +1,16 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: client
name: checkmate-client
spec:
replicas: 1
selector:
matchLabels:
app: client
app: checkmate-client
template:
metadata:
labels:
app: client
app: checkmate-client
spec:
containers:
- name: client
@@ -20,3 +20,13 @@ spec:
env:
- name: UPTIME_APP_API_BASE_URL
value: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1"
- name: UPTIME_APP_CLIENT_HOST
value: "http://{{ .Values.client.ingress.host }}"
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
volumes:
- name: config-volume
configMap:
name: checkmate-server-nginx-cm
@@ -2,10 +2,15 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: client-ingress
name: checkmate-client-ingress
{{- if .Values.client.ingress.annotations }}
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
{{- range $key, $value := .Values.client.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
spec:
ingressClassName: {{ .Values.client.ingress.className }}
rules:
- host: {{ .Values.client.ingress.host }}
http:
@@ -14,7 +19,7 @@ spec:
pathType: Prefix
backend:
service:
name: client
name: checkmate-client
port:
number: {{ .Values.client.port }}
{{- end }}
@@ -1,9 +1,9 @@
apiVersion: v1
kind: Service
metadata:
name: client
name: checkmate-client
spec:
selector:
app: client
app: checkmate-client
ports:
- port: {{ .Values.client.port }}
@@ -1,27 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongodb
spec:
replicas: 1
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
containers:
- name: mongodb
image: {{ .Values.mongodb.image }}
ports:
- containerPort: {{ .Values.mongodb.port }}
command: ["mongod", "--quiet", "--bind_ip_all"]
volumeMounts:
- name: mongodb-data
mountPath: /data/db
volumes:
- name: mongodb-data
persistentVolumeClaim:
claimName: mongodb-pvc
@@ -1,10 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongodb-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.persistence.mongodbSize }}
@@ -1,9 +1,9 @@
apiVersion: v1
kind: Service
metadata:
name: mongodb
name: checkmate-mongodb
spec:
selector:
app: mongodb
app: checkmate-mongodb
ports:
- port: {{ .Values.mongodb.port }}
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: checkmate-mongodb
spec:
replicas: 1
selector:
matchLabels:
app: checkmate-mongodb
template:
metadata:
labels:
app: checkmate-mongodb
spec:
containers:
- name: mongodb
image: {{ .Values.mongodb.image }}
ports:
- containerPort: {{ .Values.mongodb.port }}
command: ["mongod", "--quiet", "--bind_ip_all"]
volumeMounts:
- name: checkmate-mongo-persistent-storage
mountPath: /data/db
volumeClaimTemplates:
- metadata:
name: checkmate-mongo-persistent-storage
spec:
storageClassName: "gp3"
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.persistence.mongodbSize }}
@@ -1,26 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: {{ .Values.redis.image }}
ports:
- containerPort: {{ .Values.redis.port }}
volumeMounts:
- name: redis-data
mountPath: /data
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: redis-pvc
@@ -1,10 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.persistence.redisSize }}
@@ -1,9 +1,11 @@
{{- if .Values.redis.enabled }}
apiVersion: v1
kind: Service
metadata:
name: redis
name: checkmate-redis
spec:
selector:
app: redis
app: checkmate-redis
ports:
- port: {{ .Values.redis.port }}
{{- end }}
@@ -0,0 +1,33 @@
{{- if .Values.redis.enabled }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: checkmate-redis
spec:
replicas: 1
selector:
matchLabels:
app: checkmate-redis
template:
metadata:
labels:
app: checkmate-redis
spec:
containers:
- name: redis
image: {{ .Values.redis.image }}
ports:
- containerPort: {{ .Values.redis.port }}
volumeMounts:
- name: redis-data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: checkmate-redis-persistent-storage
spec:
storageClassName: "gp3"
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.persistence.redisSize }}
{{- end }}
@@ -4,10 +4,6 @@
{{- fail "secrets.JWT_SECRET must be overridden and cannot be 'change_me'" }}
{{- end }}
{{- if or (not $secrets.REFRESH_TOKEN_SECRET) (eq $secrets.REFRESH_TOKEN_SECRET "change_me") }}
{{- fail "secrets.REFRESH_TOKEN_SECRET must be overridden and cannot be 'change_me'" }}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
@@ -1,16 +1,16 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: server
name: checkmate-server
spec:
replicas: 1
selector:
matchLabels:
app: server
app: checkmate-server
template:
metadata:
labels:
app: server
app: checkmate-server
spec:
containers:
- name: server
@@ -20,3 +20,6 @@ spec:
envFrom:
- secretRef:
name: checkmate-secrets
env:
- name: CLIENT_HOST
value: "http://{{ .Values.client.ingress.host }}"
@@ -2,15 +2,22 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: server-ingress
name: checkmate-server-ingress
{{- if .Values.server.ingress.annotations }}
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
{{- range $key, $value := .Values.server.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{/*#annotations:
#nginx.ingress.kubernetes.io/rewrite-target: /
#nginx.ingress.kubernetes.io/enable-cors: "true"
#nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}"
#nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS"
#nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
#nginx.ingress.kubernetes.io/cors-allow-credentials: "true"*/}}
spec:
ingressClassName: {{ .Values.server.ingress.className }}
rules:
- host: {{ .Values.server.ingress.host }}
http:
@@ -19,7 +26,7 @@ spec:
pathType: Prefix
backend:
service:
name: server
name: checkmate-server
port:
number: {{ .Values.server.port }}
{{- end }}
@@ -0,0 +1,41 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: checkmate-server-nginx-cm
data:
default.conf: |
server {
listen 80;
listen [::]:80;
server_name checkmate-demo.bluewavelabs.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# location /api/ {
# proxy_pass http://{{ .Values.server.ingress.host }}:5000/api/;
# proxy_http_version 1.1;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
location /api-docs/ {
proxy_pass http://{{ .Values.server.ingress.host }}:5000/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
@@ -1,9 +1,9 @@
apiVersion: v1
kind: Service
metadata:
name: server
name: checkmate-server
spec:
selector:
app: server
app: checkmate-server
ports:
- port: {{ .Values.server.port }}
+22 -16
View File
@@ -1,40 +1,46 @@
client:
image: ghcr.io/bluewave-labs/checkmate-client:v2.3
image: ghcr.io/bluewave-labs/checkmate-client:v3.2.0
port: 80
ingress:
enabled: true
host: change_me
className: nginx
annotations: {}
server:
image: ghcr.io/bluewave-labs/checkmate-backend:v2.3
image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0
port: 52345
protocol: change_me
protocol: http
ingress:
enabled: true
host: change_me
className: nginx
annotations: {}
redis:
enabled: false
image: redis:7.2
port: 6379
mongodb:
image: mongo:7.0
image: ghcr.io/bluewave-labs/checkmate-mongo:v3.2.0
port: 27017
secrets:
JWT_SECRET: change_me
REFRESH_TOKEN_SECRET: change_me
SYSTEM_EMAIL_ADDRESS: test@example.com
SYSTEM_EMAIL_PASSWORD: change_me
SYSTEM_EMAIL_HOST: smtp.example.com
SYSTEM_EMAIL_PORT: "587"
PAGESPEED_API_KEY: change_me
DB_CONNECTION_STRING: mongodb://mongodb:27017/uptime_db
REDIS_HOST: redis
REDIS_PORT: "6379"
DB_TYPE: MongoDB
TOKEN_TTL: 99d
REFRESH_TOKEN_TTL: 99d
# REFRESH_TOKEN_SECRET: change_me
# SYSTEM_EMAIL_ADDRESS: test@example.com
# SYSTEM_EMAIL_PASSWORD: change_me
# SYSTEM_EMAIL_HOST: smtp.example.com
# SYSTEM_EMAIL_PORT: "587"
# PAGESPEED_API_KEY: change_me
DB_CONNECTION_STRING: mongodb://checkmate-mongodb.namespace.svc:27017/uptime_db
CLIENT_HOST: change_me
# REDIS_HOST: redis
# REDIS_PORT: "6379"
# DB_TYPE: MongoDB
# TOKEN_TTL: 99d
# REFRESH_TOKEN_TTL: 99d
persistence:
mongodbSize: 5Gi
@@ -0,0 +1,36 @@
import LeftArrow from "@/assets/icons/left-arrow.svg?react";
import LeftArrowDouble from "@/assets/icons/left-arrow-double.svg?react";
import LeftArrowLong from "@/assets/icons/left-arrow-long.svg?react";
export const ArrowLeft = ({
type,
color = "#667085",
...props
}: {
type?: string;
color?: string | undefined;
[key: string]: any;
}) => {
if (type === "double") {
return (
<LeftArrowDouble
style={{ color }}
{...props}
/>
);
} else if (type === "long") {
return (
<LeftArrowLong
style={{ color }}
{...props}
/>
);
} else {
return (
<LeftArrow
style={{ color }}
{...props}
/>
);
}
};
@@ -0,0 +1,28 @@
import RightArrow from "@/assets/icons/right-arrow.svg?react";
import RightArrowDouble from "@/assets/icons/right-arrow-double.svg?react";
export const ArrowRight = ({
type,
color = "#667085",
...props
}: {
type?: string;
color?: string | undefined;
[key: string]: any;
}) => {
if (type === "double") {
return (
<RightArrowDouble
style={{ color }}
{...props}
/>
);
} else {
return (
<RightArrow
style={{ color }}
{...props}
/>
);
}
};
@@ -1,39 +1,6 @@
import { useState, useEffect } from "react";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
import { Outlet } from "react-router";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
const COLLAPSED_WIDTH = 50;
const EXPANDED_WIDTH = 250;
const SideBar = () => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const [collapsed, setCollapsed] = useState(false);
useEffect(() => {
setCollapsed(isSmall);
}, [isSmall]);
return (
<Stack
border="1px solid red"
width={collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH}
sx={{
transition: "width 0.3s ease",
}}
>
<Box
border="1px solid blue"
onClick={() => setCollapsed(!collapsed)}
>
Sidebar Content
</Box>
</Stack>
);
};
import { SideBar } from "@/Components/v2/Layouts/Sidebar";
const RootLayout = () => {
return (
@@ -0,0 +1,16 @@
import { useTheme } from "@mui/material/styles";
import Stack from "@mui/material/Stack";
export const BottomControls = ({}) => {
const theme = useTheme();
return (
<Stack
direction="row"
height={50}
py={theme.spacing(4)}
px={theme.spacing(8)}
gap={theme.spacing(2)}
></Stack>
);
};
@@ -0,0 +1,50 @@
import IconButton from "@mui/material/IconButton";
import { ArrowRight } from "@/Components/v2/Arrows/ArrowRight";
import { ArrowLeft } from "@/Components/v2/Arrows/ArrowLeft";
import { useTheme } from "@mui/material/styles";
import { useDispatch } from "react-redux";
import { toggleSidebar } from "../../../../Features/UI/uiSlice.js";
export const CollapseButton = ({ collapsed }: { collapsed: boolean }) => {
const theme = useTheme();
const dispatch = useDispatch();
const arrowIcon = collapsed ? (
<ArrowRight
height={theme.spacing(8)}
width={theme.spacing(8)}
color={theme.palette.primary.contrastTextSecondary}
/>
) : (
<ArrowLeft
height={theme.spacing(8)}
width={theme.spacing(8)}
color={theme.palette.primary.contrastTextSecondary}
/>
);
return (
<IconButton
sx={{
position: "absolute",
/* TODO 60 is a magic number. if logo chnges size this might break */
top: 60,
right: 0,
transform: `translate(50%, 0)`,
backgroundColor: theme.palette.tertiary.main,
border: `1px solid ${theme.palette.primary.lowContrast}`,
p: theme.spacing(2.5),
"&:focus": { outline: "none" },
"&:hover": {
backgroundColor: theme.palette.primary.lowContrast,
borderColor: theme.palette.primary.lowContrast,
},
}}
onClick={() => {
dispatch(toggleSidebar());
}}
>
{arrowIcon}
</IconButton>
);
};
@@ -0,0 +1,59 @@
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 { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
export const Logo = ({ collapsed }: { collapsed: boolean }) => {
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
return (
<Stack
pt={theme.spacing(6)}
pb={theme.spacing(12)}
pl={theme.spacing(8)}
direction="row"
alignItems="center"
gap={theme.spacing(4)}
onClick={() => navigate("/")}
sx={{ cursor: "pointer" }}
>
<Typography
pl={theme.spacing("1px")}
minWidth={theme.spacing(16)}
minHeight={theme.spacing(16)}
display={"flex"}
justifyContent={"center"}
alignItems={"center"}
bgcolor={theme.palette.accent.main}
borderRadius={theme.shape.borderRadius}
color={theme.palette.accent.contrastText}
fontSize={18}
>
C
</Typography>
<Box
overflow={"hidden"}
sx={{
transition: "opacity 900ms ease, width 900ms ease",
opacity: collapsed ? 0 : 1,
whiteSpace: "nowrap",
width: collapsed ? 0 : "100%",
}}
>
{" "}
<Typography
lineHeight={1}
mt={theme.spacing(2)}
color={theme.palette.primary.contrastText}
variant="h2"
>
{t("common.appName")}
</Typography>
</Box>
</Stack>
);
};
@@ -0,0 +1,58 @@
import Notifications from "@/assets/icons/notifications.svg?react";
import Monitors from "@/assets/icons/monitors.svg?react";
import PageSpeed from "@/assets/icons/page-speed.svg?react";
import Integrations from "@/assets/icons/integrations.svg?react";
import Incidents from "@/assets/icons/incidents.svg?react";
import StatusPages from "@/assets/icons/status-pages.svg?react";
import Maintenance from "@/assets/icons/maintenance.svg?react";
import Logs from "@/assets/icons/logs.svg?react";
import Settings from "@/assets/icons/settings.svg?react";
import Support from "@/assets/icons/support.svg?react";
import Discussions from "@/assets/icons/discussions.svg?react";
import Docs from "@/assets/icons/docs.svg?react";
import ChangeLog from "@/assets/icons/changeLog.svg?react";
export const getMenu = (t: Function) => [
{ name: t("menu.uptime"), path: "v2/uptime", icon: <Monitors /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
{
name: t("menu.notifications"),
path: "notifications",
icon: <Notifications />,
},
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
{
name: t("menu.settings"),
icon: <Settings />,
path: "settings",
},
];
export const getBottomMenu = (t: Function) => [
{ name: t("menu.support"), path: "support", icon: <Support />, url: "invite" },
{
name: t("menu.discussions"),
path: "discussions",
icon: <Discussions />,
url: "https://github.com/bluewave-labs/checkmate/discussions",
},
{
name: t("menu.docs"),
path: "docs",
icon: <Docs />,
url: "https://bluewavelabs.gitbook.io/checkmate",
},
{
name: t("menu.changelog"),
path: "changelog",
icon: <ChangeLog />,
url: "https://github.com/bluewave-labs/checkmate/releases",
},
];
@@ -0,0 +1,103 @@
import Tooltip from "@mui/material/Tooltip";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
export interface NavData {
name: string;
icon: JSX.Element;
}
export const NavItem = ({
item,
collapsed,
selected,
onClick,
}: {
item: NavData;
collapsed: boolean;
selected: boolean;
onClick: (event: React.MouseEvent) => void;
}) => {
const theme = useTheme();
const iconStroke = selected
? theme.palette.primary.contrastText
: theme.palette.primary.contrastTextTertiary;
const buttonBgColor = selected ? theme.palette.secondary.main : "transparent";
const buttonBgHoverColor = selected
? theme.palette.secondary.main
: theme.palette.tertiary.main;
const fontWeight = selected ? 600 : 400;
return (
<Tooltip
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
disableInteractive
>
<ListItemButton
sx={{
backgroundColor: buttonBgColor,
"&:hover": {
backgroundColor: buttonBgHoverColor,
},
height: 37,
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
pl: theme.spacing(5),
}}
onClick={onClick}
>
<ListItemIcon
sx={{
minWidth: 0,
"& svg": {
height: 20,
width: 20,
opacity: 0.81,
},
"& svg path": {
stroke: iconStroke,
},
}}
>
{item.icon}
</ListItemIcon>
<Box
sx={{
overflow: "hidden",
transition: "opacity 900ms ease",
opacity: collapsed ? 0 : 1,
whiteSpace: "nowrap",
}}
>
<Typography
variant="body1"
color={theme.palette.primary.contrastText}
sx={{
fontWeight: fontWeight,
opacity: 0.9,
}}
>
{item.name}
</Typography>
</Box>
</ListItemButton>
</Tooltip>
);
};
@@ -0,0 +1,103 @@
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { setCollapsed } from "@/Features/UI/uiSlice";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { useNavigate, useLocation } from "react-router-dom";
import { CollapseButton } from "@/Components/v2/Layouts/Sidebar/CollapseButton";
import Stack from "@mui/material/Stack";
import List from "@mui/material/List";
import Divider from "@mui/material/Divider";
import { Logo } from "@/Components/v2/Layouts/Sidebar/Logo";
import { getMenu, getBottomMenu } from "@/Components/v2/Layouts/Sidebar/Menu";
import { NavItem } from "@/Components/v2/Layouts/Sidebar/NavItem";
import { BottomControls } from "@/Components/v2/Layouts/Sidebar/BottomControls";
export const COLLAPSED_WIDTH = 64;
export const EXPANDED_WIDTH = 250;
export const SideBar = () => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const dispatch = useDispatch();
const collapsed = useSelector((state: any) => state.ui.sidebar.collapsed);
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const menu = getMenu(t);
const bottomMenu = getBottomMenu(t);
useEffect(() => {
dispatch(setCollapsed({ collapsed: isSmall }));
}, [isSmall]);
return (
<Stack
component="aside"
position="sticky"
paddingTop={theme.spacing(6)}
paddingBottom={theme.spacing(6)}
gap={theme.spacing(6)}
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
width={collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH}
sx={{
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
}}
>
<CollapseButton collapsed={collapsed} />
<Logo collapsed={collapsed} />
<List
component="nav"
disablePadding
sx={{
px: theme.spacing(6),
height: "100%",
}}
>
{menu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
collapsed={collapsed}
selected={selected}
onClick={() => navigate(`/${item.path}`)}
/>
);
})}
</List>
<List
component="nav"
disablePadding
sx={{
px: theme.spacing(6),
}}
>
{bottomMenu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
collapsed={collapsed}
selected={selected}
onClick={() => {
if (item.url) {
window.open(item.url, "_blank", "noreferrer");
} else {
navigate(`/${item.path}`);
}
}}
/>
);
})}
</List>
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
<BottomControls />
</Stack>
);
};
+19
View File
@@ -0,0 +1,19 @@
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
isAuthenticated: false,
};
const v2AuthSlice = createSlice({
name: "v2Auth",
initialState,
reducers: {
setIsAuthenticated: (state, action) => {
const { authenticated } = action.payload;
state.isAuthenticated = authenticated;
},
},
});
export default v2AuthSlice.reducer;
export const { setIsAuthenticated } = v2AuthSlice.actions;
+5
View File
@@ -47,6 +47,10 @@ const uiSlice = createSlice({
toggleSidebar: (state) => {
state.sidebar.collapsed = !state.sidebar.collapsed;
},
setCollapsed: (state, action) => {
const { collapsed } = action.payload;
state.sidebar.collapsed = collapsed;
},
setMode: (state, action) => {
state.mode = action.payload;
},
@@ -73,6 +77,7 @@ export default uiSlice.reducer;
export const {
setRowsPerPage,
toggleSidebar,
setCollapsed,
setMode,
setShowURL,
setGreeting,
+9 -4
View File
@@ -3,7 +3,9 @@ import { z } from "zod";
import { useTranslation } from "react-i18next";
import { useForm, Controller } from "react-hook-form";
import { useTheme } from "@mui/material/styles";
import { useDispatch } from "react-redux";
import { setIsAuthenticated } from "@/Features/Auth/v2AuthSlice";
import { useNavigate } from "react-router";
import { TextInput } from "@/Components/v2/Inputs/TextInput";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
@@ -19,8 +21,10 @@ type FormData = z.infer<typeof schema>;
const Login = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const theme = useTheme();
const { post, loading, error } = usePost<FormData, ApiResponse>("/auth/login");
const { post, loading } = usePost<FormData, ApiResponse>("/auth/login");
const navigate = useNavigate();
const {
handleSubmit,
@@ -37,9 +41,10 @@ const Login = () => {
const onSubmit = async (data: FormData) => {
const result = await post(data);
if (result) {
console.log(result.message);
dispatch(setIsAuthenticated({ authenticated: true }));
navigate("/v2/uptime");
} else {
console.error("Login failed:", error);
dispatch(setIsAuthenticated({ authenticated: false }));
}
};
+5 -1
View File
@@ -25,7 +25,11 @@ const V2Routes = ({ mode = "light" }) => {
element={<RootLayout />}
>
<Route
path="test"
index
element={<h1>Uptime</h1>}
/>
<Route
path="uptime"
element={<h1>Test Page</h1>}
/>
</Route>
+12
View File
@@ -54,12 +54,18 @@ export const lightPalette = {
main: colors.offWhite,
contrastText: colors.blueGray800,
contrastTextSecondary: colors.blueGray600,
contrastTextTertiary: colors.blueGray500,
lowContrast: colors.gray250,
},
secondary: {
main: colors.gray200,
light: colors.lightBlueWave,
contrastText: colors.blueGray600,
},
tertiary: {
main: colors.gray100,
contrastText: colors.blueGray800,
},
};
export const darkPalette = {
@@ -73,10 +79,16 @@ export const darkPalette = {
main: colors.offBlack,
contrastText: colors.blueGray50,
contrastTextSecondary: colors.gray200,
contrastTextTertiary: colors.gray500,
lowContrast: colors.blueGray600,
},
secondary: {
main: "#313131",
light: colors.lightBlueWave,
contrastText: colors.gray200,
},
tertiary: {
main: colors.blueGray800,
contrastText: colors.gray100,
},
};
+3
View File
@@ -67,6 +67,9 @@ export const theme = (mode: string, palette: any) =>
},
},
},
shape: {
borderRadius: 2,
},
});
export const lightTheme = createTheme(theme("light", lightPalette));
+3 -1
View File
@@ -1,6 +1,7 @@
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import authReducer from "./Features/Auth/authSlice";
import v2AuthReducer from "./Features/Auth/v2AuthSlice";
import uiReducer from "./Features/UI/uiSlice";
import storage from "redux-persist/lib/storage";
import { persistReducer, persistStore, createTransform } from "redux-persist";
@@ -19,12 +20,13 @@ const authTransform = createTransform(
const persistConfig = {
key: "root",
storage,
whitelist: ["auth", "ui"],
whitelist: ["auth", "v2Auth", "ui"],
transforms: [authTransform],
};
const rootReducer = combineReducers({
auth: authReducer,
v2Auth: v2AuthReducer,
ui: uiReducer,
});
+8
View File
@@ -7,3 +7,11 @@ interface ImportMetaEnv {
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare module "*.svg?react" {
import * as React from "react";
const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>;
export default ReactComponent;
}
+4
View File
@@ -3,13 +3,17 @@ import "@mui/material/Button";
declare module "@mui/material/styles" {
interface Palette {
accent: Palette["primary"];
tertiary: Palette["primary"];
}
interface PaletteOptions {
accent?: PaletteOptions["primary"];
tertiary?: PaletteOptions["primary"];
}
interface PaletteColor {
contrastTextSecondary?: string;
contrastTextTertiary?: string;
lowContrast?: string;
}
}